Merge branch 'master' of https://github.com/elasticsearch/kibana into add/jscs

This commit is contained in:
Spencer Alger 2015-03-06 10:46:06 -07:00
commit 10c4d3ad3b
88 changed files with 1492 additions and 307 deletions

4
FAQ.md
View file

@ -11,3 +11,7 @@
**Q:** What happened to templated/scripted dashboards?
**A:** Check out the URL. The state of each app is stored there, including any filters, queries or columns. This should be a lot easier than constructing scripted dashboards. The encoding of the URL is RISON.
**Q:** I'm getting `bin/node/bin/node: not found` but I can see the node binary in the package?
**A:** Kibana 4 packages are architecture specific. Ensure you are using the correct package for your architecture.

View file

@ -1,4 +1,4 @@
Copyright 2012-2014 Elasticsearch BV
Copyright 20122014 Elasticsearch BV
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

View file

@ -1,8 +1,8 @@
# Kibana <!--version-->4.1.0-snapshot<!--/version-->
# Kibana 4.1.0-snapshot
[![Build Status](https://travis-ci.org/elasticsearch/kibana.svg?branch=master)](https://travis-ci.org/elasticsearch/kibana?branch=master)
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elasticsearch.
Kibana is an open source ([Apache Licensed](https://github.com/elasticsearch/kibana/blob/master/LICENSE.md)), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elasticsearch.
## Requirements

View file

@ -1,13 +1,10 @@
[[production]]
== Using Kibana in a Production Environment
When you set up Kibana in a production environment, rather than on your local
machine, you need to consider:
* <<configuring-kibana-shield, Configuring Kibana to Work with Shield>>
* <<enabling-ssl, Enabling SSL>>
* <<controlling-access, Controlling Access>>
* <<load-balancing, Load Balancing Across Multiple Elasticsearch Nodes>>
* Where you are going to run Kibana.
* Whether you need to encrypt communications to and from Kibana.
* If you need to control access to your data.
=== Deployment Considerations
How you deploy Kibana largely depends on your use case. If you are the only user,
you can run Kibana on your local machine and configure it to point to whatever
Elasticsearch instance you want to interact with. Conversely, if you have a large
@ -15,35 +12,19 @@ number of heavy Kibana users, you might need to load balance across multiple
Kibana instances that are all connected to the same Elasticsearch instance.
While Kibana isn't terribly resource intensive, we still recommend running Kibana
on its own node, rather than on one of your Elasticsearch nodes.
separate from your Elasticsearch data or master nodes. To distribute Kibana
traffic across the nodes in your Elasticsearch cluster, you can run Kibana
and an Elasticsearch client node on the same machine. For more information, see
<<load-balancing, Load Balancing Across Multiple Elasticsearch Nodes>>.
[float]
[[configuring-kibana-shield]]
=== Configuring Kibana to Work with Shield
If you are using Shield to authenticate Elasticsearch users, you need to provide
Kibana with user credentials so it can access the `.kibana` index. The Kibana user
needs permission to perform the following actions on the `.kibana` index:
the Kibana server with credentials so it can access the `.kibana` index and monitor
the cluster.
----
'.kibana':
- indices:admin/create
- indices:admin/exists
- indices:admin/mapping/put
- indices:admin/mappings/fields/get
- indices:admin/refresh
- indices:admin/validate/query
- indices:data/read/get
- indices:data/read/mget
- indices:data/read/search
- indices:data/write/delete
- indices:data/write/index
- indices:data/write/update
- indices:admin/create
----
For more information about configuring access in Shield,
see https://www.elasticsearch.org/guide/en/shield/current/authorization.html[Authorization]
in the Shield documentation.
To configure credentials for Kibana, set the `kibana_elasticsearch_username` and
To configure credentials the Kibana server, set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml`:
----
@ -51,6 +32,13 @@ To configure credentials for Kibana, set the `kibana_elasticsearch_username` and
kibana_elasticsearch_username: kibana4
kibana_elasticsearch_password: kibana4
----
For information about assigning the Kibana server the necessary permissions in Shield,
see https://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4]
in the Shield documentation.
[float]
[[enabling-ssl]]
=== Enabling SSL
Kibana supports SSL encryption for both client requests and the requests the Kibana server
sends to Elasticsearch.
@ -82,6 +70,8 @@ If you are using a self-signed certificate for Elasticsearch, set the `ca` prope
ca: /path/to/your/ca/cacert.pem
----
[float]
[[controlling-access]]
=== Controlling access
You can use http://www.elasticsearch.org/overview/shield/[Elasticsearch Shield]
(Shield) to control what Elasticsearch data users can access through Kibana.
@ -89,6 +79,47 @@ Shield provides index-level access control. If a user isn't authorized to run
the query that populates a Kibana visualization, the user just sees an empty
visualization.
To configure access to Kibana using Shield, you create one or more Shield roles
To configure access to Kibana using Shield, you create Shield roles
for Kibana using the `kibana4` default role as a starting point. For more
information, see http://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Using Shield with Kibana 4].
information, see http://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Using Shield with Kibana 4].
[float]
[[load-balancing]]
=== Load Balancing Across Multiple Elasticsearch Nodes
If you have multiple nodes in your Elasticsearch cluster, the easiest way to distribute Kibana requests
across the nodes is to run an Elasticsearch _client_ node on the same machine as Kibana.
Elasticsearch client nodes are essentially smart load balancers that are part of the cluster. They
process incoming HTTP requests, redirect operations to the other nodes in the cluster as needed, and
gather and return the results. For more information, see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/modules-node.html[Node] in the Elasticsearch reference.
To use a local client node to load balance Kibana requests:
. Install Elasticsearch on the same machine as Kibana.
. Configure the node as a client node. In `elasticsearch.yml`, set both `node.data` and `node.master` to `false`:
+
--------
# 3. You want this node to be neither master nor data node, but
# to act as a "search load balancer" (fetching data from nodes,
# aggregating results, etc.)
#
node.master: false
node.data: false
--------
. Configure the client node to join your Elasticsearch cluster. In `elasticsearch.yml`, set the `cluster.name` to the
name of your cluster.
+
--------
cluster.name: "my_cluster"
--------
. Make sure Kibana is configured to point to your local client node. In `kibana.yml`, the `elasticsearch_url` should be set to
`localhost:9200`.
+
--------
# The Elasticsearch instance to use for all your queries.
elasticsearch_url: "http://localhost:9200"
--------

View file

@ -186,11 +186,6 @@ To create a scripted field:
. Select the index pattern you want to add a scripted field to.
. Go to the pattern's *Scripted Fields* tab.
. Click *Add Scripted Field*.
+
TIP: If you are just getting started with scripted fields, you can click
*create a few examples from your date fields* to add some scripted fields
you can use as a starting point.
. Enter a name for the scripted field.
. Enter the expression that you want to use to compute a value on the fly
from your index data.

View file

@ -9,7 +9,7 @@ All you need is:
** URL of the Elasticsearch instance you want to connect to.
** Which Elasticsearch indices you want to search.
NOTE: If your Elasticsearch installation is protected by http://www.elasticsearch.org/overview/shield/[Shield] see the http://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html#_from_the_kibana_4_server_to_elasticsearch[Shield documentation] for additional setup instructions.
NOTE: If your Elasticsearch installation is protected by http://www.elasticsearch.org/overview/shield/[Shield] see https://www.elasticsearch.org/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4] for additional setup instructions.
[float]
[[install]]

View file

@ -77,7 +77,7 @@ the current visualization.
[[aggregation-builder]]
===== Aggregation Builder
Use the aggregation builder on the left of the page to configure the metric and bucket aggregations used in your
Use the aggregation builder on the left of the page to configure the {ref}/search-aggregations.html#_metrics_aggregations[metric] and {ref}/search-aggregations.html#_bucket_aggregations[bucket] aggregations used in your
visualization. Buckets are analogous to SQL `GROUP BY` statements. For more information on aggregations, see the main
{ref}/search-aggregations.html[Elasticsearch aggregations reference].

View file

@ -56,7 +56,8 @@
"request": "^2.40.0",
"requirefrom": "^0.2.0",
"semver": "^4.2.0",
"serve-favicon": "~2.2.0"
"serve-favicon": "~2.2.0",
"through": "^2.3.6"
},
"devDependencies": {
"connect": "~2.19.5",
@ -97,6 +98,8 @@
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3",
"simple-git": "^0.11.0",
"sinon": "^1.12.2",
"sinon-as-promised": "^2.0.3",
"tar": "^1.0.1"
}
}

View file

@ -16,12 +16,20 @@ define(function (require) {
while ((result = result.$parent) && result.aggConfig) {
var agg = result.aggConfig;
var value = result.value;
if (agg === datum.aggConfigResult.aggConfig && datum.yScale != null) value *= datum.yScale;
details.push({
var detail = {
value: agg.fieldFormatter()(value),
label: agg.makeLabel()
});
};
if (agg === datum.aggConfigResult.aggConfig) {
detail.percent = event.percent;
if (datum.yScale != null) {
detail.value = agg.fieldFormatter()(value * datum.yScale);
}
}
details.push(detail);
}
$tooltipScope.$apply();

View file

@ -0,0 +1,20 @@
define(function (require) {
var CidrMask = require('utils/cidr_mask');
var buildRangeFilter = require('components/filter_manager/lib/range');
return function createIpRangeFilterProvider() {
return function (aggConfig, key) {
var range;
if (aggConfig.params.ipRangeType === 'mask') {
range = new CidrMask(key).getRange();
} else {
var addresses = key.split(/\-/);
range = {
from: addresses[0],
to: addresses[1]
};
}
return buildRangeFilter(aggConfig.params.field, {gte: range.from, lte: range.to}, aggConfig.vis.indexPattern);
};
};
});

View file

@ -0,0 +1,46 @@
define(function (require) {
var _ = require('lodash');
require('directives/validate_ip');
require('directives/validate_cidr_mask');
return function RangeAggDefinition(Private) {
var BucketAggType = Private(require('components/agg_types/buckets/_bucket_agg_type'));
var createFilter = Private(require('components/agg_types/buckets/create_filter/ip_range'));
return new BucketAggType({
name: 'ip_range',
title: 'IPv4 Range',
createFilter: createFilter,
makeLabel: function (aggConfig) {
return aggConfig.params.field.displayName + ' IP ranges';
},
params: [
{
name: 'field',
filterFieldTypes: 'ip'
}, {
name: 'ipRangeType',
default: 'fromTo',
write: _.noop
}, {
name: 'ranges',
default: {
fromTo: [
{from: '0.0.0.0', to: '127.255.255.255'},
{from: '128.0.0.0', to: '191.255.255.255'}
],
mask: [
{mask: '0.0.0.0/1'},
{mask: '128.0.0.0/2'}
]
},
editor: require('text!components/agg_types/controls/ip_ranges.html'),
write: function (aggConfig, output) {
var ipRangeType = aggConfig.params.ipRangeType;
output.params.ranges = aggConfig.params.ranges[ipRangeType];
}
}
]
});
};
});

View file

@ -0,0 +1,88 @@
<div>
<p>
<button type="button" class="btn btn-default" ng-show="agg.params.ipRangeType == 'mask'" ng-click="agg.params.ipRangeType = 'fromTo'">Use From/To</button>
<button type="button" class="btn btn-default" ng-show="agg.params.ipRangeType != 'mask'" ng-click="agg.params.ipRangeType = 'mask'">Use CIDR Masks</button>
</p>
<div ng-show="agg.params.ipRangeType != 'mask'">
<table class="vis-editor-agg-editor-ranges form-group">
<tr>
<th>
<label>From</label>
</th>
<th colspan="2">
<label>To</label>
</th>
</tr>
<tr
ng-repeat="range in agg.params.ranges.fromTo track by $index">
<td>
<input
validate-ip
ng-model="range.from"
type="text"
class="form-control"
name="range.from" />
</td>
<td>
<input
validate-ip
ng-model="range.to"
class="form-control"
name="range.to" />
</td>
<td>
<button
type="button"
ng-click="agg.params.ranges.fromTo.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
</button>
</td>
</tr>
</table>
<div
ng-click="agg.params.ranges.fromTo.push({})"
class="sidebar-item-button primary">
Add Range
</div>
</div>
<div ng-show="agg.params.ipRangeType == 'mask'">
<table class="vis-editor-agg-editor-ranges form-group">
<tr>
<th>
<label>Mask</label>
</th>
</tr>
<tr
ng-repeat="range in agg.params.ranges.mask track by $index">
<td>
<input
validate-cidr-mask
ng-model="range.mask"
type="text"
class="form-control"
name="range.from" />
</td>
<td>
<button
type="button"
ng-click="agg.params.ranges.mask.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
</button>
</td>
</tr>
</table>
<div
ng-click="agg.params.ranges.mask.push({})"
class="sidebar-item-button primary">
Add Range
</div>
</div>
</div>

View file

@ -29,7 +29,7 @@
type="button"
ng-click="agg.params.ranges.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
<i class="fa fa-times"></i>
</button>
</td>
</tr>

View file

@ -17,6 +17,7 @@ define(function (require) {
Private(require('components/agg_types/buckets/date_histogram')),
Private(require('components/agg_types/buckets/histogram')),
Private(require('components/agg_types/buckets/range')),
Private(require('components/agg_types/buckets/ip_range')),
Private(require('components/agg_types/buckets/terms')),
Private(require('components/agg_types/buckets/filters')),
Private(require('components/agg_types/buckets/significant_terms')),

View file

@ -2,7 +2,7 @@ define(function (require) {
var errors = require('errors');
var qs = require('utils/query_string');
return function RedirectWhenMissingFn($location, kbnUrl, Notifier) {
return function RedirectWhenMissingFn($location, kbnUrl, Notifier, Promise) {
var SavedObjectNotFound = errors.SavedObjectNotFound;
var notify = new Notifier();
@ -31,7 +31,7 @@ define(function (require) {
notify.error(err);
kbnUrl.redirect(url);
return;
return Promise.halt();
};
};
};

View file

@ -1,6 +1,6 @@
<td colspan="{{ columns.length + 2 }}">
<a class="pull-right" ng-href="#/doc/{{indexPattern.id}}/{{row._index}}/{{row._type}}/{{row._id}}">
<small>Link to /{{row._index}}/{{row._type}}/{{row._id}}</small></i>
<a class="pull-right" ng-href="#/doc/{{indexPattern.id}}/{{row._index}}/{{row._type}}/?id={{row._id | uriescape}}">
<small>Link to /{{row._index}}/{{row._type}}/{{row._id | uriescape}}</small></i>
</a>
<doc-viewer hit="row" filter="filter" index-pattern="indexPattern"></doc-viewer>
</td>

View file

@ -27,6 +27,9 @@ define(function (require) {
$scope.formatted = _.mapValues($scope.flattened, function (value, name) {
var mapping = $scope.mapping[name];
var formatter = (mapping && mapping.format) ? mapping.format : defaultFormat;
if (_.isArray(value) && typeof value[0] === 'object') {
value = JSON.stringify(value, null, ' ');
}
return formatter.convert(value);
});
$scope.fields = _.keys($scope.flattened).sort();
@ -39,4 +42,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -91,12 +91,14 @@
</div>
<br>
<div class="small">
<input
ng-model="relative.round"
ng-checked="relative.round"
ng-change="formatRelative()"
type="checkbox">
round to the {{units[relative.unit]}}
<label>
<input
ng-model="relative.round"
ng-checked="relative.round"
ng-change="formatRelative()"
type="checkbox">
round to the {{units[relative.unit]}}
</label>
</div>
</div>
@ -169,7 +171,11 @@
<br>
<div ng-repeat="list in refreshLists" class="kbn-refresh-section">
<ul class="list-unstyled">
<li ng-repeat="interval in list"><a ng-click="setRefreshInterval(interval)">{{interval.display}}</a></li>
<li ng-repeat="inter in list">
<a class="refresh-interval" ng-class="{ 'refresh-interval-active': interval.value == inter.value }" ng-click="setRefreshInterval(inter)">
{{inter.display}}
</a>
</li>
</ul>
</div>
</div>

View file

@ -6,6 +6,7 @@ define(function (require) {
var orderKeys = Private(require('components/vislib/components/zero_injection/ordered_x_keys'));
var getLabels = Private(require('components/vislib/components/labels/labels'));
var color = Private(require('components/vislib/components/color/color'));
var errors = require('errors');
/**
* Provides an API for pulling values off the data
@ -510,6 +511,33 @@ define(function (require) {
}
};
/**
* Checks whether all pie slices have zero values.
* If so, an error is thrown.
*/
Data.prototype._validatePieData = function () {
var visData = this.getVisData();
visData.forEach(function (chartData) {
chartData.slices = (function withoutZeroSlices(slices) {
if (!slices.children) return slices;
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(withoutZeroSlices(child));
}
return children;
}, []);
return slices;
}(chartData.slices));
if (chartData.slices.children.length === 0) {
throw new errors.PieContainsAllZeros();
}
});
};
/**
* Returns an array of names ordered by appearance in the nested array
* of objects
@ -521,6 +549,8 @@ define(function (require) {
var self = this;
var names = [];
this._validatePieData();
_.forEach(this.getVisData(), function (obj) {
var columns = obj.raw ? obj.raw.columns : undefined;

View file

@ -19,7 +19,7 @@ define(function (require) {
this.handler = handler;
this.dispatch = d3.dispatch('brush', 'click', 'hover', 'mouseup',
'mousedown', 'mouseover');
'mousedown', 'mouseover', 'mouseout');
}
/**
@ -43,19 +43,7 @@ define(function (require) {
var color = handler.data.color;
var isPercentage = (handler._attr.mode === 'percentage');
if (isSeries) {
// Find object with the actual d value and add it to the point object
var object = _.find(series, { 'label': d.label });
d.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
d.percent = (100 * d.y).toFixed(1) + '%';
}
}
return {
var eventData = {
value: d.y,
point: datum,
datum: datum,
@ -69,6 +57,19 @@ define(function (require) {
e: d3.event,
handler: handler
};
if (isSeries) {
// Find object with the actual d value and add it to the point object
var object = _.find(series, { 'label': d.label });
eventData.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
eventData.percent = (100 * d.y).toFixed(1) + '%';
}
}
return eventData;
};
/**
@ -91,6 +92,7 @@ define(function (require) {
};
};
/**
*
* @method addHoverEvent
@ -100,6 +102,7 @@ define(function (require) {
var self = this;
var isClickable = (this.dispatch.on('click'));
var addEvent = this.addEvent;
var $el = this.handler.el;
function hover(d, i) {
d3.event.stopPropagation();
@ -109,12 +112,32 @@ define(function (require) {
self.addMousePointer.call(this, arguments);
}
self.highlightLegend.call(this, $el);
self.dispatch.hover.call(this, self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
};
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
Dispatch.prototype.addMouseoutEvent = function () {
var self = this;
var addEvent = this.addEvent;
var $el = this.handler.el;
function mouseout() {
d3.event.stopPropagation();
self.unHighlightLegend.call(this, $el);
}
return addEvent('mouseout', mouseout);
};
/**
*
* @method addClickEvent
@ -188,7 +211,7 @@ define(function (require) {
/**
* Mouse over Behavior
* Mouseover Behavior
*
* @method addMousePointer
* @returns {D3.Selection}
@ -197,6 +220,38 @@ define(function (require) {
return d3.select(this).style('cursor', 'pointer');
};
/**
* Mouseover Behavior
*
* @param element {D3.Selection}
* @method highlightLegend
*/
Dispatch.prototype.highlightLegend = function (element) {
var classList = d3.select(this).node().classList;
var liClass = d3.select(this).node().classList[1];
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.filter(function (d, i) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
};
/**
* Mouseout Behavior
*
* @param element {D3.Selection}
* @method unHighlightLegend
*/
Dispatch.prototype.unHighlightLegend = function (element) {
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.classed('blur_shape', false);
};
/**
* Adds D3 brush to SVG and returns the brush function
*

View file

@ -85,14 +85,16 @@ define(function (require) {
};
/**
* Creates a class name based on the colors assigned to each label
* Creates a class name based on the hexColor assigned to each label
*
* @method colorToClass
* @param name {String} Label
* @param hexColor {String} Label
* @returns {string} CSS class name
*/
Legend.prototype.colorToClass = function (name) {
return 'c' + name.replace(/[#]/g, '');
Legend.prototype.colorToClass = function (hexColor) {
if (hexColor) {
return 'c' + hexColor.replace(/[#]/g, '');
}
};
/**
@ -131,35 +133,70 @@ define(function (require) {
}
});
visEl.selectAll('.color')
legendDiv.select('.legend-ul').selectAll('li')
.on('mouseover', function (d) {
var liClass = '.' + self.colorToClass(self.color(d));
visEl.selectAll('.color').classed('blur_shape', true);
var liClass = self.colorToClass(self.color(d));
var charts = visEl.selectAll('.chart');
// legend
legendDiv.selectAll('li')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// lines/area
charts.selectAll('.color')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// circles
charts.selectAll('.line circle')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// pie slices
charts.selectAll('.slice')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
var eventEl = d3.select(this);
eventEl.style('white-space', 'inherit');
eventEl.style('word-break', 'break-all');
// select series on chart
visEl.selectAll(liClass).classed('blur_shape', false);
})
.on('mouseout', function () {
/*
* The default opacity of elements in charts may be modified by the
* chart constructor, and so may differ from that of the legend
*/
visEl.selectAll('.chart')
.selectAll('.color')
var charts = visEl.selectAll('.chart');
// legend
legendDiv.selectAll('li')
.classed('blur_shape', false);
// lines/areas
charts.selectAll('.color')
.classed('blur_shape', false);
// circles
charts.selectAll('.line circle')
.classed('blur_shape', false);
// pie slices
charts.selectAll('.slice')
.classed('blur_shape', false);
var eventEl = d3.select(this);
eventEl.style('white-space', 'nowrap');
eventEl.style('word-break', 'inherit');
// Legend values should always return to their default opacity of 1
visEl.select('.legend-ul')
.selectAll('.color')
.classed('blur_shape', false);
});
};

View file

@ -55,7 +55,7 @@ define(function (require) {
* @returns {String} CSS class name
*/
Chart.prototype.colorToClass = function (label) {
return 'color ' + Legend.prototype.colorToClass.call(null, label);
return Legend.prototype.colorToClass.call(null, label);
};
/**

View file

@ -96,7 +96,7 @@ define(function (require) {
// Append path
path = layer.append('path')
.attr('class', function (d) {
return self.colorToClass(color(d[0].label));
return 'color ' + self.colorToClass(color(d[0].label));
})
.style('fill', function (d) {
return color(d[0].label);
@ -125,8 +125,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
@ -144,6 +145,7 @@ define(function (require) {
* @returns {D3.UpdateSelection} SVG with circles added
*/
AreaChart.prototype.addCircles = function (svg, data) {
var self = this;
var color = this.handler.data.getColorFunc();
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
@ -162,7 +164,7 @@ define(function (require) {
.append('g')
.attr('class', 'points area');
// Append the bars
// append the bars
circles = layer
.selectAll('rect')
.data(function appendData(data) {
@ -179,7 +181,7 @@ define(function (require) {
.enter()
.append('circle')
.attr('class', function circleClass(d) {
return d.label;
return d.label + ' ' + self.colorToClass(color(d.label));
})
.attr('stroke', function strokeColor(d) {
return color(d.label);

View file

@ -69,7 +69,7 @@ define(function (require) {
.enter()
.append('rect')
.attr('class', function (d) {
return self.colorToClass(color(d.label));
return 'color ' + self.colorToClass(color(d.label));
})
.attr('fill', function (d) {
return color(d.label);
@ -146,6 +146,13 @@ define(function (require) {
return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0));
}
// Due to an issue with D3 not returning zeros correctly when using
// an offset='expand', need to add conditional statement to handle zeros
// appropriately
if (d._input.y === 0) {
return 0;
}
// for split bars or for one series,
// last series will have d.y0 = 0
if (d.y0 === 0 && yMin > 0) {
@ -239,8 +246,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);

View file

@ -45,8 +45,9 @@ define(function (require) {
var isBrushable = events.isBrushable();
var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
var hover = events.addHoverEvent();
var mouseout = events.addMouseoutEvent();
var click = events.addClickEvent();
var attachedEvents = element.call(hover).call(click);
var attachedEvents = element.call(hover).call(mouseout).call(click);
if (isBrushable) {
attachedEvents.call(brush);
@ -65,6 +66,7 @@ define(function (require) {
*/
LineChart.prototype.addCircles = function (svg, data) {
var self = this;
var showCircles = this._attr.showCircles;
var color = this.handler.data.getColorFunc();
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
@ -81,7 +83,7 @@ define(function (require) {
.attr('class', 'points line');
var circles = layer
.selectAll('rect')
.selectAll('circle')
.data(function appendData(d) {
return d;
});
@ -105,14 +107,26 @@ define(function (require) {
return color(d.label);
}
function colorCircle(d) {
var parent = d3.select(this).node().parentNode;
var lengthOfParent = d3.select(parent).data()[0].length;
var isVisible = (lengthOfParent === 1);
// If only 1 point exists, show circle
if (!showCircles && !isVisible) return 'none';
return cColor(d);
}
circles
.enter()
.append('circle')
.attr('r', visibleRadius)
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', cColor)
.attr('class', 'circle-decoration');
.attr('fill', colorCircle)
.attr('class', function circleClass(d) {
return 'circle-decoration ' + self.colorToClass(color(d.label));
});
circles
.enter()
@ -172,7 +186,7 @@ define(function (require) {
lines.append('path')
.attr('class', function lineClass(d) {
return self.colorToClass(color(d.label));
return 'color ' + self.colorToClass(color(d.label));
})
.attr('d', function lineD(d) {
return line(d.values);
@ -299,7 +313,6 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
return svg;
});
};

View file

@ -24,9 +24,6 @@ define(function (require) {
}
PieChart.Super.apply(this, arguments);
// Check whether pie chart should be rendered.
this._validatePieData();
this._attr = _.defaults(handler._attr || {}, {
isDonut: handler._attr.isDonut || false
});
@ -44,6 +41,7 @@ define(function (require) {
return element
.call(events.addHoverEvent())
.call(events.addMouseoutEvent())
.call(events.addClickEvent());
};
@ -150,29 +148,6 @@ define(function (require) {
return path;
};
/**
* Checks whether all pie slices have zero values.
* If so, an error is thrown.
*/
PieChart.prototype._validatePieData = function () {
this.chartData.slices = (function withoutZeroSlices(slices) {
if (!slices.children) return slices;
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(withoutZeroSlices(child));
}
return children;
}, []);
return slices;
}(this.chartData.slices));
if (this.chartData.slices.children.length === 0) {
throw new errors.PieContainsAllZeros();
}
};
/**
* Renders d3 visualization
*

View file

@ -0,0 +1,18 @@
define(function (require) {
var module = require('modules').get('kibana');
module.directive('autoSelectIfOnlyOne', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModelCtrl) {
scope.$watch(attributes.autoSelectIfOnlyOne, function (options) {
if (options && options.length === 1) {
ngModelCtrl.$setViewValue(options[0]);
ngModelCtrl.$render();
}
});
}
};
});
});

View file

@ -3,7 +3,7 @@ define(function (require) {
var $ = require('jquery');
var _ = require('lodash');
module.directive('cssTruncate', function ($compile) {
module.directive('cssTruncate', function ($timeout) {
return {
restrict: 'A',
scope: {},
@ -16,11 +16,18 @@ define(function (require) {
'word-break': 'break-all',
});
if (!_.isUndefined(attrs.cssTruncateExpandable)) {
$elem.css({'cursor': 'pointer'});
$elem.bind('click', function () {
$scope.toggle();
});
if (attrs.cssTruncateExpandable != null) {
$scope.$watch(
function () { return $elem.html(); },
function () {
if ($elem[0].offsetWidth < $elem[0].scrollWidth) {
$elem.css({'cursor': 'pointer'});
$elem.bind('click', function () {
$scope.toggle();
});
}
}
);
}
$scope.toggle = function () {

View file

@ -6,7 +6,8 @@ define(function (require) {
restrict: 'A',
link: function ($scope, $elem, attrs) {
$timeout(function () {
$elem[0].focus();
$elem.focus();
if (attrs.inputFocus === 'select') $elem.select();
});
}
};

View file

@ -0,0 +1,28 @@
define(function (require) {
var _ = require('lodash');
var CidrMask = require('utils/cidr_mask');
require('modules').get('kibana').directive('validateCidrMask', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
'ngModel': '='
},
link: function ($scope, elem, attr, ngModel) {
ngModel.$parsers.unshift(validateCidrMask);
ngModel.$formatters.unshift(validateCidrMask);
function validateCidrMask(mask) {
try {
mask = new CidrMask(mask);
ngModel.$setValidity('cidrMaskInput', true);
return mask.toString();
} catch (e) {
ngModel.$setValidity('cidrMaskInput', false);
}
}
}
};
});
});

View file

@ -1,5 +1,6 @@
define(function (require) {
var _ = require('lodash');
var Ipv4Address = require('utils/ipv4_address');
require('modules')
.get('kibana')
@ -11,35 +12,21 @@ define(function (require) {
'ngModel': '=',
},
link: function ($scope, elem, attr, ngModel) {
var isIP = function (value) {
if (!value) return false;
var parts = value.match(/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
var valid = true;
if (parts) {
_.each(parts.slice(1, 5), function (octet) {
if (octet > 255 || octet < 0) valid = false;
});
} else {
valid = false;
function validateIp(ipAddress) {
try {
ipAddress = new Ipv4Address(ipAddress);
ngModel.$setValidity('ipInput', true);
return ipAddress.toString();
} catch (e) {
ngModel.$setValidity('ipInput', false);
}
return valid;
};
}
// From User
ngModel.$parsers.unshift(function (value) {
var valid = isIP(value);
ngModel.$setValidity('ipInput', valid);
return valid ? value : undefined;
});
ngModel.$parsers.unshift(validateIp);
// To user
ngModel.$formatters.unshift(function (value) {
ngModel.$setValidity('ipInput', isIP(value));
return value;
});
ngModel.$formatters.unshift(validateIp);
}
};
});

View file

@ -18,7 +18,7 @@
<img width="128" src="images/initial_load.gif">
<h1>
<strong>Kibana</strong>
<small>is loading. Give me a moment here. I'm loading a whole bunch of code. Don't worry, all this good stuff will be cached up for next time!</small>
<small id="cache-message">is loading. Give me a moment here. I'm loading a whole bunch of code. Don't worry, all this good stuff will be cached up for next time!</small>
</h1>
</center>
</div>
@ -33,6 +33,9 @@
<script src="bower_components/requirejs/require.js?_b=@@buildNum"></script>
<script src="require.config.js?_b=@@buildNum"></script>
<script>
var showCacheMessage = location.href.indexOf('?embed') < 0 && location.href.indexOf('&embed') < 0;
if (!showCacheMessage) document.getElementById('cache-message').style.display = 'none';
if (window.KIBANA_BUILD_NUM.substr(0, 2) !== '@@') {
// only cache bust if this is really the build number
require.config({ urlArgs: '_b=' + window.KIBANA_BUILD_NUM });

View file

@ -10,10 +10,11 @@
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input type="text" input-focus
<input type="text"
placeholder="Filter..."
class="form-control"
ng-model="state.query"
input-focus
kbn-typeahead-input
validate-query>
<button type="submit" class="btn btn-default" ng-disabled="queryInput.$invalid">

View file

@ -1,7 +1,7 @@
<form ng-submit="opts.save()">
<div class="form-group">
<label for="exampleInputEmail1">Save As</label>
<input type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus>
<input type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus="select">
</div>
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary">Save</button>
</form>

View file

@ -28,7 +28,7 @@ define(function (require) {
var warnings = [];
if (!field.scripted) {
if (!field.doc_values) {
if (!field.doc_values && !(field.analyzed && field.type === 'string')) {
warnings.push('Doc values are not enabled on this field. This may lead to excess heap consumption when visualizing.');
}

View file

@ -38,11 +38,11 @@
class="sidebar-item-button primary">
Visualize
<span class="discover-field-vis-warning" ng-show="warnings.length" tooltip="{{warnings.join(' ')}}">
( {{warnings.length}} warnings <i class="fa fa-warning"></i> )
( {{warnings.length}} <ng-pluralize count="warnings.length" when="{'1':'warning', 'other':'warnings'}"></ng-pluralize> <i class="fa fa-warning"></i> )
</span>
</div>
<div ng-show="!field.indexed && !field.scripted"
disabled="disabled"
tooltip="This field is not indexed thus unavailable for visualization and search"
class="sidebar-item-button primary">Not Indexed</div>
class="sidebar-item-button primary">Not Indexed</div>

View file

@ -1,6 +1,23 @@
<div class="sidebar-list">
<div css-truncate css-truncate-expandable="true" class="index-pattern">
{{ indexPattern.id }}
<div ng-show="indexPatternList.length > 1">
<div class="index-pattern" ng-click="showIndexPatternSelection = !showIndexPatternSelection">
<div css-truncate>{{ indexPattern.id }}</div>
<i ng-hide="showIndexPatternSelection" class="fa fa-caret-down"></i>
<i ng-show="showIndexPatternSelection" class="fa fa-caret-up"></i>
</div>
<div ng-show="showIndexPatternSelection">
<ul class="list-unstyled sidebar-item index-pattern-selection">
<li css-truncate class="sidebar-item-title" ng-repeat="id in indexPatternList" ng-show="indexPattern.id != id" ng-click="setIndexPattern(id)">{{id}}</li>
</ul>
<div ng-click="showIndexPatternSelection = !showIndexPatternSelection" class="discover-field-details-close">
<i class="fa fa-chevron-up"></i>
</div>
</div>
</div>
<div ng-hide="indexPatternList.length > 1">
<div class="index-pattern">
<div css-truncate>{{ indexPattern.id }}</div>
</div>
</div>
<div class="sidebar-list-header">

View file

@ -21,17 +21,24 @@ define(function (require) {
data: '=',
state: '=',
indexPattern: '=',
indexPatternList: '=',
updateFilterInQuery: '=filter'
},
template: html,
controller: function ($scope) {
controller: function ($scope, $route) {
$scope.setIndexPattern = function (indexPattern) {
$scope.state.index = indexPattern;
$scope.state.save();
$route.reload();
};
var filter = $scope.filter = {
props: [
'type',
'indexed',
'analyzed',
'missing'
'missing',
'name'
],
defaults: {
missing: true

View file

@ -77,7 +77,6 @@ define(function (require) {
// config panel templates
$scope.configTemplate = new ConfigTemplate({
config: require('text!plugins/discover/partials/settings.html'),
load: require('text!plugins/discover/partials/load_search.html'),
save: require('text!plugins/discover/partials/save_search.html')
});
@ -139,7 +138,7 @@ define(function (require) {
$scope.fields = _($scope.indexPattern.fields)
.sortBy('name')
.transform(function (fields, field) {
// clone the field with Object.create so that it's getters
// clone the field with Object.create so that its getters
// and non-enumerable props are preserved
var clone = Object.create(field);
clone.display = _.contains($state.columns, field.name);
@ -204,18 +203,6 @@ define(function (require) {
timefilter.enabled = !!timefield;
});
$scope.$watch('opts.index', changeIndexPattern($scope.opts, $state));
$scope.$watch('state.index', changeIndexPattern($state, $scope.opts));
function changeIndexPattern(from, to) {
return function () {
if (from.index === to.index) return;
to.index = from.index;
if (to === $state) $state.save();
$route.reload();
};
}
$scope.$watchMulti([
'rows',
'fetchStatus'

View file

@ -30,9 +30,6 @@
<kbn-tooltip text="Load Saved Search" placement="bottom" append-to-body="1">
<button ng-click="configTemplate.toggle('load')"><i class="fa fa-folder-open-o"></i></button>
</kbn-tooltip>
<kbn-tooltip text="Settings" placement="bottom" append-to-body="1">
<button ng-click="configTemplate.toggle('config')"><i class="fa fa-gear"></i></button>
</kbn-tooltip>
</div>
</navbar>
@ -51,6 +48,7 @@
data="rows"
filter="filterQuery"
index-pattern="searchSource.get('index')"
index-pattern-list="opts.indexPatternList"
state="state">
</disc-field-chooser>
</div>

View file

@ -3,7 +3,7 @@
<div class="col-md-12">
<div class="form-group">
<label class="control-label">Save Search</label>
<input ng-model="opts.savedSearch.title" input-focus class="form-control" placeholder="Name this search...">
<input ng-model="opts.savedSearch.title" input-focus="select" class="form-control" placeholder="Name this search...">
</div>
<div class="form-group">
<button ng-click="opts.saveDataSource()" ng-disabled="!opts.savedSearch.title" type="submit" class="btn btn-primary">

View file

@ -1,19 +0,0 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
<label class="control-label">
Index Pattern&nbsp;
<a class="small" ng-href="#/settings/indices/{{opts.index | uriescape}}"><i class="fa fa-pencil"></i></a>
</label>
<select
class="form-control"
ng-model="opts.index"
ng-options="id as id for id in opts.indexPatternList | orderBy:'toString()'"
ng-change="opts.changeIndexAndReload()">
</select>
<small>
Time field: <strong>{{opts.timefield || 'not configured'}}</strong>
</small>
</div>
</div>
</div>

View file

@ -13,14 +13,22 @@ define(function (require) {
'kibana/index_patterns'
]);
var html = require('text!plugins/doc/index.html');
var resolveIndexPattern = {
indexPattern: function (courier, savedSearches, $route) {
return courier.indexPatterns.get($route.current.params.indexPattern);
}
};
require('routes')
.when('/doc/:indexPattern/:index/:type/:id', {
template: require('text!plugins/doc/index.html'),
resolve: {
indexPattern: function (courier, savedSearches, $route) {
return courier.indexPatterns.get($route.current.params.indexPattern);
}
}
template: html,
resolve: resolveIndexPattern
})
.when('/doc/:indexPattern/:index/:type', {
template: html,
resolve: resolveIndexPattern
});
app.controller('doc', function ($scope, $route, es, timefilter) {

View file

@ -38,7 +38,7 @@
<!-- result -->
<div class="col-md-12" ng-if="status === 'found'">
<h2><b>Doc:</b> {{hit._index}}/{{hit._type}}/{{hit._id}}</h2>
<h2><b>Doc:</b> {{hit._index}}/{{hit._type}}/{{hit._id | uriescape}}</h2>
<doc-viewer hit="hit" index-pattern="indexPattern"></doc-viewer>
</div>

View file

@ -4,7 +4,7 @@ define(function (require) {
$rootScope.globalState = globalState;
// and some local values
$scope.appEmbedded = $location.search().embed;
$scope.appEmbedded = $location.search().embed || false;
$scope.httpActive = $http.pendingRequests;
$scope.notifList = notify._notifs;
@ -13,4 +13,4 @@ define(function (require) {
courier.start();
});
};
});
});

View file

@ -1,6 +1,6 @@
<kbn-notifications list="notifList"></kbn-notifications>
<div class="content" style="display: none;">
<nav ng-hide="appEmbedded" bindonce class="navbar navbar-inverse navbar-static-top">
<nav ng-class="{show: appEmbedded === false}" bindonce class="hide navbar navbar-inverse navbar-static-top">
<div class="navbar-header">
<button ng-click="showCollapsed = !showCollapsed" type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
@ -51,4 +51,4 @@
</config>
<div class="application" ng-view></div>
</div>
</div>

View file

@ -23,7 +23,7 @@
</table>
</p>
<small>© 2014 All Rights Reserved - Elasticsearch</small>
<small>© 2015 All Rights Reserved - Elasticsearch</small>
</center>
</div>
</div>

View file

@ -117,6 +117,7 @@
ng-if="!index.fetchFieldsError"
ng-options="field.name for field in index.dateFields"
ng-model="index.timeField"
auto-select-if-only-one="index.dateFields"
class="form-control">
</select>
</div>

View file

@ -4,6 +4,7 @@ define(function (require) {
var errors = require('errors');
require('directives/validate_index_name');
require('directives/auto_select_if_only_one');
require('routes')
.when('/settings/indices/', {

View file

@ -49,7 +49,7 @@
<label>Script <small>Please familiarize yourself with <a target="_window" href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with
<a target="_window" href="http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a>
before using scripted fields.</small></label>
<textarea required class="form-control span12" ng-model="scriptedField.script"></textarea>
<textarea required class="scripted-field-script form-control span12" ng-model="scriptedField.script"></textarea>
</div>
</form>
<div ng-if="namingConflict" class="alert alert-danger">

View file

@ -149,6 +149,10 @@ kbn-settings-objects-view {
.flex(4, 0, auto);
}
}
.scripted-field-script {
font-family: @font-family-monospace;
}
}
kbn-settings-indices .fields {

View file

@ -0,0 +1,8 @@
<!-- vis type specific options -->
<vislib-basic-options></vislib-basic-options>
<div class="vis-option-item form-group">
<label>
<input type="checkbox" value="{{showCircles}}" ng-model="vis.params.showCircles" name="showCircles" ng-checked="vis.params.showCircles">
Show Circles
</label>
</div>

View file

@ -14,9 +14,10 @@ define(function (require) {
shareYAxis: true,
addTooltip: true,
addLegend: true,
showCircles: true,
defaultYExtents: false
},
editor: require('text!plugins/vis_types/vislib/editors/basic.html')
editor: require('text!plugins/vis_types/vislib/editors/line.html')
},
schemas: new Schemas([
{

View file

@ -1,7 +1,7 @@
<form role="form" ng-submit="conf.doSave()">
<div class="form-group">
<label for="visTitle">Title</label>
<input class="form-control" input-focus type="text" name="visTitle" ng-model="conf.savedVis.title" required>
<input class="form-control" input-focus="select" type="text" name="visTitle" ng-model="conf.savedVis.title" required>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>

View file

@ -1,12 +1,12 @@
<li class="sidebar-item" ng-show="vis.type.params.editor">
<div class="sidebar-item-title" ng-click="showVisOptions = !showVisOptions">
<div ng-hide="alwaysShowOptions" class="sidebar-item-title" ng-click="showVisOptions = !showVisOptions">
view options
<i
class="fa fa-caret-down"
ng-if="!alwaysShowOptions"
ng-class="{'fa-caret-down': showVisOptions, 'fa-caret-right': !showVisOptions}">
</i>
</div>
<div ng-show="alwaysShowOptions" class="sidebar-item-title">view options</div>
<div class="visualization-options" ng-show="alwaysShowOptions || showVisOptions"></div>
</li>

View file

@ -57,6 +57,12 @@ define(function (require) {
// https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335
return obj && typeof obj.then === 'function';
};
Promise.halt = _.once(function () {
var promise = new Promise();
promise.then = _.constant(promise);
promise.catch = _.constant(promise);
return promise;
});
Promise.try = function (fn, args, ctx) {
if (typeof fn !== 'function') {
return Promise.reject('fn must be a function');

View file

@ -118,10 +118,19 @@
}
.index-pattern {
text-align: center;
background-color: @sidebar-active-bg !important;
background-color: @sidebar-active-bg;
font-weight: bold;
padding: 5px 10px;
color: @sidebar-active-color;
.display(flex);
.justify-content(space-between);
> * {
.flex;
}
}
.index-pattern-selection .sidebar-item-title {
background-color: white;
}
}

View file

@ -276,6 +276,16 @@ notifications {
text-shadow: none;
}
.kbn-timepicker .refresh-interval {
padding: 0.2em 0.4em;
border-radius: @border-radius-small;
}
.kbn-timepicker .refresh-interval-active {
background-color: @btn-info-bg;
color: @btn-info-color;
}
//== Table
kbn-table, .kbn-table {

View file

@ -0,0 +1,32 @@
define(function (require) {
var Ipv4Address = require('utils/ipv4_address');
var NUM_BITS = 32;
function throwError(mask) {
throw Error('Invalid CIDR mask: ' + mask);
}
function CidrMask(mask) {
var splits = mask.split('\/');
if (splits.length !== 2) throwError(mask);
this.initialAddress = new Ipv4Address(splits[0]);
this.prefixLength = Number(splits[1]);
if (this.prefixLength < 1 || this.prefixLength > NUM_BITS) throwError(mask);
}
CidrMask.prototype.getRange = function () {
var variableBits = NUM_BITS - this.prefixLength;
var fromAddress = this.initialAddress.valueOf() >> variableBits << variableBits >>> 0; // >>> 0 coerces to unsigned
var numAddresses = Math.pow(2, variableBits);
return {
from: new Ipv4Address(fromAddress).toString(),
to: new Ipv4Address(fromAddress + numAddresses - 1).toString()
};
};
CidrMask.prototype.toString = function () {
return this.initialAddress.toString() + '/' + this.prefixLength;
};
return CidrMask;
});

View file

@ -0,0 +1,50 @@
define(function () {
var NUM_BYTES = 4;
var BYTE_SIZE = 256;
function throwError(ipAddress) {
throw Error('Invalid IPv4 address: ' + ipAddress);
}
function isIntegerInRange(integer, min, max) {
return !isNaN(integer)
&& integer >= min
&& integer < max
&& integer % 1 === 0;
}
function Ipv4Address(ipAddress) {
this.value = ipAddress;
if (typeof ipAddress === 'string') {
this.value = 0;
var bytes = ipAddress.split('.');
if (bytes.length !== NUM_BYTES) throwError(ipAddress);
for (var i = 0; i < bytes.length; i++) {
var byte = Number(bytes[i]);
if (!isIntegerInRange(byte, 0, BYTE_SIZE)) throwError(ipAddress);
this.value += Math.pow(BYTE_SIZE, NUM_BYTES - 1 - i) * byte;
}
}
if (!isIntegerInRange(this.value, 0, Math.pow(BYTE_SIZE, NUM_BYTES))) throwError(ipAddress);
}
Ipv4Address.prototype.toString = function () {
var value = this.value;
var bytes = [];
for (var i = 0; i < NUM_BYTES; i++) {
bytes.unshift(value % 256);
value = Math.floor(value / 256);
}
return bytes.join('.');
};
Ipv4Address.prototype.valueOf = function () {
return this.value;
};
return Ipv4Address;
});

View file

@ -12,7 +12,7 @@ set CONFIG_PATH=%DIR%\config\kibana.yml
TITLE Kibana Server @@version
%NODE% %SERVER% %*
"%NODE%" "%SERVER%" %*
:finally

View file

@ -18,6 +18,7 @@ program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
program.parse(process.argv);
@ -49,6 +50,10 @@ if (program.quiet) {
config.quiet = program.quiet;
}
if (program.logFile) {
config.log_file = program.logFile;
}
if (program.host) {
config.host = program.host;
}

View file

@ -44,7 +44,9 @@ var config = module.exports = {
bundled_plugins_folder : path.resolve(public_folder, 'plugins'),
kibana : kibana,
package : require(packagePath),
htpasswd : htpasswdPath
htpasswd : htpasswdPath,
buildNum : '@@buildNum',
log_file : kibana.log_file || null
};
config.plugins = listPlugins(config);

View file

@ -49,3 +49,6 @@ verify_ssl: true
# Set the path to where you would like the process id file to be created.
# pid_file: /var/run/kibana.pid
# If you would like to send the log output to a file you can set the path below.
# This will also turn off the STDOUT log output.
# log_file: ./kibana.log

View file

@ -69,8 +69,8 @@ function onListening() {
}
function start() {
var port = parseInt(process.env.PORT, 10) || config.port || 3000;
var host = process.env.HOST || config.host || '127.0.0.1';
var port = config.port || 3000;
var host = config.host || '127.0.0.1';
var listen = Promise.promisify(server.listen.bind(server));
app.set('port', port);
return listen(port, host);
@ -81,10 +81,16 @@ module.exports = {
start: function (cb) {
return initialization()
.then(start)
.catch(function (err) {
throw err;
})
.nodeify(cb);
.then(function () {
cb && cb();
}, function (err) {
logger.error({ err: err });
if (cb) {
cb(err);
} else {
process.exit();
}
});
}
};

View file

@ -1,6 +1,5 @@
var _ = require('lodash');
var Writable = require('stream').Writable;
var util = require('util');
var through = require('through');
var levels = {
10: 'trace',
@ -11,14 +10,7 @@ var levels = {
60: 'fatal'
};
function JSONStream(options) {
options = options || {};
Writable.call(this, options);
}
util.inherits(JSONStream, Writable);
JSONStream.prototype._write = function (entry, encoding, callback) {
function write(entry) {
entry = JSON.parse(entry.toString('utf8'));
var env = process.env.NODE_ENV || 'development';
@ -36,8 +28,13 @@ JSONStream.prototype._write = function (entry, encoding, callback) {
if (!output.message) output.message = output.error.message;
}
process.stdout.write(JSON.stringify(output) + "\n");
callback();
};
this.queue(JSON.stringify(output) + '\n');
}
module.exports = JSONStream;
function end() {
this.queue(null);
}
module.exports = function () {
return through(write, end);
};

View file

@ -2,18 +2,33 @@ var _ = require('lodash');
var morgan = require('morgan');
var env = process.env.NODE_ENV || 'development';
var bunyan = require('bunyan');
var fs = require('fs');
var StdOutStream = require('./StdOutStream');
var JSONStream = require('./JSONStream');
var createJSONStream = require('./createJSONStream');
var config = require('../config');
var stream = { stream: new JSONStream() };
var streams = [];
// Set the default stream based on the enviroment. If we are on development then
// then we are going to create a pretty stream. Everytyhing else will get the
// JSON stream to stdout.
var defaultStream;
if (env === 'development') {
stream.stream = new StdOutStream();
defaultStream = new StdOutStream();
} else {
defaultStream = createJSONStream()
.pipe(process.stdout);
}
if (!config.quiet) {
streams.push(stream);
// If we are not being oppressed and we are not sending the output to a log file
// push the default stream to the list of streams
if (!config.quiet && !config.log_file) {
streams.push({ stream: defaultStream });
}
// Send the stream to a file using the json format.
if (config.log_file) {
var fileStream = fs.createWriteStream(config.log_file);
streams.push({ stream: createJSONStream().pipe(fileStream) });
}
var logger = module.exports = bunyan.createLogger({

View file

@ -1,8 +1,7 @@
var config = require('../config');
var upgrade = require('./upgradeConfig');
var client = require('./elasticsearch_client');
module.exports = function () {
module.exports = function (client) {
var options = {
index: config.kibana.kibana_index,
type: 'config',
@ -22,7 +21,7 @@ module.exports = function () {
};
return client.search(options)
.then(upgrade)
.then(upgrade(client))
.catch(function (err) {
if (!/SearchParseException.+mapping.+\[buildNum\]|^IndexMissingException/.test(err.message)) throw err;
});

View file

@ -1,10 +1,9 @@
var Promise = require('bluebird');
var waitForEs = require('./waitForEs');
var migrateConfig = require('./migrateConfig');
var client = require('./elasticsearch_client');
module.exports = function () {
var tasks = [
migrateConfig()
];
return Promise.all(tasks);
return waitForEs().then(function () {
return migrateConfig(client);
});
};

View file

@ -3,22 +3,31 @@ var isUpgradeable = require('./isUpgradeable');
var config = require('../config');
var _ = require('lodash');
var client = require('./elasticsearch_client');
module.exports = function (response) {
var newConfig = {};
// Check to see if there are any doc. If not then we can assume
// nothing needs to be done
if (response.hits.hits.length === 0) return Promise.resolve();
module.exports = function (client) {
return function (response) {
var newConfig = {};
// Check to see if there are any doc. If not then we can assume
// nothing needs to be done
if (response.hits.hits.length === 0) return Promise.resolve();
// Look for upgradeable configs. If none of them are upgradeable
// then resolve with null.
var body = _.find(response.hits.hits, isUpgradeable);
if (!body) return Promise.resolve();
// if we already have a the current version in the index then we need to stop
if (_.find(response.hits.hits, { _id: config.package.version })) return Promise.resolve();
return client.create({
index: config.kibana.kibana_index,
type: 'config',
body: body._source,
id: config.package.version
});
// Look for upgradeable configs. If none of them are upgradeable
// then resolve with null.
var body = _.find(response.hits.hits, isUpgradeable);
if (!body) return Promise.resolve();
// if the build number is still the template string (which it wil be in development)
// then we need to set it to the max interger. Otherwise we will set it to the build num
body._source.buildNum = (/^@@/.test(config.buildNum)) ? Math.pow(2, 53) - 1 : parseInt(config.buildNum, 10);
return client.create({
index: config.kibana.kibana_index,
type: 'config',
body: body._source,
id: config.package.version
});
};
};

View file

@ -0,0 +1,28 @@
var Promise = require('bluebird');
var NoConnections = require('elasticsearch').errors.NoConnections;
var client = require('./elasticsearch_client');
var logger = require('./logger');
var config = require('../config');
function waitForPong() {
return client.ping({ requestTimeout: 1500 }).catch(function (err) {
if (!(err instanceof NoConnections)) throw err;
logger.info('Unable to connect to elasticsearch at %s. Retrying in 2.5 seconds.', config.elasticsearch);
return Promise.delay(2500).then(waitForPong);
});
}
function waitForShards() {
return client.cluster.health().then(function (resp) {
if (resp.initializing_shards <= 0) return;
logger.info('Elasticsearch is still initializaing... Trying again in 2500 seconds.');
return Promise.delay(2500).then(waitForShards);
});
}
module.exports = function () {
return waitForPong().then(waitForShards);
};

View file

@ -33,7 +33,7 @@ module.exports = function (grunt) {
{
expand: true,
cwd: '<%= server %>/config/',
src: '**',
src: '*.yml',
dest: '<%= build %>/kibana/config'
},
{

View file

@ -22,6 +22,10 @@ module.exports = function (grunt) {
{
src: [join(src, 'server', 'bin', 'kibana.bat')],
dest: join(build, 'dist', 'kibana', 'bin', 'kibana.bat')
},
{
src: [join(src, 'server', 'config', 'index.js')],
dest: join(build, 'dist', 'kibana', 'src', 'config', 'index.js')
}
]
},

View file

@ -4,8 +4,10 @@ module.exports = function (grunt) {
var README_PATH = root('README.md');
var PKG_JSON_PATH = root('package.json');
var START = '<!--version-->';
var END = '<!--/version-->';
function replace(source, from, to) {
return String(source).split(from).join(to);
}
grunt.registerTask('version', function (updateExpr) {
var oldVersion = grunt.config.get('pkg.version');
@ -17,32 +19,13 @@ module.exports = function (grunt) {
// write back to package.json
var pkgJson = grunt.file.read(PKG_JSON_PATH);
pkgJson = pkgJson.replace(JSON.stringify(oldVersion), JSON.stringify(version));
pkgJson = replace(pkgJson, JSON.stringify(oldVersion), JSON.stringify(version));
grunt.file.write(PKG_JSON_PATH, pkgJson);
grunt.log.ok('updated package.json', version);
// write the readme
var input = grunt.file.read(README_PATH);
var readme = '';
var startI, endI, before;
while (input.length) {
startI = input.indexOf(START);
endI = input.indexOf(END);
if (endI < startI) throw new Error('version tag mismatch in ' + input);
if (startI < 0) {
readme += input;
break;
}
before = input.substr(0, startI);
input = input.substr(endI ? endI + END.length : startI);
readme += before + START + version + END;
}
grunt.file.write(README_PATH, readme);
var readme = grunt.file.read(README_PATH);
grunt.file.write(README_PATH, replace(readme, oldVersion, version));
grunt.log.ok('updated readme', version);
});
};

View file

@ -0,0 +1,25 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,35 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.1-snapshot",
"_score": 1,
"_source": {
"buildNum": 5921,
"defaultIndex": "logstash-*"
}
},
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,35 @@
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.1",
"_score": 1,
"_source": {
"buildNum": 5921,
"defaultIndex": "logstash-*"
}
},
{
"_index": ".kibana",
"_type": "config",
"_id": "4.0.0",
"_score": 1,
"_source": {
"buildNum": 5888,
"defaultIndex": "logstash-*"
}
}
]
}
}

View file

@ -0,0 +1,75 @@
var root = require('requirefrom')('');
var upgradeConfig = root('src/server/lib/upgradeConfig');
var expect = require('expect.js');
var sinon = require('sinon');
var sinonAsPromised = require('sinon-as-promised')(require('bluebird'));
var util = require('util');
var package = root('package.json');
var config = root('src/server/config');
var upgradeFrom4_0_0_to_4_0_1 = root('test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json');
var upgradeFrom4_0_0_to_4_0_1_snapshot = root('test/unit/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json');
var upgradeFrom4_0_0 = root('test/unit/fixtures/config_upgrade_from_4.0.0.json');
describe('lib/upgradeConfig', function () {
var client, oldPackageVersion, oldBuildNum;
beforeEach(function () {
oldPackageVersion = config.package.version;
oldBuildNum = config.buildNum;
client = { create: sinon.stub() };
});
afterEach(function () {
config.package.version = oldPackageVersion;
config.buildNum = oldBuildNum;
});
it('should not upgrade if the current version of the config exits', function () {
config.package.version = '4.0.1';
var fn = upgradeConfig(client);
client.create.rejects(new Error('DocumentAlreadyExistsException'));
return fn(upgradeFrom4_0_0_to_4_0_1).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should not upgrade if there are no hits', function () {
config.package.version = '4.0.1';
var fn = upgradeConfig(client);
return fn({ hits: { hits: [] } }).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should not upgrade even if a snapshot exists', function () {
config.package.version = '4.0.1-snapshot';
client.create.rejects(new Error('DocumentAlreadyExistsException'));
var fn = upgradeConfig(client);
return fn(upgradeFrom4_0_0_to_4_0_1_snapshot).finally(function () {
sinon.assert.notCalled(client.create);
});
});
it('should upgrade from 4.0.0 to 4.0.1', function () {
config.package.version = '4.0.1';
config.buildNum = 5921;
var fn = upgradeConfig(client);
client.create.resolves({ _index: '.kibana', _type: 'config', _id: '4.0.1', _version: 1, created: true });
return fn(upgradeFrom4_0_0).finally(function () {
sinon.assert.calledOnce(client.create);
var body = client.create.args[0][0];
expect(body).to.eql({
index: '.kibana',
type: 'config',
id: '4.0.1',
body: {
'buildNum': 5921,
'defaultIndex': 'logstash-*'
}
});
});
});
});

View file

@ -0,0 +1,78 @@
define(function (require) {
describe('AggConfig Filters', function () {
describe('IP range', function () {
var AggConfig;
var indexPattern;
var Vis;
var createFilter;
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
Vis = Private(require('components/vis/vis'));
AggConfig = Private(require('components/vis/_agg_config'));
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
createFilter = Private(require('components/agg_types/buckets/create_filter/ip_range'));
}));
it('should return a range filter for ip_range agg', function () {
var vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{
type: 'ip_range',
schema: 'segment',
params: {
field: 'ip',
ipRangeType: 'fromTo',
ranges: {
fromTo: [
{ from: '0.0.0.0', to: '1.1.1.1' }
]
}
}
}
]
});
var aggConfig = vis.aggs.byTypeName.ip_range[0];
var filter = createFilter(aggConfig, '0.0.0.0-1.1.1.1');
expect(filter).to.have.property('range');
expect(filter).to.have.property('meta');
expect(filter.meta).to.have.property('index', indexPattern.id);
expect(filter.range).to.have.property('ip');
expect(filter.range.ip).to.have.property('gte', '0.0.0.0');
expect(filter.range.ip).to.have.property('lte', '1.1.1.1');
});
it('should return a range filter for ip_range agg using a CIDR mask', function () {
var vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{
type: 'ip_range',
schema: 'segment',
params: {
field: 'ip',
ipRangeType: 'mask',
ranges: {
mask: [
{ mask: '67.129.65.201/27' }
]
}
}
}
]
});
var aggConfig = vis.aggs.byTypeName.ip_range[0];
var filter = createFilter(aggConfig, '67.129.65.201/27');
expect(filter).to.have.property('range');
expect(filter).to.have.property('meta');
expect(filter.meta).to.have.property('index', indexPattern.id);
expect(filter.range).to.have.property('ip');
expect(filter.range.ip).to.have.property('gte', '67.129.65.192');
expect(filter.range.ip).to.have.property('lte', '67.129.65.223');
});
});
});
});

View file

@ -0,0 +1,39 @@
define(function (require) {
var angular = require('angular');
require('directives/auto_select_if_only_one');
describe('Auto-select if only one directive', function () {
var $compile, $rootScope;
var html = '<select ng-model="value" ng-options="option.name for option in options" auto-select-if-only-one="options"></select>';
var zeroOptions = [];
var oneOption = [{label: 'foo'}];
var multiOptions = [{label: 'foo'}, {label: 'bar'}];
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$compile(html)($rootScope);
$rootScope.value = null;
}));
it('should not auto-select if there are no options', function () {
$rootScope.options = zeroOptions;
$rootScope.$digest();
expect($rootScope.value).to.not.be.ok();
});
it('should not auto-select if there are multiple options', function () {
$rootScope.options = multiOptions;
$rootScope.$digest();
expect($rootScope.value).to.not.be.ok();
});
it('should auto-select if there is only one option', function () {
$rootScope.options = oneOption;
$rootScope.$digest();
expect($rootScope.value).to.be(oneOption[0]);
});
});
});

View file

@ -0,0 +1,51 @@
define(function (require) {
var angular = require('angular');
var $ = require('jquery');
require('directives/input_focus');
describe('Input focus directive', function () {
var $compile, $rootScope, $timeout, element;
var $el, selectedEl, selectedText;
var inputValue = 'Input Text Value';
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_, _$timeout_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$timeout = _$timeout_;
$el = $('<div>');
$el.appendTo('body');
}));
afterEach(function () {
$el.remove();
$el = null;
});
function renderEl(html) {
$rootScope.value = inputValue;
element = $compile(html)($rootScope);
element.appendTo($el);
$rootScope.$digest();
$timeout.flush();
selectedEl = document.activeElement;
selectedText = window.getSelection().toString();
}
it('should focus the input', function () {
renderEl('<input type="text" ng-model="value" input-focus />');
expect(selectedEl).to.equal(element[0]);
expect(selectedText.length).to.equal(0);
});
it('should select the text in the input', function () {
renderEl('<input type="text" ng-model="value" input-focus="select" />');
expect(selectedEl).to.equal(element[0]);
expect(selectedText.length).to.equal(inputValue.length);
expect(selectedText).to.equal(inputValue);
});
});
});

View file

@ -111,6 +111,14 @@ define(function (require) {
done();
});
it('should highlight the current active interval', function (done) {
$scope.setRefreshInterval({ value: 300000 });
$elem.scope().$digest();
expect($elem.find('.refresh-interval-active').length).to.be(1);
expect($elem.find('.refresh-interval-active').text().trim()).to.be('5 minutes');
done();
});
it('should default the interval on the courier with incorrect values', function (done) {
// Change refresh interval and digest
$scope.setRefreshInterval('undefined');

View file

@ -0,0 +1,72 @@
define(function (require) {
var angular = require('angular');
require('directives/validate_cidr_mask');
describe('Validate CIDR mask directive', function () {
var $compile, $rootScope;
var html = '<input type="text" ng-model="value" validate-cidr-mask />';
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should allow valid CIDR masks', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '0.0.0.0/1';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '128.0.0.1/31';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '1.2.3.4/2';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '67.129.65.201/27';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should disallow invalid CIDR masks', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = 'hello, world';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '0.0.0.0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '0.0.0.0/0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '0.0.0.0/33';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '256.0.0.0/32';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '0.0.0.0/32/32';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '1.2.3/1';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
});
});
});

View file

@ -0,0 +1,64 @@
define(function (require) {
var angular = require('angular');
require('directives/validate_ip');
describe('Validate IP directive', function () {
var $compile, $rootScope;
var html = '<input type="text" ng-model="value" validate-ip />';
beforeEach(module('kibana'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should allow valid IP addresses', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '0.0.0.0';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '0.0.0.1';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '126.45.211.34';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
$rootScope.value = '255.255.255.255';
$rootScope.$digest();
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should disallow invalid IP addresses', function () {
var element = $compile(html)($rootScope);
$rootScope.value = '';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = 'hello, world';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '0.0.0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '256.0.0.0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = '-1.0.0.0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
$rootScope.value = Number.MAX_SAFE_INTEGER;
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
});
});
});

View file

@ -0,0 +1,78 @@
define(function (require) {
var CidrMask = require('utils/cidr_mask');
describe('CidrMask', function () {
it('should throw errors with invalid CIDR masks', function () {
expect(function () {
new CidrMask();
}).to.throwError();
expect(function () {
new CidrMask('');
}).to.throwError();
expect(function () {
new CidrMask('hello, world');
}).to.throwError();
expect(function () {
new CidrMask('0.0.0.0');
}).to.throwError();
expect(function () {
new CidrMask('0.0.0.0/0');
}).to.throwError();
expect(function () {
new CidrMask('0.0.0.0/33');
}).to.throwError();
expect(function () {
new CidrMask('256.0.0.0/32');
}).to.throwError();
expect(function () {
new CidrMask('0.0.0.0/32/32');
}).to.throwError();
expect(function () {
new CidrMask('1.2.3/1');
}).to.throwError();
});
it('should correctly grab IP address and prefix length', function () {
var mask = new CidrMask('0.0.0.0/1');
expect(mask.initialAddress.toString()).to.be('0.0.0.0');
expect(mask.prefixLength).to.be(1);
mask = new CidrMask('128.0.0.1/31');
expect(mask.initialAddress.toString()).to.be('128.0.0.1');
expect(mask.prefixLength).to.be(31);
});
it('should calculate a range of IP addresses', function () {
var mask = new CidrMask('0.0.0.0/1');
var range = mask.getRange();
expect(range.from.toString()).to.be('0.0.0.0');
expect(range.to.toString()).to.be('127.255.255.255');
mask = new CidrMask('1.2.3.4/2');
range = mask.getRange();
expect(range.from.toString()).to.be('0.0.0.0');
expect(range.to.toString()).to.be('63.255.255.255');
mask = new CidrMask('67.129.65.201/27');
range = mask.getRange();
expect(range.from.toString()).to.be('67.129.65.192');
expect(range.to.toString()).to.be('67.129.65.223');
});
it('toString()', function () {
var mask = new CidrMask('.../1');
expect(mask.toString()).to.be('0.0.0.0/1');
mask = new CidrMask('128.0.0.1/31');
expect(mask.toString()).to.be('128.0.0.1/31');
});
});
});

View file

@ -0,0 +1,58 @@
define(function (require) {
var Ipv4Address = require('utils/ipv4_address');
describe('Ipv4Address', function () {
it('should throw errors with invalid IP addresses', function () {
expect(function () {
new Ipv4Address();
}).to.throwError();
expect(function () {
new Ipv4Address('');
}).to.throwError();
expect(function () {
new Ipv4Address('hello, world');
}).to.throwError();
expect(function () {
new Ipv4Address('0.0.0');
}).to.throwError();
expect(function () {
new Ipv4Address('256.0.0.0');
}).to.throwError();
expect(function () {
new Ipv4Address('-1.0.0.0');
}).to.throwError();
expect(function () {
new Ipv4Address(Number.MAX_SAFE_INTEGER);
}).to.throwError();
});
it('should allow creation with an integer or string', function () {
expect(new Ipv4Address(2116932386).toString()).to.be(new Ipv4Address('126.45.211.34').toString());
});
it('should correctly calculate the decimal representation of an IP address', function () {
var ipAddress = new Ipv4Address('0.0.0.0');
expect(ipAddress.valueOf()).to.be(0);
ipAddress = new Ipv4Address('0.0.0.1');
expect(ipAddress.valueOf()).to.be(1);
ipAddress = new Ipv4Address('126.45.211.34');
expect(ipAddress.valueOf()).to.be(2116932386);
});
it('toString()', function () {
var ipAddress = new Ipv4Address('0.000.00000.1');
expect(ipAddress.toString()).to.be('0.0.0.1');
ipAddress = new Ipv4Address('123.123.123.123');
expect(ipAddress.toString()).to.be('123.123.123.123');
});
});
});

View file

@ -48,6 +48,33 @@ define(function (require) {
vis = null;
});
describe('legend item color matches slice color', function () {
var items;
var paths;
var getColor;
if (chartTypes[i] === 'pie') {
it('should match the slice color', function () {
paths = $(vis.el).find('path').toArray();
items = vis.handler.legend.labels;
getColor = vis.handler.legend.color;
items.forEach(function (label) {
var slices = paths.filter(function (path) {
if (path.__data__.name === undefined) return false;
return path.__data__.name.toString() === label;
}).map(function (path) {
return $(path).attr('class').split(/\s+/)[1].replace('c', '#');
});
slices.forEach(function (hex) {
expect(hex).to.be(getColor(label));
});
});
});
}
});
describe('header method', function () {
it('should append the legend header', function () {
expect($(vis.el).find('.header').length).to.be(1);

View file

@ -1,4 +1,5 @@
var updateVersion = require('../../../../tasks/utils/updateVersion');
var expect = require('expect.js');
describe('tasks/utils/updateVersion', function () {
@ -29,5 +30,4 @@ describe('tasks/utils/updateVersion', function () {
it('changes a tag', function () {
expect(updateVersion('4.1.0-snapshot', 'tag=rc1')).to.be('4.1.0-rc1');
});
});
});