mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
New timepicker, collapsable query and filter sections
This commit is contained in:
parent
377aa16f2c
commit
d5bc550bb1
@ -1,5 +1,5 @@
|
||||
define(['jquery', 'underscore'],
|
||||
function($, _) {
|
||||
define(['jquery', 'underscore','moment'],
|
||||
function($, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var kbn = {};
|
||||
@ -247,6 +247,139 @@ function($, _) {
|
||||
return new Date(new Date().getTime() - (kbn.interval_to_ms(string)));
|
||||
};
|
||||
|
||||
/* This is a simplified version of elasticsearch's date parser */
|
||||
kbn.parseDate = function(text) {
|
||||
if(_.isDate(text)) {
|
||||
return text;
|
||||
}
|
||||
var time,
|
||||
mathString = "",
|
||||
index,
|
||||
parseString;
|
||||
if (text.substring(0,3) === "now") {
|
||||
time = new Date();
|
||||
mathString = text.substring("now".length);
|
||||
} else {
|
||||
index = text.indexOf("||");
|
||||
parseString;
|
||||
if (index === -1) {
|
||||
parseString = text;
|
||||
mathString = ""; // nothing else
|
||||
} else {
|
||||
parseString = text.substring(0, index);
|
||||
mathString = text.substring(index + 2);
|
||||
}
|
||||
// We're going to just require ISO8601 timestamps, k?
|
||||
time = new Date(parseString);
|
||||
}
|
||||
|
||||
if (!mathString.length) {
|
||||
return time;
|
||||
}
|
||||
|
||||
//return [time,parseString,mathString];
|
||||
return kbn.parseDateMath(mathString, time);
|
||||
};
|
||||
|
||||
kbn.parseDateMath = function(mathString, time, roundUp) {
|
||||
var dateTime = moment(time);
|
||||
for (var i = 0; i < mathString.length; ) {
|
||||
var c = mathString.charAt(i++),
|
||||
type,
|
||||
num,
|
||||
unit;
|
||||
if (c === '/') {
|
||||
type = 0;
|
||||
} else if (c === '+') {
|
||||
type = 1;
|
||||
} else if (c === '-') {
|
||||
type = 2;
|
||||
} else {
|
||||
console.log("no type");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNaN(mathString.charAt(i))) {
|
||||
num = 1;
|
||||
} else {
|
||||
var numFrom = i;
|
||||
while (!isNaN(mathString.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
num = parseInt(mathString.substring(numFrom, i),10);
|
||||
}
|
||||
if (type === 0) {
|
||||
// rounding is only allowed on whole numbers
|
||||
if (num !== 1) {
|
||||
console.log("nbad rounding");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
unit = mathString.charAt(i++);
|
||||
switch (unit) {
|
||||
case 'M':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('months',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('months',num);
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('weeks',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('weeks',num);
|
||||
}
|
||||
break;
|
||||
case 'd':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('days',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('days',num);
|
||||
}
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('hours',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('hours',num);
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('minutes',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('minutes',num);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
if (type === 0) {
|
||||
roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
|
||||
} else if (type === 1) {
|
||||
dateTime.add('seconds',num);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract('seconds',num);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("unknown unit");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return dateTime.toDate();
|
||||
};
|
||||
|
||||
// LOL. hahahahaha. DIE.
|
||||
kbn.flatten_json = function(object,root,array) {
|
||||
if (typeof array === 'undefined') {
|
||||
|
@ -1,5 +1,5 @@
|
||||
define([
|
||||
'./dash',
|
||||
'./dashLoader',
|
||||
'./row',
|
||||
'./row'
|
||||
], function () {});
|
@ -16,6 +16,7 @@ function (angular, app, _) {
|
||||
collapsable: true,
|
||||
editable: true,
|
||||
panels: [],
|
||||
notice: false
|
||||
};
|
||||
|
||||
_.defaults($scope.row,_d);
|
||||
@ -34,6 +35,8 @@ function (angular, app, _) {
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
} else {
|
||||
row.notice = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ define([
|
||||
'./arrayJoin',
|
||||
'./dashUpload',
|
||||
'./kibanaPanel',
|
||||
'./kibanaSimplePanel',
|
||||
'./ngBlur',
|
||||
'./ngModelOnBlur',
|
||||
'./tip',
|
||||
|
53
src/app/directives/kibanaSimplePanel.js
Normal file
53
src/app/directives/kibanaSimplePanel.js
Normal file
@ -0,0 +1,53 @@
|
||||
define([
|
||||
'angular'
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('kibanaSimplePanel', function($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, elem, attr) {
|
||||
// once we have the template, scan it for controllers and
|
||||
// load the module.js if we have any
|
||||
|
||||
// compile the module and uncloack. We're done
|
||||
function loadModule($module) {
|
||||
$module.appendTo(elem);
|
||||
/* jshint indent:false */
|
||||
$compile(elem.contents())($scope);
|
||||
elem.removeClass("ng-cloak");
|
||||
}
|
||||
|
||||
$scope.$watch(attr.type, function (name) {
|
||||
elem.addClass("ng-cloak");
|
||||
|
||||
// load the panels module file, then render it in the dom.
|
||||
$scope.require([
|
||||
'jquery',
|
||||
'text!panels/'+name+'/module.html'
|
||||
], function ($, moduleTemplate) {
|
||||
var $module = $(moduleTemplate);
|
||||
// top level controllers
|
||||
var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
|
||||
// add child controllers
|
||||
$controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
|
||||
|
||||
if ($controllers.length) {
|
||||
$scope.require([
|
||||
'panels/'+name+'/module'
|
||||
], function() {
|
||||
loadModule($module);
|
||||
});
|
||||
} else {
|
||||
loadModule($module);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
define(['angular', 'jquery', 'underscore'], function (angular, $, _) {
|
||||
define(['angular', 'jquery', 'underscore', 'moment'], function (angular, $, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.filters');
|
||||
@ -42,6 +42,16 @@ define(['angular', 'jquery', 'underscore'], function (angular, $, _) {
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('moment', function() {
|
||||
return function(date,mode) {
|
||||
switch(mode) {
|
||||
case 'ago':
|
||||
return moment(date).fromNow();
|
||||
}
|
||||
return moment(date).fromNow();
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('noXml', function() {
|
||||
var noXml = function(text) {
|
||||
return _.isString(text)
|
||||
|
@ -55,7 +55,7 @@
|
||||
|
||||
<i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(id)"></i>
|
||||
<i class="filter-action pointer" ng-class="{'icon-check': filterSrv.list[id].active,'icon-check-empty': !filterSrv.list[id].active}" bs-tooltip="'Toggle'" ng-click="toggle(id)"></i>
|
||||
<i class="filter-action pointer icon-edit" ng-hide="filterSrv.list[id].editing" bs-tooltip="'Edit'" ng-click="filterSrv.list[id].editing = true"></i>
|
||||
<i class="filter-action pointer icon-edit" ng-hide="filterSrv.list[id].editing || !isEditable(filterSrv.list[id])" bs-tooltip="'Edit'" ng-click="filterSrv.list[id].editing = true"></i>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -27,6 +27,10 @@ function (angular, app, _) {
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.$on('filter', function() {
|
||||
$scope.row.notice = true;
|
||||
});
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.filterSrv = filterSrv;
|
||||
};
|
||||
|
@ -137,7 +137,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
$scope.get_time_range = function () {
|
||||
var range = $scope.range = filterSrv.timeRange('min');
|
||||
var range = $scope.range = filterSrv.timeRange('last');
|
||||
return range;
|
||||
};
|
||||
|
||||
@ -289,7 +289,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
// function $scope.zoom
|
||||
// factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
|
||||
$scope.zoom = function(factor) {
|
||||
var _range = filterSrv.timeRange('min');
|
||||
var _range = filterSrv.timeRange('last');
|
||||
var _timespan = (_range.to.valueOf() - _range.from.valueOf());
|
||||
var _center = _range.to.valueOf() - _timespan/2;
|
||||
|
||||
@ -496,8 +496,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
filterSrv.set({
|
||||
type : 'time',
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
from : moment.utc(ranges.xaxis.from).toDate(),
|
||||
to : moment.utc(ranges.xaxis.to).toDate(),
|
||||
field : scope.panel.time_field
|
||||
});
|
||||
});
|
||||
|
78
src/app/panels/timepicker2/custom.html
Normal file
78
src/app/panels/timepicker2/custom.html
Normal file
@ -0,0 +1,78 @@
|
||||
<div class="modal-body">
|
||||
<style>
|
||||
.timepicker-to-column {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.timepicker-input input {
|
||||
outline: 0 !important;
|
||||
border: 0px !important;
|
||||
-webkit-box-shadow: 0;
|
||||
-moz-box-shadow: 0;
|
||||
box-shadow: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timepicker-input input::-webkit-outer-spin-button,
|
||||
.timepicker-input input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input.timepicker-date {
|
||||
width: 90px;
|
||||
}
|
||||
input.timepicker-hms {
|
||||
width: 20px;
|
||||
}
|
||||
input.timepicker-ms {
|
||||
width: 25px;
|
||||
}
|
||||
div.timepicker-now {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="timepicker form-horizontal">
|
||||
<form name="input">
|
||||
|
||||
<div class="timepicker-from-column">
|
||||
<label class="small">From</label>
|
||||
<div class="fake-input timepicker-input">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="mm/dd/yyyy" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timepicker-to-column">
|
||||
|
||||
<label class="small">To (<a class="link" ng-class="{'strong':panel.now}" ng-click="setNow();panel.now=true">now</a>)</label>
|
||||
|
||||
<div class="fake-input timepicker-input">
|
||||
<div ng-hide="panel.now">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="mm/dd/yyyy" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
<span type="text" ng-show="panel.now" ng-disabled="panel.now">  <i class="pointer icon-remove-sign" ng-click="setNow();panel.now=false"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<form name="input" style="margin-bottom:0">
|
||||
<span class="" ng-hide="input.$valid">Invalid date or range</span>
|
||||
<button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
|
||||
<button ng-click="dismiss();" class="btn btn-danger">Cancel</button>
|
||||
|
||||
</form>
|
||||
</div>
|
10
src/app/panels/timepicker2/editor.html
Executable file
10
src/app/panels/timepicker2/editor.html
Executable file
@ -0,0 +1,10 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<label class="small">Relative time options <small>comma seperated</small></label>
|
||||
<input type="text" array-join class="input-large" ng-model="panel.time_options">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Time Field</label>
|
||||
<input type="text" class="input-small" ng-model="panel.timefield">
|
||||
</div>
|
||||
</div>
|
39
src/app/panels/timepicker2/module.html
Executable file
39
src/app/panels/timepicker2/module.html
Executable file
@ -0,0 +1,39 @@
|
||||
<div ng-controller='timepicker2' ng-init="init()">
|
||||
<style>
|
||||
.timepicker-timestring {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.timepicker-dropdown {
|
||||
margin: 0px !important;
|
||||
border: 0px !important;
|
||||
}
|
||||
</style>
|
||||
<!-- This is a complete hack. The form actually exists in the modal, but due to transclusion
|
||||
$scope.input isn't available on the controller unless the form element is in this file -->
|
||||
<form name="input" style="margin:3px 0 0 0">
|
||||
<ul class="nav nav-pills timepicker-dropdown">
|
||||
<li class="dropdown" bs-tooltip="(time.from.date | date:'yyyy-MM-dd HH:mm:ss.sss') + ' to ' +(time.to.date | date:'yyyy-MM-dd HH:mm:ss.sss')" data-placement="bottom" ng-click="dismiss();">
|
||||
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="#">
|
||||
|
||||
<span class="small" ng-show="filterSrv.idsByType('time').length">
|
||||
<span class="pointer" ng-hide="panel.now">{{time.from.date | date:'MMM d, y HH:mm:ss'}}</span>
|
||||
<span class="pointer" ng-show="panel.now">{{time.from.date | moment:'ago'}}</span>
|
||||
to
|
||||
<span class="pointer" ng-hide="panel.now" >{{time.to.date | date:'MMM d, y HH:mm:ss'}}</span>
|
||||
<span class="pointer" ng-show="panel.now">{{time.to.date | moment:'ago'}}</span>
|
||||
</span>
|
||||
<span ng-hide="filterSrv.idsByType('time').length">Set a time filter</span>
|
||||
<i class="small icon-caret-down"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat='timespan in panel.time_options'>
|
||||
<a ng-click="setRelativeFilter(timespan)">Last {{timespan}}</a>
|
||||
</li>
|
||||
<li><a ng-click="customTime()">Custom</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</form>
|
||||
</div>
|
223
src/app/panels/timepicker2/module.js
Executable file
223
src/app/panels/timepicker2/module.js
Executable file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
|
||||
## Timepicker2
|
||||
|
||||
### Parameters
|
||||
* mode :: The default mode of the panel. Options: 'relative', 'absolute' 'since' Default: 'relative'
|
||||
* time_options :: An array of possible time options. Default: ['5m','15m','1h','6h','12h','24h','2d','7d','30d']
|
||||
* timespan :: The default options selected for the relative view. Default: '15m'
|
||||
* timefield :: The field in which time is stored in the document.
|
||||
* refresh: Object containing refresh parameters
|
||||
* enable :: true/false, enable auto refresh by default. Default: false
|
||||
* interval :: Seconds between auto refresh. Default: 30
|
||||
* min :: The lowest interval a user may set
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'moment',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _, moment, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.timepicker2', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('timepicker2', function($scope, $modal, $q, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "A panel for controlling the time range filters. If you have time based data, "+
|
||||
" or if you're using time stamped indices, you need one of these"
|
||||
};
|
||||
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
status : "Stable",
|
||||
mode : "relative",
|
||||
time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
|
||||
timespan : '15m',
|
||||
timefield : '@timestamp',
|
||||
timeformat : "",
|
||||
refresh : {
|
||||
enable : false,
|
||||
interval: 30,
|
||||
min : 3
|
||||
}
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
var customTimeModal = $modal({
|
||||
template: './app/panels/timepicker2/custom.html',
|
||||
persist: true,
|
||||
show: false,
|
||||
scope: $scope,
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
$scope.filterSrv = filterSrv;
|
||||
|
||||
// ng-pattern regexs
|
||||
$scope.patterns = {
|
||||
date: /^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/,
|
||||
hour: /^([01]?[0-9]|2[0-3])$/,
|
||||
minute: /^[0-5][0-9]$/,
|
||||
second: /^[0-5][0-9]$/,
|
||||
millisecond: /^[0-9]*$/
|
||||
};
|
||||
|
||||
$scope.$on('refresh', function(){$scope.init();});
|
||||
|
||||
$scope.init = function() {
|
||||
var time = filterSrv.timeRange('last');
|
||||
if(time) {
|
||||
$scope.panel.now = filterSrv.timeRange(false).to === "now" ? true : false;
|
||||
$scope.time = getScopeTimeObj(time.from,time.to);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.customTime = function() {
|
||||
// Assume the form is valid since we're setting it to something valid
|
||||
$scope.input.$setValidity("dummy", true);
|
||||
$scope.temptime = cloneTime($scope.time);
|
||||
|
||||
// Date picker needs the date to be at the start of the day
|
||||
$scope.temptime.from.date.setHours(0,0,0,0);
|
||||
$scope.temptime.to.date.setHours(0,0,0,0);
|
||||
|
||||
$q.when(customTimeModal).then(function(modalEl) {
|
||||
modalEl.modal('show');
|
||||
});
|
||||
};
|
||||
|
||||
// Constantly validate the input of the fields. This function does not change any date variables
|
||||
// outside of its own scope
|
||||
$scope.validate = function(time) {
|
||||
// Assume the form is valid. There is a hidden dummy input for invalidating it programatically.
|
||||
$scope.input.$setValidity("dummy", true);
|
||||
|
||||
var _from = datepickerToLocal(time.from.date),
|
||||
_to = datepickerToLocal(time.to.date),
|
||||
_t = time;
|
||||
|
||||
if($scope.input.$valid) {
|
||||
|
||||
_from.setHours(_t.from.hour,_t.from.minute,_t.from.second,_t.from.millisecond);
|
||||
_to.setHours(_t.to.hour,_t.to.minute,_t.to.second,_t.to.millisecond);
|
||||
|
||||
// Check that the objects are valid and to is after from
|
||||
if(isNaN(_from.getTime()) || isNaN(_to.getTime()) || _from.getTime() >= _to.getTime()) {
|
||||
$scope.input.$setValidity("dummy", false);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {from:_from,to:_to};
|
||||
};
|
||||
|
||||
$scope.setNow = function() {
|
||||
$scope.time.to = getTimeObj(new Date());
|
||||
};
|
||||
|
||||
/*
|
||||
time : {
|
||||
from: Date
|
||||
to: Date
|
||||
}
|
||||
*/
|
||||
$scope.setAbsoluteTimeFilter = function (time) {
|
||||
|
||||
// Create filter object
|
||||
var _filter = _.clone(time);
|
||||
|
||||
_filter.type = 'time';
|
||||
_filter.field = $scope.panel.timefield;
|
||||
|
||||
if($scope.panel.now) {
|
||||
_filter.to = "now";
|
||||
}
|
||||
|
||||
// Clear all time filters, set a new one
|
||||
filterSrv.removeByType('time',true);
|
||||
|
||||
// Set the filter
|
||||
$scope.panel.filter_id = filterSrv.set(_filter);
|
||||
|
||||
// Update our representation
|
||||
$scope.time = getScopeTimeObj(time.from,time.to);
|
||||
|
||||
return $scope.panel.filter_id;
|
||||
};
|
||||
|
||||
$scope.setRelativeFilter = function(timespan) {
|
||||
|
||||
$scope.panel.now = true;
|
||||
// Create filter object
|
||||
var _filter = {
|
||||
type : 'time',
|
||||
field : $scope.panel.timefield,
|
||||
from : "now-"+timespan,
|
||||
to: "now"
|
||||
};
|
||||
|
||||
// Clear all time filters, set a new one
|
||||
filterSrv.removeByType('time',true);
|
||||
|
||||
// Set the filter
|
||||
$scope.panel.filter_id = filterSrv.set(_filter);
|
||||
|
||||
// Update our representation
|
||||
$scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
|
||||
|
||||
return $scope.panel.filter_id;
|
||||
};
|
||||
|
||||
var pad = function(n, width, z) {
|
||||
z = z || '0';
|
||||
n = n + '';
|
||||
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
|
||||
};
|
||||
|
||||
var cloneTime = function(time) {
|
||||
var _n = {
|
||||
from: _.clone(time.from),
|
||||
to: _.clone(time.to)
|
||||
};
|
||||
// Create new dates as _.clone is shallow.
|
||||
_n.from.date = new Date(_n.from.date);
|
||||
_n.to.date = new Date(_n.to.date);
|
||||
return _n;
|
||||
};
|
||||
|
||||
var getScopeTimeObj = function(from,to) {
|
||||
return {
|
||||
from: getTimeObj(from),
|
||||
to: getTimeObj(to)
|
||||
};
|
||||
};
|
||||
|
||||
var getTimeObj = function(date) {
|
||||
return {
|
||||
date: new Date(date),
|
||||
hour: pad(date.getHours(),2),
|
||||
minute: pad(date.getMinutes(),2),
|
||||
second: pad(date.getSeconds(),2),
|
||||
millisecond: pad(date.getMilliseconds(),3)
|
||||
};
|
||||
};
|
||||
|
||||
// Do not use the results of this function unless you plan to use setHour/Minutes/etc on the result
|
||||
var datepickerToLocal = function(date) {
|
||||
console.log(date);
|
||||
date = moment(date).clone().toDate();
|
||||
console.log(moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate());
|
||||
return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000)).toDate();
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
});
|
5
src/app/panels/timepicker2/refreshctrl.html
Executable file
5
src/app/panels/timepicker2/refreshctrl.html
Executable file
@ -0,0 +1,5 @@
|
||||
<form name="refreshPopover" class='form-inline input-append' style="margin:0px">
|
||||
<label><small>Interval (seconds)</small></label><br>
|
||||
<input type="number" class="input-mini" ng-model="refresh_interval">
|
||||
<button type="button" class="btn" ng-click="set_interval(refresh_interval);dismiss()"><i class="icon-ok"></i></button>
|
||||
</form>
|
@ -87,7 +87,8 @@ function (angular, app, _, kbn) {
|
||||
timeField = timeField[0];
|
||||
}
|
||||
|
||||
$scope.time = filterSrv.timeRange('min');
|
||||
// This logic can be simplifie greatly with the new kbn.parseDate
|
||||
$scope.time = filterSrv.timeRange('last');
|
||||
$scope.old_time = {
|
||||
from : new Date($scope.time.from.getTime() - kbn.interval_to_ms($scope.panel.ago)),
|
||||
to : new Date($scope.time.to.getTime() - kbn.interval_to_ms($scope.panel.ago))
|
||||
|
@ -1,3 +1,4 @@
|
||||
<li ng-controller="RowCtrl"><kibana-simple-panel type="'timepicker2'" ng-cloak></kibana-simple-panel></li>
|
||||
|
||||
<li><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/dashboard'><i class='icon-home'></i></a></li>
|
||||
|
||||
|
@ -1,77 +1,93 @@
|
||||
<div class="row-fluid container" style="margin-top:10px; width:98%">
|
||||
<!-- is there a better way to repeat without actually affecting the page? -->
|
||||
<nil ng-repeat="row in dashboard.current.pulldowns" ng-controller="RowCtrl">
|
||||
<div class="top-row-open" ng-hide="row.collapse">
|
||||
<kibana-simple-panel type="row.type" ng-cloak></kibana-simple-panel>
|
||||
</div>
|
||||
<div class="top-row-close pointer" ng-click="toggle_row(row);dismiss();" bs-tooltip="'Toggle '+row.type" data-placement="bottom">
|
||||
<span class="small row-text">{{row.type}}</span>
|
||||
<i class="small" ng-class="{'icon-expand':row.collapse,'icon-collapse-top':!row.collapse}"></i>
|
||||
<i class="small icon-circle text-warning" ng-show="row.notice && row.collapse"></i>
|
||||
</div>
|
||||
</nil>
|
||||
|
||||
<!-- Rows -->
|
||||
<div class="row-fluid kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
|
||||
<div class="row-control">
|
||||
<div class="row-fluid" style="padding:0px;margin:0px;position:relative;">
|
||||
<div class="container-fluid main">
|
||||
<div class="row-fluid">
|
||||
<div class="row-fluid container" style="margin-top:10px; width:98%">
|
||||
<!-- Rows -->
|
||||
<div class="row-fluid kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
|
||||
<div class="row-control">
|
||||
<div class="row-fluid" style="padding:0px;margin:0px;position:relative;">
|
||||
|
||||
<div class="row-close span12" ng-show="row.collapse" data-placement="bottom" >
|
||||
<span class="row-button" bs-modal="'app/partials/roweditor.html'" class="pointer">
|
||||
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
|
||||
</span>
|
||||
<span class="row-button" ng-click="toggle_row(row)" ng-show="row.collapsable">
|
||||
<i bs-tooltip="'Expand row'" data-placement="right" ng-show="row.editable" class="icon-expand pointer" ></i>
|
||||
</span>
|
||||
<span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
|
||||
</div>
|
||||
|
||||
<div style="text-align:left" class="row-open" ng-show="!row.collapse">
|
||||
<span ng-show="row.collapsable">
|
||||
<i bs-tooltip="'Hide row'" data-placement="right" class="icon-collapse-top" ng-click="toggle_row(row)"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable">
|
||||
<i bs-tooltip="'Configure row'" data-placement="right" class="icon-cog pointer"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span ng-show="rowSpan(row) == 12 && row.editable">
|
||||
<i bs-tooltip="'Row full. Create a new row to add more panels'" data-placement="right" class="icon-trello"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span ng-show="rowSpan(row) > 12">
|
||||
<i bs-tooltip="'Total span > 12. This row may format poorly'" data-placement="right" class="icon-warning-sign text-warning"></i>
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row-fluid" style="padding-top:0px" ng-if="!row.collapse">
|
||||
|
||||
<!-- Panels -->
|
||||
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.span == 0 || panel.hide" class="span{{panel.span}} panel nospace" style="min-height:{{row.height}}; position:relative" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
|
||||
<!-- Error Panel -->
|
||||
<div class="row-fluid">
|
||||
<div class="span12 alert alert-error panel-error" ng-hide="!panel.error">
|
||||
<a class="close" ng-click="panel.error=false">×</a>
|
||||
<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
|
||||
<div class="row-close span12" ng-show="row.collapse" data-placement="bottom" >
|
||||
<span class="row-button" bs-modal="'app/partials/roweditor.html'" class="pointer">
|
||||
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
|
||||
</span>
|
||||
<span class="row-button" ng-click="toggle_row(row)" ng-show="row.collapsable">
|
||||
<i bs-tooltip="'Expand row'" data-placement="right" ng-show="row.editable" class="icon-expand pointer" ></i>
|
||||
</span>
|
||||
<span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
|
||||
</div>
|
||||
|
||||
<div style="text-align:left" class="row-open" ng-show="!row.collapse">
|
||||
<span ng-show="row.collapsable">
|
||||
<i bs-tooltip="'Hide row'" data-placement="right" class="icon-collapse-top" ng-click="toggle_row(row)"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable">
|
||||
<i bs-tooltip="'Configure row'" data-placement="right" class="icon-cog pointer"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span ng-show="rowSpan(row) == 12 && row.editable">
|
||||
<i bs-tooltip="'Row full. Create a new row to add more panels'" data-placement="right" class="icon-trello"></i>
|
||||
<br>
|
||||
</span>
|
||||
<span ng-show="rowSpan(row) > 12">
|
||||
<i bs-tooltip="'Total span > 12. This row may format poorly'" data-placement="right" class="icon-warning-sign text-warning"></i>
|
||||
<br>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Content Panel -->
|
||||
<div class="row-fluid" style="position:relative" ng-class="{'dragInProgress':dashboard.panelDragging}" >
|
||||
<kibana-panel type="panel.type" ng-cloak></kibana-panel>
|
||||
<div class="row-fluid" style="padding-top:0px" ng-if="!row.collapse">
|
||||
|
||||
<!-- Panels -->
|
||||
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.span == 0 || panel.hide" class="span{{panel.span}} panel nospace" style="min-height:{{row.height}}; position:relative" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}">
|
||||
<!-- Error Panel -->
|
||||
<div class="row-fluid">
|
||||
<div class="span12 alert alert-error panel-error" ng-hide="!panel.error">
|
||||
<a class="close" ng-click="panel.error=false">×</a>
|
||||
<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Panel -->
|
||||
<div class="row-fluid" style="position:relative" ng-class="{'dragInProgress':dashboard.panelDragging}" >
|
||||
<kibana-panel type="panel.type" ng-cloak></kibana-panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="(12-rowSpan(row)) < 1 || !dashboard.current.panel_hints" class="panel span{{(12-rowSpan(row))}}" ng-class="{'dragInProgress':dashboard.panelDragging}" style="height:{{row.height}};" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
|
||||
|
||||
<span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable && !dashboard.panelDragging">
|
||||
<i ng-hide="rowSpan(row) == 0" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
|
||||
<span ng-click="editor.index = 2" style="margin-top: 8px; margin-left: 3px" ng-show="rowSpan(row) == 0" class="btn btn-mini">Add panel to empty row</btn>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-hide="(12-rowSpan(row)) < 1 || !dashboard.current.panel_hints" class="panel span{{(12-rowSpan(row))}}" ng-class="{'dragInProgress':dashboard.panelDragging}" style="height:{{row.height}};" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
|
||||
|
||||
<span bs-modal="'app/partials/roweditor.html'" ng-show="row.editable && !dashboard.panelDragging">
|
||||
<i ng-hide="rowSpan(row) == 0" class="pointer icon-plus-sign" ng-click="editor.index = 2" bs-tooltip="'Add a panel to this row'" data-placement="right"></i>
|
||||
<span ng-click="editor.index = 2" style="margin-top: 8px; margin-left: 3px" ng-show="rowSpan(row) == 0" class="btn btn-mini">Add panel to empty row</btn>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row-fluid" ng-show='dashboard.current.editable'>
|
||||
<div class="span12" style="text-align:right;">
|
||||
<span style="margin-left: 0px;" class="pointer btn btn-mini" bs-modal="'app/partials/dasheditor.html'">
|
||||
<span ng-click="editor.index = 2"><i class="icon-plus-sign"></i> ADD A ROW</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid" ng-show='dashboard.current.editable'>
|
||||
<div class="span12" style="text-align:right;">
|
||||
<span style="margin-left: 0px;" class="pointer btn btn-mini" bs-modal="'app/partials/dasheditor.html'">
|
||||
<span ng-click="editor.index = 2"><i class="icon-plus-sign"></i> ADD A ROW</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
@ -25,6 +25,14 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
|
||||
failover: false,
|
||||
panel_hints: true,
|
||||
rows: [],
|
||||
pulldowns: [
|
||||
{
|
||||
type: 'query',
|
||||
},
|
||||
{
|
||||
type: 'filtering'
|
||||
}
|
||||
],
|
||||
services: {},
|
||||
loader: {
|
||||
save_gist: false,
|
||||
@ -109,7 +117,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
|
||||
this.refresh = function() {
|
||||
if(self.current.index.interval !== 'none') {
|
||||
if(filterSrv.idsByType('time').length > 0) {
|
||||
var _range = filterSrv.timeRange('min');
|
||||
var _range = filterSrv.timeRange('last');
|
||||
kbnIndex.indices(_range.from,_range.to,
|
||||
self.current.index.pattern,self.current.index.interval
|
||||
).then(function (p) {
|
||||
@ -175,7 +183,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
|
||||
filterSrv.init();
|
||||
|
||||
// If there's an index interval set and no existing time filter, send a refresh to set one
|
||||
if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length === 0) {
|
||||
if(dashboard.index.interval !== 'none') {
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore',
|
||||
'config'
|
||||
], function (angular, _, config) {
|
||||
'config',
|
||||
'kbn'
|
||||
], function (angular, _, config, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.services');
|
||||
@ -35,10 +36,13 @@ define([
|
||||
self.ids = dashboard.current.services.filter.ids;
|
||||
_f = dashboard.current.services.filter;
|
||||
|
||||
// Date filters hold strings now, not dates
|
||||
/*
|
||||
_.each(self.getByType('time',true),function(time) {
|
||||
self.list[time.id].from = new Date(time.from);
|
||||
self.list[time.id].to = new Date(time.to);
|
||||
});
|
||||
*/
|
||||
|
||||
};
|
||||
|
||||
@ -78,6 +82,7 @@ define([
|
||||
dashboard.refresh();
|
||||
},0);
|
||||
}
|
||||
$rootScope.$broadcast('filter');
|
||||
return _r;
|
||||
};
|
||||
|
||||
@ -101,6 +106,7 @@ define([
|
||||
dashboard.refresh();
|
||||
},0);
|
||||
}
|
||||
$rootScope.$broadcast('filter');
|
||||
return _r;
|
||||
};
|
||||
|
||||
@ -150,10 +156,11 @@ define([
|
||||
switch(filter.type)
|
||||
{
|
||||
case 'time':
|
||||
return ejs.RangeFilter(filter.field)
|
||||
.from(filter.from.valueOf())
|
||||
//.from("now-1d")
|
||||
.to(filter.to.valueOf());
|
||||
var _f = ejs.RangeFilter(filter.field).from(kbn.parseDate(filter.from).valueOf());
|
||||
if(!_.isUndefined(filter.to)) {
|
||||
_f = _f.to(filter.to.valueOf());
|
||||
}
|
||||
return _f;
|
||||
case 'range':
|
||||
return ejs.RangeFilter(filter.field)
|
||||
.from(filter.from)
|
||||
@ -187,26 +194,26 @@ define([
|
||||
return _.pluck(self.getByType('time'),'field');
|
||||
};
|
||||
|
||||
// This special function looks for all time filters, and returns a time range according to the mode
|
||||
// No idea when max would actually be used
|
||||
this.timeRange = function(mode) {
|
||||
var _t = _.where(self.list,{type:'time',active:true});
|
||||
if(_t.length === 0) {
|
||||
// Parse is used when you need to know about the raw filter
|
||||
this.timeRange = function(parse) {
|
||||
var _t = _.last(_.where(self.list,{type:'time',active:true}));
|
||||
if(_.isUndefined(_t)) {
|
||||
return false;
|
||||
}
|
||||
switch(mode) {
|
||||
case "min":
|
||||
if(parse === false) {
|
||||
return {
|
||||
from: new Date(_.max(_.pluck(_t,'from'))),
|
||||
to: new Date(_.min(_.pluck(_t,'to')))
|
||||
from: _t.from,
|
||||
to: _t.to
|
||||
};
|
||||
case "max":
|
||||
} else {
|
||||
var
|
||||
_from = _t.from,
|
||||
_to = _t.to || new Date();
|
||||
|
||||
return {
|
||||
from: new Date(_.min(_.pluck(_t,'from'))),
|
||||
to: new Date(_.max(_.pluck(_t,'to')))
|
||||
from : kbn.parseDate(_from),
|
||||
to : kbn.parseDate(_to)
|
||||
};
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,4 +51,4 @@ function (Settings) {
|
||||
'terms'
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
2
src/css/bootstrap.dark.min.css
vendored
2
src/css/bootstrap.dark.min.css
vendored
File diff suppressed because one or more lines are too long
2
src/css/bootstrap.light.min.css
vendored
2
src/css/bootstrap.light.min.css
vendored
File diff suppressed because one or more lines are too long
@ -34,16 +34,14 @@
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<span class="brand">{{dashboard.current.title}}</span>
|
||||
|
||||
<ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid main">
|
||||
<div class="row-fluid">
|
||||
<div ng-view></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
34
src/vendor/bootstrap/less/overrides.less
vendored
34
src/vendor/bootstrap/less/overrides.less
vendored
@ -16,6 +16,10 @@ div.fake-input {
|
||||
.border-radius(@inputBorderRadius @inputBorderRadius @inputBorderRadius @inputBorderRadius);
|
||||
}
|
||||
|
||||
form input.ng-invalid {
|
||||
color: @errorText;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
margin-right: 10px;
|
||||
font-size: 1.7em;
|
||||
@ -80,6 +84,22 @@ div.fake-input {
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.top-row-open {
|
||||
background: darken(@bodyBackground, 3%);
|
||||
padding: 5px 25px 5px 25px;
|
||||
}
|
||||
|
||||
.top-row-close {
|
||||
outline: 1px solid darken(@bodyBackground, 10%);
|
||||
border-top: 1px solid lighten(@bodyBackground, 10%);
|
||||
padding: 0px;
|
||||
margin: 1px 0px 0px 0px;
|
||||
text-align: center;
|
||||
min-height: 16px !important;
|
||||
line-height: 16px;
|
||||
background: darken(@bodyBackground, 3%);
|
||||
}
|
||||
|
||||
.row-open i {
|
||||
font-size: 10pt;
|
||||
}
|
||||
@ -146,9 +166,15 @@ div.fake-input {
|
||||
}
|
||||
|
||||
.link {
|
||||
color: @linkColor;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: @linkColorHover;
|
||||
}
|
||||
|
||||
|
||||
.pointer:hover {
|
||||
color: @linkColorHover;
|
||||
}
|
||||
@ -157,10 +183,6 @@ div.fake-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: @linkColorHover;
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 480px;
|
||||
}
|
||||
@ -191,6 +213,10 @@ div.fake-input {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user