mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
use the visualize config panels to create the vis data-structure
This commit is contained in:
parent
f28ca585e3
commit
3eb84d1866
16 changed files with 555 additions and 263 deletions
|
@ -17,30 +17,7 @@ define(function (require) {
|
|||
location: 'Visualize Controller'
|
||||
});
|
||||
|
||||
var vis = $scope.vis = window.vis = new Vis({
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
max: 1
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
max: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// the object detailing the visualization
|
||||
// // the object detailing the visualization
|
||||
// var vis = $scope.vis = window.vis = new Vis({
|
||||
// config: {
|
||||
// metric: {
|
||||
|
@ -55,43 +32,68 @@ define(function (require) {
|
|||
// },
|
||||
// group: {
|
||||
// label: 'Color',
|
||||
// max: 10
|
||||
// max: 1
|
||||
// },
|
||||
// split: {
|
||||
// label: 'Rows & Columns',
|
||||
// max: 2
|
||||
// }
|
||||
// },
|
||||
// state: {
|
||||
// split: [
|
||||
// {
|
||||
// field: 'response',
|
||||
// size: 5,
|
||||
// agg: 'terms'
|
||||
// },
|
||||
// {
|
||||
// field: '_type',
|
||||
// size: 5,
|
||||
// agg: 'terms'
|
||||
// }
|
||||
// ],
|
||||
// segment: [
|
||||
// {
|
||||
// field: '@timestamp',
|
||||
// interval: 'week'
|
||||
// }
|
||||
// ],
|
||||
// group: [
|
||||
// {
|
||||
// field: 'extension',
|
||||
// size: 5,
|
||||
// agg: 'terms',
|
||||
// global: true
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// });
|
||||
|
||||
var vis = $scope.vis = window.vis = new Vis({
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
max: 10
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
max: Infinity
|
||||
}
|
||||
},
|
||||
state: {
|
||||
split: [
|
||||
{
|
||||
field: '_type',
|
||||
size: 5,
|
||||
agg: 'terms',
|
||||
row: false
|
||||
},
|
||||
{
|
||||
field: 'response',
|
||||
size: 5,
|
||||
agg: 'terms',
|
||||
row: true
|
||||
}
|
||||
],
|
||||
segment: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
interval: 'day'
|
||||
}
|
||||
],
|
||||
group: [
|
||||
{
|
||||
field: 'extension',
|
||||
size: 5,
|
||||
agg: 'terms',
|
||||
global: true
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
vis.dataSource.$scope($scope);
|
||||
|
||||
$scope.refreshFields = function () {
|
||||
|
@ -125,7 +127,10 @@ define(function (require) {
|
|||
|
||||
$scope.updateDataSource = function () {
|
||||
notify.event('update data source');
|
||||
var config = _.groupBy(vis.getConfig(), function (config) {
|
||||
|
||||
var config = vis.getConfig();
|
||||
|
||||
config = _.groupBy(config, function (config) {
|
||||
switch (config.categoryName) {
|
||||
case 'group':
|
||||
case 'segment':
|
||||
|
|
|
@ -16,6 +16,21 @@ define(function (require) {
|
|||
template: html,
|
||||
link: function ($scope, $el) {
|
||||
$scope.category = $scope.vis[$scope.categoryName];
|
||||
|
||||
$scope.moveHandler = function (config, delta) {
|
||||
var configs = $scope.category.configs;
|
||||
var i = configs.indexOf(config);
|
||||
if (delta === false) {
|
||||
// means remove
|
||||
configs.splice(i, 1);
|
||||
} else {
|
||||
// move to a new position (iTarget)
|
||||
var iTarget = Math.max(0, Math.min(configs.length - 1, i + delta));
|
||||
if (i !== iTarget) {
|
||||
configs.splice(iTarget, 0, configs.splice(i, 1).pop());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ define(function (require) {
|
|||
require('filters/field_type');
|
||||
|
||||
app.directive('visConfigEditor', function ($compile, Vis, Aggs) {
|
||||
var headerHtml = require('text!../partials/editor/header.html');
|
||||
|
||||
var categoryOptions = {
|
||||
metric: {
|
||||
template: require('text!../partials/editor/metric.html')
|
||||
|
@ -24,7 +26,7 @@ define(function (require) {
|
|||
}
|
||||
};
|
||||
|
||||
var controlTemplates = {
|
||||
var controlHtml = {
|
||||
orderAndSize: require('text!../partials/controls/order_and_size.html'),
|
||||
interval: require('text!../partials/controls/interval.html'),
|
||||
globalLocal: require('text!../partials/controls/global_local.html')
|
||||
|
@ -87,16 +89,16 @@ define(function (require) {
|
|||
});
|
||||
|
||||
if (params.order && params.size) {
|
||||
controlsHtml += ' ' + controlTemplates.orderAndSize;
|
||||
controlsHtml += ' ' + controlHtml.orderAndSize;
|
||||
}
|
||||
|
||||
if (params.interval) {
|
||||
controlsHtml += ' ' + controlTemplates.interval;
|
||||
controlsHtml += ' ' + controlHtml.interval;
|
||||
if (!controlsHtml.match(/aggParams\.interval\.options/)) ; //debugger;
|
||||
}
|
||||
|
||||
if ($scope.config.categoryName === 'group') {
|
||||
controlsHtml += ' ' + controlTemplates.globalLocal;
|
||||
controlsHtml += ' ' + controlHtml.globalLocal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,8 +110,10 @@ define(function (require) {
|
|||
restrict: 'E',
|
||||
scope: {
|
||||
config: '=',
|
||||
category: '=',
|
||||
fields: '=',
|
||||
vis: '='
|
||||
vis: '=',
|
||||
move: '='
|
||||
},
|
||||
link: function ($scope, $el, attr) {
|
||||
var categoryName = $scope.config.categoryName;
|
||||
|
@ -119,7 +123,7 @@ define(function (require) {
|
|||
$scope.Vis = Vis;
|
||||
|
||||
// attach a copy of the template to the scope and render
|
||||
$el.html($compile(opts.template)($scope));
|
||||
$el.html($compile(headerHtml + '\n' + opts.template)($scope));
|
||||
|
||||
_.defaults($scope.val, opts.defVal || {});
|
||||
if (opts.setup) opts.setup($scope, $el);
|
||||
|
|
|
@ -15,15 +15,13 @@ define(function (require) {
|
|||
vis
|
||||
.dataSource
|
||||
.on('results', function (resp) {
|
||||
$scope.results = vis.buildChartDataFromResponse(resp).groups;
|
||||
$scope.results = vis.buildChartDataFromResponse(resp);
|
||||
});
|
||||
|
||||
if (!vis.dataSource._$scope) {
|
||||
// only link if the dataSource isn't already linked
|
||||
vis.dataSource.$scope($scope);
|
||||
}
|
||||
|
||||
vis.dataSource.fetch();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,12 +20,8 @@ define(function (require) {
|
|||
// the dataSource that will populate the
|
||||
this.dataSource = $rootScope.rootDataSource.extend().size(0);
|
||||
|
||||
// master list of configs, addConfig() writes here and to the list within each
|
||||
// config category, removeConfig() does the inverse
|
||||
this.configs = [];
|
||||
|
||||
// setup each config category
|
||||
Vis.configCategories.forEach(function (category) {
|
||||
Vis.configCategoriesInFetchOrder.forEach(function (category) {
|
||||
var myCat = _.defaults(config[category.name] || {}, category.defaults);
|
||||
myCat.configs = [];
|
||||
this[category.name] = myCat;
|
||||
|
@ -42,6 +38,8 @@ define(function (require) {
|
|||
Vis.configCategories = [
|
||||
{
|
||||
name: 'segment',
|
||||
displayOrder: 2,
|
||||
fetchOrder: 1,
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: Infinity
|
||||
|
@ -52,6 +50,8 @@ define(function (require) {
|
|||
},
|
||||
{
|
||||
name: 'metric',
|
||||
displayOrder: 1,
|
||||
fetchOrder: 2,
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 1
|
||||
|
@ -62,6 +62,8 @@ define(function (require) {
|
|||
},
|
||||
{
|
||||
name: 'group',
|
||||
displayOrder: 3,
|
||||
fetchOrder: 3,
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 1
|
||||
|
@ -73,15 +75,20 @@ define(function (require) {
|
|||
},
|
||||
{
|
||||
name: 'split',
|
||||
displayOrder: 4,
|
||||
fetchOrder: 4,
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 2
|
||||
},
|
||||
configDefaults: {
|
||||
size: 5
|
||||
size: 5,
|
||||
row: true
|
||||
}
|
||||
}
|
||||
];
|
||||
Vis.configCategoriesInFetchOrder = _.sortBy(Vis.configCategories, 'fetchOrder');
|
||||
Vis.configCategoriesInDisplayOrder = _.sortBy(Vis.configCategories, 'displayOrder');
|
||||
Vis.configCategoriesByName = _.indexBy(Vis.configCategories, 'name');
|
||||
|
||||
Vis.prototype.addConfig = function (categoryName) {
|
||||
|
@ -89,7 +96,6 @@ define(function (require) {
|
|||
var config = _.defaults({}, category.configDefaults);
|
||||
config.categoryName = category.name;
|
||||
|
||||
this.configs.push(config);
|
||||
this[category.name].configs.push(config);
|
||||
|
||||
return config;
|
||||
|
@ -98,20 +104,19 @@ define(function (require) {
|
|||
Vis.prototype.removeConfig = function (config) {
|
||||
if (!config) return;
|
||||
|
||||
_.pull(this.configs, config);
|
||||
_.pull(this[config.categoryName].configs, config);
|
||||
};
|
||||
|
||||
Vis.prototype.configCounts = {};
|
||||
|
||||
Vis.prototype.setState = function (state) {
|
||||
var vis = this;
|
||||
|
||||
vis.dataSource.getFields(function (fields) {
|
||||
vis.configs = [];
|
||||
|
||||
_.each(state, function (categoryStates, configCategoryName) {
|
||||
if (!vis[configCategoryName]) return;
|
||||
|
||||
vis[configCategoryName].configs = [];
|
||||
vis[configCategoryName].configs.splice(0);
|
||||
|
||||
categoryStates.forEach(function (configState) {
|
||||
var config = vis.addConfig(configCategoryName);
|
||||
|
@ -127,7 +132,7 @@ define(function (require) {
|
|||
var vis = this;
|
||||
|
||||
// satify the min count for each category
|
||||
Vis.configCategories.forEach(function (category) {
|
||||
Vis.configCategoriesInFetchOrder.forEach(function (category) {
|
||||
var myCat = vis[category.name];
|
||||
if (myCat.configs.length < myCat.min) {
|
||||
_.times(myCat.min - myCat.configs.length, function () {
|
||||
|
@ -144,7 +149,8 @@ define(function (require) {
|
|||
* @return {Array} - The list of config objects
|
||||
*/
|
||||
Vis.prototype.getConfig = function () {
|
||||
var cats = {
|
||||
var vis = this;
|
||||
var positions = {
|
||||
split: [],
|
||||
global: [],
|
||||
segment: [],
|
||||
|
@ -152,43 +158,67 @@ define(function (require) {
|
|||
metric: []
|
||||
};
|
||||
|
||||
this.configs.forEach(function (config) {
|
||||
var pos = config.categoryName;
|
||||
if (pos === 'group') pos = config.global ? 'global' : 'local';
|
||||
function moveValidatedParam(config, params, paramDef, name) {
|
||||
if (!config[name]) return false;
|
||||
if (!paramDef.custom && paramDef.options && !_.find(paramDef.options, { val: config[name] })) return false;
|
||||
|
||||
if (!config.field || !config.agg) return;
|
||||
// copy over the param
|
||||
params[name] = config[name];
|
||||
|
||||
// provide a hook to covert string values into more complex structures
|
||||
if (paramDef.toJSON) {
|
||||
params[name] = paramDef.toJSON(params[name]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function readAndValidate(config) {
|
||||
// filter out plain unusable configs
|
||||
if (!config || !config.agg || !config.field) return;
|
||||
|
||||
// get the agg used by this config
|
||||
var agg = Aggs.aggsByName[config.agg];
|
||||
if (!agg || agg.name === 'count') return;
|
||||
|
||||
var params = {
|
||||
categoryName: config.categoryName,
|
||||
agg: config.agg,
|
||||
aggParams: {
|
||||
field: config.field
|
||||
}
|
||||
// copy parts of the config to the "validated" config object
|
||||
var validated = _.pick(config, 'categoryName', 'agg');
|
||||
validated.aggParams = {
|
||||
field: config.field
|
||||
};
|
||||
|
||||
// copy over the row if this is a split
|
||||
if (config.categoryName === 'split') {
|
||||
validated.row = !!config.row;
|
||||
}
|
||||
|
||||
// this function will move valus from config.* to validated.aggParams.* when they are
|
||||
// needed for that aggregation, and return true or false based on if all requirements
|
||||
// are meet
|
||||
var moveToAggParams = _.partial(moveValidatedParam, config, validated.aggParams);
|
||||
|
||||
// ensure that all of the declared params for the agg are declared on the config
|
||||
var valid = _.every(agg.params, function (paramDef, name) {
|
||||
if (!config[name]) return;
|
||||
if (!paramDef.custom && paramDef.options && !_.find(paramDef.options, { val: config[name] })) return;
|
||||
if (_.every(agg.params, moveToAggParams)) return validated;
|
||||
}
|
||||
|
||||
// copy over the param
|
||||
params.aggParams[name] = config[name];
|
||||
// collect all of the configs from each category,
|
||||
// validate them, filter the invalid ones, and put them into positions
|
||||
Vis.configCategoriesInFetchOrder.forEach(function (category) {
|
||||
var configs = vis[category.name].configs;
|
||||
|
||||
// allow provide a hook to covert string values into more complex structures
|
||||
if (paramDef.toJSON) {
|
||||
params.aggParams[name] = paramDef.toJSON(params.aggParams[name]);
|
||||
}
|
||||
configs = configs
|
||||
.map(readAndValidate)
|
||||
.filter(Boolean);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (valid) cats[pos].push(params);
|
||||
if (category.name === 'group') {
|
||||
positions.global = _.where(configs, { global: true });
|
||||
positions.local = _.where(configs, { global: false });
|
||||
} else {
|
||||
positions[category.name] = configs;
|
||||
}
|
||||
});
|
||||
|
||||
return cats.split.concat(cats.global, cats.segment, cats.local, cats.metric);
|
||||
return positions.split.concat(positions.global, positions.segment, positions.local, positions.metric);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -199,103 +229,155 @@ define(function (require) {
|
|||
Vis.prototype.buildChartDataFromResponse = function (resp) {
|
||||
notify.event('convert ES response');
|
||||
|
||||
function createGroup(bucket) {
|
||||
var g = {};
|
||||
if (bucket) g.key = bucket.key;
|
||||
return g;
|
||||
}
|
||||
|
||||
function finishRow(bucket) {
|
||||
// collect the count and bail, free metric!!
|
||||
level.rows.push(row.concat(bucket.value === void 0 ? bucket.doc_count : bucket.value));
|
||||
}
|
||||
|
||||
// all aggregations will be prefixed with:
|
||||
var aggKeyPrefix = '_agg_';
|
||||
|
||||
// this will transform our flattened rows and columns into the
|
||||
// data structure expected for a visualization
|
||||
var converter = converters[this.type];
|
||||
|
||||
// as we move into the different aggs, shift configs
|
||||
var childConfigs = this.getConfig();
|
||||
var lastCol = childConfigs[childConfigs.length - 1];
|
||||
// the list of configs that make up the aggs and eventually
|
||||
// splits and columns, label added
|
||||
var configs = this.getConfig().map(function (col) {
|
||||
var agg = Aggs.aggsByName[col.agg];
|
||||
if (!agg) {
|
||||
col.label = col.agg;
|
||||
} else if (agg.makeLabel) {
|
||||
col.label = Aggs.aggsByName[col.agg].makeLabel(col.aggParams);
|
||||
} else {
|
||||
col.label = agg.display || agg.name;
|
||||
}
|
||||
return col;
|
||||
});
|
||||
|
||||
// into stack, and then back when we leave a level
|
||||
var stack = [];
|
||||
var row = [];
|
||||
var lastCol = configs[configs.length - 1];
|
||||
|
||||
var chartData = createGroup();
|
||||
var level = chartData;
|
||||
// column stack, when we are deap within recursion this
|
||||
// will hold the previous columns
|
||||
var colStack = [];
|
||||
|
||||
// row stack, similar to the colStack but tracks unfinished rows
|
||||
var rowStack = [];
|
||||
|
||||
// all chart data will be written here or to child chartData
|
||||
// formatted objects
|
||||
var chartData = {};
|
||||
|
||||
var writeRow = function (rows, bucket) {
|
||||
// collect the count and bail, free metric!!
|
||||
rows.push(rowStack.concat(bucket.value === void 0 ? bucket.doc_count : bucket.value));
|
||||
};
|
||||
|
||||
var writeChart = function (chart) {
|
||||
var rows = chart.rows;
|
||||
var cols = chart.columns;
|
||||
delete chart.rows;
|
||||
delete chart.columns;
|
||||
|
||||
converter(chart, cols, rows);
|
||||
};
|
||||
|
||||
var getAggKey = function (bucket) {
|
||||
return Object.keys(bucket)
|
||||
.filter(function (key) {
|
||||
return key.substr(0, aggKeyPrefix.length) === aggKeyPrefix;
|
||||
})
|
||||
.pop();
|
||||
};
|
||||
|
||||
var splitAndFlatten = function (chartData, bucket) {
|
||||
// pull the next column from the configs list
|
||||
var col = configs.shift();
|
||||
|
||||
(function splitAndFlatten(bucket) {
|
||||
var col = childConfigs.shift();
|
||||
// add it to the top of the stack
|
||||
stack.unshift(col);
|
||||
colStack.unshift(col);
|
||||
|
||||
_.forOwn(bucket, function (result, key) {
|
||||
// filter out the non prefixed keys
|
||||
if (key.substr(0, aggKeyPrefix.length) !== aggKeyPrefix) return;
|
||||
// the actual results for the aggregation is under an _agg_* key
|
||||
var result = bucket[getAggKey(bucket)];
|
||||
|
||||
if (col.categoryName === 'split') {
|
||||
var parent = level;
|
||||
result.buckets.forEach(function (bucket) {
|
||||
var group = createGroup(bucket);
|
||||
switch (col.categoryName) {
|
||||
case 'split':
|
||||
// pick the key for the split's groups
|
||||
var groupsKey = col.row ? 'rows' : 'columns';
|
||||
|
||||
if (parent.groups) parent.groups.push(group);
|
||||
else parent.groups = [group];
|
||||
// the groups will be written here
|
||||
chartData[groupsKey] = [];
|
||||
|
||||
level = group;
|
||||
splitAndFlatten(bucket);
|
||||
if (group.rows && group.columns) {
|
||||
group.data = converter(group.columns, group.rows);
|
||||
delete group.rows;
|
||||
delete group.columns;
|
||||
}
|
||||
});
|
||||
result.buckets.forEach(function (bucket) {
|
||||
// create a new group for each bucket
|
||||
var group = {
|
||||
label: col.label
|
||||
};
|
||||
chartData[groupsKey].push(group);
|
||||
|
||||
level = parent;
|
||||
return;
|
||||
}
|
||||
// down the rabbit hole
|
||||
splitAndFlatten(group, bucket);
|
||||
|
||||
if (!level.columns || !level.rows) {
|
||||
// setup this level to receive records
|
||||
level.columns = [stack[0]].concat(childConfigs);
|
||||
level.rows = [];
|
||||
// flattening this bucket caused a chart to be created
|
||||
// convert the rows and columns into a legit chart
|
||||
if (group.rows && group.columns) writeChart(group);
|
||||
});
|
||||
break;
|
||||
case 'group':
|
||||
case 'segment':
|
||||
case 'metric':
|
||||
// this column represents actual chart data
|
||||
if (!chartData.columns || !chartData.rows) {
|
||||
// copy the current column and remaining columns into the column list
|
||||
chartData.columns = [col].concat(configs);
|
||||
|
||||
// the columns might now end in a metric, but the rows will
|
||||
if (childConfigs[childConfigs.length - 1].categoryName !== 'metric') {
|
||||
level.columns.push({
|
||||
// write rows here
|
||||
chartData.rows = [];
|
||||
|
||||
// if the columns don't end in a metric then we will be
|
||||
// pulling the count of the final bucket as the metric.
|
||||
// Ensure that there is a column for this data
|
||||
if (chartData.columns[chartData.columns.length - 1].categoryName !== 'metric') {
|
||||
chartData.columns.push({
|
||||
categoryName: 'metric',
|
||||
agg: 'count'
|
||||
agg: Aggs.aggsByName.count.name,
|
||||
label: Aggs.aggsByName.count.display
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (col.categoryName === 'metric') {
|
||||
// one row per bucket
|
||||
finishRow(result);
|
||||
// there are no buckets, just values to collect.
|
||||
// Write the the row to the chartData
|
||||
writeRow(chartData.rows, result);
|
||||
} else {
|
||||
// keep digging
|
||||
// non-metric aggs create buckets that we need to add
|
||||
// to the rows
|
||||
result.buckets.forEach(function (bucket) {
|
||||
// track this bucket's "value" in our temporary row
|
||||
row.push(bucket.key);
|
||||
rowStack.push(bucket.key);
|
||||
|
||||
if (col === lastCol) {
|
||||
// also grab the bucket's count
|
||||
finishRow(bucket);
|
||||
// since this is the last column, there is no metric (otherwise it
|
||||
// would be last) so write the row into the chart data
|
||||
writeRow(chartData.rows, bucket);
|
||||
} else {
|
||||
splitAndFlatten(bucket);
|
||||
// into the rabbit hole
|
||||
splitAndFlatten(chartData, bucket);
|
||||
}
|
||||
|
||||
row.pop();
|
||||
rowStack.pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
childConfigs.unshift(stack.shift());
|
||||
})(resp.aggregations);
|
||||
configs.unshift(colStack.shift());
|
||||
};
|
||||
|
||||
if (resp.aggregations) {
|
||||
splitAndFlatten(chartData, resp.aggregations);
|
||||
}
|
||||
|
||||
// flattening the chart does not always result in a split,
|
||||
// so we need to check for a chart before we return
|
||||
if (chartData.rows && chartData.columns) writeChart(chartData);
|
||||
|
||||
notify.event('convert ES response', true);
|
||||
|
||||
|
||||
return chartData;
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-md-3">
|
||||
<form ng-submit="updateDataSource()">
|
||||
<ul class="vis-config-panel">
|
||||
<li ng-repeat="category in Vis.configCategories">
|
||||
<li ng-repeat="category in Vis.configCategoriesInDisplayOrder">
|
||||
<vis-config-category
|
||||
category-name="category.name"
|
||||
vis="vis"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<div class="panel-heading" >
|
||||
{{ category.label }}
|
||||
<button
|
||||
type="button"
|
||||
ng-if="category.configs.length < category.max"
|
||||
ng-click="vis.addConfig(categoryName)"
|
||||
class="btn btn-default" >
|
||||
|
@ -11,12 +12,13 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div ng-repeat="config in category.configs">
|
||||
<vis-config-editor
|
||||
config="config"
|
||||
vis="vis"
|
||||
fields="fields">
|
||||
</vis-config-editor>
|
||||
</div>
|
||||
<vis-config-editor
|
||||
ng-repeat="config in category.configs"
|
||||
config="config"
|
||||
category="category"
|
||||
vis="vis"
|
||||
fields="fields"
|
||||
move="moveHandler">
|
||||
</vis-config-editor>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<div class="btn-group pull-right">
|
||||
<div class="form-group btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
<table class="agg-config-interval">
|
||||
<tr>
|
||||
<td>
|
||||
<select
|
||||
ng-model="config.interval"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.interval.options"
|
||||
class="form-control"
|
||||
name="interval">
|
||||
</select>
|
||||
</td>
|
||||
<!-- <td ng-if="config.interval === 'customInterval'">
|
||||
<input
|
||||
ng-model="config.customInterval"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="custom interval">
|
||||
</td>
|
||||
<td ng-if="config.interval === 'customInterval'">
|
||||
<kbn-info info="'interval bewteen timestamp measurements, in milliseconds'"></kbn-info>
|
||||
</td> -->
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="agg-config-interval">
|
||||
<tr>
|
||||
<td>
|
||||
<select
|
||||
ng-model="config.interval"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.interval.options"
|
||||
class="form-control"
|
||||
name="interval">
|
||||
</select>
|
||||
</td>
|
||||
<!-- <td ng-if="config.interval === 'customInterval'">
|
||||
<input
|
||||
ng-model="config.customInterval"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="custom interval">
|
||||
</td>
|
||||
<td ng-if="config.interval === 'customInterval'">
|
||||
<kbn-info info="'interval bewteen timestamp measurements, in milliseconds'"></kbn-info>
|
||||
</td> -->
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<select
|
||||
class="form-control"
|
||||
name="order"
|
||||
ng-model="config.order"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.order.options">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="number" ng-model="config.size" name="size">
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<select
|
||||
class="form-control"
|
||||
name="order"
|
||||
ng-model="config.order"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.order.options">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="number" ng-model="config.size" name="size">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -12,8 +12,27 @@
|
|||
<div class="form-group" ng-show="availableAggs">
|
||||
<select
|
||||
name="agg"
|
||||
class="form-control"
|
||||
ng-model="config.agg"
|
||||
ng-options="agg.name as agg.display for agg in availableAggs">
|
||||
</select>
|
||||
</div>
|
||||
<div class="agg-param-controls">
|
||||
<div class="agg-param-controls"></div>
|
||||
<div ng-if="config.categoryName === 'split'" class="form-group">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-model="config.row"
|
||||
btn-radio="true">
|
||||
Row
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-model="config.row"
|
||||
btn-radio="false">
|
||||
Column
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
25
src/kibana/apps/visualize/partials/editor/header.html
Normal file
25
src/kibana/apps/visualize/partials/editor/header.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class="config-controls">
|
||||
<button
|
||||
ng-if="category.configs.length > 1"
|
||||
ng-click="move(config, -1)"
|
||||
type="button"
|
||||
ng-disabled="category.configs.indexOf(config) < 1"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-caret-up"></i>
|
||||
</button>
|
||||
<button
|
||||
ng-if="category.configs.length > 1"
|
||||
ng-click="move(config, 1)"
|
||||
type="button"
|
||||
ng-disabled="category.configs.indexOf(config) >= category.configs.length - 1"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</button>
|
||||
<button
|
||||
ng-if="category.configs.length > category.min"
|
||||
ng-click="move(config, false)"
|
||||
type="button"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
|
@ -1,35 +1,46 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
|
||||
return function (columns, rows) {
|
||||
var serieses = [];
|
||||
var seriesesByLabel = {};
|
||||
|
||||
return function (chart, columns, rows) {
|
||||
// index of color
|
||||
var iColor = _.findIndex(columns, { categoryName: 'group' });
|
||||
var hasColor = iColor !== -1;
|
||||
|
||||
// index of x-axis
|
||||
var iX = _.findIndex(columns, { categoryName: 'segment'});
|
||||
// index of y-axis
|
||||
var iY = _.findIndex(columns, { categoryName: 'metric'});
|
||||
|
||||
rows.forEach(function (row) {
|
||||
var series = seriesesByLabel[iColor === -1 ? 'undefined' : row[iColor]];
|
||||
chart.xAxisLabel = columns[iX].label;
|
||||
chart.yAxisLabel = columns[iY].label;
|
||||
|
||||
if (!series) {
|
||||
series = {
|
||||
label: '' + row[iColor],
|
||||
children: []
|
||||
};
|
||||
serieses.push(series);
|
||||
seriesesByLabel[series.label] = series;
|
||||
var series = chart.series = [];
|
||||
var seriesByLabel = {};
|
||||
|
||||
rows.forEach(function (row) {
|
||||
var seriesLabel = hasColor && row[iColor];
|
||||
var s = hasColor ? seriesByLabel[seriesLabel] : series[0];
|
||||
|
||||
if (!s) {
|
||||
// I know this could be simplified but I wanted to keep the key order
|
||||
if (hasColor) {
|
||||
s = {
|
||||
label: seriesLabel,
|
||||
values: []
|
||||
};
|
||||
seriesByLabel[seriesLabel] = s;
|
||||
} else {
|
||||
s = {
|
||||
values: []
|
||||
};
|
||||
}
|
||||
series.push(s);
|
||||
}
|
||||
|
||||
series.children.push([
|
||||
row[iX], // x-axis value
|
||||
row[iY === -1 ? row.length - 1 : iY] // y-axis value
|
||||
]);
|
||||
s.values.push({
|
||||
x: row[iX],
|
||||
y: row[iY === -1 ? row.length - 1 : iY] // y-axis value
|
||||
});
|
||||
});
|
||||
|
||||
return serieses;
|
||||
};
|
||||
});
|
|
@ -3,7 +3,9 @@ define(function (require) {
|
|||
require('utils/mixins');
|
||||
var _ = require('lodash');
|
||||
|
||||
function AggsService() {
|
||||
var app = require('modules').get('app/visualize');
|
||||
|
||||
app.service('Aggs', function () {
|
||||
this.metricAggs = [
|
||||
{
|
||||
name: 'count',
|
||||
|
@ -29,23 +31,26 @@ define(function (require) {
|
|||
this.metricAggsByName = _.indexBy(this.metricAggs, 'name');
|
||||
|
||||
this.bucketAggs = [
|
||||
{
|
||||
name: 'histogram',
|
||||
display: 'Histogram',
|
||||
params: {
|
||||
size: {},
|
||||
order: {
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
default: 'desc',
|
||||
toJSON: function (val) {
|
||||
return { _count: val };
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// {
|
||||
// name: 'histogram',
|
||||
// display: 'Histogram',
|
||||
// params: {
|
||||
// size: {},
|
||||
// order: {
|
||||
// options: [
|
||||
// { display: 'Top', val: 'desc' },
|
||||
// { display: 'Bottom', val: 'asc' }
|
||||
// ],
|
||||
// default: 'desc',
|
||||
// toJSON: function (val) {
|
||||
// return { _count: val };
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// makeLabel: function (params) {
|
||||
|
||||
// }
|
||||
// },
|
||||
{
|
||||
name: 'terms',
|
||||
display: 'Terms',
|
||||
|
@ -61,6 +66,10 @@ define(function (require) {
|
|||
return { _count: val };
|
||||
}
|
||||
}
|
||||
},
|
||||
makeLabel: function (params) {
|
||||
var order = _.find(this.params.order.options, { val: params.order._count });
|
||||
return order.display + ' ' + params.size + ' ' + params.field;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -78,6 +87,10 @@ define(function (require) {
|
|||
],
|
||||
default: 'hour'
|
||||
},
|
||||
},
|
||||
makeLabel: function (params) {
|
||||
var interval = _.find(this.params.interval.options, { val: params.interval });
|
||||
return interval.display + ' ' + params.field;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -87,7 +100,7 @@ define(function (require) {
|
|||
|
||||
this.aggsByFieldType = {
|
||||
number: [
|
||||
this.bucketAggsByName.histogram,
|
||||
// this.bucketAggsByName.histogram,
|
||||
this.bucketAggsByName.terms,
|
||||
// 'range'
|
||||
],
|
||||
|
@ -113,8 +126,5 @@ define(function (require) {
|
|||
// 'range'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
require('modules').get('app/visualize')
|
||||
.service('Aggs', AggsService);
|
||||
});
|
||||
});
|
|
@ -1,18 +1,108 @@
|
|||
.thumbnail > img,
|
||||
.thumbnail a > img,
|
||||
.carousel-inner > .item > img,
|
||||
.carousel-inner > .item > a > img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.btn-group-lg > .btn {
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.33;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.btn-group-sm > .btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.btn-group-xs > .btn {
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.container:before,
|
||||
.container:after,
|
||||
.container-fluid:before,
|
||||
.container-fluid:after,
|
||||
.row:before,
|
||||
.row:after,
|
||||
.form-horizontal .form-group:before,
|
||||
.form-horizontal .form-group:after,
|
||||
.btn-toolbar:before,
|
||||
.btn-toolbar:after,
|
||||
.btn-group-vertical > .btn-group:before,
|
||||
.btn-group-vertical > .btn-group:after,
|
||||
.nav:before,
|
||||
.nav:after,
|
||||
.navbar:before,
|
||||
.navbar:after,
|
||||
.navbar-header:before,
|
||||
.navbar-header:after,
|
||||
.navbar-collapse:before,
|
||||
.navbar-collapse:after,
|
||||
.pager:before,
|
||||
.pager:after,
|
||||
.panel-body:before,
|
||||
.panel-body:after,
|
||||
.modal-footer:before,
|
||||
.modal-footer:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.container:after,
|
||||
.container-fluid:after,
|
||||
.row:after,
|
||||
.form-horizontal .form-group:after,
|
||||
.btn-toolbar:after,
|
||||
.btn-group-vertical > .btn-group:after,
|
||||
.nav:after,
|
||||
.navbar:after,
|
||||
.navbar-header:after,
|
||||
.navbar-collapse:after,
|
||||
.pager:after,
|
||||
.panel-body:after,
|
||||
.modal-footer:after {
|
||||
clear: both;
|
||||
}
|
||||
.vis-config-panel {
|
||||
padding: 0;
|
||||
}
|
||||
.vis-config-panel > li {
|
||||
list-style: none;
|
||||
}
|
||||
.vis-config-panel > li .panel-heading {
|
||||
.vis-config-panel .panel-heading {
|
||||
position: relative;
|
||||
}
|
||||
.vis-config-panel > li .panel-heading button {
|
||||
.vis-config-panel .panel-heading button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 7px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
.vis-config-panel .panel-body {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.vis-config-panel vis-config-editor {
|
||||
display: block;
|
||||
border-top: 1px dashed #dddddd;
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
.vis-config-panel vis-config-editor:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.vis-config-panel vis-config-editor .config-controls {
|
||||
float: right;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.vis-config-panel vis-config-editor .config-controls button {
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
font-weight: 100;
|
||||
}
|
||||
.vis-config-panel > li {
|
||||
list-style: none;
|
||||
}
|
||||
.vis-config-panel .agg-config-interval td {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,49 @@
|
|||
@import (reference) "../../../../bower_components/bootstrap/less/bootstrap.less";
|
||||
|
||||
.vis-config-panel {
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
.panel-heading {
|
||||
position: relative;
|
||||
|
||||
.panel-heading {
|
||||
position: relative;
|
||||
button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 7px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
vis-config-editor {
|
||||
display: block;
|
||||
border-top: 1px dashed @panel-inner-border;
|
||||
padding: 10px 10px 0;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.config-controls {
|
||||
float: right;
|
||||
// just a tiny boost up
|
||||
margin-top: -2px;
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 7px;
|
||||
padding: 3px 8px;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
font-weight: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.agg-config-interval {
|
||||
td {
|
||||
padding-left: 10px;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue