Commit 076a9456 by Torkel Ödegaard

Merge branch 'table-options'

parents 823b40a3 be284ada
...@@ -85,8 +85,9 @@ The column styles allow you control how dates and numbers are formatted. ...@@ -85,8 +85,9 @@ The column styles allow you control how dates and numbers are formatted.
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values. 1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
2. `Type`: The three supported types of types are `Number`, `String` and `Date`. 2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
3. `Format`: Specify date format. Only available when `Type` is set to `Date`. 3. `Title`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits. 4. `Format`: Specify date format. Only available when `Type` is set to `Date`.
5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers. 5. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
6. `Add column style rule`: Add new column rule. 6. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
7. `Add column style rule`: Add new column rule.
...@@ -53,7 +53,7 @@ export function exportTableDataToCsv(table) { ...@@ -53,7 +53,7 @@ export function exportTableDataToCsv(table) {
var text = 'sep=;\n'; var text = 'sep=;\n';
// add header // add header
_.each(table.columns, function(column) { _.each(table.columns, function(column) {
text += column.text + ';'; text += (column.title || column.text) + ';';
}); });
text += '\n'; text += '\n';
// process data // process data
......
...@@ -81,8 +81,8 @@ describe('grafanaHeatmap', function () { ...@@ -81,8 +81,8 @@ describe('grafanaHeatmap', function () {
getTimezone: sinon.stub().returns('utc') getTimezone: sinon.stub().returns('utc')
}, },
range: { range: {
from: moment.utc("01 Mar 2017 10:00:00"), from: moment.utc("01 Mar 2017 10:00:00", 'DD MMM YYYY HH:mm:ss'),
to: moment.utc("01 Mar 2017 11:00:00"), to: moment.utc("01 Mar 2017 11:00:00", 'DD MMM YYYY HH:mm:ss'),
}, },
}; };
...@@ -263,5 +263,5 @@ function getTicks(element, axisSelector) { ...@@ -263,5 +263,5 @@ function getTicks(element, axisSelector) {
function formatLocalTime(timeStr) { function formatLocalTime(timeStr) {
let format = "HH:mm"; let format = "HH:mm";
return moment.utc(timeStr).local().format(format); return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').local().format(format);
} }
...@@ -52,89 +52,101 @@ ...@@ -52,89 +52,101 @@
</div> </div>
</div> </div>
<div class="editor-row"> <h5 class="section-heading">
<span style="padding-right: 10px;">Column Style Rules</span>
<button class="btn btn-secondary btn-small" ng-click="editor.addColumnStyle()">
<i class="fa fa-plus"></i>&nbsp;Add
</button>
</h5>
<div class="form-tabs-wrapper">
<ul class="gf-tabs">
<li class="gf-tabs-item" ng-repeat="style in editor.panel.styles">
<a class="gf-tabs-link" ng-click="editor.activeStyleIndex = $index" ng-class="{active: editor.activeStyleIndex === $index}">{{style.pattern || 'New rule'}}</a>
</li>
</ul>
</div>
<div class="form-tabs-content" ng-repeat="style in editor.panel.styles" ng-if="editor.activeStyleIndex === $index">
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Column Styles</h5> <h5 class="section-heading">Options</h5>
<div ng-repeat="style in editor.panel.styles"> <div class="gf-form-inline">
<div class="gf-form-inline"> <div class="gf-form">
<div class="gf-form"> <label class="gf-form-label width-13">Apply to columns named</label>
<label class="gf-form-label">Name or regex</label> <input type="text" placeholder="Name or regex" class="gf-form-input width-13" ng-model="style.pattern" bs-tooltip="'Specify regex using /my.*regex/ syntax'" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur data-placement="right">
<input type="text" class="gf-form-input" ng-model="style.pattern" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Type</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form" ng-if="style.type === 'date'">
<label class="gf-form-label">Format</label>
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</div>
<gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="editor.removeColumnStyle(style)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div> </div>
</div>
<div class="gf-form" ng-if="style.type !== 'hidden'">
<label class="gf-form-label width-13">Column Header</label>
<input type="text" class="gf-form-input width-13" ng-model="style.alias" ng-change="editor.render()" ng-model-onblur placeholder="Override header label">
</div>
</div>
<div class="gf-form-inline" ng-if="style.type === 'number'"> <div class="section gf-form-group">
<div class="gf-form offset-width-8"> <h5 class="section-heading">Type</h5>
<label class="gf-form-label width-8">Unit</label>
</div>
<div class="gf-form">
<div class="gf-form-dropdown-typeahead" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">Decimals</label>
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Coloring</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="style.type === 'number'"> <div class="gf-form">
<div class="gf-form offset-width-8"> <label class="gf-form-label width-8">Type</label>
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label> <div class="gf-form-select-wrapper width-12">
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur> <select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
</div> </div>
<div class="gf-form"> </div>
<label class="gf-form-label width-5">Colors</label> <div class="gf-form" ng-if="style.type === 'date'">
<span class="gf-form-label"> <label class="gf-form-label width-8">Date Format</label>
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker> <metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</span> </div>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
</span>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow">
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
</div>
</div>
</div>
</div> <div ng-if="style.type === 'string'">
</div> <gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
<div class="gf-form-button"> </div>
<button class="btn btn-inverse" ng-click="editor.addColumnStyle()">
<i class="fa fa-plus"></i>&nbsp;Add column style rule <div ng-if="style.type === 'number'">
</button> <div class="gf-form">
</div> <label class="gf-form-label width-8">Unit</label>
<div class="gf-form-dropdown-typeahead width-12" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Decimals</label>
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</div>
</div>
</div>
<div class="section gf-form-group" ng-if="style.type === 'number'">
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label>
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Color Mode</label>
<div class="gf-form-select-wrapper width-10">
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Colors</label>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
</span>
<div class="gf-form-label">
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
</div>
</div>
</div>
<div class="clearfix"></div>
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
<i class="fa fa-trash"></i> Remove Rule
</button>
<br />
<br />
</div> </div>
...@@ -21,10 +21,12 @@ export class TablePanelEditorCtrl { ...@@ -21,10 +21,12 @@ export class TablePanelEditorCtrl {
addColumnSegment: any; addColumnSegment: any;
unitFormats: any; unitFormats: any;
getColumnNames: any; getColumnNames: any;
activeStyleIndex: number;
/** @ngInject */ /** @ngInject */
constructor($scope, private $q, private uiSegmentSrv) { constructor($scope, private $q, private uiSegmentSrv) {
$scope.editor = this; $scope.editor = this;
this.activeStyleIndex = 0;
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.transformers = transformers; this.transformers = transformers;
...@@ -104,18 +106,32 @@ export class TablePanelEditorCtrl { ...@@ -104,18 +106,32 @@ export class TablePanelEditorCtrl {
} }
addColumnStyle() { addColumnStyle() {
var columnStyleDefaults = { var newStyleRule = {
unit: 'short', unit: 'short',
type: 'number', type: 'number',
alias: '',
decimals: 2, decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null, colorMode: null,
pattern: '/.*/', pattern: '',
dateFormat: 'YYYY-MM-DD HH:mm:ss', dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [], thresholds: [],
}; };
this.panel.styles.push(angular.copy(columnStyleDefaults)); var styles = this.panel.styles;
var stylesCount = styles.length;
var indexToInsert = stylesCount;
// check if last is a catch all rule, then add it before that one
if (stylesCount > 0) {
var last = styles[stylesCount-1];
if (last.pattern === '/.*/') {
indexToInsert = stylesCount-1;
}
}
styles.splice(indexToInsert, 0, newStyleRule);
this.activeStyleIndex = indexToInsert;
} }
removeColumnStyle(style) { removeColumnStyle(style) {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<tr> <tr>
<th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden"> <th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden">
<div class="table-panel-table-header-inner pointer" ng-click="ctrl.toggleColumnSort(col, $index)"> <div class="table-panel-table-header-inner pointer" ng-click="ctrl.toggleColumnSort(col, $index)">
{{col.text}} {{col.title}}
<span class="table-panel-table-header-controls" ng-if="col.sort"> <span class="table-panel-table-header-controls" ng-if="col.sort">
<i class="fa fa-caret-down" ng-show="col.desc"></i> <i class="fa fa-caret-down" ng-show="col.desc"></i>
<i class="fa fa-caret-up" ng-hide="col.desc"></i> <i class="fa fa-caret-up" ng-hide="col.desc"></i>
......
...@@ -16,6 +16,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -16,6 +16,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
pageIndex: number; pageIndex: number;
dataRaw: any; dataRaw: any;
table: any; table: any;
renderer: any;
panelDefaults = { panelDefaults = {
targets: [{}], targets: [{}],
...@@ -26,11 +27,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -26,11 +27,13 @@ class TablePanelCtrl extends MetricsPanelCtrl {
{ {
type: 'date', type: 'date',
pattern: 'Time', pattern: 'Time',
alias: 'Time',
dateFormat: 'YYYY-MM-DD HH:mm:ss', dateFormat: 'YYYY-MM-DD HH:mm:ss',
}, },
{ {
unit: 'short', unit: 'short',
type: 'number', type: 'number',
alias: '',
decimals: 2, decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null, colorMode: null,
...@@ -118,6 +121,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -118,6 +121,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
render() { render() {
this.table = transformDataToTable(this.dataRaw, this.panel); this.table = transformDataToTable(this.dataRaw, this.panel);
this.table.sort(this.panel.sort); this.table.sort(this.panel.sort);
this.renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
return super.render(this.table); return super.render(this.table);
} }
...@@ -141,8 +147,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -141,8 +147,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
} }
exportCsv() { exportCsv() {
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize); FileExport.exportTableDataToCsv(this.renderer.render_values());
FileExport.exportTableDataToCsv(renderer.render_values());
} }
link(scope, elem, attrs, ctrl) { link(scope, elem, attrs, ctrl) {
...@@ -162,9 +167,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -162,9 +167,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
} }
function appendTableRows(tbodyElem) { function appendTableRows(tbodyElem) {
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize); ctrl.renderer.setTable(data);
tbodyElem.empty(); tbodyElem.empty();
tbodyElem.html(renderer.render(ctrl.pageIndex)); tbodyElem.html(ctrl.renderer.render(ctrl.pageIndex));
} }
function switchPage(e) { function switchPage(e) {
......
...@@ -5,12 +5,44 @@ import moment from 'moment'; ...@@ -5,12 +5,44 @@ import moment from 'moment';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
export class TableRenderer { export class TableRenderer {
formaters: any[]; formatters: any[];
colorState: any; colorState: any;
constructor(private panel, private table, private isUtc, private sanitize) { constructor(private panel, private table, private isUtc, private sanitize) {
this.formaters = []; this.initColumns();
}
setTable(table) {
this.table = table;
this.initColumns();
}
initColumns() {
this.formatters = [];
this.colorState = {}; this.colorState = {};
for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) {
let column = this.table.columns[colIndex];
column.title = column.text;
for (let i = 0; i < this.panel.styles.length; i++) {
let style = this.panel.styles[i];
var regex = kbn.stringToJsRegex(style.pattern);
if (column.text.match(regex)) {
column.style = style;
if (style.alias) {
column.title = column.text.replace(regex, style.alias);
}
break;
}
}
this.formatters[colIndex] = this.createColumnFormatter(column);
}
} }
getColorForValue(value, style) { getColorForValue(value, style) {
...@@ -24,7 +56,7 @@ export class TableRenderer { ...@@ -24,7 +56,7 @@ export class TableRenderer {
return _.first(style.colors); return _.first(style.colors);
} }
defaultCellFormater(v, style) { defaultCellFormatter(v, style) {
if (v === null || v === void 0 || v === undefined) { if (v === null || v === void 0 || v === undefined) {
return ''; return '';
} }
...@@ -40,18 +72,18 @@ export class TableRenderer { ...@@ -40,18 +72,18 @@ export class TableRenderer {
} }
} }
createColumnFormater(style, column) { createColumnFormatter(column) {
if (!style) { if (!column.style) {
return this.defaultCellFormater; return this.defaultCellFormatter;
} }
if (style.type === 'hidden') { if (column.style.type === 'hidden') {
return v => { return v => {
return undefined; return undefined;
}; };
} }
if (style.type === 'date') { if (column.style.type === 'date') {
return v => { return v => {
if (v === undefined || v === null) { if (v === undefined || v === null) {
return '-'; return '-';
...@@ -62,12 +94,12 @@ export class TableRenderer { ...@@ -62,12 +94,12 @@ export class TableRenderer {
if (this.isUtc) { if (this.isUtc) {
date = date.utc(); date = date.utc();
} }
return date.format(style.dateFormat); return date.format(column.style.dateFormat);
}; };
} }
if (style.type === 'number') { if (column.style.type === 'number') {
let valueFormater = kbn.valueFormats[column.unit || style.unit]; let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
return v => { return v => {
if (v === null || v === void 0) { if (v === null || v === void 0) {
...@@ -75,39 +107,24 @@ export class TableRenderer { ...@@ -75,39 +107,24 @@ export class TableRenderer {
} }
if (_.isString(v)) { if (_.isString(v)) {
return this.defaultCellFormater(v, style); return this.defaultCellFormatter(v, column.style);
} }
if (style.colorMode) { if (column.style.colorMode) {
this.colorState[style.colorMode] = this.getColorForValue(v, style); this.colorState[column.style.colorMode] = this.getColorForValue(v, column.style);
} }
return valueFormater(v, style.decimals, null); return valueFormatter(v, column.style.decimals, null);
}; };
} }
return (value) => { return (value) => {
return this.defaultCellFormater(value, style); return this.defaultCellFormatter(value, column.style);
}; };
} }
formatColumnValue(colIndex, value) { formatColumnValue(colIndex, value) {
if (this.formaters[colIndex]) { return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
return this.formaters[colIndex](value);
}
for (let i = 0; i < this.panel.styles.length; i++) {
let style = this.panel.styles[i];
let column = this.table.columns[colIndex];
var regex = kbn.stringToJsRegex(style.pattern);
if (column.text.match(regex)) {
this.formaters[colIndex] = this.createColumnFormater(style, column);
return this.formaters[colIndex](value);
}
}
this.formaters[colIndex] = this.defaultCellFormater;
return this.formaters[colIndex](value);
} }
renderCell(columnIndex, value, addWidthHack = false) { renderCell(columnIndex, value, addWidthHack = false) {
...@@ -126,7 +143,7 @@ export class TableRenderer { ...@@ -126,7 +143,7 @@ export class TableRenderer {
// this hack adds header content to cell (not visible) // this hack adds header content to cell (not visible)
var widthHack = ''; var widthHack = '';
if (addWidthHack) { if (addWidthHack) {
widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].text + '</div>'; widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
} }
if (value === undefined) { if (value === undefined) {
......
...@@ -22,13 +22,15 @@ describe('when rendering table', () => { ...@@ -22,13 +22,15 @@ describe('when rendering table', () => {
{ {
pattern: 'Time', pattern: 'Time',
type: 'date', type: 'date',
format: 'LLL' format: 'LLL',
alias: 'Timestamp'
}, },
{ {
pattern: 'Value', pattern: '/(Val)ue/',
type: 'number', type: 'number',
unit: 'ms', unit: 'ms',
decimals: 3, decimals: 3,
alias: '$1'
}, },
{ {
pattern: 'Colored', pattern: 'Colored',
...@@ -132,6 +134,18 @@ describe('when rendering table', () => { ...@@ -132,6 +134,18 @@ describe('when rendering table', () => {
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>'); var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
expect(html).to.be('<td>sanitized</td>'); expect(html).to.be('<td>sanitized</td>');
}); });
it('Time column title should be Timestamp', () => {
expect(table.columns[0].title).to.be('Timestamp');
});
it('Value column title should be Val', () => {
expect(table.columns[1].title).to.be('Val');
});
it('Colored column title should be Colored', () => {
expect(table.columns[2].title).to.be('Colored');
});
}); });
}); });
......
...@@ -229,7 +229,7 @@ function transformDataToTable(data, panel) { ...@@ -229,7 +229,7 @@ function transformDataToTable(data, panel) {
var transformer = transformers[panel.transform]; var transformer = transformers[panel.transform];
if (!transformer) { if (!transformer) {
throw {message: 'Transformer ' + panel.transformer + ' not found'}; throw {message: 'Transformer ' + panel.transform + ' not found'};
} }
if (panel.filterNull) { if (panel.filterNull) {
...@@ -239,6 +239,7 @@ function transformDataToTable(data, panel) { ...@@ -239,6 +239,7 @@ function transformDataToTable(data, panel) {
} }
transformer.transform(copyData, panel, model); transformer.transform(copyData, panel, model);
return model; return model;
} }
......
...@@ -68,3 +68,12 @@ ...@@ -68,3 +68,12 @@
top: 1px; top: 1px;
} }
} }
.form-tabs-wrapper {
@include brand-bottom-border();
@include clearfix();
}
.form-tabs-content {
padding: $spacer*2 $spacer;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment