mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge pull request #212 from w33ble/feature/170
Typeahead History - FINALLY!
This commit is contained in:
commit
c2d27c7bbf
23 changed files with 1018 additions and 73 deletions
4
TODOS.md
4
TODOS.md
|
@ -31,4 +31,6 @@
|
|||
- **test/unit/specs/apps/dashboard/directives/panel.js**
|
||||
- This should not be needed, timefilter is only included here – [L14](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/apps/dashboard/directives/panel.js#L14)
|
||||
- **test/unit/specs/directives/timepicker.js**
|
||||
- This should not be needed, timefilter is only included here, it should move – [L17](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/directives/timepicker.js#L17)
|
||||
- This should not be needed, timefilter is only included here, it should move – [L17](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/directives/timepicker.js#L17)
|
||||
- **test/unit/specs/directives/typeahead.js**
|
||||
- This should not be needed, timefilter is only included here, it should move – [L12](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/directives/typeahead.js#L12)
|
|
@ -60,9 +60,11 @@
|
|||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<config
|
||||
config-template="globalConfigTemplate" config-object="opts">
|
||||
</config>
|
||||
|
||||
<div class="application" ng-view></div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
<div dashboard-app class="dashboard-container">
|
||||
<div dashboard-app class="app-container dashboard-container">
|
||||
<nav ng-show="!appEmbedded" class="navbar navbar-default navbar-static-top">
|
||||
<navbar>
|
||||
<span class="name">
|
||||
{{dash.title}}
|
||||
</span>
|
||||
|
||||
<form class="fill inline-form" ng-submit="filterResults()" name="queryInput">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
<input query-input
|
||||
placeholder="Filter..."
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="state.query">
|
||||
<button class="btn btn-default" type="submit"
|
||||
ng-disabled="queryInput.$invalid">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
<form name="queryInput"
|
||||
class="fill inline-form"
|
||||
ng-submit="filterResults()">
|
||||
|
||||
<div class="typeahead" kbn-typeahead="dashboard">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
|
||||
<input type="text"
|
||||
placeholder="Filter..."
|
||||
class="form-control"
|
||||
ng-model="state.query"
|
||||
kbn-typeahead-input
|
||||
query-input>
|
||||
<button type="submit" class="btn btn-default" ng-disabled="queryInput.$invalid">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="button-group">
|
||||
|
@ -29,6 +38,8 @@
|
|||
</div>
|
||||
</navbar>
|
||||
</nav>
|
||||
|
||||
<config config-template="configTemplate" config-object="opts"></config>
|
||||
|
||||
<dashboard-grid></dashboard-grid>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ define(function (require) {
|
|||
require('components/courier/courier');
|
||||
require('components/config/config');
|
||||
require('components/notify/notify');
|
||||
require('components/typeahead/typeahead');
|
||||
|
||||
require('apps/dashboard/directives/grid');
|
||||
require('apps/dashboard/directives/panel');
|
||||
|
@ -18,7 +19,8 @@ define(function (require) {
|
|||
'ngRoute',
|
||||
'kibana/courier',
|
||||
'kibana/config',
|
||||
'kibana/notify'
|
||||
'kibana/notify',
|
||||
'kibana/typeahead'
|
||||
]);
|
||||
|
||||
require('routes')
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
<div ng-controller="discover">
|
||||
<div ng-controller="discover" class="app-container">
|
||||
<navbar>
|
||||
<form class="fill inline-form" ng-submit="fetch()" name="discoverSearch">
|
||||
<div class="input-group"
|
||||
ng-class="discoverSearch.$invalid ? 'has-error' : ''">
|
||||
<input query-input="searchSource"
|
||||
ng-model="state.query"
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
class="form-control">
|
||||
<button type="submit"
|
||||
ng-disabled="discoverSearch.$invalid">
|
||||
<span class="fa fa-search"></span></button>
|
||||
<button type="button" ng-click="resetQuery()"><span class="fa fa-ban"></span></button>
|
||||
<div class="typeahead" kbn-typeahead="discover">
|
||||
<div class="input-group"
|
||||
ng-class="discoverSearch.$invalid ? 'has-error' : ''">
|
||||
<input query-input="searchSource"
|
||||
kbn-typeahead-input
|
||||
ng-model="state.query"
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
class="form-control">
|
||||
<button type="submit"
|
||||
ng-disabled="discoverSearch.$invalid">
|
||||
<span class="fa fa-search"></span></button>
|
||||
<button type="button" ng-click="resetQuery()"><span class="fa fa-ban"></span></button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -21,6 +25,7 @@
|
|||
<button ng-click="toggleConfig()"><i class="fa fa-gear"></i></button>
|
||||
</div>
|
||||
</navbar>
|
||||
|
||||
<config config-template="configTemplate" config-object="opts" config-close="configClose" config-submit="fetch"></config>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
@ -75,12 +80,14 @@
|
|||
>
|
||||
</select> -->
|
||||
</center>
|
||||
|
||||
<div ng-show="searchSource.activeFetchCount" class="discover-overlay">
|
||||
<div class="spinner large">
|
||||
</div>
|
||||
<div class="spinner large"> </div>
|
||||
</div>
|
||||
|
||||
<visualize vis="vis" es-resp="mergedEsResp"></visualize>
|
||||
</div>
|
||||
|
||||
<div class="discover-table"
|
||||
fixed-scroll='table'
|
||||
fixed-scroll-trigger="state.columns">
|
||||
|
@ -95,6 +102,7 @@
|
|||
timefield="opts.timefield"
|
||||
mapping="fieldsByName">
|
||||
</kbn-table>
|
||||
|
||||
<div ng-if="rows.length == opts.sampleSize" class="discover-table-footer">
|
||||
<center>
|
||||
These are the first {{opts.sampleSize}} results matching your query,
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<nav class="navbar navbar-default navbar-static-top subnav">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav navbar-nav">
|
||||
<li ng-repeat="s in sections" ng-class="s.class">
|
||||
<a class="navbar-link" ng-href="{{s.url}}">{{s.display}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="settings-section-container {{section.name}}-settings" ng-transclude></div>
|
||||
<div class="app-container">
|
||||
<nav class="navbar navbar-default navbar-static-top subnav">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav navbar-nav">
|
||||
<li ng-repeat="s in sections" ng-class="s.class">
|
||||
<a class="navbar-link" ng-href="{{s.url}}">{{s.display}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="settings-section-container {{section.name}}-settings" ng-transclude></div>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<div ng-controller="VisualizeEditor" class="vis-editor">
|
||||
|
||||
<div ng-if="!appEmbedded">
|
||||
<div ng-controller="VisualizeEditor" class="vis-editor">
|
||||
<div ng-if="!appEmbedded" class="app-container">
|
||||
<navbar>
|
||||
<span ng-if="!!vis.title" class="name" ng-bind="vis.title"></span>
|
||||
<div class="fill bitty-modal-container">
|
||||
<div ng-if="linked && !unlinking"
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="Double click to unlink this visualization from the saved search"
|
||||
class="bitty-modal visualize-linked">
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="Double click to unlink this visualization from the saved search"
|
||||
class="bitty-modal visualize-linked">
|
||||
<i class="fa fa-link"></i>
|
||||
|
||||
This visualization is linked to a saved search:
|
||||
|
@ -22,17 +21,22 @@
|
|||
</div>
|
||||
|
||||
<form ng-submit="doVisualize()" class="inline-form" name="queryInput">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
<input query-input="vis.searchSource"
|
||||
<div class="typeahead" kbn-typeahead="visualize">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
<input query-input="vis.searchSource"
|
||||
kbn-typeahead-input
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="state.query">
|
||||
<button class="btn btn-default" type="submit"
|
||||
|
||||
<button class="btn btn-default" type="submit"
|
||||
ng-disabled="queryInput.$invalid">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -45,10 +49,12 @@
|
|||
<button ng-click="doVisualize()"><i class="fa fa-refresh"></i></button>
|
||||
</div>
|
||||
</navbar>
|
||||
|
||||
<config
|
||||
config-template="configTemplate"
|
||||
config-object="conf">
|
||||
</config>
|
||||
|
||||
<div class="vis-editor-content">
|
||||
<div class="vis-sidebar">
|
||||
<div class="sidebar-container">
|
||||
|
@ -56,24 +62,25 @@
|
|||
<ul class="list-unstyled">
|
||||
<li ng-repeat="category in visConfigCategories.displayOrder" class="sidebar-item">
|
||||
<vis-config-category
|
||||
vis="vis"
|
||||
category="vis[category.name]"
|
||||
fields="fields">
|
||||
vis="vis"
|
||||
category="vis[category.name]"
|
||||
fields="fields">
|
||||
</vis-config-category>
|
||||
</li>
|
||||
<li class="sidebar-item">
|
||||
<button
|
||||
ng-click="doVisualize()"
|
||||
ng-if="vis.dirty"
|
||||
ng-disabled="httpActive.length || visualizeEditor.$invalid"
|
||||
class="sidebar-item-button success">
|
||||
Apply
|
||||
ng-click="doVisualize()"
|
||||
ng-if="vis.dirty"
|
||||
ng-disabled="httpActive.length || visualizeEditor.$invalid"
|
||||
class="sidebar-item-button success">
|
||||
Apply
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vis-canvas">
|
||||
<vis-canvas><visualize vis="vis"></visualize></vis-canvas>
|
||||
</div>
|
||||
|
@ -87,4 +94,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,8 @@ define(function (require) {
|
|||
'histogram:maxBars': 100,
|
||||
|
||||
'csv:separator': ',',
|
||||
'csv:quoteValues': true
|
||||
'csv:quoteValues': true,
|
||||
|
||||
'history:limit': 10
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@ define(function (require) {
|
|||
|
||||
var inherits = require('lodash').inherits;
|
||||
|
||||
return function DocSourceFactory(Private, Promise, es) {
|
||||
return function DocSourceFactory(Private, Promise, es, storage) {
|
||||
var sendToEs = Private(require('components/courier/data_source/_doc_send_to_es'));
|
||||
var SourceAbstract = Private(require('components/courier/data_source/_abstract'));
|
||||
|
||||
|
@ -18,6 +18,7 @@ define(function (require) {
|
|||
this.onUpdate = this.onResults;
|
||||
this.onResults = void 0;
|
||||
}
|
||||
|
||||
inherits(DocSource, SourceAbstract);
|
||||
|
||||
/*****
|
||||
|
@ -115,20 +116,20 @@ define(function (require) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Fetches the stored version from localStorage
|
||||
* Fetches the stored version from storage
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
DocSource.prototype._getStoredVersion = function () {
|
||||
var key = this._versionKey();
|
||||
if (!key) return;
|
||||
|
||||
var v = localStorage.getItem(key);
|
||||
var v = storage.get(key);
|
||||
this._version = v ? _.parseInt(v) : void 0;
|
||||
return this._version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores the version into localStorage
|
||||
* Stores the version into storage
|
||||
* @param {number, NaN} version - the current version number, NaN works well forcing a refresh
|
||||
* @return {undefined}
|
||||
*/
|
||||
|
@ -138,7 +139,7 @@ define(function (require) {
|
|||
var key = this._versionKey();
|
||||
if (!key) return;
|
||||
this._version = version;
|
||||
localStorage.setItem(key, version);
|
||||
storage.set(key, version);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -147,7 +148,7 @@ define(function (require) {
|
|||
DocSource.prototype._clearVersion = function () {
|
||||
var key = this._versionKey();
|
||||
if (!key) return;
|
||||
localStorage.removeItem(key);
|
||||
storage.remove(key);
|
||||
};
|
||||
|
||||
return DocSource;
|
||||
|
|
48
src/kibana/components/persisted_log/persisted_log.js
Normal file
48
src/kibana/components/persisted_log/persisted_log.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
define(function (require) {
|
||||
var modules = require('modules');
|
||||
var _ = require('lodash');
|
||||
|
||||
modules.get('kibana/persisted_log')
|
||||
.factory('PersistedLog', function ($window, storage) {
|
||||
function PersistedLog(name, options) {
|
||||
options = options || {};
|
||||
this.name = name;
|
||||
this.maxLength = options.maxLength || 0;
|
||||
this.filterDuplicates = options.filterDuplicates || false;
|
||||
this.items = storage.get(this.name) || [];
|
||||
}
|
||||
|
||||
PersistedLog.prototype.add = function (val) {
|
||||
if (val == null) {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
var stack = this.items;
|
||||
|
||||
// remove any matching items from the stack if option is set
|
||||
if (this.filterDuplicates) {
|
||||
stack = _.pull(this.items, val);
|
||||
stack = _.filter(stack, function (item) {
|
||||
return !_.isEqual(item, val);
|
||||
});
|
||||
}
|
||||
|
||||
stack.unshift(val);
|
||||
|
||||
// if maxLength is set, truncate the stack
|
||||
if (this.maxLength > 0) {
|
||||
stack = stack.slice(0, this.maxLength);
|
||||
}
|
||||
|
||||
// persist the stack
|
||||
storage.set(this.name, stack);
|
||||
return this.items = stack;
|
||||
};
|
||||
|
||||
PersistedLog.prototype.get = function () {
|
||||
return this.items;
|
||||
};
|
||||
|
||||
return PersistedLog;
|
||||
});
|
||||
});
|
37
src/kibana/components/storage/storage.js
Normal file
37
src/kibana/components/storage/storage.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
define(function (require) {
|
||||
var modules = require('modules');
|
||||
|
||||
modules.get('kibana/storage')
|
||||
.service('storage', function ($window) {
|
||||
function Storage() {
|
||||
var self = this;
|
||||
self.store = $window.localStorage;
|
||||
|
||||
self.get = function (key) {
|
||||
try {
|
||||
return JSON.parse(self.store.getItem(key));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
self.set = function (key, value) {
|
||||
try {
|
||||
return self.store.setItem(key, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
self.remove = function (key) {
|
||||
return self.store.removeItem(key);
|
||||
};
|
||||
|
||||
self.clear = function () {
|
||||
return self.store.clear();
|
||||
};
|
||||
}
|
||||
|
||||
return new Storage();
|
||||
});
|
||||
});
|
47
src/kibana/components/typeahead/_input.js
Normal file
47
src/kibana/components/typeahead/_input.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var typeahead = require('modules').get('kibana/typeahead');
|
||||
|
||||
require('components/notify/directives');
|
||||
|
||||
typeahead.directive('kbnTypeaheadInput', function ($rootScope) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: ['^ngModel', '^kbnTypeahead'],
|
||||
|
||||
link: function ($scope, $el, $attr, deps) {
|
||||
var model = deps[0];
|
||||
var typeaheadCtrl = deps[1];
|
||||
|
||||
typeaheadCtrl.setInputModel(model);
|
||||
|
||||
// handle keypresses
|
||||
$el.on('keydown', function (ev) {
|
||||
typeaheadCtrl.keypressHandler(ev);
|
||||
digest();
|
||||
});
|
||||
|
||||
// update focus state based on the input focus state
|
||||
$el.on('focus', function () {
|
||||
typeaheadCtrl.setFocused(true);
|
||||
digest();
|
||||
});
|
||||
|
||||
$el.on('blur', function () {
|
||||
typeaheadCtrl.setFocused(false);
|
||||
digest();
|
||||
});
|
||||
|
||||
// unbind event listeners
|
||||
$scope.$on('$destroy', function () {
|
||||
$el.off();
|
||||
});
|
||||
|
||||
function digest() {
|
||||
$rootScope.$$phase || $scope.$digest();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
20
src/kibana/components/typeahead/_items.js
Normal file
20
src/kibana/components/typeahead/_items.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var typeahead = require('modules').get('kibana/typeahead');
|
||||
var listTemplate = require('text!components/typeahead/partials/typeahead-items.html');
|
||||
|
||||
require('components/notify/directives');
|
||||
|
||||
typeahead.directive('kbnTypeaheadItems', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^kbnTypeahead',
|
||||
replace: true,
|
||||
template: listTemplate,
|
||||
|
||||
link: function ($scope, $el, attr, typeaheadCtrl) {
|
||||
$scope.typeahead = typeaheadCtrl;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
<div
|
||||
ng-show="typeahead.isVisible()"
|
||||
ng-mouseenter="typeahead.setMouseover(true);"
|
||||
ng-mouseleave="typeahead.setMouseover(false);"
|
||||
class="typeahead-items"
|
||||
>
|
||||
|
||||
<div
|
||||
ng-repeat="item in typeahead.getItems()"
|
||||
ng-class="{active: item === typeahead.active}"
|
||||
ng-click="typeahead.selectItem(item, $event);"
|
||||
ng-mouseenter="typeahead.activateItem(item);"
|
||||
class="typeahead-item"
|
||||
>
|
||||
{{item}}
|
||||
</div>
|
||||
</div>
|
240
src/kibana/components/typeahead/typeahead.js
Normal file
240
src/kibana/components/typeahead/typeahead.js
Normal file
|
@ -0,0 +1,240 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var typeahead = require('modules').get('kibana/typeahead');
|
||||
|
||||
require('components/typeahead/_input');
|
||||
require('components/typeahead/_items');
|
||||
|
||||
typeahead.directive('kbnTypeahead', function () {
|
||||
var keyMap = {
|
||||
ESC: 27,
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
TAB: 9,
|
||||
ENTER: 13
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
historyKey: '@kbnTypeahead'
|
||||
},
|
||||
controllerAs: 'typeahead',
|
||||
|
||||
controller: function ($scope, $element, $timeout, PersistedLog, config) {
|
||||
var self = this;
|
||||
self.form = $element.closest('form');
|
||||
self.query = '';
|
||||
self.hidden = true;
|
||||
self.focused = false;
|
||||
self.mousedOver = false;
|
||||
|
||||
// instantiate history and add items to the scope
|
||||
self.history = new PersistedLog('typeahead:' + $scope.historyKey, {
|
||||
maxLength: config.get('history:limit'),
|
||||
filterDuplicates: true
|
||||
});
|
||||
|
||||
$scope.items = self.history.get();
|
||||
$scope.filteredItems = [];
|
||||
|
||||
self.setInputModel = function (model) {
|
||||
$scope.inputModel = model;
|
||||
|
||||
// watch for changes to the query parameter, delegate to typeaheadCtrl
|
||||
$scope.$watch('inputModel.$viewValue', self.filterItemsByQuery);
|
||||
};
|
||||
|
||||
self.setHidden = function (hidden) {
|
||||
self.hidden = !!(hidden);
|
||||
};
|
||||
|
||||
self.setFocused = function (focused) {
|
||||
self.focused = !!(focused);
|
||||
};
|
||||
|
||||
self.setMouseover = function (mousedOver) {
|
||||
self.mousedOver = !!(mousedOver);
|
||||
};
|
||||
|
||||
// activation methods
|
||||
self.activateItem = function (item) {
|
||||
self.active = item;
|
||||
};
|
||||
|
||||
self.getActiveIndex = function () {
|
||||
if (!self.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $scope.filteredItems.indexOf(self.active);
|
||||
};
|
||||
|
||||
self.getItems = function () {
|
||||
return $scope.filteredItems;
|
||||
};
|
||||
|
||||
self.activateNext = function () {
|
||||
var index = self.getActiveIndex();
|
||||
if (index == null) {
|
||||
index = 0;
|
||||
} else if (index < $scope.filteredItems.length - 1) {
|
||||
++index;
|
||||
}
|
||||
|
||||
self.activateItem($scope.filteredItems[index]);
|
||||
};
|
||||
|
||||
self.activatePrev = function () {
|
||||
var index = self.getActiveIndex();
|
||||
|
||||
if (index > 0 && index != null) {
|
||||
--index;
|
||||
} else if (index === 0) {
|
||||
self.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
self.activateItem($scope.filteredItems[index]);
|
||||
};
|
||||
|
||||
self.isActive = function (item) {
|
||||
return item === self.active;
|
||||
};
|
||||
|
||||
// selection methods
|
||||
self.selectItem = function (item, ev) {
|
||||
self.hidden = true;
|
||||
self.active = false;
|
||||
$scope.inputModel.$setViewValue(item);
|
||||
$scope.inputModel.$render();
|
||||
self.persistEntry();
|
||||
|
||||
if (ev && ev.type === 'click') {
|
||||
$timeout(function () {
|
||||
self.submitForm();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.submitForm = function () {
|
||||
if (self.form.length) {
|
||||
self.form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
self.persistEntry = function () {
|
||||
if ($scope.inputModel.$viewValue.length) {
|
||||
// push selection into the history
|
||||
$scope.items = self.history.add($scope.inputModel.$viewValue);
|
||||
}
|
||||
};
|
||||
|
||||
self.selectActive = function () {
|
||||
if (self.active) {
|
||||
self.selectItem(self.active);
|
||||
}
|
||||
};
|
||||
|
||||
self.keypressHandler = function (ev) {
|
||||
var keyCode = ev.which || ev.keyCode;
|
||||
|
||||
if (self.focused) {
|
||||
self.hidden = false;
|
||||
}
|
||||
|
||||
// hide on escape
|
||||
if (_.contains([keyMap.ESC], keyCode)) {
|
||||
self.hidden = true;
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
// change selection with arrow up/down
|
||||
// on down key, attempt to load all items if none are loaded
|
||||
if (_.contains([keyMap.DOWN], keyCode) && $scope.filteredItems.length === 0) {
|
||||
$scope.filteredItems = $scope.items;
|
||||
$scope.$digest();
|
||||
} else if (_.contains([keyMap.UP, keyMap.DOWN], keyCode)) {
|
||||
if (self.isVisible() && $scope.filteredItems.length) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (keyCode === keyMap.DOWN) {
|
||||
self.activateNext();
|
||||
} else {
|
||||
self.activatePrev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// persist selection on enter, when not selecting from the list
|
||||
if (_.contains([keyMap.ENTER], keyCode)) {
|
||||
if (! self.active) {
|
||||
self.persistEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// select on enter or tab
|
||||
if (_.contains([keyMap.ENTER, keyMap.TAB], keyCode)) {
|
||||
self.selectActive();
|
||||
self.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
self.filterItemsByQuery = function (query) {
|
||||
// cache query so we can call it again if needed
|
||||
if (query) {
|
||||
self.query = query;
|
||||
}
|
||||
|
||||
// if the query is empty, clear the list items
|
||||
if (!self.query.length) {
|
||||
$scope.filteredItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// update the filteredItems using the query
|
||||
var beginningMatches = $scope.items.filter(function (item) {
|
||||
return item.indexOf(query) === 0;
|
||||
});
|
||||
|
||||
var otherMatches = $scope.items.filter(function (item) {
|
||||
return item.indexOf(query) > 0;
|
||||
});
|
||||
|
||||
$scope.filteredItems = beginningMatches.concat(otherMatches);
|
||||
};
|
||||
|
||||
self.isVisible = function () {
|
||||
return !self.hidden && ($scope.filteredItems.length > 0) && (self.focused || self.mousedOver);
|
||||
};
|
||||
|
||||
// handle updates to parent scope history
|
||||
$scope.$watch('items', function (items) {
|
||||
if (self.query) {
|
||||
self.filterItemsByQuery(self.query);
|
||||
}
|
||||
});
|
||||
|
||||
// watch for changes to the filtered item list
|
||||
$scope.$watch('filteredItems', function (filteredItems) {
|
||||
|
||||
// if list is empty, or active item is missing, unset active item
|
||||
if (!filteredItems.length || !_.contains(filteredItems, self.active)) {
|
||||
self.active = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
link: function ($scope, $el, attr) {
|
||||
// should be defined via setInput() method
|
||||
if (!$scope.inputModel) {
|
||||
throw new Error('kbn-typeahead-input must be defined');
|
||||
}
|
||||
|
||||
$scope.$watch('typeahead.isVisible()', function (vis) {
|
||||
$el.toggleClass('visible', vis);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -11,6 +11,8 @@ define(function (require) {
|
|||
require('components/notify/notify');
|
||||
require('components/state_management/app_state_factory');
|
||||
require('components/filter_bar/filter_bar');
|
||||
require('components/storage/storage');
|
||||
require('components/persisted_log/persisted_log');
|
||||
require('directives/info');
|
||||
require('directives/spinner');
|
||||
require('directives/paginate');
|
||||
|
@ -49,7 +51,7 @@ define(function (require) {
|
|||
config.init()
|
||||
]).then(function () {
|
||||
$scope.setupComplete = true;
|
||||
$injector.invoke(function ($rootScope, courier, config, configFile, $timeout, $location, timefilter, globalState) {
|
||||
$injector.invoke(function ($rootScope, courier, config, configFile, storage, $timeout, $location, timefilter, globalState) {
|
||||
|
||||
$rootScope.globalState = globalState;
|
||||
|
||||
|
@ -86,16 +88,16 @@ define(function (require) {
|
|||
var lastPathFor = function (app, path) {
|
||||
var key = 'lastPath:' + app.id;
|
||||
if (path === void 0) {
|
||||
app.lastPath = localStorage.getItem(key) || '/' + app.id;
|
||||
app.lastPath = storage.get(key) || '/' + app.id;
|
||||
return app.lastPath;
|
||||
} else {
|
||||
app.lastPath = path;
|
||||
return localStorage.setItem(key, path);
|
||||
return storage.set(key, path);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.apps = configFile.apps;
|
||||
// initialize each apps lastPath (fetch it from localStorage)
|
||||
// initialize each apps lastPath (fetch it from storage)
|
||||
$scope.apps.forEach(function (app) { lastPathFor(app); });
|
||||
|
||||
function onRouteChange() {
|
||||
|
|
35
src/kibana/styles/_typeahead.less
Normal file
35
src/kibana/styles/_typeahead.less
Normal file
|
@ -0,0 +1,35 @@
|
|||
.typeahead {
|
||||
position: relative;
|
||||
|
||||
.typeahead-items {
|
||||
border: 1px solid @popover-border-color;
|
||||
color: @text-color;
|
||||
background-color: @body-bg;
|
||||
position: absolute;
|
||||
z-index: @zindex-typeahead;
|
||||
width: 100%;
|
||||
|
||||
.typeahead-item {
|
||||
border-bottom: 1px solid lighten(@popover-border-color, 60%);
|
||||
padding: @padding-base-vertical @padding-base-horizontal;
|
||||
}
|
||||
|
||||
.typeahead-item:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.typeahead-item.active {
|
||||
background-color: @well-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-group .typeahead.visible .input-group,
|
||||
.inline-form .typeahead.visible .input-group {
|
||||
> :first-child {
|
||||
.border-bottom-radius(0);
|
||||
}
|
||||
> :last-child {
|
||||
.border-bottom-radius(0);
|
||||
}
|
||||
}
|
|
@ -84,11 +84,12 @@ ul.navbar-inline li {
|
|||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
.navbar {
|
||||
> .navbar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.application {
|
||||
|
||||
> .application {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
@ -142,8 +143,25 @@ notifications {
|
|||
}
|
||||
}
|
||||
|
||||
.app-container {
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
> config {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> nav,
|
||||
> navbar {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./_table.less";
|
||||
@import "./_notify.less";
|
||||
@import "./_typeahead.less";
|
||||
|
||||
//== Nav tweaks
|
||||
.nav-condensed > li > a {
|
||||
|
|
|
@ -248,6 +248,7 @@
|
|||
@zindex-navbar: 1000;
|
||||
@zindex-dropdown: 1000;
|
||||
@zindex-popover: 1010;
|
||||
@zindex-typeahead: 1020;
|
||||
@zindex-tooltip: 1030;
|
||||
@zindex-navbar-fixed: 1030;
|
||||
@zindex-modal-background: 1040;
|
||||
|
|
|
@ -63,12 +63,15 @@
|
|||
'specs/apps/discover/hit_sort_fn',
|
||||
'specs/directives/timepicker',
|
||||
'specs/directives/truncate',
|
||||
'specs/directives/typeahead',
|
||||
'specs/directives/css_truncate',
|
||||
'specs/directives/spinner',
|
||||
'specs/filters/field_type',
|
||||
'specs/filters/uriescape',
|
||||
'specs/filters/moment',
|
||||
'specs/filters/start_from',
|
||||
'specs/services/storage',
|
||||
'specs/services/persisted_log',
|
||||
'specs/utils/datemath',
|
||||
'specs/utils/interval',
|
||||
'specs/utils/versionmath',
|
||||
|
|
218
test/unit/specs/directives/typeahead.js
Normal file
218
test/unit/specs/directives/typeahead.js
Normal file
|
@ -0,0 +1,218 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var sinon = require('sinon/sinon');
|
||||
|
||||
// Load the kibana app dependencies.
|
||||
require('angular-route');
|
||||
|
||||
// Load kibana and its applications
|
||||
require('index');
|
||||
require('components/typeahead/typeahead');
|
||||
|
||||
// TODO: This should not be needed, timefilter is only included here, it should move
|
||||
require('apps/discover/index');
|
||||
|
||||
var typeaheadHistoryCount = 10;
|
||||
var typeaheadName = 'unittest';
|
||||
var $parentScope;
|
||||
var $typeaheadScope, $elem;
|
||||
var $typeaheadInputScope;
|
||||
var typeaheadCtrl, PersistedLog;
|
||||
|
||||
var markup = '<div class="typeahead" kbn-typeahead="' + typeaheadName + '">' +
|
||||
'<input type="text" placeholder="Filter..." class="form-control" ng-model="query" kbn-typeahead-input>' +
|
||||
'<kbn-typeahead-items></kbn-typeahead-items>' +
|
||||
'</div>';
|
||||
var typeaheadItems = ['abc', 'def', 'ghi'];
|
||||
|
||||
var init = function () {
|
||||
// Load the application
|
||||
module('kibana');
|
||||
|
||||
module('kibana/typeahead', function ($provide) {
|
||||
$provide.factory('PersistedLog', function () {
|
||||
function PersistedLogMock(name, options) {
|
||||
this.name = name;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
PersistedLogMock.prototype.add = sinon.stub();
|
||||
PersistedLogMock.prototype.get = sinon.stub().returns(typeaheadItems);
|
||||
|
||||
return PersistedLogMock;
|
||||
});
|
||||
|
||||
$provide.service('config', function () {
|
||||
this.get = sinon.stub().returns(typeaheadHistoryCount);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Create the scope
|
||||
inject(function ($injector, $controller, $rootScope, $compile) {
|
||||
// Give us a scope
|
||||
$parentScope = $rootScope;
|
||||
|
||||
$elem = angular.element(markup);
|
||||
|
||||
PersistedLog = $injector.get('PersistedLog');
|
||||
|
||||
$compile($elem)($parentScope);
|
||||
$elem.scope().$digest();
|
||||
$typeaheadScope = $elem.isolateScope();
|
||||
typeaheadCtrl = $elem.controller('kbnTypeahead');
|
||||
});
|
||||
};
|
||||
|
||||
describe('typeahead directive', function () {
|
||||
describe('typeahead requirements', function () {
|
||||
describe('missing input', function () {
|
||||
var goodMarkup = markup;
|
||||
|
||||
before(function () {
|
||||
markup = '<div class="typeahead" kbn-typeahead="' + typeaheadName + '">' +
|
||||
'<kbn-typeahead-items></kbn-typeahead-items>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
after(function () {
|
||||
markup = goodMarkup;
|
||||
});
|
||||
|
||||
it('should throw with message', function () {
|
||||
expect(init).to.throwException(/kbn-typeahead-input must be defined/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('internal functionality', function () {
|
||||
beforeEach(function () {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('PersistedLog', function () {
|
||||
it('should instantiate PersistedLog', function () {
|
||||
expect(typeaheadCtrl.history.name).to.equal('typeahead:' + typeaheadName);
|
||||
expect(typeaheadCtrl.history.options.maxLength).to.equal(typeaheadHistoryCount);
|
||||
expect(typeaheadCtrl.history.options.filterDuplicates).to.equal(true);
|
||||
});
|
||||
|
||||
it('should read data when directive is instantiated', function () {
|
||||
expect(typeaheadCtrl.history.get.callCount).to.be(1);
|
||||
});
|
||||
|
||||
it('should not save empty entries', function () {
|
||||
var entries = typeaheadItems.slice(0);
|
||||
entries.push('', 'jkl');
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
$typeaheadScope.inputModel.$setViewValue(entries[i]);
|
||||
typeaheadCtrl.persistEntry();
|
||||
}
|
||||
expect(typeaheadCtrl.history.add.callCount).to.be(4);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('controller scope', function () {
|
||||
it('should contain the input model', function () {
|
||||
expect($typeaheadScope.inputModel).to.be.an('object');
|
||||
expect($typeaheadScope.inputModel).to.have.keys(['$viewValue', '$modelValue', '$setViewValue']);
|
||||
});
|
||||
|
||||
it('should save data to the scope', function () {
|
||||
// $scope.items is set via history.add, so mock the output
|
||||
typeaheadCtrl.history.add.returns(typeaheadItems);
|
||||
|
||||
// a single call will call history.add, which will respond with the mocked data
|
||||
$typeaheadScope.inputModel.$setViewValue(typeaheadItems[0]);
|
||||
typeaheadCtrl.persistEntry();
|
||||
|
||||
expect($typeaheadScope.items).to.be.an('array');
|
||||
expect($typeaheadScope.items.length).to.be(typeaheadItems.length);
|
||||
});
|
||||
|
||||
it('should order fitlered results', function () {
|
||||
var entries = ['ac/dc', 'anthrax', 'abba', 'phantogram', 'skrillex'];
|
||||
var allEntries = typeaheadItems.concat(entries);
|
||||
var startMatches = allEntries.filter(function (item) {
|
||||
return /^a/.test(item);
|
||||
});
|
||||
typeaheadCtrl.history.add.returns(allEntries);
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
$typeaheadScope.inputModel.$setViewValue(entries[i]);
|
||||
typeaheadCtrl.persistEntry();
|
||||
}
|
||||
|
||||
typeaheadCtrl.filterItemsByQuery('a');
|
||||
|
||||
expect($typeaheadScope.filteredItems).to.contain('phantogram');
|
||||
var nonStarterIndex = $typeaheadScope.filteredItems.indexOf('phantogram');
|
||||
|
||||
startMatches.forEach(function (item) {
|
||||
expect($typeaheadScope.filteredItems).to.contain(item);
|
||||
expect($typeaheadScope.filteredItems.indexOf(item)).to.be.below(nonStarterIndex);
|
||||
});
|
||||
|
||||
expect($typeaheadScope.filteredItems).not.to.contain('skrillex');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list appearance', function () {
|
||||
beforeEach(function () {
|
||||
typeaheadCtrl.history.add.returns(typeaheadItems);
|
||||
$typeaheadScope.inputModel.$setViewValue(typeaheadItems[0]);
|
||||
typeaheadCtrl.persistEntry();
|
||||
|
||||
// make sure the data looks how we expect
|
||||
expect($typeaheadScope.items.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should default to hidden', function () {
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
});
|
||||
|
||||
it('should appear when not hidden, has matches input and focused', function () {
|
||||
typeaheadCtrl.setHidden(false);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
|
||||
typeaheadCtrl.filterItemsByQuery(typeaheadItems[0]);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
|
||||
// only visible when all conditions match
|
||||
typeaheadCtrl.setFocused(true);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(true);
|
||||
|
||||
typeaheadCtrl.setFocused(false);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
});
|
||||
|
||||
it('should appear when not hidden, has matches input and moused over', function () {
|
||||
typeaheadCtrl.setHidden(false);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
|
||||
typeaheadCtrl.filterItemsByQuery(typeaheadItems[0]);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
|
||||
// only visible when all conditions match
|
||||
typeaheadCtrl.setMouseover(true);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(true);
|
||||
|
||||
typeaheadCtrl.setMouseover(false);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
});
|
||||
|
||||
it('should hide when no matches', function () {
|
||||
typeaheadCtrl.setHidden(false);
|
||||
typeaheadCtrl.setFocused(true);
|
||||
|
||||
typeaheadCtrl.filterItemsByQuery(typeaheadItems[0]);
|
||||
expect(typeaheadCtrl.isVisible()).to.be(true);
|
||||
|
||||
typeaheadCtrl.filterItemsByQuery('a8h4o8ah48thal4i7rlia4ujru4glia47gf');
|
||||
expect(typeaheadCtrl.isVisible()).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
115
test/unit/specs/services/persisted_log.js
Normal file
115
test/unit/specs/services/persisted_log.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
define(function (require) {
|
||||
var sinon = require('sinon/sinon');
|
||||
|
||||
var storage;
|
||||
var config;
|
||||
var PersistedLog;
|
||||
|
||||
var historyName = 'testHistory';
|
||||
var historyLimit = 10;
|
||||
var payload = [
|
||||
{ first: 'clark', last: 'kent' },
|
||||
{ first: 'peter', last: 'parker' },
|
||||
{ first: 'bruce', last: 'wayne' }
|
||||
];
|
||||
|
||||
require('components/persisted_log/persisted_log');
|
||||
|
||||
function init() {
|
||||
module('kibana/persisted_log', function ($provide) {
|
||||
// mock storage service
|
||||
$provide.service('storage', function () {
|
||||
this.get = sinon.stub();
|
||||
this.set = sinon.stub();
|
||||
this.remove = sinon.spy();
|
||||
this.clear = sinon.spy();
|
||||
});
|
||||
});
|
||||
|
||||
inject(function ($injector) {
|
||||
storage = $injector.get('storage');
|
||||
PersistedLog = $injector.get('PersistedLog');
|
||||
});
|
||||
}
|
||||
|
||||
describe('PersistedLog', function () {
|
||||
beforeEach(function () {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('expected API', function () {
|
||||
it('has expected methods', function () {
|
||||
var log = new PersistedLog(historyName);
|
||||
|
||||
expect(log.add).to.be.a('function');
|
||||
expect(log.get).to.be.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('internal functionality', function () {
|
||||
it('reads from storage', function () {
|
||||
var log = new PersistedLog(historyName);
|
||||
|
||||
expect(storage.get.calledOnce).to.be(true);
|
||||
expect(storage.get.calledWith(historyName)).to.be(true);
|
||||
});
|
||||
|
||||
it('writes to storage', function () {
|
||||
var log = new PersistedLog(historyName);
|
||||
var newItem = { first: 'diana', last: 'prince' };
|
||||
|
||||
var data = log.add(newItem);
|
||||
|
||||
expect(storage.set.calledOnce).to.be(true);
|
||||
expect(data).to.eql([newItem]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persisting data', function () {
|
||||
it('fetches records from storage', function () {
|
||||
storage.get.returns(payload);
|
||||
var log = new PersistedLog(historyName);
|
||||
|
||||
var items = log.get();
|
||||
expect(items.length).to.equal(3);
|
||||
expect(items).to.eql(payload);
|
||||
});
|
||||
|
||||
it('prepends new records', function () {
|
||||
storage.get.returns(payload.slice(0));
|
||||
var log = new PersistedLog(historyName);
|
||||
var newItem = { first: 'selina', last: 'kyle' };
|
||||
|
||||
var items = log.add(newItem);
|
||||
expect(items.length).to.equal(payload.length + 1);
|
||||
expect(items[0]).to.eql(newItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stack options', function () {
|
||||
it('should observe the maxLength option', function () {
|
||||
var bulkData = [];
|
||||
|
||||
for (var i = 0; i < historyLimit; i++) {
|
||||
bulkData.push(['record ' + i]);
|
||||
}
|
||||
storage.get.returns(bulkData);
|
||||
|
||||
var log = new PersistedLog(historyName, { maxLength: historyLimit });
|
||||
log.add(['new array 1']);
|
||||
var items = log.add(['new array 2']);
|
||||
|
||||
expect(items.length).to.equal(historyLimit);
|
||||
});
|
||||
|
||||
it('should observe the filterDuplicates option', function () {
|
||||
storage.get.returns(payload.slice(0));
|
||||
var log = new PersistedLog(historyName, { filterDuplicates: true });
|
||||
var newItem = payload[1];
|
||||
|
||||
var items = log.add(newItem);
|
||||
expect(items.length).to.equal(payload.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
106
test/unit/specs/services/storage.js
Normal file
106
test/unit/specs/services/storage.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
define(function (require) {
|
||||
var sinon = require('sinon/sinon');
|
||||
|
||||
var storage;
|
||||
var $window;
|
||||
var payload = { first: 'john', last: 'smith' };
|
||||
|
||||
require('components/storage/storage');
|
||||
|
||||
function init() {
|
||||
module('kibana/storage', function ($provide) {
|
||||
// mock $window.localStorage for storage
|
||||
$provide.value('$window', {
|
||||
localStorage: {
|
||||
getItem: sinon.stub(),
|
||||
setItem: sinon.spy(),
|
||||
removeItem: sinon.spy(),
|
||||
clear: sinon.spy()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
inject(function ($injector) {
|
||||
storage = $injector.get('storage');
|
||||
$window = $injector.get('$window');
|
||||
});
|
||||
}
|
||||
|
||||
describe('StorageService', function () {
|
||||
beforeEach(function () {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('expected API', function () {
|
||||
it('should have expected methods', function () {
|
||||
expect(storage.get).to.be.a('function');
|
||||
expect(storage.set).to.be.a('function');
|
||||
expect(storage.remove).to.be.a('function');
|
||||
expect(storage.clear).to.be.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('call behavior', function () {
|
||||
it('should call getItem on the store', function () {
|
||||
storage.get('name');
|
||||
|
||||
expect($window.localStorage.getItem.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call setItem on the store', function () {
|
||||
storage.set('name', 'john smith');
|
||||
|
||||
expect($window.localStorage.setItem.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call removeItem on the store', function () {
|
||||
storage.remove('name');
|
||||
|
||||
expect($window.localStorage.removeItem.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call clear on the store', function () {
|
||||
storage.clear();
|
||||
|
||||
expect($window.localStorage.clear.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('json data', function () {
|
||||
it('should parse JSON when reading from the store', function () {
|
||||
var getItem = $window.localStorage.getItem;
|
||||
getItem.returns(JSON.stringify(payload));
|
||||
|
||||
var data = storage.get('name');
|
||||
expect(data).to.eql(payload);
|
||||
});
|
||||
|
||||
it('should write JSON string to the store', function () {
|
||||
var setItem = $window.localStorage.setItem;
|
||||
var key = 'name';
|
||||
var value = payload;
|
||||
|
||||
storage.set(key, value);
|
||||
|
||||
var call = setItem.getCall(0);
|
||||
expect(call.args[0]).to.equal(key);
|
||||
expect(call.args[1]).to.equal(JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
|
||||
describe('expected responses', function () {
|
||||
it('should return null when not exists', function () {
|
||||
var data = storage.get('notexists');
|
||||
expect(data).to.equal(null);
|
||||
});
|
||||
|
||||
it('should return null when invalid JSON', function () {
|
||||
var getItem = $window.localStorage.getItem;
|
||||
getItem.returns('not: json');
|
||||
|
||||
var data = storage.get('name');
|
||||
expect(data).to.equal(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue