diff --git a/CHANGELOG b/CHANGELOG index 5caf9c529b..d1de3ddf99 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,8 @@ v1.4 v1.3.1 (2013-XX-XX) ------------------- +* issue #540: suppress return of temporary internal variables in AQL + * issue #530: ReferenceError: ArangoError is not a constructor * issue #535: Problem with AQL user functions javascript API diff --git a/Documentation/Makefile.files b/Documentation/Makefile.files index df571ab137..dc1feb042a 100644 --- a/Documentation/Makefile.files +++ b/Documentation/Makefile.files @@ -295,7 +295,8 @@ man: Doxygen/.setup-directories ################################################################################ CLEANUP += \ - Doxygen/* + Doxygen/.setup-directories \ + Doxygen/* ## ----------------------------------------------------------------------------- ## --SECTION-- EXAMPLES diff --git a/Documentation/Manual/NewFeatures11.md b/Documentation/Manual/NewFeatures11.md index 27bf661f31..89643a3b31 100644 --- a/Documentation/Manual/NewFeatures11.md +++ b/Documentation/Manual/NewFeatures11.md @@ -209,8 +209,7 @@ Server Statistics {#NewFeatures11ServerStatistics} -------------------------------------------------- ArangoDB 1.1 allows querying the server status via the administration -front-end (see @ref UserManualWebInterfaceStatistics) or via REST API -methods. +front-end or via REST API methods. The following methods are available: - `GET /_admin/connection-statistics`: provides connection statistics diff --git a/Documentation/UserManual/WebInterface.md b/Documentation/UserManual/WebInterface.md index 506836f142..1b36bd1a86 100644 --- a/Documentation/UserManual/WebInterface.md +++ b/Documentation/UserManual/WebInterface.md @@ -7,7 +7,7 @@ ArangoDB's Web-Interface {#UserManualWebInterface} Accessing the Web-Interface {#UserManualWebInterfaceAccess} =========================================================== -The web interfaced can be access as +The web interface can be accessed via the URL http://localhost:8529 @@ -20,64 +20,55 @@ application instead. In this case use Collections Tab {#UserManualWebInterfaceCollections} ---------------------------------------------------- -The collection tabs shows an overview about the loaded and unloaded -collections of the database. +The *Collections* tab shows an overview of the loaded and unloaded +collections present in ArangoDB. System collections (i.e. collections +whose names start with an underscore) are not shown by default. -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-collections.png}@endlatexonly +The list of collections can be restricted using the search bar, or by +using the filtering at the top. The filter can also be used to show or +hide system collections. -You can load, unloaded, delete, or inspect the collections. Please -note that you should not delete or change system collections, i. e., -collections starting with an underscore. +Clicking on a collection will show the documents contained in it. +Clicking the small icon on a collection's badge will bring up a dialog +that allows loading/unloading, renaming and deleting the collection. -If you click on the magnifying glass, you will get a list of all documents -in the collection. +Please note that you should not change or delete system collections. -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-documents.png}@endlatexonly +In the list of documents of a collection, you can click on the *Add document* +line to add a new document to the collection. The document will be created +instantly, with a system-defined key. The key and all other attributes of the +document can be adjusted in the following view. -Using the pencil you can edit the document. -Query Tab {#UserManualWebInterfaceQuery} ----------------------------------------- +AQL Editor Tab {#UserManualWebInterfaceQuery} +--------------------------------------------- -The query tabs allows you to execute AQL queries. +The *AQL Editor* tab allow to execute AQL queries. -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-query.png}@endlatexonly +Type in a query in the bottom box and execute it by pressing the *Submit* button. +The query result will be shown in the box at the top. -Type in a query and execute it. -Shell Tab {#UserManualWebInterfaceShell} ----------------------------------------- +JS Shell Tab {#UserManualWebInterfaceShell} +------------------------------------------- -The shell tabs give you access to a JavaScript shell connection to the +The *JS Shell* tab provides access to a JavaScript shell connection to the database server. -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-shell.png}@endlatexonly +Any valid JavaScript code can be executed inside the shell. The code will be +executed inside your browser. To contact the ArangoDB server, you can use the +`db` object, for example as follows: + + JSH> db._create("mycollection"); + JSH> db.mycollection.save({ _key: "test", value: "something" }); -Use the OK button or return to execute a command. Logs Tab {#UserManualWebInterfaceLogs} -------------------------------------- -You can browse the log files. - -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-logs.png}@endlatexonly +You can use the *Logs* tab to browse the most recent log entries provided by the +ArangoDB database server. Note that the server only keeps a limited number of log entries. For real log analyses write the logs to disk using syslog or a similar -mechanism. - -Statistics Tab {#UserManualWebInterfaceStatistics} --------------------------------------------------- - -Use the statistics tab to display information about the server. - -@htmlonly ArangoDB Front-End@endhtmlonly -@latexonly\includegraphics[width=12cm]{images/fe-statistics.png}@endlatexonly - -Initially no statistics will be display. You must use the add button -to configure what type of information should be displayed. +mechanism. ArangoDB provides several startup options for this. diff --git a/Documentation/UserManual/WebInterfaceTOC.md b/Documentation/UserManual/WebInterfaceTOC.md index 4a29ec7410..a17c13ad43 100644 --- a/Documentation/UserManual/WebInterfaceTOC.md +++ b/Documentation/UserManual/WebInterfaceTOC.md @@ -6,4 +6,3 @@ TOC {#UserManualWebInterfaceTOC} - @ref UserManualWebInterfaceQuery - @ref UserManualWebInterfaceShell - @ref UserManualWebInterfaceLogs - - @ref UserManualWebInterfaceStatistics diff --git a/arangod/Actions/RestActionHandler.cpp b/arangod/Actions/RestActionHandler.cpp index 8b843a9d77..d54ef1196a 100644 --- a/arangod/Actions/RestActionHandler.cpp +++ b/arangod/Actions/RestActionHandler.cpp @@ -103,7 +103,7 @@ bool RestActionHandler::isDirect () { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// -string const& RestActionHandler::queue () { +string const& RestActionHandler::queue () const { return _queue; } diff --git a/arangod/Actions/RestActionHandler.h b/arangod/Actions/RestActionHandler.h index 3c90e2ba5d..6cacfa27bd 100644 --- a/arangod/Actions/RestActionHandler.h +++ b/arangod/Actions/RestActionHandler.h @@ -126,7 +126,7 @@ namespace triagens { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// - string const& queue (); + string const& queue () const; //////////////////////////////////////////////////////////////////////////////// /// {@inheritDoc} diff --git a/arangod/Ahuacatl/ahuacatl-ast-node.c b/arangod/Ahuacatl/ahuacatl-ast-node.c index 41bd476115..7b37ab5d43 100644 --- a/arangod/Ahuacatl/ahuacatl-ast-node.c +++ b/arangod/Ahuacatl/ahuacatl-ast-node.c @@ -437,10 +437,13 @@ TRI_aql_node_t* TRI_CreateNodeVariableAql (TRI_aql_context_t* const context, ABORT_OOM } - if (! TRI_AddVariableScopeAql(context, name, definingNode)) { - // duplicate variable name - TRI_SetErrorContextAql(context, TRI_ERROR_QUERY_VARIABLE_REDECLARED, name); - return NULL; + // if not a temporary variable + if (*name != '_') { + if (! TRI_AddVariableScopeAql(context, name, definingNode)) { + // duplicate variable name + TRI_SetErrorContextAql(context, TRI_ERROR_QUERY_VARIABLE_REDECLARED, name); + return NULL; + } } TRI_AQL_NODE_STRING(node) = (char*) name; diff --git a/arangod/Ahuacatl/ahuacatl-codegen.c b/arangod/Ahuacatl/ahuacatl-codegen.c index 9ae2d7a5a6..ad0d63b7db 100644 --- a/arangod/Ahuacatl/ahuacatl-codegen.c +++ b/arangod/Ahuacatl/ahuacatl-codegen.c @@ -668,7 +668,7 @@ static void InitArray (TRI_aql_codegen_js_t* const generator, static void EnterSymbol (TRI_aql_codegen_js_t* const generator, const char* const name, const TRI_aql_codegen_register_t registerIndex) { - TRI_aql_codegen_scope_t* scope = CurrentScope(generator); + TRI_aql_codegen_scope_t* scope; TRI_aql_codegen_variable_t* variable = CreateVariable(name, registerIndex); if (variable == NULL) { @@ -676,6 +676,17 @@ static void EnterSymbol (TRI_aql_codegen_js_t* const generator, return; } + // if not a temporary variable + if (*name != '_') { + scope = CurrentScope(generator); + } + else { + assert(generator->_scopes._length > 0); + + // get scope at level 0 + scope = (TRI_aql_codegen_scope_t*) TRI_AtVectorPointer(&generator->_scopes, 0); + } + if (TRI_InsertKeyAssociativePointer(&scope->_variables, name, (void*) variable, false)) { // variable already exists in symbol table. this should never happen LOG_TRACE("variable already registered: %s", name); diff --git a/arangod/RestHandler/RestBatchHandler.cpp b/arangod/RestHandler/RestBatchHandler.cpp index 3d4bbf02b0..ccb7923f53 100644 --- a/arangod/RestHandler/RestBatchHandler.cpp +++ b/arangod/RestHandler/RestBatchHandler.cpp @@ -87,7 +87,7 @@ bool RestBatchHandler::isDirect () { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// -string const& RestBatchHandler::queue () { +string const& RestBatchHandler::queue () const { static string const client = "STANDARD"; return client; diff --git a/arangod/RestHandler/RestBatchHandler.h b/arangod/RestHandler/RestBatchHandler.h index 793ff2b315..979af30ce7 100644 --- a/arangod/RestHandler/RestBatchHandler.h +++ b/arangod/RestHandler/RestBatchHandler.h @@ -153,7 +153,7 @@ namespace triagens { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// - string const& queue (); + string const& queue () const; //////////////////////////////////////////////////////////////////////////////// /// {@inheritDoc} diff --git a/arangod/RestHandler/RestDocumentHandler.cpp b/arangod/RestHandler/RestDocumentHandler.cpp index 4651c30464..409b0dad78 100644 --- a/arangod/RestHandler/RestDocumentHandler.cpp +++ b/arangod/RestHandler/RestDocumentHandler.cpp @@ -85,7 +85,7 @@ bool RestDocumentHandler::isDirect () { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// -string const& RestDocumentHandler::queue () { +string const& RestDocumentHandler::queue () const { static string const client = "STANDARD"; return client; diff --git a/arangod/RestHandler/RestDocumentHandler.h b/arangod/RestHandler/RestDocumentHandler.h index 21c579f952..de6d0c831c 100644 --- a/arangod/RestHandler/RestDocumentHandler.h +++ b/arangod/RestHandler/RestDocumentHandler.h @@ -94,7 +94,7 @@ namespace triagens { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// - string const& queue (); + string const& queue () const; //////////////////////////////////////////////////////////////////////////////// /// {@inheritDoc} diff --git a/arangod/RestHandler/RestImportHandler.cpp b/arangod/RestHandler/RestImportHandler.cpp index f7e5a8e8aa..8810cf58d8 100644 --- a/arangod/RestHandler/RestImportHandler.cpp +++ b/arangod/RestHandler/RestImportHandler.cpp @@ -83,7 +83,7 @@ bool RestImportHandler::isDirect () { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// -string const& RestImportHandler::queue () { +string const& RestImportHandler::queue () const { static string const client = "STANDARD"; return client; diff --git a/arangod/RestHandler/RestImportHandler.h b/arangod/RestHandler/RestImportHandler.h index a1cc7631a9..dbece94f34 100644 --- a/arangod/RestHandler/RestImportHandler.h +++ b/arangod/RestHandler/RestImportHandler.h @@ -94,7 +94,7 @@ namespace triagens { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// - string const& queue (); + string const& queue () const; //////////////////////////////////////////////////////////////////////////////// /// {@inheritDoc} diff --git a/arangod/RestHandler/RestUploadHandler.cpp b/arangod/RestHandler/RestUploadHandler.cpp index 9b42c126b2..f478a20fe7 100644 --- a/arangod/RestHandler/RestUploadHandler.cpp +++ b/arangod/RestHandler/RestUploadHandler.cpp @@ -87,7 +87,7 @@ bool RestUploadHandler::isDirect () { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// -string const& RestUploadHandler::queue () { +string const& RestUploadHandler::queue () const { static string const client = "STANDARD"; return client; diff --git a/arangod/RestHandler/RestUploadHandler.h b/arangod/RestHandler/RestUploadHandler.h index 9f84809a3e..cb98faead8 100644 --- a/arangod/RestHandler/RestUploadHandler.h +++ b/arangod/RestHandler/RestUploadHandler.h @@ -105,7 +105,7 @@ namespace triagens { /// {@inheritDoc} //////////////////////////////////////////////////////////////////////////////// - string const& queue (); + string const& queue () const; //////////////////////////////////////////////////////////////////////////////// /// {@inheritDoc} diff --git a/arangod/V8Server/ApplicationV8.cpp b/arangod/V8Server/ApplicationV8.cpp index 37f6dd4f96..c0137c783c 100644 --- a/arangod/V8Server/ApplicationV8.cpp +++ b/arangod/V8Server/ApplicationV8.cpp @@ -527,7 +527,7 @@ void ApplicationV8::setupOptions (map ("javascript.gc-frequency", &_gcFrequency, "JavaScript time-based garbage collection frequency (each x seconds)") ("javascript.action-directory", &_actionPath, "path to the JavaScript action directory") ("javascript.app-path", &_appPath, "one directory for applications") - ("javascript.dev-app-path", &_devAppPath, "one directory for dev aaplications") + ("javascript.dev-app-path", &_devAppPath, "one directory for dev applications") ("javascript.modules-path", &_modulesPath, "one or more directories separated by semi-colons") ("javascript.package-path", &_packagePath, "one or more directories separated by semi-colons") ("javascript.startup-directory", &_startupPath, "path to the directory containing alternate JavaScript startup scripts") diff --git a/arangod/VocBase/headers.c b/arangod/VocBase/headers.c index 1309488ea9..2325dd3442 100644 --- a/arangod/VocBase/headers.c +++ b/arangod/VocBase/headers.c @@ -235,18 +235,14 @@ static void MoveHeader (TRI_headers_t* h, headers->_begin = header; } else if (headers->_begin == header) { - if (header->_next != NULL) { - headers->_begin = header->_next; - } + headers->_begin = header->_next; } if (old->_next == NULL) { headers->_end = header; } else if (headers->_end == header) { - if (header->_prev != NULL) { - headers->_end = header->_prev; - } + headers->_end = header->_prev; } if (header->_prev != NULL) { diff --git a/arangod/VocBase/vocbase.c b/arangod/VocBase/vocbase.c index acfb172e12..a6886bbba8 100644 --- a/arangod/VocBase/vocbase.c +++ b/arangod/VocBase/vocbase.c @@ -215,96 +215,6 @@ static bool EqualKeyCollectionName (TRI_associative_pointer_t* array, void const /// @{ //////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -/// @brief create a JSON array with collection meta data -/// -/// this function is called when a collection is created or dropped -//////////////////////////////////////////////////////////////////////////////// - -static TRI_json_t* CreateJsonCollectionInfo (TRI_vocbase_col_t const* collection, - const char* situation) { - TRI_json_t* json; - TRI_json_t* details; - char* cidString; - - details = TRI_CreateArrayJson(TRI_CORE_MEM_ZONE); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, details, "type", TRI_CreateStringCopyJson(TRI_CORE_MEM_ZONE, TRI_TypeNameCollection(collection->_type))); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, details, "name", TRI_CreateStringCopyJson(TRI_CORE_MEM_ZONE, collection->_name)); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, details, "action", TRI_CreateStringCopyJson(TRI_CORE_MEM_ZONE, situation)); - - cidString = TRI_StringUInt64((uint64_t) collection->_cid); - - json = TRI_CreateArrayJson(TRI_CORE_MEM_ZONE); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, json, "id", TRI_CreateStringCopyJson(TRI_CORE_MEM_ZONE, cidString)); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, json, "type", TRI_CreateStringCopyJson(TRI_CORE_MEM_ZONE, "collection")); - TRI_Insert3ArrayJson(TRI_CORE_MEM_ZONE, json, "details", details); - - TRI_FreeString(TRI_CORE_MEM_ZONE, cidString); - - return json; -} - -//////////////////////////////////////////////////////////////////////////////// -/// @brief insert the id of a collection into the "_ids" collection -//////////////////////////////////////////////////////////////////////////////// - -static int InsertIdCallback (TRI_transaction_collection_t* trxCollection, - void* data) { - TRI_shaped_json_t* shaped; - TRI_primary_collection_t* primary; - TRI_json_t* json; - TRI_doc_mptr_t mptr; - int res; - - primary = (TRI_primary_collection_t*) trxCollection->_collection->_collection; - json = data; - - shaped = TRI_ShapedJsonJson(primary->_shaper, json); - - if (shaped == NULL) { - return TRI_ERROR_OUT_OF_MEMORY; - } - - res = primary->insert(trxCollection, NULL, &mptr, TRI_DOC_MARKER_KEY_DOCUMENT, shaped, NULL, false, false); - TRI_FreeShapedJson(primary->_shaper, shaped); - - return res; -} - -//////////////////////////////////////////////////////////////////////////////// -/// @brief save collection info on create or drop -/// -/// the info will be stored permanently in the "_ids" collection, so we can -/// later reconstruct ids of collections that are/were dropped -//////////////////////////////////////////////////////////////////////////////// - -static bool WriteCollectionInfo (TRI_vocbase_t* vocbase, - TRI_vocbase_col_t const* collection, - const char* situation) { - TRI_json_t* json; - int res; - - if (collection == NULL) { - return false; - } - - json = CreateJsonCollectionInfo(collection, situation); - - if (json == NULL) { - return false; - } - - res = TRI_ExecuteSingleOperationTransaction(vocbase, - "_ids", - TRI_TRANSACTION_WRITE, - InsertIdCallback, - json); - - TRI_FreeJson(TRI_CORE_MEM_ZONE, json); - - return (res == TRI_ERROR_NO_ERROR); -} - //////////////////////////////////////////////////////////////////////////////// /// @brief returns the current tick value, without using a lock //////////////////////////////////////////////////////////////////////////////// @@ -456,8 +366,6 @@ static void FreeCollection (TRI_vocbase_t* vocbase, TRI_vocbase_col_t* collectio //////////////////////////////////////////////////////////////////////////////// static bool UnregisterCollection (TRI_vocbase_t* vocbase, TRI_vocbase_col_t* collection) { - WriteCollectionInfo(vocbase, collection, "drop"); - TRI_WRITE_LOCK_COLLECTIONS_VOCBASE(vocbase); TRI_RemoveKeyAssociativePointer(&vocbase->_collectionsByName, collection->_name); diff --git a/html/admin/css/dashboardView.css b/html/admin/css/dashboardView.css index 1efd58413a..5de5ab45ee 100644 --- a/html/admin/css/dashboardView.css +++ b/html/admin/css/dashboardView.css @@ -12,21 +12,36 @@ } */ +.dbNotVisible { + opacity: 0 !important; +} + #dashboardHeader .btn-group { margin-left: 10px; margin-top: 6px; } -.db-zoom, .db-minimize, .db-hide, .db-info { +.group-close, .group-open { + float:right; + margin-top: 17px !important; + margin-right: 7px !important; +} + +.db-zoom, .db-minimize, .db-hide, .db-info, #db-collectionMinimize { float: right; margin-top: -4px !important; margin-right: 4px !important; } -.db-zoom:hover, .db-minimize:hover, .db-hide, .db-info:hover { +.db-zoom:hover, .db-minimize:hover, .db-hide, .db-info:hover, .group-close:hover, .group-open:hover, +#db-collectionMinimize:hover { cursor:pointer; } +.groupHidden li { + display:none; +} + .statGroups { margin-left: 0px; float:left; @@ -66,36 +81,49 @@ .statClient { float: left; - height: 150px; - width: 280px; + height: 120px; + width: 203px; margin-left: 9px; - margin-bottom: 15px; + margin-bottom: 12px; border: 1px solid black; border-radius: 2px 2px 2px 2px; background-color: #F4F3F3; box-shadow: 0 0 3px #333333; } -.statChart { +#detailGraphChart { + margin-left: -10px !important; +} +.statGroups .statChart { + margin-left: -45px !important; +} + +.nv-axislabel { + margin-left: 20px; } .nv-axisMaxMin > text { font: 10px sans-serif; } +.svgCollections { + height: 300px; + width: 300px; +} + .svgClass { margin-top: 0 !important; padding-top: 0 !important; - height: 150px; - width: 270px; + height: 140px; + width: 255px; } .svgDetailClass { margin-top: 0 !important; padding-top: 0 !important; height: 300px; - width: 877px; + width: 887px; } .boxHeader { @@ -110,6 +138,7 @@ } .statsHeader { + margin-top: 12px; margin-left: -10px; width: 940px; background-color: #686766; @@ -143,3 +172,83 @@ .nv-point { display: none; } + +/*Dashboard Dropdown*/ + +.dropdown-menu li > a { + padding: 0px 20px !important; +} + +.checkboxLabel { + margin-top: 4px; + padding-left: 0; +} + +.svgClass .nv-axisMaxMin text { + visibility: hidden; +} + +.svgClass .major text { + visibility: hidden; +} + +.svgClass .nv-axislabel { + visibility: hidden; +} + +/*Dashboard Checkbox*/ +input[type=checkbox].css-checkbox { + display:none; +} + +input[type=checkbox].css-checkbox + label.css-label { + padding-left:20px; + margin-top: 0px; + margin-bottom: -0px; + height:15px; + display:inline-block; + line-height:15px; + background-repeat:no-repeat; + background-position: 0 0; + font-size:15px; + vertical-align:middle; + cursor:pointer; +} + +input[type=checkbox].css-checkbox:checked + label.css-label { + background-position: 0 -15px; +} + +.css-label { + background-image:url(../img/dark-check-green.png); +} + +/*Dashboard Radio */ + +.dropdown-menu .radio { + margin-left: -21px; +} + +input[type="radio"] { + display:none; +} + +input[type="radio"] + label { + /*color:#f2f2f2;*/ + /*font-family:Arial, sans-serif; + font-size:14px;*/ +} + +input[type="radio"] + label span { + display:inline-block; + width:19px; + height:19px; + margin:-1px 4px 0 0; + vertical-align:middle; + background:url(../img/check_radio_sheet.png) -38px top no-repeat; + cursor:pointer; +} + +input[type="radio"]:checked + label span { + background:url(../img/check_radio_sheet.png) -57px top no-repeat; +} diff --git a/html/admin/img/check_radio_sheet.png b/html/admin/img/check_radio_sheet.png new file mode 100644 index 0000000000..fcb4776faa Binary files /dev/null and b/html/admin/img/check_radio_sheet.png differ diff --git a/html/admin/img/dark-check-green.png b/html/admin/img/dark-check-green.png new file mode 100644 index 0000000000..cb6c4f5958 Binary files /dev/null and b/html/admin/img/dark-check-green.png differ diff --git a/html/admin/index.html b/html/admin/index.html index 1ff35e8b67..3f43b8de3f 100644 --- a/html/admin/index.html +++ b/html/admin/index.html @@ -110,6 +110,7 @@ + diff --git a/html/admin/js/collections/arangoCollections.js b/html/admin/js/collections/arangoCollections.js index b3bd1b0ac1..b13889694b 100644 --- a/html/admin/js/collections/arangoCollections.js +++ b/html/admin/js/collections/arangoCollections.js @@ -17,6 +17,9 @@ window.arangoCollections = Backbone.Collection.extend({ }, translateStatus : function (status) { + if (status == 0) { + return 'corrupted'; + } if (status == 1) { return 'new born collection'; } diff --git a/html/admin/js/graphViewer/graph/JSONAdapter.js b/html/admin/js/graphViewer/graph/JSONAdapter.js index d9a9d8e2a9..cfa894b226 100644 --- a/html/admin/js/graphViewer/graph/JSONAdapter.js +++ b/html/admin/js/graphViewer/graph/JSONAdapter.js @@ -79,6 +79,9 @@ function JSONAdapter(jsonPath, nodes, edges, width, height) { return this.start + Math.random() * this.range; }; + self.loadNode = function(nodeId, callback) { + self.loadNodeFromTreeById(nodeId, callback); + }; self.loadNodeFromTreeById = function(nodeId, callback) { var json = jsonPath + nodeId + ".json"; @@ -159,4 +162,8 @@ function JSONAdapter(jsonPath, nodes, edges, width, height) { }; + self.expandCommunity = function (commNode, callback) { + + }; + } \ No newline at end of file diff --git a/html/admin/js/graphViewer/graph/arangoAdapter.js b/html/admin/js/graphViewer/graph/arangoAdapter.js index 93c5cc3501..ddd2959431 100644 --- a/html/admin/js/graphViewer/graph/arangoAdapter.js +++ b/html/admin/js/graphViewer/graph/arangoAdapter.js @@ -53,6 +53,7 @@ function ArangoAdapter(nodes, edges, config) { api = {}, queries = {}, cachedCommunities = {}, + joinedInCommunities = {}, nodeCollection, edgeCollection, limit, @@ -60,6 +61,7 @@ function ArangoAdapter(nodes, edges, config) { arangodb, width, height, + direction, setWidth = function(w) { initialX.range = w / 2; @@ -93,12 +95,22 @@ function ArangoAdapter(nodes, edges, config) { if (config.height !== undefined) { setHeight(config.height); } + if (config.undirected !== undefined) { + if (config.undirected === true) { + direction = "any"; + } else { + direction = "outbound"; + } + } else { + direction = "outbound"; + } }, findNode = function(id) { - var res = $.grep(nodes, function(e){ - return e._id === id; - }); + var intId = joinedInCommunities[id] || id, + res = $.grep(nodes, function(e){ + return e._id === intId; + }); if (res.length === 0) { return false; } @@ -118,7 +130,7 @@ function ArangoAdapter(nodes, edges, config) { if (res.length === 1) { return res[0]; } - throw "Too many nodes with the same ID, should never happen"; + throw "Too many edges with the same ID, should never happen"; }, insertNode = function(data) { @@ -145,7 +157,8 @@ function ArangoAdapter(nodes, edges, config) { _data: data, _id: data._id }, - e = findEdge(edge._id); + e = findEdge(edge._id), + edgeToPush; if (e) { return e; } @@ -160,8 +173,32 @@ function ArangoAdapter(nodes, edges, config) { edge.source = source; edge.target = target; edges.push(edge); - source._outboundCounter++; - target._inboundCounter++; + + + if (cachedCommunities[source._id] !== undefined) { + edgeToPush = {}; + edgeToPush.type = "s"; + edgeToPush.id = edge._id; + edgeToPush.source = $.grep(cachedCommunities[source._id].nodes, function(e){ + return e._id === data._from; + })[0]; + edgeToPush.source._outboundCounter++; + cachedCommunities[source._id].edges.push(edgeToPush); + } else { + source._outboundCounter++; + } + if (cachedCommunities[target._id] !== undefined) { + edgeToPush = {}; + edgeToPush.type = "t"; + edgeToPush.id = edge._id; + edgeToPush.target = $.grep(cachedCommunities[target._id].nodes, function(e){ + return e._id === data._to; + })[0]; + edgeToPush.target._inboundCounter++; + cachedCommunities[target._id].edges.push(edgeToPush); + } else { + target._inboundCounter++; + } return edge; }, @@ -187,7 +224,7 @@ function ArangoAdapter(nodes, edges, config) { removeEdgesForNode = function (node) { var i; - for ( i = 0; i < edges.length; i++ ) { + for (i = 0; i < edges.length; i++ ) { if (edges[i].source === node) { node._outboundCounter--; edges[i].target._inboundCounter--; @@ -202,6 +239,53 @@ function ArangoAdapter(nodes, edges, config) { } }, + combineCommunityEdges = function (nodes, commNode) { + var i, j, s, t, + cachedCommEdges = cachedCommunities[commNode._id].edges, + edgeToPush; + for (i = 0; i < edges.length; i++ ) { + edgeToPush = {}; + // s and t keep old values yay! + s = edges[i].source; + t = edges[i].target; + for (j = 0; j < nodes.length; j++) { + if (s === nodes[j]) { + if (edgeToPush.type !== undefined) { + edges[i].target = edgeToPush.target; + delete edgeToPush.target; + edgeToPush.type = "b"; + edgeToPush.edge = edges[i]; + edges.splice( i, 1 ); + i--; + break; + } + edges[i].source = commNode; + edgeToPush.type = "s"; + edgeToPush.id = edges[i]._id; + edgeToPush.source = s; + } + if (t === nodes[j]) { + if (edgeToPush.type !== undefined) { + edges[i].source = edgeToPush.source; + delete edgeToPush.source; + edgeToPush.type = "b"; + edgeToPush.edge = edges[i]; + edges.splice( i, 1 ); + i--; + break; + } + edges[i].target = commNode; + edgeToPush.type = "t"; + edgeToPush.id = edges[i]._id; + edgeToPush.target = t; + } + } + if (edgeToPush.type !== undefined) { + cachedCommEdges.push(edgeToPush); + } + } + }, + // Helper function to easily remove all outbound edges for one node removeOutboundEdgesFromNode = function ( node ) { if (node._outboundCounter > 0) { @@ -228,6 +312,9 @@ function ArangoAdapter(nodes, edges, config) { if (query !== queries.connectedEdges) { bindVars["@nodes"] = nodeCollection; } + if (query !== queries.childrenCentrality) { + bindVars["@dir"] = direction; + } bindVars["@edges"] = edgeCollection; var data = { query: query, @@ -257,24 +344,61 @@ function ArangoAdapter(nodes, edges, config) { }, collapseCommunity = function (community) { - var commId = "community_1", + var commId = "*community_" + Math.floor(Math.random()* 1000000), commNode = { _id: commId, - x: 1, - y: 1 + edges: [] }, nodesToRemove = _.map(community, function(id) { return findNode(id); }); - cachedCommunities[commId] = nodesToRemove; + commNode.x = nodesToRemove[0].x; + commNode.y = nodesToRemove[0].y; + cachedCommunities[commId] = {}; + cachedCommunities[commId].nodes = nodesToRemove; + cachedCommunities[commId].edges = []; + combineCommunityEdges(nodesToRemove, commNode); _.each(nodesToRemove, function(n) { + joinedInCommunities[n._id] = commId; removeNode(n); - removeEdgesForNode(n); }); nodes.push(commNode); }, + expandCommunity = function (commNode) { + var commId = commNode._id, + nodesToAdd = cachedCommunities[commId].nodes, + edgesToChange = cachedCommunities[commId].edges, + com; + removeNode(commNode); + if (limit < nodes.length + nodesToAdd.length) { + com = reducer.getCommunity(limit); + collapseCommunity(com); + } + _.each(nodesToAdd, function(n) { + delete joinedInCommunities[n._id]; + nodes.push(n); + }); + _.each(edgesToChange, function(e) { + var edge; + switch(e.type) { + case "t": + edge = findEdge(e.id); + edge.target = e.target; + break; + case "s": + edge = findEdge(e.id); + edge.source = e.source; + break; + case "b": + edges.push(e.edge); + break; + } + }); + delete cachedCommunities[commId]; + }, + parseResultOfTraversal = function (result, callback) { result = result[0]; _.each(result, function(visited) { @@ -366,7 +490,7 @@ function ArangoAdapter(nodes, edges, config) { + "@@nodes, " + "@@edges, " + "@id, " - + "\"outbound\", {" + + "@dir, {" + "strategy: \"depthfirst\"," + "maxDepth: 1," + "paths: true" @@ -379,7 +503,7 @@ function ArangoAdapter(nodes, edges, config) { + "@@nodes, " + "@@edges, " + "n._id, " - + "\"outbound\", {" + + "@dir, {" + "strategy: \"depthfirst\"," + "maxDepth: 1," + "paths: true" @@ -399,7 +523,6 @@ function ArangoAdapter(nodes, edges, config) { reducer = new NodeReducer(nodes, edges); - self.oldLoadNodeFromTreeById = function(nodeId, callback) { sendQuery(queries.nodeById, { id: nodeId @@ -408,6 +531,10 @@ function ArangoAdapter(nodes, edges, config) { }); }; + self.loadNode = function(nodeId, callback) { + self.loadNodeFromTreeById(nodeId, callback); + }; + self.loadNodeFromTreeById = function(nodeId, callback) { sendQuery(queries.traversalById, { id: nodeId @@ -554,9 +681,16 @@ function ArangoAdapter(nodes, edges, config) { }); }; - self.changeTo = function (nodesCol, edgesCol ) { + self.changeTo = function (nodesCol, edgesCol, dir) { nodeCollection = nodesCol; edgeCollection = edgesCol; + if (dir !== undefined) { + if (dir === true) { + direction = "any"; + } else { + direction = "outbound"; + } + } api.node = api.base + "document?collection=" + nodeCollection; api.edge = api.base + "edge?collection=" + edgeCollection; }; @@ -572,4 +706,11 @@ function ArangoAdapter(nodes, edges, config) { } }; + self.expandCommunity = function (commNode, callback) { + expandCommunity(commNode); + if (callback !== undefined) { + callback(); + } + }; + } \ No newline at end of file diff --git a/html/admin/js/graphViewer/graph/eventLibrary.js b/html/admin/js/graphViewer/graph/eventLibrary.js index 67198b1cb7..3cefa65244 100644 --- a/html/admin/js/graphViewer/graph/eventLibrary.js +++ b/html/admin/js/graphViewer/graph/eventLibrary.js @@ -67,8 +67,8 @@ function EventLibrary() { if (config.startCallback === undefined) { throw "A callback to the Start-method has to be defined"; } - if (config.loadNode === undefined) { - throw "A callback to load a node has to be defined"; + if (config.adapter === undefined) { + throw "An adapter to load data has to be defined"; } if (config.reshapeNodes === undefined) { throw "A callback to reshape nodes has to be defined"; @@ -81,7 +81,9 @@ function EventLibrary() { var edges = config.edges, nodes = config.nodes, startCallback = config.startCallback, - loadNode = config.loadNode, + adapter = config.adapter, + loadNode = adapter.loadNode, + expandCom = adapter.expandCommunity, reshapeNodes = config.reshapeNodes, removeNode = function (node) { var i; @@ -126,8 +128,12 @@ function EventLibrary() { }, expandNode = function(n) { - n._expanded = true; - loadNode(n._id, startCallback); + if (/^\*community/.test(n._id)) { + expandCom(n, startCallback); + } else { + n._expanded = true; + loadNode(n._id, startCallback); + } }; return function(n) { diff --git a/html/admin/js/graphViewer/graph/nodeReducer.js b/html/admin/js/graphViewer/graph/nodeReducer.js index 839bb36a03..334d3767cd 100644 --- a/html/admin/js/graphViewer/graph/nodeReducer.js +++ b/html/admin/js/graphViewer/graph/nodeReducer.js @@ -148,7 +148,7 @@ function NodeReducer(nodes, edges) { _.each(nodes, function (n) { var id = n._id, c1, c2; - if (id == sID || id == lID) { + if (id === sID || id === lID) { return null; } c1 = getDQValue(dQ, id, sID); @@ -277,6 +277,7 @@ function NodeReducer(nodes, edges) { res = [], dist = {}, dist2 = {}, + detectSteps = true, sortByDistance = function (a, b) { var d1 = dist[_.min(a,minDist(dist))], d2 = dist[_.min(b,minDist(dist))], @@ -290,7 +291,9 @@ function NodeReducer(nodes, edges) { throw "Load some nodes first."; } populateValues(dQ, a, heap); - while (communityDetectionStep(dQ, a, heap, coms)) {} + while (detectSteps) { + detectSteps = communityDetectionStep(dQ, a, heap, coms); + } res = _.pluck(_.values(coms), "com"); if (focus !== undefined) { dist = floatDist(focus._id); diff --git a/html/admin/js/graphViewer/graph/nodeShaper.js b/html/admin/js/graphViewer/graph/nodeShaper.js index 3d80af419e..d60e7cd1cd 100644 --- a/html/admin/js/graphViewer/graph/nodeShaper.js +++ b/html/admin/js/graphViewer/graph/nodeShaper.js @@ -64,6 +64,7 @@ function NodeShaper(parent, flags, idfunc) { "use strict"; var self = this, + communityRegEx = /^\*community/, nodes = [], visibleLabels = true, noop = function (node) { @@ -91,7 +92,17 @@ function NodeShaper(parent, flags, idfunc) { addColor = noop, addShape = noop, addLabel = noop, - + addCommunityShape = function(g) { + g.append("polygon") + .attr("points", "0,-25 -16,20 23,-10 -23,-10 16,20"); + }, + addCommunityLabel = function(g) { + g.append("text") // Append a label for the node + .attr("text-anchor", "middle") // Define text-anchor + .text(function(d) { + return d._size; + }); + }, unbindEvents = function() { // Hard unbind the dragging self.parent @@ -120,9 +131,18 @@ function NodeShaper(parent, flags, idfunc) { }, addQue = function (g) { - addShape(g); + var community = g.filter(function(n) { + return communityRegEx.test(n._id); + }), + normal = g.filter(function(n) { + return !communityRegEx.test(n._id); + }); + addCommunityShape(community); + addShape(normal); + if (visibleLabels) { - addLabel(g); + addCommunityLabel(community); + addLabel(normal); } addColor(g); addEvents(g); @@ -158,7 +178,12 @@ function NodeShaper(parent, flags, idfunc) { // Append the group and class to all new g.enter() .append("g") - .attr("class", "node") // node is CSS class that might be edited + .attr("class", function(d) { + if (communityRegEx.test(d._id)) { + return "node communitynode"; + } + return "node"; + }) // node is CSS class that might be edited .attr("id", idFunction); // Remove all old g.exit().remove(); @@ -176,7 +201,8 @@ function NodeShaper(parent, flags, idfunc) { case NodeShaper.shapes.CIRCLE: radius = shape.radius || 25; addShape = function (node) { - node.append("circle") // Display nodes as circles + node + .append("circle") // Display nodes as circles .attr("r", radius); // Set radius }; break; diff --git a/html/admin/js/graphViewer/graphViewer.js b/html/admin/js/graphViewer/graphViewer.js index fafdf81048..75cc74e204 100644 --- a/html/admin/js/graphViewer/graphViewer.js +++ b/html/admin/js/graphViewer/graphViewer.js @@ -85,7 +85,7 @@ function GraphViewer(svg, width, height, adapterConfig, config) { }, nodeLimitCallBack = function(limit) { - self.adapter.setNodeLimit(limit); + self.adapter.setNodeLimit(limit, self.start); }, parseZoomConfig = function(config) { @@ -174,7 +174,7 @@ function GraphViewer(svg, width, height, adapterConfig, config) { edges: edges, nodes: nodes, startCallback: self.start, - loadNode: self.adapter.loadNodeFromTreeById, + adapter: self.adapter, reshapeNodes: self.nodeShaper.reshapeNodes }, drag: { diff --git a/html/admin/js/graphViewer/jasmine_test/helper/mocks.js b/html/admin/js/graphViewer/jasmine_test/helper/mocks.js index f11f015c7b..da8e5270db 100644 --- a/html/admin/js/graphViewer/jasmine_test/helper/mocks.js +++ b/html/admin/js/graphViewer/jasmine_test/helper/mocks.js @@ -43,7 +43,9 @@ var mocks = mocks || {}; patchNode: function(){}, createEdge: function(){}, deleteEdge: function(){}, - patchEdge: function(){} + patchEdge: function(){}, + loadNode: function(){}, + expandCommunity: function(){} }; }()); \ No newline at end of file diff --git a/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js b/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js index bf3c65f866..c721531ab2 100644 --- a/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specAdapter/arangoAdapterSpec.js @@ -58,6 +58,17 @@ checkCallbackFunction = function() { callbackCheck = true; }, + + getCommunityNodes = function() { + return _.filter(nodes, function(n) { + return n._id.match(/^\*community/); + }); + }, + + getCommunityNodesIds = function() { + return _.pluck(getCommunityNodes(), "_id"); + }, + nodeWithID = function(id) { return $.grep(nodes, function(e){ return e._id === id; @@ -295,26 +306,40 @@ height: 40 } ); - traversalQuery = function(id, nods, edgs) { + traversalQuery = function(id, nods, edgs, undirected) { + var dir; + if (undirected === true) { + dir = "any"; + } else { + dir = "outbound"; + } return JSON.stringify({ - query: "RETURN TRAVERSAL(@@nodes, @@edges, @id, \"outbound\"," + query: "RETURN TRAVERSAL(@@nodes, @@edges, @id, @dir," + " {strategy: \"depthfirst\",maxDepth: 1,paths: true})", bindVars: { id: id, "@nodes": nods, + "@dir": dir, "@edges": edgs } }); }; - filterQuery = function(v, nods, edgs) { + filterQuery = function(v, nods, edgs, undirected) { + var dir; + if (undirected === true) { + dir = "any"; + } else { + dir = "outbound"; + } return JSON.stringify({ query: "FOR n IN @@nodes FILTER n.id == @value" - + " RETURN TRAVERSAL(@@nodes, @@edges, n._id, \"outbound\"," + + " RETURN TRAVERSAL(@@nodes, @@edges, n._id, @dir," + " {strategy: \"depthfirst\",maxDepth: 1,paths: true})", bindVars: { value: v, "@nodes": nods, - "@edges": edgs + "@dir": dir, + "@edges": edgs } }); }; @@ -455,6 +480,12 @@ }); }); + it('should map loadNode to loadByID', function() { + spyOn(adapter, "loadNodeFromTreeById"); + adapter.loadNode("a", "b"); + expect(adapter.loadNodeFromTreeById).toHaveBeenCalledWith("a", "b"); + }); + it('should be able to load a tree node from ArangoDB' + ' by internal attribute and value', function() { @@ -638,6 +669,40 @@ }); + it('should be able to switch to different collections and change to directed', function() { + + runs(function() { + + spyOn($, "ajax"); + + adapter.changeTo(altNodesCollection, altEdgesCollection, false); + + adapter.loadNode("42"); + + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery("42", altNodesCollection, altEdgesCollection, false)) + ); + + }); + }); + + it('should be able to switch to different collections' + + ' and change to undirected', function() { + + runs(function() { + + spyOn($, "ajax"); + + adapter.changeTo(altNodesCollection, altEdgesCollection, true); + + adapter.loadNode("42"); + + expect($.ajax).toHaveBeenCalledWith( + requests.cursor(traversalQuery("42", altNodesCollection, altEdgesCollection, true)) + ); + + }); + }); describe('that has already loaded one graph', function() { var c0, c1, c2, c3, c4, c5, c6, c7, @@ -877,46 +942,58 @@ runs(function() { adapter.setNodeLimit(6); - spyOn(this, "fakeReducerRequest"); + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0]; + }); adapter.loadNodeFromTreeById(c1, checkCallbackFunction); expect(this.fakeReducerRequest).toHaveBeenCalledWith(6, nodeWithID(c1)); }); }); - it('should not trigger the reducer if the limit is set large enough', function() { - spyOn(this, "fakeReducerRequest"); - adapter.setNodeLimit(10); - expect(this.fakeReducerRequest).not.toHaveBeenCalled(); - }); - - - it('should trigger the reducer if the limit is set too small', function() { - spyOn(this, "fakeReducerRequest"); - adapter.setNodeLimit(2); - expect(this.fakeReducerRequest).toHaveBeenCalledWith(2); - }); - describe('checking community nodes', function() { - it('should create a community node if limit is set too small', function() { - var called = false, - callback = function() { - called = true; - }; + it('should not trigger the reducer if the limit is set large enough', function() { spyOn(this, "fakeReducerRequest").andCallFake(function() { - return [c0, c1, c2]; + return [c0]; }); - adapter.setNodeLimit(2, callback); + adapter.setNodeLimit(10); + expect(this.fakeReducerRequest).not.toHaveBeenCalled(); + }); + + it('should trigger the reducer if the limit is set too small', function() { + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0]; + }); + adapter.setNodeLimit(2); + expect(this.fakeReducerRequest).toHaveBeenCalledWith(2); + }); - notExistNodes([c0, c1, c2]); - existNode("community_1"); - existNodes([c3]); - expect(nodes.length).toEqual(2); - existEdge("community_1", c3); - expect(edges.length).toEqual(1); - - expect(called).toBeTruthy(); + it('should create a community node if limit is set too small', function() { + var called; + + runs(function() { + callbackCheck = false; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [c0, c1, c2]; + }); + adapter.setNodeLimit(2, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var commId = getCommunityNodesIds()[0]; + notExistNodes([c0, c1, c2]); + existNode(commId); + existNodes([c3, c4]); + expect(nodes.length).toEqual(3); + existEdge(commId, c3); + existEdge(commId, c4); + expect(edges.length).toEqual(2); + }); }); it('should create a community node if too many nodes are added', function() { @@ -933,20 +1010,312 @@ }); runs(function() { + var commId = getCommunityNodesIds()[0]; notExistNodes([c0, c1, c2, c3]); - existNode("community_1"); + existNode(commId); existNodes([c4, c5, c6, c7]); expect(nodes.length).toEqual(5); - existEdge("community_1", c4); - existEdge("community_1", c5); - existEdge("community_1", c6); - existEdge("community_1", c7); + existEdge(commId, c4); + existEdge(commId, c5); + existEdge(commId, c6); + existEdge(commId, c7); expect(edges.length).toEqual(4); }); }); + describe('expanding after a while', function() { + + it('should connect edges of internal nodes accordingly', function() { + + var commNode, called, counterCallback, + v0, v1, v2, v3, v4, + e0_1, e0_2, e1_3, e1_4, e2_3, e2_4; + + runs(function() { + var v = "vertices", + e = "edges"; + nodes.length = 0; + edges.length = 0; + v0 = insertNode(v, 0); + v1 = insertNode(v, 1); + v2 = insertNode(v, 2); + v3 = insertNode(v, 3); + v4 = insertNode(v, 4); + e0_1 = insertEdge(e, v0, v1); + e0_2 = insertEdge(e, v0, v2); + e1_3 = insertEdge(e, v1, v3); + e1_4 = insertEdge(e, v1, v4); + e2_3 = insertEdge(e, v2, v3); + e2_4 = insertEdge(e, v2, v4); + called = 0; + counterCallback = function() { + called++; + }; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [v1, v3, v4]; + }); + adapter.setNodeLimit(3); + + adapter.changeTo(v, e); + adapter.loadNode(v0, counterCallback); + adapter.loadNode(v1, counterCallback); + + }); + + waitsFor(function() { + return called === 2; + }); + + runs(function() { + adapter.loadNode(v2, counterCallback); + commNode = getCommunityNodes()[0]; + }); + + waitsFor(function() { + return called === 3; + }); + + runs(function() { + var commId = commNode._id; + // Check start condition + existNodes([commId, v0, v2]); + expect(nodes.length).toEqual(3); + + existEdge(v0, v2); + existEdge(v0, commId); + existEdge(v2, commId); + expect(edges.length).toEqual(4); + + adapter.setNodeLimit(20); + adapter.expandCommunity(commNode, counterCallback); + }); + + waitsFor(function() { + return called === 4; + }); + + runs(function() { + existNodes([v0, v1, v2, v3, v4]); + expect(nodes.length).toEqual(5); + + existEdge(v0, v1); + existEdge(v0, v2); + existEdge(v1, v3); + existEdge(v1, v4); + existEdge(v2, v3); + existEdge(v2, v4); + expect(edges.length).toEqual(6); + + }); + }); + + it('set inbound and outboundcounter correctly', function() { + + var commNode, called, counterCallback, + v0, v1, v2, v3, v4, + e0_1, e0_2, e1_3, e1_4, e2_3, e2_4; + + runs(function() { + var v = "vertices", + e = "edges"; + nodes.length = 0; + edges.length = 0; + v0 = insertNode(v, 0); + v1 = insertNode(v, 1); + v2 = insertNode(v, 2); + v3 = insertNode(v, 3); + v4 = insertNode(v, 4); + e0_1 = insertEdge(e, v0, v1); + e0_2 = insertEdge(e, v0, v2); + e1_3 = insertEdge(e, v1, v3); + e1_4 = insertEdge(e, v1, v4); + e2_3 = insertEdge(e, v2, v3); + e2_4 = insertEdge(e, v2, v4); + called = 0; + counterCallback = function() { + called++; + }; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return [v1, v3, v4]; + }); + adapter.setNodeLimit(3); + + adapter.changeTo(v, e); + adapter.loadNode(v0, counterCallback); + adapter.loadNode(v1, counterCallback); + + }); + + waitsFor(function() { + return called === 2; + }); + + runs(function() { + adapter.loadNode(v2, counterCallback); + commNode = getCommunityNodes()[0]; + }); + + waitsFor(function() { + return called === 3; + }); + + runs(function() { + adapter.setNodeLimit(20); + adapter.expandCommunity(commNode, counterCallback); + }); + + waitsFor(function() { + return called === 4; + }); + + runs(function() { + var checkNodeWithInAndOut = function(id, inbound, outbound) { + var n = nodeWithID(id); + expect(n._outboundCounter).toEqual(outbound); + expect(n._inboundCounter).toEqual(inbound); + }; + checkNodeWithInAndOut(v0, 0, 2); + checkNodeWithInAndOut(v1, 1, 2); + checkNodeWithInAndOut(v2, 1, 2); + checkNodeWithInAndOut(v3, 2, 0); + checkNodeWithInAndOut(v4, 2, 0); + }); + }); + + }); + + describe('that displays a community node already', function() { + + var firstCommId, + fakeResult; + + beforeEach(function() { + runs(function() { + callbackCheck = false; + adapter.setNodeLimit(7); + fakeResult = [c0, c2]; + spyOn(this, "fakeReducerRequest").andCallFake(function() { + return fakeResult; + }); + adapter.loadNodeFromTreeById(c1, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + firstCommId = getCommunityNodesIds()[0]; + }); + }); + + it('should expand a community if enough space is available', function() { + runs(function() { + adapter.setNodeLimit(10); + callbackCheck = false; + adapter.expandCommunity(nodeWithID(firstCommId), checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(getCommunityNodes().length).toEqual(0); + existNodes([c0, c1, c2, c3, c4, c5, c6, c7]); + existEdge(c0, c1); + existEdge(c0, c2); + existEdge(c0, c3); + existEdge(c0, c4); + }); + + }); + + it('should expand a community and join another ' + + 'one if not enough space is available', function() { + runs(function() { + fakeResult = [c1, c7]; + callbackCheck = false; + adapter.expandCommunity(nodeWithID(firstCommId), checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + var newCommId = getCommunityNodesIds()[0]; + expect(getCommunityNodes().length).toEqual(1); + existNodes([c0, c2, c3, c4, c5, c6, newCommId]); + notExistNodes([c1, c7]); + + existEdge(c0, c2); + existEdge(c0, c3); + existEdge(c0, c4); + + existEdge(c0, newCommId); + existEdge(newCommId, c5); + existEdge(newCommId, c6); + }); + }); + + it('should join another community if space is further reduced', function() { + runs(function() { + fakeResult = [c1, c7]; + callbackCheck = false; + adapter.setNodeLimit(6, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + expect(getCommunityNodes().length).toEqual(2); + var ids = getCommunityNodesIds(), + newCommId; + + if (firstCommId === ids[0]) { + newCommId = ids[1]; + } else { + newCommId = ids[0]; + } + + existNodes([c3, c4, c5, c6, firstCommId, newCommId]); + notExistNodes([c0, c1, c2, c7]); + + existEdge(firstCommId, c3); + existEdge(firstCommId, c4); + existEdge(firstCommId, newCommId); + existEdge(newCommId, c5); + existEdge(newCommId, c6); + }); + }); + + it('should connect edges to internal nodes', function() { + + runs(function() { + insertEdge(edgesCollection, c3, c0); + + adapter.setNodeLimit(20); + callbackCheck = false; + adapter.loadNode(c3, checkCallbackFunction); + }); + + waitsFor(function() { + return callbackCheck; + }); + + runs(function() { + existEdge(c3, firstCommId); + }); + + }); + + }); + }); describe('that has loaded several queries', function() { diff --git a/html/admin/js/graphViewer/jasmine_test/specAdapter/interfaceSpec.js b/html/admin/js/graphViewer/jasmine_test/specAdapter/interfaceSpec.js index eefd4b42e3..485dd09d1b 100644 --- a/html/admin/js/graphViewer/jasmine_test/specAdapter/interfaceSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specAdapter/interfaceSpec.js @@ -56,6 +56,7 @@ var describeInterface = function (testee) { }); // Add functions to load here: + expect(testee).toHaveFunction("loadNode", 2); expect(testee).toHaveFunction("loadNodeFromTreeById", 2); expect(testee).toHaveFunction("requestCentralityChildren", 2); expect(testee).toHaveFunction("loadNodeFromTreeByAttributeValue", 3); @@ -66,6 +67,7 @@ var describeInterface = function (testee) { expect(testee).toHaveFunction("deleteNode", 2); expect(testee).toHaveFunction("patchNode", 3); expect(testee).toHaveFunction("setNodeLimit", 2); + expect(testee).toHaveFunction("expandCommunity", 2); }); }; diff --git a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherSpec.js b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherSpec.js index 2aea9bda5f..ac7bd7ee46 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherSpec.js @@ -62,6 +62,8 @@ spyOn(adapter, "createEdge"); spyOn(adapter, "patchEdge"); spyOn(adapter, "deleteEdge"); + spyOn(adapter, "loadNode"); + spyOn(adapter, "expandCommunity"); }; beforeEach(function() { @@ -74,9 +76,6 @@ nodes = []; edges = []; - this.loadNode = function() {}; - spyOn(this, "loadNode"); - defaultPosition = { x: 1, y: 1, @@ -87,7 +86,7 @@ edges: edges, nodes: nodes, startCallback: function() {}, - loadNode: this.loadNode, + adapter: adapter, reshapeNodes: function() {} }; @@ -437,7 +436,7 @@ }); waitsFor(function() { - return this.loadNode.wasCalled; + return adapter.loadNode.wasCalled; }, 1000, "The loadNode function should have been called."); runs(function() { diff --git a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js index f49038b680..e641dccb45 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js @@ -51,6 +51,8 @@ spyOn(adapter, "createEdge"); spyOn(adapter, "patchEdge"); spyOn(adapter, "deleteEdge"); + spyOn(adapter, "loadNode"); + spyOn(adapter, "expandCommunity"); }; @@ -87,15 +89,13 @@ }]; adapter = mocks.adapter; layouter = mocks.layouter; - this.loadNode = function() {}; - spyOn(this, "loadNode"); addSpies(); var expandConfig = { edges: edges, nodes: nodes, startCallback: function() {}, - loadNode: this.loadNode, + adapter: adapter, reshapeNodes: function() {} }, @@ -307,7 +307,7 @@ helper.simulateMouseEvent("click", "1"); - expect(this.loadNode).toHaveBeenCalledWith(nodes[0]._id, jasmine.any(Function)); + expect(adapter.loadNode).toHaveBeenCalledWith(nodes[0]._id, jasmine.any(Function)); }); }); diff --git a/html/admin/js/graphViewer/jasmine_test/specEvents/eventLibrarySpec.js b/html/admin/js/graphViewer/jasmine_test/specEvents/eventLibrarySpec.js index 377a58fc9f..6a39dc7b7e 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEvents/eventLibrarySpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEvents/eventLibrarySpec.js @@ -1,6 +1,6 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ /*global beforeEach, afterEach */ -/*global describe, it, expect */ +/*global describe, it, expect, jasmine */ /*global runs, spyOn, waitsFor */ /*global window, eb, loadFixtures, document, $ */ /*global EventLibrary*/ @@ -40,13 +40,15 @@ var eventLib, nodeShaperDummy = {}, - edgeShaperDummy = {}; + edgeShaperDummy = {}, + adapterDummy = {}; beforeEach(function() { eventLib = new EventLibrary(); nodeShaperDummy.reshapeNodes = function() {}; edgeShaperDummy.reshapeEdges = function() {}; - + adapterDummy.loadNode = function() {}; + adapterDummy.expandCommunity = function() {}; spyOn(nodeShaperDummy, "reshapeNodes"); spyOn(edgeShaperDummy, "reshapeEdges"); }); @@ -59,10 +61,6 @@ edges, loadedNodes, started, - loadNodeCallback = function(node) { - loaded++; - loadedNodes.push(node); - }, reshapeNodesCallback = function() { reshaped++; }, @@ -84,7 +82,7 @@ edges: edges, nodes: nodes, startCallback: startCallback, - loadNode: loadNodeCallback, + adapter: adapterDummy, reshapeNodes: reshapeNodesCallback }; }); @@ -96,6 +94,11 @@ _inboundCounter: 0 }; nodes.push(node); + spyOn(adapterDummy, "loadNode").andCallFake(function(node) { + loaded++; + loadedNodes.push(node); + }); + //config.adapter = adapterDummy.loadNode; testee = eventLib.Expand(config); testee(node); @@ -206,6 +209,124 @@ expect(c2._outboundCounter).toEqual(1); }); + describe('with community nodes', function() { + + it('should expand a community node properly', function() { + var comm = { + _id: "*community_1" + }; + nodes.push(comm); + + spyOn(adapterDummy, "expandCommunity"); + + testee = eventLib.Expand(config); + testee(comm); + + expect(adapterDummy.expandCommunity).toHaveBeenCalledWith(comm, jasmine.any(Function)); + }); + + it('should remove a community if last pointer to it is collapsed', function() { + + runs(function() { + var c0 = { + _id: 0, + _outboundCounter: 1, + _inboundCounter: 0 + }, + c1 = { + _id: 1, + _expanded: true, + _outboundCounter: 1, + _inboundCounter: 1 + }, + comm = { + _id: "*community_1", + _outboundCounter: 1, + _inboundCounter: 1 + }, + c2 = { + _id: 1, + _outboundCounter: 0, + _inboundCounter: 1 + }, + e0 = { + source: c0, + target: c1 + }, + e1 = { + source: c1, + target: comm + }, + e2 = { + source: comm, + target: c2 + }; + nodes.push(c0); + nodes.push(c1); + nodes.push(comm); + nodes.push(c2); + edges.push(e0); + edges.push(e1); + edges.push(e2); + + testee = eventLib.Expand(config); + testee(c1); + + expect(nodes).toEqual([c0, c1]); + expect(edges).toEqual([e0]); + }); + + }); + + it('should not remove a community if a pointer to it still exists', function() { + + runs(function() { + var c0 = { + _id: 0, + _outboundCounter: 2, + _inboundCounter: 0 + }, + c1 = { + _id: 1, + _expanded: true, + _outboundCounter: 1, + _inboundCounter: 1 + }, + comm = { + _id: "*community_1", + _outboundCounter: 0, + _inboundCounter: 2 + }, + e0 = { + source: c0, + target: c1 + }, + e1 = { + source: c0, + target: comm + }, + e2 = { + source: c1, + target: comm + }; + nodes.push(c0); + nodes.push(c1); + nodes.push(comm); + edges.push(e0); + edges.push(e1); + edges.push(e2); + + testee = eventLib.Expand(config); + testee(c1); + + expect(nodes).toEqual([c0, c1, comm]); + expect(edges).toEqual([e0, e1]); + }); + + }); + + }); + @@ -248,14 +369,14 @@ function() { eventLib.Expand(testConfig); } - ).toThrow("A callback to load a node has to be defined"); + ).toThrow("An adapter to load data has to be defined"); }); it('should throw an error if reshape node callback is not given', function() { testConfig.edges = []; testConfig.nodes = []; testConfig.startCallback = function(){}; - testConfig.loadNode = function(){}; + testConfig.adapter = adapterDummy; expect( function() { eventLib.Expand(testConfig); diff --git a/html/admin/js/graphViewer/jasmine_test/specGraphViewer/graphViewerSpec.js b/html/admin/js/graphViewer/jasmine_test/specGraphViewer/graphViewerSpec.js index e74f3b6d5d..4dc6c49423 100644 --- a/html/admin/js/graphViewer/jasmine_test/specGraphViewer/graphViewerSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specGraphViewer/graphViewerSpec.js @@ -153,7 +153,7 @@ describe("Graph Viewer", function() { edges: [], nodes: [], startCallback: jasmine.any(Function), - loadNode: jasmine.any(Function), + adapter: jasmine.any(Object), reshapeNodes: jasmine.any(Function) }, drag: { @@ -172,7 +172,7 @@ describe("Graph Viewer", function() { edges: [], nodes: [], startCallback: jasmine.any(Function), - loadNode: jasmine.any(Function), + adapter: jasmine.any(Object), reshapeNodes: jasmine.any(Function) }); expect(viewer.dispatcherConfig.drag).toEqual({ @@ -270,6 +270,15 @@ describe("Graph Viewer", function() { expect(viewer.adapter.setNodeLimit).wasCalled(); }); + it('should trigger the start function if node limit is reduced to far', function() { + spyOn(viewer.adapter, "setNodeLimit").andCallFake(function(l, callback) { + callback(); + }); + spyOn(viewer, "start"); + helper.simulateScrollUpMouseEvent("outersvg"); + expect(viewer.start).wasCalled(); + }); + }); diff --git a/html/admin/js/graphViewer/jasmine_test/specNodeReducer/nodeReducerSpec.js b/html/admin/js/graphViewer/jasmine_test/specNodeReducer/nodeReducerSpec.js index 73d69f90d0..384b37d18e 100644 --- a/html/admin/js/graphViewer/jasmine_test/specNodeReducer/nodeReducerSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specNodeReducer/nodeReducerSpec.js @@ -149,7 +149,7 @@ edges.push(helper.createSimpleEdge(nodes, 5, 7)); var com = reducer.getCommunity(6); - expect(com).toContainNodes([0, 1, 2]); + expect(com).toContainNodes([0, 1, 2, 3]); }); }); diff --git a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js index 119e7e134b..b60db1b0f2 100644 --- a/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specNodeShaper/nodeShaperSpec.js @@ -988,6 +988,76 @@ expect(n.attr("transform")).toEqual("translate(10,10)scale(1)"); }); }); + + describe('testing community nodes', function() { + var shaper; + + beforeEach(function() { + shaper = new NodeShaper(d3.select("svg")); + }); + + it('should render community nodes', function() { + var nodes = helper.createSimpleNodes([0, 1, 2]), + commNode = { + _id: "*community_42", + _inboundCounter: 0, + _outboundCounter: 0, + position: { + x: 1, + y: 1, + z: 1 + } + }; + nodes.push(commNode); + shaper.drawNodes(nodes); + expect($("svg .node").length).toEqual(4); + expect($("svg #\\*community_42")[0]).toBeDefined(); + }); + + it('should render communtiy nodes as stars', function() { + var nodes = helper.createSimpleNodes([0, 1, 2]), + commNode = { + _id: "*community_42", + _size: 4, + _inboundCounter: 0, + _outboundCounter: 0, + position: { + x: 1, + y: 1, + z: 1 + } + }, + star; + nodes.push(commNode); + shaper.drawNodes(nodes); + expect($("svg .communitynode").length).toEqual(1); + expect($("svg #\\*community_42")[0]).toBeDefined(); + star = $("svg #\\*community_42 polygon"); + expect(star.length).toEqual(1); + expect(star.attr("points")).toEqual("0,-25 -16,20 23,-10 -23,-10 16,20"); + }); + + it('should print the size of the capsulated community', function() { + var nodes = helper.createSimpleNodes([0, 1, 2]), + commNode = { + _id: "*community_42", + _size: 4, + _inboundCounter: 0, + _outboundCounter: 0, + position: { + x: 1, + y: 1, + z: 1 + } + }, + text; + nodes.push(commNode); + shaper.drawNodes(nodes); + text = $("svg #\\*community_42 text")[0].textContent; + expect(text).toEqual("4"); + }); + + }); }); diff --git a/html/admin/js/lib/nv.d3.js b/html/admin/js/lib/nv.d3.js index 61734e2db1..64065ad344 100644 --- a/html/admin/js/lib/nv.d3.js +++ b/html/admin/js/lib/nv.d3.js @@ -479,8 +479,9 @@ nv.models.axis = function() { .attr('transform', function(d,i,j) { return 'rotate(' + rotateLabels + ' 0,0)' }) .attr('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end'); } - axisLabel.enter().append('text').attr('class', 'nv-axislabel') + axisLabel.enter().append('text').attr('class', 'nv-axislabel nv-x-axislabel') .attr('text-anchor', 'middle') + .attr('class', 'heikotestclass') .attr('y', xLabelMargin); var w = (scale.range().length==2) ? scale.range()[1] : (scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0])); axisLabel @@ -565,7 +566,7 @@ nv.models.axis = function() { .attr('text-anchor', rotateYLabel ? 'middle' : 'end') .attr('transform', rotateYLabel ? 'rotate(-90)' : '') //Edited 25 in next line -> origin was 12 - .attr('y', rotateYLabel ? (-Math.max(margin.left,width) + 28) : -10); //TODO: consider calculating this based on largest tick width... OR at least expose this on chart + .attr('y', rotateYLabel ? (-Math.max(margin.left,width) + 40) : -10); //TODO: consider calculating this based on largest tick width... OR at least expose this on chart axisLabel .attr('x', rotateYLabel ? (-scale.range()[0] / 2) : -axis.tickPadding()); if (showMaxMin) { diff --git a/html/admin/js/templates/collectionsView.ejs b/html/admin/js/templates/collectionsView.ejs index 62b18893dc..42ce3355de 100644 --- a/html/admin/js/templates/collectionsView.ejs +++ b/html/admin/js/templates/collectionsView.ejs @@ -8,17 +8,66 @@ diff --git a/html/admin/js/templates/dashboardView.ejs b/html/admin/js/templates/dashboardView.ejs index af1e83b8ea..028612dece 100644 --- a/html/admin/js/templates/dashboardView.ejs +++ b/html/admin/js/templates/dashboardView.ejs @@ -22,11 +22,11 @@ @@ -35,6 +35,16 @@