From 48b4b0c3cfeae06055f7ca11781ec534284a7f52 Mon Sep 17 00:00:00 2001 From: blavenie <benoit.lavenier@e-is.pro> Date: Thu, 19 Oct 2017 12:23:27 +0200 Subject: [PATCH] [enh] ES: Add a document search page [enh] docstats graphs: add link to document search page --- www/index.html | 2 + www/js/services/http-services.js | 83 ++++--- www/plugins/es/i18n/locale-fr-FR.json | 30 +++ .../es/js/controllers/document-controllers.js | 213 ++++++++++++++++++ .../controllers/notification-controllers.js | 5 + www/plugins/es/js/plugin.js | 3 +- www/plugins/es/js/services.js | 3 +- .../es/js/services/blockchain-services.js | 8 +- .../es/js/services/document-services.js | 170 ++++++++++++++ .../es/js/services/message-services.js | 3 +- .../es/js/services/notification-services.js | 6 + .../es/templates/document/item_document.html | 49 ++++ .../templates/document/items_documents.html | 31 +++ www/plugins/es/templates/document/lookup.html | 17 ++ .../es/templates/document/lookup_form.html | 100 ++++++++ .../document/lookup_popover_actions.html | 15 ++ .../js/controllers/docstats-controllers.js | 79 +++++-- .../graph/templates/docstats/graph.html | 5 +- 18 files changed, 764 insertions(+), 58 deletions(-) create mode 100644 www/plugins/es/js/controllers/document-controllers.js create mode 100644 www/plugins/es/js/services/document-services.js create mode 100644 www/plugins/es/templates/document/item_document.html create mode 100644 www/plugins/es/templates/document/items_documents.html create mode 100644 www/plugins/es/templates/document/lookup.html create mode 100644 www/plugins/es/templates/document/lookup_form.html create mode 100644 www/plugins/es/templates/document/lookup_popover_actions.html diff --git a/www/index.html b/www/index.html index f8e4ed8f..8a836b7b 100644 --- a/www/index.html +++ b/www/index.html @@ -176,6 +176,7 @@ <script src="dist/dist_js/plugins/es/js/services/wot-services.js"></script> <script src="dist/dist_js/plugins/es/js/services/tx-services.js"></script> <script src="dist/dist_js/plugins/es/js/services/geo-services.js"></script> +<script src="dist/dist_js/plugins/es/js/services/document-services.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/common-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/app-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/settings-controllers.js"></script> @@ -191,6 +192,7 @@ <script src="dist/dist_js/plugins/es/js/controllers/group-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/invitation-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/subscription-controllers.js"></script> +<script src="dist/dist_js/plugins/es/js/controllers/document-controllers.js"></script> <!-- Graph plugin --> <!--removeIf(ubuntu)--> <!-- FIXME: issue #463 --> diff --git a/www/js/services/http-services.js b/www/js/services/http-services.js index e684b0e5..60827088 100644 --- a/www/js/services/http-services.js +++ b/www/js/services/http-services.js @@ -147,65 +147,76 @@ angular.module('cesium.http.services', ['cesium.cache.services']) }; } - function ws(host, port, path, useSsl) { + function ws(host, port, path, useSsl, timeout) { if (!path) { console.error('calling csHttp.ws without path argument'); throw 'calling csHttp.ws without path argument'; } var uri = getWsUrl(host, port, path, useSsl); - var delegate = null; - var callbacks = []; + timeout = timeout || csSettings.data.timeout; - function _waitOpen() { - if (!delegate) throw new Error('Websocket not opened'); - if (delegate.readyState == 1) { - return $q.when(delegate); + function _waitOpen(self) { + if (!self.delegate) throw new Error('Websocket not opened'); + if (self.delegate.readyState == 1) { + return $q.when(self.delegate); } - if (delegate.readyState == 3) { - return $q.reject('Unable to connect to websocket ['+delegate.url+']'); + if (self.delegate.readyState == 3) { + return $q.reject('Unable to connect to websocket ['+self.delegate.url+']'); } - console.debug('[http] Waiting websocket ['+path+'] opening...'); - return $timeout(_waitOpen, 200); + console.debug('[http] Waiting websocket ['+self.path+'] opening...'); + + if (self.waitDuration >= timeout) { + console.debug("[http] Will retry openning websocket later..."); + self.waitRetryDelay = 2000; // 2 seconds + } + + return $timeout(function(){ + self.waitDuration += self.waitRetryDelay; + return _waitOpen(self); + }, self.waitRetryDelay); } function _open(self, callback, params) { - if (!delegate) { + if (!self.delegate) { self.path = path; + self.callbacks = []; + self.waitDuration = 0; + self.waitRetryDelay = 200; prepare(uri, params, {}, function(uri) { - delegate = new WebSocket(uri); - delegate.onerror = function(e) { - delegate.readyState=3; + self.delegate = new WebSocket(uri); + self.delegate.onerror = function(e) { + self.delegate.readyState=3; }; - delegate.onmessage = function(e) { + self.delegate.onmessage = function(e) { var obj = JSON.parse(e.data); - _.forEach(callbacks, function(callback) { + _.forEach(self.callbacks, function(callback) { callback(obj); }); }; - delegate.onopen = function(e) { - console.debug('[http] Listening on websocket ['+path+']...'); + self.delegate.onopen = function(e) { + console.debug('[http] Listening on websocket ['+self.path+']...'); sockets.push(self); - delegate.openTime = new Date().getTime(); + self.delegate.openTime = new Date().getTime(); }; - delegate.onclose = function() { + self.delegate.onclose = function() { // Remove from sockets arrays - var index = _.findIndex(sockets, function(socket){return socket.path === path;}); + var index = _.findIndex(sockets, function(socket){return socket.path === self.path;}); if (index >= 0) { sockets.splice(index,1); } // If close event comes from Cesium - if (delegate.closing) { - delegate = null; + if (self.delegate.closing) { + self.delegate = null; } // If unexpected close event, reopen the socket (fix #535) else { - console.debug('[http] Unexpected close of websocket ['+path+'] (open '+ (new Date().getTime() - delegate.openTime) +'ms ago): re-opening...'); + console.debug('[http] Unexpected close of websocket ['+path+'] (open '+ (new Date().getTime() - self.delegate.openTime) +'ms ago): re-opening...'); - delegate = null; + self.delegate = null; // Loop, but without the already registered callback _open(self, null, params); @@ -213,8 +224,8 @@ angular.module('cesium.http.services', ['cesium.cache.services']) }; }); } - if (callback) callbacks.push(callback); - return _waitOpen(); + if (callback) self.callbacks.push(callback); + return _waitOpen(self); } return { @@ -225,17 +236,19 @@ angular.module('cesium.http.services', ['cesium.cache.services']) return _open(this, callback, params); }, send: function(data) { - return _waitOpen() + var self = this; + return _waitOpen(self) .then(function(){ - delegate.send(data); + self.delegate.send(data); }); }, close: function() { - if (delegate) { - delegate.closing = true; - console.debug('[http] Closing websocket ['+path+']...'); - delegate.close(); - callbacks = []; + var self = this; + if (self.delegate) { + self.delegate.closing = true; + console.debug('[http] Closing websocket ['+self.path+']...'); + self.delegate.close(); + self.callbacks = []; } } }; diff --git a/www/plugins/es/i18n/locale-fr-FR.json b/www/plugins/es/i18n/locale-fr-FR.json index 87810374..36ff7cc5 100644 --- a/www/plugins/es/i18n/locale-fr-FR.json +++ b/www/plugins/es/i18n/locale-fr-FR.json @@ -413,6 +413,36 @@ "PROVIDER": "Prestataire du service :" } }, + "DOCUMENT": { + "HASH": "Hash: ", + "LOOKUP": { + "TITLE": "Recherche de documents", + "BTN_ACTIONS": "Actions", + "SEARCH_HELP": "issuer:AAA*, time:1508406169", + "LAST_DOCUMENTS": "Derniers documents :", + "SHOW_QUERY": "Voir la requête", + "HIDE_QUERY": "Masquer la requête", + "HEADER_TIME": "Date/Heure", + "HEADER_ISSUER": "Emetteur", + "HEADER_RECIPIENT": "Destinataire", + "READ": "Lu", + "POPOVER_ACTIONS": { + "TITLE": "Actions", + "REMOVE_ALL": "Supprimer ces documents..." + } + }, + "INFO": { + "REMOVED": "Document supprimé" + }, + "CONFIRM": { + "REMOVE": "Etes-vous sûr de vouloir <b>supprimer ce document</b> ?", + "REMOVE_ALL": "Etes-vous sûr de vouloir <b>supprimer ces documents</b> ?" + }, + "ERROR": { + "REMOVE_FAILED": "Erreur lors de la suppression du document", + "REMOVE_ALL_FAILED": "Erreur lors de la suppression des documents" + } + }, "ES_SETTINGS": { "PLUGIN_NAME": "Cesium+", "PLUGIN_NAME_HELP": "Profils, notifications, messages privés", diff --git a/www/plugins/es/js/controllers/document-controllers.js b/www/plugins/es/js/controllers/document-controllers.js new file mode 100644 index 00000000..a37042ea --- /dev/null +++ b/www/plugins/es/js/controllers/document-controllers.js @@ -0,0 +1,213 @@ +angular.module('cesium.es.document.controllers', ['cesium.es.services']) + + .config(function($stateProvider) { + 'ngInject'; + + $stateProvider + + .state('app.document_search', { + url: "/data/search/:index/:type?q", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/document/lookup.html", + controller: 'ESDocumentLookupCtrl' + } + }, + data: { + silentLocationChange: true + } + }) + ; + }) + + .controller('ESDocumentLookupCtrl', ESDocumentLookupController) + +; + +function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, + csSettings, csWallet, UIUtils, esDocument) { + 'ngInject'; + + $scope.search = { + loading: true, + hasMore: true, + text: undefined, + index: 'invitation', + type: 'certification', + results: undefined, + sort: 'time', + asc: false + }; + $scope.entered = false; + $scope.searchTextId = 'documentSearchText'; + $scope.ionItemClass = 'item-border-large'; + $scope.defaultSizeLimit = UIUtils.screen.isSmall() ? 50 : 100; + $scope.helptipPrefix = 'helptip-document'; + + $scope.$on('$ionicView.enter', function(e, state) { + + if (!$scope.entered) { + $scope.entered = true; + $scope.search.index = state.stateParams && state.stateParams.index || $scope.search.index; + $scope.search.type = state.stateParams && state.stateParams.type || $scope.search.type; + $scope.search.text = state.stateParams && state.stateParams.q || $scope.search.text; + + $scope.search.last = !$scope.search.text; + $scope.load(); + } + $scope.expertMode = angular.isDefined($scope.expertMode) ? $scope.expertMode : !UIUtils.screen.isSmall() && csSettings.data.expertMode; + }); + + $scope.load = function(size, offset) { + var options = { + index: $scope.search.index, + type: $scope.search.type, + from: offset || 0, + size: size || $scope.defaultSizeLimit + }; + + // add sort + if ($scope.search.sort) { + options.sort = $scope.search.sort + ':' + (!$scope.search.asc ? "desc" : "asc"); + } + else { // default sort + options.sort = "time:desc"; + } + + $scope.search.loading = true; + + var searchFn = $scope.search.last + ? esDocument.search(options) + : esDocument.searchText($scope.search.text||'', options); + return searchFn + .then(function(res) { + $scope.search.results = res.hits; + $scope.search.total = res.total; + $scope.search.took = res.took; + + UIUtils.loading.hide(); + $scope.search.loading = false; + + if (res.hits && res.hits.length > 0) { + $scope.motion.show({selector: '.list .item.item-document'}); + $scope.search.hasMore = res.total > $scope.search.results.length; + } + else { + $scope.search.hasMore = false; + } + + $scope.$broadcast('$$rebind::rebind'); // notify binder + }) + .catch(function(err) { + UIUtils.onError('DOCUMENT.ERROR.LOAD_DOCUMENTS_FAILED')(err); + $scope.search.results = []; + $scope.search.loading = false; + }); + }; + + $scope.doSearchText = function() { + $scope.search.last = $scope.search.text ? false : true; + return $scope.load() + .then(function() { + // Update location href + $location.search({q: $scope.search.text}).replace(); + }); + }; + + $scope.doSearchLast = function() { + $scope.search.last = true; + $scope.search.text = undefined; + return $scope.load(); + }; + + $scope.removeAll = function() { + $scope.hideActionsPopover(); + if (!$scope.search.results || !$scope.search.results.length) return; + + return UIUtils.alert.confirm('DOCUMENT.CONFIRM.REMOVE_ALL') + .then(function(confirm) { + if (confirm) { + return esDocument.removeAll($scope.search.results) + .catch(UIUtils.onError('DOCUMENT.ERROR.REMOVE_ALL_FAILED')); + } + }); + }; + + $scope.remove = function(index) { + var doc = $scope.search.results[index]; + if (!doc) return; + + UIUtils.alert.confirm('DOCUMENT.CONFIRM.REMOVE') + .then(function(confirm) { + if (confirm) { + esDocument.remove(doc) + .then(function () { + $scope.search.results.splice(index,1); // remove from messages array + UIUtils.toast.show('DOCUMENT.INFO.REMOVED'); + }) + .catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_FAILED')); + } + }); + }; + + $scope.selectDocument = function(doc) { + console.debug("Selected document: ", doc); + }; + + $scope.toggleSort = function(sort){ + if ($scope.search.sort === sort && !$scope.search.asc) { + $scope.search.asc = undefined; + $scope.search.sort = undefined; + } + else { + $scope.search.asc = ($scope.search.sort === sort) ? !$scope.search.asc : true; + $scope.search.sort = sort; + } + $scope.load(); + }; + + /* -- Modals -- */ + + /* -- Popover -- */ + + $scope.showActionsPopover = function(event) { + if (!$scope.actionsPopover) { + $ionicPopover.fromTemplateUrl('plugins/es/templates/document/lookup_popover_actions.html', { + scope: $scope + }).then(function(popover) { + $scope.actionsPopover = popover; + //Cleanup the popover when we're done with it! + $scope.$on('$destroy', function() { + $scope.actionsPopover.remove(); + }); + $scope.actionsPopover.show(event); + }); + } + else { + $scope.actionsPopover.show(event); + } + }; + + $scope.hideActionsPopover = function() { + if ($scope.actionsPopover) { + $scope.actionsPopover.hide(); + } + }; + + /* -- watch events -- */ + + // Watch unauth + $scope.onUnauth = function() { + // Reset all data + $scope.search.results = undefined; + $scope.search.loading = false; + $scope.entered = false; + }; + csWallet.api.data.on.unauth($scope, $scope.onUnauth); + + // for DEV only + /*$timeout(function() { + // launch default action fo DEV + }, 900); + */ +} diff --git a/www/plugins/es/js/controllers/notification-controllers.js b/www/plugins/es/js/controllers/notification-controllers.js index d84635da..4568da45 100644 --- a/www/plugins/es/js/controllers/notification-controllers.js +++ b/www/plugins/es/js/controllers/notification-controllers.js @@ -62,6 +62,11 @@ function NotificationsController($scope, $rootScope, $ionicPopover, $state, $tim }); $scope.load = function(from, size) { + if (!csWallet.data.pubkey) { + $scope.search.loading = true; + return; + } + var options = angular.copy($scope.search.options); options.from = options.from || from || 0; options.size = options.size || size || defaultSearchLimit; diff --git a/www/plugins/es/js/plugin.js b/www/plugins/es/js/plugin.js index a31bb56c..c1b1dc07 100644 --- a/www/plugins/es/js/plugin.js +++ b/www/plugins/es/js/plugin.js @@ -17,6 +17,7 @@ angular.module('cesium.es.plugin', [ 'cesium.es.registry.controllers', 'cesium.es.group.controllers', 'cesium.es.invitation.controllers', - 'cesium.es.subscription.controllers' + 'cesium.es.subscription.controllers', + 'cesium.es.document.controllers' ]) ; diff --git a/www/plugins/es/js/services.js b/www/plugins/es/js/services.js index 4ff9ef1a..a4a8f888 100644 --- a/www/plugins/es/js/services.js +++ b/www/plugins/es/js/services.js @@ -18,6 +18,7 @@ angular.module('cesium.es.services', [ 'cesium.es.subscription.services', 'cesium.es.wot.services', 'cesium.es.tx.services', - 'cesium.es.geo.services' + 'cesium.es.geo.services', + 'cesium.es.document.services' ]) ; diff --git a/www/plugins/es/js/services/blockchain-services.js b/www/plugins/es/js/services/blockchain-services.js index 3e87c427..dd768ba3 100644 --- a/www/plugins/es/js/services/blockchain-services.js +++ b/www/plugins/es/js/services/blockchain-services.js @@ -64,7 +64,7 @@ angular.module('cesium.es.blockchain.services', ['cesium.services', 'cesium.es.h options.cleanData = angular.isDefined(options.cleanData) ? options.cleanData : true; var hasExcludedCurrent = false; - var hits = res && res.hits ? res.hits.hits.reduce(function(res, hit) { + var hits = (res && res.hits && res.hits.hits || []).reduce(function(res, hit) { if (hit._id == 'current' && options.excludeCurrent) { hasExcludedCurrent = true; return res; @@ -75,15 +75,13 @@ angular.module('cesium.es.blockchain.services', ['cesium.services', 'cesium.es.h block.cleanData(); // release data's arrays } return res.concat(block); - }, []) : []; - var result = { + }, []); + return { hits: hits, took: res.took, total: res && res.hits && res.hits.total ? ( hasExcludedCurrent ? res.hits.total-1 : res.hits.total) : 0 }; - - return result; }; exports.block.search = function(currency, options) { diff --git a/www/plugins/es/js/services/document-services.js b/www/plugins/es/js/services/document-services.js new file mode 100644 index 00000000..24bf8ece --- /dev/null +++ b/www/plugins/es/js/services/document-services.js @@ -0,0 +1,170 @@ +angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', 'cesium.es.http.services']) + .config(function(PluginServiceProvider, csConfig) { + 'ngInject'; + + var enable = csConfig.plugins && csConfig.plugins.es; + if (enable) { + // Will force to load this service + PluginServiceProvider.registerEagerLoadingService('esDocument'); + } + + }) + + .factory('esDocument', function($q, $rootScope, $timeout, UIUtils, Api, CryptoUtils, + csPlatform, csConfig, csSettings, csWot, csWallet, esHttp) { + 'ngInject'; + + var + constants = { + DEFAULT_LOAD_SIZE: 40 + }, + fields = { + commons: ["issuer", "pubkey", "hash", "time", "recipient", "nonce", "read_signature"], + peer: ["*"] + }, + raw = { + search: esHttp.post('/:index/:type/_search'), + searchText: esHttp.get('/:index/:type/_search?q=:text') + }; + + function _readSearchHits(res, options) { + options.issuerField = options.issuerField || 'pubkey'; + + var hits = (res && res.hits && res.hits.hits || []).reduce(function(res, hit) { + var doc = hit._source || {}; + doc.index = hit._index; + doc.type = hit._type; + doc.id = hit._id; + doc.pubkey = doc.issuer || options.issuerField && doc[options.issuerField]; // need to call csWot.extendAll() + doc.time = doc.time || options.getTimeFunction && options.getTimeFunction(doc); + return res.concat(doc); + }, []); + + + var recipients = hits.reduce(function(res, doc) { + if (doc.recipient) { + doc.recipient = { + pubkey: doc.recipient + }; + return res.concat(doc.recipient); + } + return res; + }, []); + + return csWot.extendAll(hits.concat(recipients)) + .then(function() { + return { + hits: hits, + took: res.took, + total: res && res.hits && res.hits.total || 0 + }; + }); + } + + function search(options) { + options = options || {}; + + if (options.type == 'peer') { + if (!options.sort) { + options.sort = 'stats.medianTime:desc'; + } + else { + var sortParts = options.sort.split(':'); + var side = sortParts.length > 1 ? sortParts[1] : 'desc'; + + options.sort = [ + {'stats.medianTime': { + nested_path : 'stats', + order: side + }} + ]; + options._source = fields.peer; + options.getTimeFunction = function(doc) { + return doc.stats && doc.stats.medianTime; + }; + } + } + + if (!options || !options.index || !options.type) throw new Error('Missing mandatory options [index, type]'); + var request = { + from: options.from || 0, + size: options.size || constants.DEFAULT_LOAD_SIZE, + sort: options.sort || 'time:desc', + _source: options._source || fields.commons + }; + if (options.query) { + request.query = options.query; + } + + return raw.search(request, { + index: options.index, + type: options.type + }) + .then(function(res) { + return _readSearchHits(res, options); + }); + } + + function searchText(queryString, options) { + + options = options || {}; + + var request = { + text: queryString, + index: options.index || 'user', + type: options.type || 'event', + from: options.from || 0, + size: options.size || constants.DEFAULT_LOAD_SIZE, + sort: options.sort || 'time:desc', + _source: options._source || fields.commons.join(',') + }; + + console.debug('[ES] [wallet] [document] [{0}/{1}] Loading documents...'.format( + options.index, + options.type + )); + var now = new Date().getTime(); + + return raw.searchText(request) + .then(function(res) { + return _readSearchHits(res, options); + }) + .then(function(res) { + console.debug('[ES] [document] [{0}/{1}] Loading {2} documents in {3}ms'.format( + options.index, + options.type, + res && res.hits && res.hits.length || 0, + new Date().getTime() - now + )); + return res; + }); + } + + function remove(document) { + if (document || !document.index || !document.type || !document.id) return; + return esHttp.record.remove(document.index, document.type)(document.id); + } + + function removeAll(documents) { + if (!documents || !documents.length) return; + + return csWallet.auth() + .then(function(walletData) { + // Remove each doc + return $q.all(documents.reduce(function (res, doc) { + return res.concat(esHttp.record.remove(doc.index, doc.type)(doc.id, walletData)); + }, [])); + }); + } + + return { + search: search, + searchText: searchText, + remove: remove, + removeAll: removeAll, + fields: { + commons: fields.commons + } + }; + }) +; diff --git a/www/plugins/es/js/services/message-services.js b/www/plugins/es/js/services/message-services.js index 3469630d..7016878c 100644 --- a/www/plugins/es/js/services/message-services.js +++ b/www/plugins/es/js/services/message-services.js @@ -25,6 +25,7 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.platform', }, raw = { postSearch: esHttp.post('/message/inbox/_search'), + postSearchByType: esHttp.post('/message/:type/_search'), getByTypeAndId : esHttp.get('/message/:type/:id'), postReadById: esHttp.post('/message/inbox/:id/_read') }, @@ -200,7 +201,7 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.platform', request.query = {bool: {filter: {term: {issuer: pubkey}}}}; } - return esHttp.post('/message/:type/_search')(request, {type: options.type}) + return raw.postSearchByType(request, {type: options.type}) .then(function(res) { if (!res || !res.hits || !res.hits.total) { return []; diff --git a/www/plugins/es/js/services/notification-services.js b/www/plugins/es/js/services/notification-services.js index ff68c042..42dc11ec 100644 --- a/www/plugins/es/js/services/notification-services.js +++ b/www/plugins/es/js/services/notification-services.js @@ -91,6 +91,9 @@ angular.module('cesium.es.notification.services', ['cesium.platform', 'cesium.es // Load unread notifications count function loadUnreadNotificationsCount(pubkey, options) { + if (!pubkey) { + return $q.reject('[ES] [notification] Unable to load - missing pubkey'); + } var request = { query: createFilterQuery(pubkey, options) }; @@ -104,6 +107,9 @@ angular.module('cesium.es.notification.services', ['cesium.platform', 'cesium.es // Load user notifications function loadNotifications(pubkey, options) { + if (!pubkey) { + return $q.reject('[ES] [notification] Unable to load - missing pubkey'); + } options = options || {}; options.from = options.from || 0; options.size = options.size || constants.DEFAULT_LOAD_SIZE; diff --git a/www/plugins/es/templates/document/item_document.html b/www/plugins/es/templates/document/item_document.html new file mode 100644 index 00000000..b2698d1e --- /dev/null +++ b/www/plugins/es/templates/document/item_document.html @@ -0,0 +1,49 @@ +<ion-item id="doc-{{::doc.id}}" + class="item item-document item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" + ng-class="{{::ionItemClass}}" + ng-click="selectDocument(doc)"> + + <i class="icon ion-document stable" ng-if=":rebind:!doc.avatar"></i> + <i class="avatar" ng-if=":rebind:doc.avatar" style="background-image: url('{{:rebind:doc.avatar.src}}')"></i> + + <div class="row no-padding"> + <div class="col"> + <h3 class="dark"> + <i class="ion-locked" ng-if=":rebind:doc.nonce"></i> + {{:rebind:doc.time|formatDate}}</h3> + <h4 class="gray">{{:rebind:'DOCUMENT.HASH'|translate}} {{:rebind:doc.hash|formatHash}}</h4> + </div> + + <div class="col"> + <h3> + <a ui-sref="app.wot_identity({pubkey: doc.pubkey, uid: doc.uid})"> + <span class="gray"> + <i class="ion-key"></i> {{:rebind:doc.pubkey|formatPubkey}} + </span> + <span class="positive" ng-if=":rebind:doc.uid"> + <i class="ion-person"></i> {{:rebind:doc.name||doc.uid}} + </span> + </a> + </h3> + </div> + + <div class="col"> + <h3 ng-if=":rebind:doc.recipient"> + <a ui-sref="app.wot_identity({pubkey: doc.recipient.pubkey, uid: doc.recipient.uid})"> + <span class="gray"> + <i class="ion-key"></i> {{:rebind:doc.recipient.pubkey|formatPubkey}} + </span> + <span class="positive" ng-if=":rebind:doc.recipient.uid"> + <i class="ion-person"></i> {{:rebind:doc.recipient.name||doc.recipient.uid}} + </span> + </a> + </h3> + <h4 class="gray" ng-if=":rebind:doc.read_signature"> + <i class="ion-checkmark"></i> + <span translate>DOCUMENT.LOOKUP.READ</span> + </h4> + + </div> + + </div> +</ion-item> diff --git a/www/plugins/es/templates/document/items_documents.html b/www/plugins/es/templates/document/items_documents.html new file mode 100644 index 00000000..6306e779 --- /dev/null +++ b/www/plugins/es/templates/document/items_documents.html @@ -0,0 +1,31 @@ + +<div class="item row row-header done in hidden-xs hidden-sm"> + + <a class="no-padding dark col col-header" + ng-if=":rebind:expertMode" + ng-click="toggleSort('time')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'time'"></cs-sort-icon> + {{'DOCUMENT.LOOKUP.HEADER_TIME' | translate}} + </a> + <a class="no-padding dark col col-header" + ng-if=":rebind:expertMode" + ng-click="toggleSort('issuer')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'issuer'"></cs-sort-icon> + {{'DOCUMENT.LOOKUP.HEADER_ISSUER' | translate}} + </a> + <a class="no-padding dark col col-header" + ng-if=":rebind:expertMode" + ng-click="toggleSort('recipient')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'recipient'"></cs-sort-icon> + {{'DOCUMENT.LOOKUP.HEADER_RECIPIENT' | translate}} + </a> +</div> + +<div class="padding gray" ng-if=":rebind:!search.loading && !search.results.length" translate> + COMMON.SEARCH_NO_RESULT +</div> + +<!-- for each doc --> +<ng-repeat ng-repeat="doc in :rebind:search.results track by doc.id" + ng-include="'plugins/es/templates/document/item_document.html'"> +</ng-repeat> diff --git a/www/plugins/es/templates/document/lookup.html b/www/plugins/es/templates/document/lookup.html new file mode 100644 index 00000000..26b19071 --- /dev/null +++ b/www/plugins/es/templates/document/lookup.html @@ -0,0 +1,17 @@ +<ion-view> + <ion-nav-title> + <span translate>DOCUMENT.LOOKUP.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + + <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)"> + </button> + + </ion-nav-buttons> + + <ion-content class="padding no-padding-xs" scroll="true"> + + <ng-include src="'plugins/es/templates/document/lookup_form.html'"></ng-include> + </ion-content> +</ion-view> diff --git a/www/plugins/es/templates/document/lookup_form.html b/www/plugins/es/templates/document/lookup_form.html new file mode 100644 index 00000000..b0d2f504 --- /dev/null +++ b/www/plugins/es/templates/document/lookup_form.html @@ -0,0 +1,100 @@ +<div class="lookupForm"> + + + <div class="item no-padding"> + + <!--<div class="button button-small button-text button-stable button-icon-event padding no-padding-right ink" + ng-repeat="filter in search.filters" ng-if="filter"> + <span ng-bind-html="'DOCUMENT.LOOKUP.TX_SEARCH_FILTER.'+filter.type|translate:filter"></span> + <i class="icon ion-close" ng-click="itemRemove($index)"></i> + + </div>--> + + <label class="item-input"> + <i class="icon ion-search placeholder-icon"></i> + <input type="text" + class="visible-xs visible-sm" + placeholder="{{'DOCUMENT.LOOKUP.SEARCH_HELP'|translate}}" + ng-model="search.text" + ng-model-options="{ debounce: 650 }" + ng-change="doSearchText()"> + <input type="text" + class="hidden-xs hidden-sm" + id="{{searchTextId}}" placeholder="{{'DOCUMENT.LOOKUP.SEARCH_HELP'|translate}}" + ng-model="search.text" + on-return="doSearchText()"> + <div class="helptip-anchor-center"> + <a id="{{helptipPrefix}}-search-text"></a> + </div> + + </label> + </div> + + + <div class="padding-top padding-xs" style="display: block; height: 60px;"> + <div class="pull-left"> + <h4 + ng-if="search.last" translate> + DOCUMENT.LOOKUP.LAST_DOCUMENTS + </h4> + <h4 ng-if="!search.last"> + {{'COMMON.RESULTS_LIST'|translate}} + </h4> + <h5 class="dark" ng-if="!search.loading && search.total"> + <span translate="COMMON.RESULTS_COUNT" translate-values="{count: search.total}"></span> + <small class="gray" ng-if=":rebind:search.took && expertMode"> + - {{:rebind:'COMMON.EXECUTION_TIME'|translate: {duration: search.took} }} + </small> + <small class="gray" ng-if=":rebind:expertMode && search.filters && search.filters.length"> + - <a ng-click="toggleShowQuery()" + ng-if="!showQuery" > + <span translate>DOCUMENT.LOOKUP.SHOW_QUERY</span> + <i class="icon ion-arrow-down-b gray"></i> + </a> + <a ng-click="toggleShowQuery()" + ng-if="showQuery" > + <span translate>DOCUMENT.LOOKUP.HIDE_QUERY</span> + <i class="icon ion-arrow-up-b gray"></i> + </a> + </small> + </h5> + <h5 class="gray" ng-if="search.loading" > + <ion-spinner class="icon ion-spinner-small" icon="android"></ion-spinner> + <span translate>COMMON.SEARCHING</span> + <br/> + </h5> + </div> + + <div class=" pull-right hidden-xs hidden-sm"> + <a class="button button-text button-small ink" + ng-click="showActionsPopover($event)"> + {{'DOCUMENT.LOOKUP.BTN_ACTIONS' | translate}} + <i class="icon ion-arrow-down-b"></i> + </a> + + <button class="button button-small button-stable ink" + ng-click="doSearchText()"> + {{'COMMON.BTN_SEARCH' | translate:search}} + </button> + </div> + </div> + + <div class="item no-border no-padding" ng-if=":rebind:search.filters && search.filters.length && expertMode"> + <small class="no-padding no-margin" ng-if="showQuery"> + <span class="gray text-wrap dark">{{:rebind:search.query}}</span> + </small> + </div> + + <ion-list class="list" ng-class="::motion.ionListClass"> + + <ng-include src="'plugins/es/templates/document/items_documents.html'"></ng-include> + + </ion-list> + + <ion-infinite-scroll + ng-if="search.hasMore" + spinner="android" + on-infinite="showMore()" + distance="1%"> + </ion-infinite-scroll> + diff --git a/www/plugins/es/templates/document/lookup_popover_actions.html b/www/plugins/es/templates/document/lookup_popover_actions.html new file mode 100644 index 00000000..7d9dba60 --- /dev/null +++ b/www/plugins/es/templates/document/lookup_popover_actions.html @@ -0,0 +1,15 @@ +<ion-popover-view class="fit has-header"> + <ion-header-bar> + <h1 class="title" translate>DOCUMENT.LOOKUP.POPOVER_ACTIONS.TITLE</h1> + </ion-header-bar> + <ion-content scroll="false"> + <div class="list item-text-wrap"> + <a class="item item-icon-left assertive ink" + ng-class="{'gray': !search.total}" + ng-click="removeAll()"> + <i class="icon ion-trash-a"></i> + {{'DOCUMENT.LOOKUP.POPOVER_ACTIONS.REMOVE_ALL' | translate}} + </a> + </div> + </ion-content> +</ion-popover-view> diff --git a/www/plugins/graph/js/controllers/docstats-controllers.js b/www/plugins/graph/js/controllers/docstats-controllers.js index d868d110..a3c20f9e 100644 --- a/www/plugins/graph/js/controllers/docstats-controllers.js +++ b/www/plugins/graph/js/controllers/docstats-controllers.js @@ -25,7 +25,7 @@ angular.module('cesium.graph.docstats.controllers', ['chart.js', 'cesium.graph.s .controller('GpDocStatsCtrl', GpDocStatsController) ; -function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpData, $filter) { +function GpDocStatsController($scope, $state, $controller, $q, $translate, gpColor, gpData, $filter) { 'ngInject'; // Initialize the super class and extend it. @@ -33,6 +33,7 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa $scope.hiddenDatasets = []; + $scope.chartIdPrefix = 'docstats-chart-'; $scope.charts = [ // User count @@ -44,13 +45,21 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa key: 'user_profile', label: 'GRAPH.DOC_STATS.USER.USER_PROFILE', color: gpColor.rgba.royal(1), - pointHoverBackgroundColor: gpColor.rgba.royal(1) + pointHoverBackgroundColor: gpColor.rgba.royal(1), + clickState: { + name: 'app.document_search', + params: {index:'user', type: 'profile'} + } }, { key: 'user_settings', label: 'GRAPH.DOC_STATS.USER.USER_SETTINGS', color: gpColor.rgba.gray(0.5), - pointHoverBackgroundColor: gpColor.rgba.gray(1) + pointHoverBackgroundColor: gpColor.rgba.gray(1), + clickState: { + name: 'app.document_search', + params: {index:'user', type: 'settings'} + } } ] }, @@ -64,19 +73,31 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa key: 'message_inbox', label: 'GRAPH.DOC_STATS.MESSAGE.MESSAGE_INBOX', color: gpColor.rgba.royal(1), - pointHoverBackgroundColor: gpColor.rgba.royal(1) + pointHoverBackgroundColor: gpColor.rgba.royal(1), + clickState: { + name: 'app.document_search', + params: {index:'message', type: 'inbox'} + } }, { key: 'message_outbox', label: 'GRAPH.DOC_STATS.MESSAGE.MESSAGE_OUTBOX', color: gpColor.rgba.calm(1), - pointHoverBackgroundColor: gpColor.rgba.calm(1) + pointHoverBackgroundColor: gpColor.rgba.calm(1), + clickState: { + name: 'app.document_search', + params: {index:'message', type: 'outbox'} + } }, { key: 'invitation_certification', label: 'GRAPH.DOC_STATS.MESSAGE.INVITATION_CERTIFICATION', color: gpColor.rgba.gray(0.5), - pointHoverBackgroundColor: gpColor.rgba.gray(1) + pointHoverBackgroundColor: gpColor.rgba.gray(1), + clickState: { + name: 'app.document_search', + params: {index:'invitation', type: 'certification'} + } } ] }, @@ -90,19 +111,31 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa key: 'page_record', label: 'GRAPH.DOC_STATS.SOCIAL.PAGE_RECORD', color: gpColor.rgba.royal(1), - pointHoverBackgroundColor: gpColor.rgba.royal(1) + pointHoverBackgroundColor: gpColor.rgba.royal(1), + clickState: { + name: 'app.document_search', + params: {index:'page', type: 'record'} + } }, { key: 'group_record', label: 'GRAPH.DOC_STATS.SOCIAL.GROUP_RECORD', color: gpColor.rgba.calm(1), - pointHoverBackgroundColor: gpColor.rgba.calm(1) + pointHoverBackgroundColor: gpColor.rgba.calm(1), + clickState: { + name: 'app.document_search', + params: {index:'group', type: 'record'} + } }, { key: 'page_comment', label: 'GRAPH.DOC_STATS.SOCIAL.PAGE_COMMENT', color: gpColor.rgba.gray(0.5), - pointHoverBackgroundColor: gpColor.rgba.gray(1) + pointHoverBackgroundColor: gpColor.rgba.gray(1), + clickState: { + name: 'app.document_search', + params: {index:'page', type: 'comment'} + } } ] }, @@ -116,7 +149,11 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa key: 'history_delete', label: 'GRAPH.DOC_STATS.OTHER.HISTORY_DELETE', color: gpColor.rgba.gray(0.5), - pointHoverBackgroundColor: gpColor.rgba.gray(1) + pointHoverBackgroundColor: gpColor.rgba.gray(1), + clickState: { + name: 'app.document_search', + params: {index:'history', type: 'delete'} + } } ] } @@ -237,9 +274,25 @@ function GpDocStatsController($scope, $controller, $q, $translate, gpColor, gpDa $scope.onChartClick = function(data, e, item) { if (!item) return; - console.log('Click on item index='+ item._index); - var from = $scope.times[item._index]; - var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix(); + var chart = _.find($scope.charts , function(chart) { + return ($scope.chartIdPrefix + chart.id) == item._chart.canvas.id; + }); + + var serie = chart.series[item._datasetIndex]; + + if (serie && serie.clickState && serie.clickState.name) { + var stateParams = serie.clickState.params ? angular.copy(serie.clickState.params) : {}; + + // Compute query + var from = $scope.times[item._index]; + var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix(); + stateParams.q = 'time:>={0} AND time:<{1}'.format(from, to); + + return $state.go(serie.clickState.name, stateParams); + } + else { + console.debug('Click on item index={0} on range [{1},{2}]'.format(item._index, from, to)); + } }; diff --git a/www/plugins/graph/templates/docstats/graph.html b/www/plugins/graph/templates/docstats/graph.html index 724c4418..8cdde39e 100644 --- a/www/plugins/graph/templates/docstats/graph.html +++ b/www/plugins/graph/templates/docstats/graph.html @@ -9,14 +9,15 @@ </button> </div> - <canvas id="docstats-chart-{{chart.id}}" + <canvas id="{{::chartIdPrefix}}{{chart.id}}" class="chart-line" height="{{height}}" width="{{width}}" chart-data="chart.data" chart-labels="labels" chart-dataset-override="chart.datasetOverride" - chart-options="chart.options"> + chart-options="chart.options" + chart-click="onChartClick"> </canvas> <ng-include src="'plugins/graph/templates/common/graph_range_bar.html'"></ng-include> -- GitLab