├── .esformatter ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── TRANSLATION.md ├── index.js ├── package.json ├── public ├── computed-columns-params.html ├── computed-columns-params.js ├── computed-columns-vis-controller.js ├── computed-columns-vis.html ├── computed-columns-vis.js ├── computed-columns.less └── images │ └── icon-table.svg ├── run-dependencies.sh ├── server ├── __tests__ │ └── index.js └── routes │ └── example.js └── translations └── es.json /.esformatter: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "esformatter-collapse-objects", 4 | "esformatter-quotes", 5 | "esformatter-braces", 6 | "esformatter-semicolons", 7 | "esformatter-remove-trailing-commas", 8 | "esformatter-flow" 9 | ], 10 | "quotes": { 11 | "type": "single", 12 | "avoidEscape": true 13 | }, 14 | "whiteSpace": { 15 | "before": { 16 | "PropertyName": 1, 17 | "FunctionExpressionOpeningBrace": 1 18 | } 19 | }, 20 | "collapseObjects": { 21 | "ObjectExpression": { 22 | "maxLineLength": 180, 23 | "maxKeys": 3, 24 | "maxDepth": 2 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "@elastic/kibana" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | /build/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.7.1] - 2017-07-22 10 | 11 | ### Fixed 12 | - Minor code clean bugs. 13 | 14 | ### Added 15 | - Support for Kibana 5.5.5. 16 | - Support for string cols. 17 | 18 | ## [0.6.0] - 2017-06-14 19 | 20 | ### Added 21 | - Visualization type Data thanks to @fbaligand. PR #7 22 | 23 | ### Fixed 24 | - Split table support. 25 | 26 | ## [0.5.0] - 2017-05-31 27 | 28 | ### Added 29 | - Custom ouput format (default 0,0.[00]). 30 | 31 | ### Fixed 32 | - Formulas mapped to math constant like e (euler). 33 | 34 | ## [0.2.0] - 2017-03-31 35 | 36 | ### Added 37 | - Output format. 38 | 39 | ### Fixed 40 | - Create new AggConfigResult instead clone cell. 41 | - Validate column value is a number. 42 | 43 | ## [0.1.1] - 2017-03-13 44 | 45 | ### Changed 46 | - Use String.prototype.replace instead lodash replace. 47 | 48 | ## [0.1.0] - 2017-03-13 49 | 50 | ### Added 51 | - Computed properties. 52 | - Multiples computed properties. 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Computed Columns 2 | 3 | > An awesome Kibana plugin. 4 | 5 | This visualization plugin is like a table, but with computed columns. 6 | Every new columns is a computation from normal basics columns. Every new column 7 | has it own expression (e.g. col[0] * col[1] / 100). 8 | 9 | --- 10 | 11 | ## Considerations 12 | 13 | * Any mathematical expression can be used to create computed columns. Even parentheses can be used to group expressions - e.g. (col[0] - col[1]) / col[0]. 14 | * To refence a colums use word _col_ followed by brackets with zero base index inside - e.g. col[1] = column 1 (second column). 15 | * Computed column can be used to create new computed columns. 16 | * Multiples computed colums will be computed in order, thous, you can only use column index n-1. 17 | * Current Release Version: 0.4.1. 18 | * Hidden columns are evaluated after computed columns. 19 | 20 | ## Install 21 | 22 | From 0.5.x every release includes plugins version (x.y.z) and Kibana version (a.b.c). 23 | 24 | #### From Kibana Installation Path: 25 | `./bin/kibana-plugin install https://github.com/seadiaz/computed-columns/releases/download/x.y.z/computed-columns-x.y.z-a.b.c.zip` 26 | 27 | #### From Docker Image: 28 | `kibana-plugin install https://github.com/seadiaz/computed-columns/releases/download/x.y.z/computed-columns-x.y.z-a.b.c.zip` 29 | 30 | ## Roadmap 31 | 32 | * Numeric data validation. 33 | * Hidden columns format validation. 34 | 35 | ## development 36 | 37 | See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following npm tasks. 38 | 39 | - `npm start` 40 | 41 | Start kibana and have it include this plugin 42 | 43 | - `npm start -- --config kibana.yml` 44 | 45 | You can pass any argument that you would normally send to `bin/kibana` by putting them after `--` when running `npm start` 46 | 47 | - `npm run build` 48 | 49 | Build a distributable archive 50 | 51 | - `npm run test:browser` 52 | 53 | Run the browser tests in a real web browser 54 | 55 | - `npm run test:server` 56 | 57 | Run the server tests using mocha 58 | 59 | For more information about any of these commands run `npm run ${task} -- --help`. 60 | -------------------------------------------------------------------------------- /TRANSLATION.md: -------------------------------------------------------------------------------- 1 | A Kibana translation plugin structure. 2 | 3 | The main goal is to keep the plugin extremely simple so non-technical translators will have no trouble 4 | creating new translations for Kibana. Everything except for the translations themselves can be generated 5 | automatically with some enhancements to the Kibana plugin generator. The generator would only need a 6 | plugin name and a list of one or more languages the user wants to create translations for. 7 | 8 | The plugin exports its translation file(s) on the server when it it starts up. This is achieved by publishing the files 9 | via 'uiExports'.This is configurable by modifying the 'translations' item in the 'uiExports'. 10 | 11 | Translation files are broken up by language and must have names that match IETF BCP 47 language codes. 12 | Each translation file contains a single flat object with translation strings matched to their unique keys. Keys are 13 | prefixed with plugin names and a dash to ensure uniqueness between plugins. A translation plugin is not restricted to 14 | providing translations only for itself, the provided translations can cover other plugins as well. 15 | 16 | For example, this template plugin shows how a third party plugin might provide spanish translations for the Kibana core "kibana" app, which is itself a separate plugin. 17 | 18 | To create a translation plugin using this template, follow these steps: 19 | 1. Generate the plugin using the generator 20 | 2. Add your translations files to /translations directory. Remove/Overwrite the existing translation file (i.e. 'es.json'). 21 | 3. Edit /index.js, updating the 'translations' item as per your plugin translations. 22 | 4. Restart the Kibana server to publish your plugin translations. 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export default function (kibana) { 4 | return new kibana.Plugin({ 5 | uiExports: { 6 | visTypes: [ 7 | 'plugins/computed-columns/computed-columns-vis' 8 | ] 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "computed-columns", 3 | "version": "0.7.1", 4 | "description": "This visualization plugin is like a table, but with computed columns", 5 | "main": "index.js", 6 | "kibana": { 7 | "version": "kibana" 8 | }, 9 | "scripts": { 10 | "lint": "eslint", 11 | "start": "plugin-helpers start", 12 | "test:server": "plugin-helpers test:server", 13 | "test:browser": "plugin-helpers test:browser", 14 | "build": "plugin-helpers build", 15 | "postinstall": "plugin-helpers postinstall" 16 | }, 17 | "devDependencies": { 18 | "@elastic/eslint-config-kibana": "0.0.2", 19 | "@elastic/plugin-helpers": "^6.0.0", 20 | "babel-eslint": "4.1.8", 21 | "chai": "^3.5.0", 22 | "eslint": "1.10.3", 23 | "eslint-plugin-mocha": "1.1.0" 24 | }, 25 | "dependencies": { 26 | "expr-eval": "^1.2.1" 27 | } 28 | } -------------------------------------------------------------------------------- /public/computed-columns-params.html: -------------------------------------------------------------------------------- 1 |
2 |
4 |
7 |
8 | 9 | 15 | 16 | 17 | 18 | Computed Column 19 | 20 | 21 | 22 | 23 | {{output.label || output.formula}} 24 | 25 | 26 | 27 |
28 | 38 | 39 | 47 | 48 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | Add column 80 |
81 |
82 |
83 | 84 |
85 | 86 | 87 |
88 | 89 |
90 | 91 | 92 |
93 | 94 |
95 | 99 |
100 | 101 |
102 | 106 |
107 | 108 |
109 | 113 |
114 | 115 |
116 | 120 |
121 | 122 |
123 | 127 |
128 | 129 |
130 | 131 | 136 |
137 | 138 |
139 | -------------------------------------------------------------------------------- /public/computed-columns-params.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { uiModules } from 'ui/modules'; 3 | 4 | const module = uiModules.get('kibana/computed-columns', ['kibana']); 5 | module.controller('ComputedColumnsParamsVisController', ($scope) => { 6 | $scope.totalAggregations = ['sum', 'avg', 'min', 'max', 'count']; 7 | $scope.$watchMulti(['vis.params.showPartialRows', 'vis.params.showMeticsAtAllLevels'], () => { 8 | if (!$scope.vis) { 9 | return; 10 | } 11 | 12 | const params = $scope.vis.params; 13 | if (params.showPartialRows || params.showMeticsAtAllLevels) { 14 | $scope.metricsAtAllLevels = true; 15 | } else { 16 | $scope.metricsAtAllLevels = false; 17 | } 18 | }); 19 | 20 | $scope.addComputedColumn = (computedColumns) => { 21 | computedColumns.push({ 22 | formula: 'col[0] * col[0]', 23 | label: 'Value squared', 24 | format: '0,0', 25 | enabled: true 26 | }); 27 | }; 28 | 29 | $scope.removeComputedColumn = (output, computedColumns) => { 30 | if (computedColumns.length === 1) { 31 | return; 32 | } 33 | const index = computedColumns.indexOf(output); 34 | if (index >= 0) { 35 | computedColumns.splice(index, 1); 36 | } 37 | 38 | if (computedColumns.length === 1) { 39 | computedColumns[0].enabled = true; 40 | } 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /public/computed-columns-vis-controller.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Parser } from 'expr-eval'; 3 | import numeral from 'numeral'; 4 | 5 | import { VisAggConfigProvider } from 'ui/vis/agg_config'; 6 | import AggConfigResult from 'ui/vis/agg_config_result'; 7 | import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify'; 8 | import { uiModules } from 'ui/modules'; 9 | 10 | const module = uiModules.get('kibana/computed-columns', ['kibana']); 11 | 12 | module.controller('ComputedColumnsVisController', ($scope, $element, Private) => { 13 | 14 | const tabifyAggResponse = Private(AggResponseTabifyProvider); 15 | const AggConfig = Private(VisAggConfigProvider); 16 | const uiStateSort = ($scope.uiState) ? $scope.uiState.get('vis.params.sort') : {}; 17 | _.assign($scope.vis.params.sort, uiStateSort); 18 | 19 | $scope.sort = $scope.vis.params.sort; 20 | $scope.$watchCollection('sort', (newSort) => { 21 | $scope.uiState.set('vis.params.sort', newSort); 22 | }); 23 | 24 | const createExpressionsParams = (formula, row) => { 25 | let regex = /col\[(\d+)\]/g; 26 | let myArray; 27 | let output = {}; 28 | while ((myArray = regex.exec(formula)) !== null) { 29 | output[`x${myArray[1]}`] = (typeof row[myArray[1]].value === 'number') ? 30 | numeral(row[myArray[1]].value).value() : row[myArray[1]].value; 31 | } 32 | return output; 33 | }; 34 | 35 | const createParser = (computedColumn) => { 36 | let expression = computedColumn.formula.replace(/col\[\d+\]/g, (value) => { 37 | let cleanValue = /(\d+)/.exec(value)[1]; 38 | return `x${cleanValue}`; 39 | }); 40 | return Parser.parse(expression); 41 | }; 42 | 43 | const createColumn = (computedColumn, index) => { 44 | let newColumn = {aggConfig: new AggConfig($scope.vis, {schema: 'metric', type: 'count'}), title: computedColumn.label}; 45 | newColumn.aggConfig.id = `1.computed-column-${index}`; 46 | newColumn.aggConfig.key = `computed-column-${index}`; 47 | return newColumn; 48 | }; 49 | 50 | const createRows = (column, rows, computedColumn) => { 51 | let parser = createParser(computedColumn); 52 | 53 | return _.map(rows, (row) => { 54 | let expressionParams = createExpressionsParams(computedColumn.formula, row); 55 | let value = parser.evaluate(expressionParams); 56 | let newCell = new AggConfigResult(column.aggConfig, void 0, value, value); 57 | 58 | newCell.toString = () => { 59 | return (typeof value === 'number') ? numeral(value).format(computedColumn.format) : value; 60 | }; 61 | row.push(newCell); 62 | return row; 63 | }); 64 | }; 65 | 66 | const createTables = (tables, computedColumn, index) => { 67 | _.forEach(tables, (table) => { 68 | if (table.tables) { 69 | createTables(table.tables, computedColumn, index); 70 | return; 71 | } 72 | 73 | let newColumn = createColumn(computedColumn, index); 74 | table.columns.push(newColumn); 75 | table.rows = createRows(newColumn, table.rows, computedColumn); 76 | }); 77 | }; 78 | 79 | const hideColumns = (tables, hiddenColumns) => { 80 | if (!hiddenColumns) { 81 | return; 82 | } 83 | 84 | let removedCounter = 0; 85 | _.forEach(hiddenColumns.split(','), (item) => { 86 | let index = item * 1; 87 | _.forEach(tables, (table) => { 88 | table.columns.splice(index - removedCounter, 1); 89 | _.forEach(table.rows, (row) => { 90 | row.splice(index - removedCounter, 1); 91 | }); 92 | }); 93 | removedCounter++; 94 | }); 95 | }; 96 | 97 | const shouldShowPagination = (tables, perPage) => { 98 | return tables.some(function (table) { 99 | if (table.tables) { 100 | return shouldShowPagination(table.tables, perPage); 101 | } 102 | else { 103 | return table.rows.length > perPage; 104 | } 105 | }); 106 | }; 107 | 108 | $scope.sort = $scope.vis.params.sort; 109 | $scope.$watchCollection('sort', (newSort) => { 110 | $scope.uiState.set('vis.params.sort', newSort); 111 | }); 112 | 113 | $scope.$watchMulti(['esResponse', 'vis.params'], ([resp]) => { 114 | let tableGroups = $scope.tableGroups = null; 115 | let hasSomeRows = $scope.hasSomeRows = null; 116 | let computedColumns = $scope.vis.params.computedColumns; 117 | let hiddenColumns = $scope.vis.params.hiddenColumns; 118 | 119 | if (resp) { 120 | const vis = $scope.vis; 121 | const params = vis.params; 122 | 123 | tableGroups = tabifyAggResponse(vis, resp, { 124 | partialRows: params.showPartialRows, 125 | minimalColumns: vis.isHierarchical() && !params.showMeticsAtAllLevels, 126 | asAggConfigResults: true 127 | }); 128 | 129 | _.forEach(computedColumns, (computedColumn, index) => { 130 | createTables(tableGroups.tables, computedColumn, index); 131 | }); 132 | 133 | hideColumns(tableGroups.tables, hiddenColumns); 134 | 135 | hasSomeRows = tableGroups.tables.some(function haveRows(table) { 136 | if (table.tables) return table.tables.some(haveRows); 137 | return table.rows.length > 0; 138 | }); 139 | 140 | $scope.tableVisContainerClass = { 141 | 'hide-pagination': !shouldShowPagination, 142 | 'hide-export-links': params.hideExportLinks 143 | }; 144 | 145 | $scope.renderComplete(); 146 | } 147 | 148 | $scope.hasSomeRows = hasSomeRows; 149 | if (hasSomeRows) { 150 | $scope.tableGroups = tableGroups; 151 | } 152 | }); 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /public/computed-columns-vis.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

No results found

5 |
6 | 7 |
8 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /public/computed-columns-vis.js: -------------------------------------------------------------------------------- 1 | import 'plugins/computed-columns/computed-columns.less'; 2 | import 'plugins/computed-columns/computed-columns-vis-controller'; 3 | import 'plugins/computed-columns/computed-columns-params'; 4 | import mainTemplate from 'plugins/computed-columns/computed-columns-vis.html'; 5 | import optionsTemplate from 'plugins/computed-columns/computed-columns-params.html'; 6 | 7 | import {CATEGORY} from 'ui/vis/vis_category'; 8 | import {VisFactoryProvider} from 'ui/vis/vis_factory'; 9 | import {VisTypesRegistryProvider} from 'ui/registry/vis_types'; 10 | import {VisSchemasProvider} from 'ui/vis/editors/default/schemas'; 11 | 12 | import image from './images/icon-table.svg'; 13 | 14 | VisTypesRegistryProvider.register(ExtendedMetricVisProvider); 15 | 16 | function ExtendedMetricVisProvider(Private) { 17 | const VisFactory = Private(VisFactoryProvider); 18 | const Schemas = Private(VisSchemasProvider); 19 | 20 | return VisFactory.createAngularVisualization({ 21 | name: 'computed-columns', 22 | title: 'Computed Cols', 23 | icon: 'fa-table', 24 | description: 'Same functionality than Data Table, but after data processing, computed columns can be added with math expressions.', 25 | category: CATEGORY.DATA, 26 | responseHandler: 'none', 27 | visConfig: { 28 | defaults: { 29 | perPage: 10, 30 | showPartialRows: false, 31 | showMeticsAtAllLevels: false, 32 | sort: { 33 | columnIndex: null, 34 | direction: null 35 | }, 36 | showTotal: false, 37 | totalFunc: 'sum', 38 | computedColumns: [{ 39 | formula: 'col[0] * col[0]', 40 | label: 'Value squared', 41 | format: '0,0.[00]', 42 | enabled: true 43 | }], 44 | hideExportLinks: false 45 | }, 46 | template: mainTemplate, 47 | }, 48 | editorConfig: { 49 | optionsTemplate: optionsTemplate, 50 | schemas: new Schemas([{ 51 | group: 'metrics', 52 | name: 'metric', 53 | title: 'Metric', 54 | min: 1, 55 | }, { 56 | group: 'buckets', 57 | name: 'bucket', 58 | title: 'Split Rows' 59 | }, { 60 | group: 'buckets', 61 | name: 'split', 62 | title: 'Split Table' 63 | }]), 64 | } 65 | }); 66 | } 67 | 68 | export default ExtendedMetricVisProvider; -------------------------------------------------------------------------------- /public/computed-columns.less: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 30px; 3 | } 4 | 5 | .computed-columns-options-container { 6 | margin-bottom: 20px; 7 | } 8 | 9 | kbn-agg-table-group .table { 10 | margin-bottom: 0px; 11 | } 12 | 13 | .hide-export-links .agg-table-controls { 14 | display: none; 15 | } 16 | 17 | .hide-pagination paginate-controls { 18 | display: none; 19 | } 20 | -------------------------------------------------------------------------------- /public/images/icon-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon-table 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /run-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ELASTICSEARCH_NAME=elasticsearch 6 | ELASTICSEARCH_VERSION=5.5-alpine 7 | 8 | ELASTICSEARCH_STATUS=$(docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep $ELASTICSEARCH_NAME | awk '{print $2}') 9 | 10 | echo "ELASTICSEARCH_STATUS: $ELASTICSEARCH_STATUS" 11 | 12 | function runElasticsearch() { 13 | docker run -d -p 9200:9200 --name $ELASTICSEARCH_NAME $ELASTICSEARCH_NAME:$ELASTICSEARCH_VERSION 14 | sleep 5 15 | curl -XPOST localhost:9200/dummy_index/dummy_type -d '{"key1":100,"key2":"dummy"}' -H "Content-Type:application/json" 16 | } 17 | 18 | 19 | if [ -z $ELASTICSEARCH_STATUS ] 20 | then 21 | runElasticsearch 22 | echo "Elasticsearch is running" 23 | else 24 | echo "Elasticsearch is already running" 25 | fi 26 | -------------------------------------------------------------------------------- /server/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('suite', () => { 4 | it('is a test test', () => { 5 | expect(true).to.equal(false); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /server/routes/example.js: -------------------------------------------------------------------------------- 1 | export default function (server) { 2 | 3 | server.route({ 4 | path: '/api/ratio/example', 5 | method: 'GET', 6 | handler(req, reply) { 7 | reply({ time: (new Date()).toISOString() }); 8 | } 9 | }); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "UI-WELCOME_MESSAGE": "Cargando Kibana", 3 | "UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Mira la salida del servidor para obtener más información." 4 | } 5 | --------------------------------------------------------------------------------