use the visualize config panels to create the vis data-structure

This commit is contained in:
Spencer Alger 2014-04-01 14:41:49 -07:00
parent f28ca585e3
commit 3eb84d1866
16 changed files with 555 additions and 263 deletions

View file

@ -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':

View file

@ -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());
}
}
};
}
};
});

View file

@ -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);

View file

@ -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();
}
};
}

View file

@ -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;
};

View file

@ -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"

View file

@ -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>

View file

@ -1,4 +1,4 @@
<div class="btn-group pull-right">
<div class="form-group btn-group">
<button
type="button"
class="btn btn-primary"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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;
};
});

View file

@ -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);
});
});

View file

@ -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;
}

View file

@ -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;