Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added basic multi line console to data browser.

  • Loading branch information...
commit fcd004e5ef15f7cbb4276fedf752781f4b618088 1 parent a263a1c
@jakewins jakewins authored committed
Showing with 5,860 additions and 198 deletions.
  1. +12 −7 server/README.md
  2. +23 −0 server/src/main/coffeescript/lib/amd/CodeMirror.coffee
  3. +188 −0 server/src/main/coffeescript/neo4j/codemirror/cypher.coffee
  4. +5 −1 server/src/main/coffeescript/neo4j/webadmin/ApplicationState.coffee
  5. +11 −4 server/src/main/coffeescript/neo4j/webadmin/Bootstrapper.coffee
  6. +17 −13 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/BaseUI.coffee
  7. +49 −0 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/MenuView.coffee
  8. +1 −11 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/base.haml
  9. +10 −0 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/menuTemplate.haml
  10. +55 −0 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/models/MainMenuModel.coffee
  11. +15 −1 server/src/main/coffeescript/neo4j/webadmin/modules/console/ConsoleRouter.coffee
  12. +14 −1 server/src/main/coffeescript/neo4j/webadmin/modules/dashboard/DashboardRouter.coffee
  13. +77 −33 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/DataBrowserRouter.coffee
  14. +118 −33 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/models/DataBrowserState.coffee
  15. +1 −1  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/search/CypherSearcher.coffee
  16. +130 −0 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/ConsoleView.coffee
  17. +4 −3 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/CreateRelationshipDialog.coffee
  18. +3 −3 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/CypherResultView.coffee
  19. +62 −25 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/DataBrowserView.coffee
  20. +3 −3 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/NodeListView.coffee
  21. +1 −2  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/PropertyContainerView.coffee
  22. +3 −3 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/RelationshipListView.coffee
  23. +19 −11 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/TabularView.coffee
  24. +19 −7 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/VisualizedView.coffee
  25. +33 −4 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/base.haml
  26. +3 −0  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/consoleTemplate.haml
  27. +6 −0 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/errorTemplate.haml
  28. +1 −0  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/notExecutedTemplate.haml
  29. +1 −1  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/notfound.haml
  30. +5 −0 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/queryMetadataTemplate.haml
  31. +15 −1 server/src/main/coffeescript/neo4j/webadmin/modules/indexmanager/IndexManagerRouter.coffee
  32. +15 −1 server/src/main/coffeescript/neo4j/webadmin/modules/serverinfo/ServerInfoRouter.coffee
  33. +26 −0 server/src/main/coffeescript/neo4j/webadmin/utils/Keys.coffee
  34. +36 −0 server/src/main/coffeescript/ribcage/time/Timer.coffee
  35. +16 −0 server/src/main/coffeescript/test/neo4j/webadmin/modules/databrowser/TestDataBrowserRouter.coffee
  36. +1 −1  server/src/main/java/org/neo4j/server/rest/repr/ExceptionRepresentation.java
  37. +3 −3 server/src/main/resources/webadmin-html/css/buttons.css
  38. +8 −3 server/src/main/resources/webadmin-html/css/forms.css
  39. +45 −1 server/src/main/resources/webadmin-html/css/style.css
  40. +168 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/codemirror.css
  41. +3,176 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/codemirror.js
  42. +146 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/closetag.js
  43. +23 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/dialog.css
  44. +63 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/dialog.js
  45. +191 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/foldcode.js
  46. +297 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/formatting.js
  47. +134 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/javascript-hint.js
  48. +51 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/loadmode.js
  49. +44 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/match-highlighter.js
  50. +51 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/overlay.js
  51. +49 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/runmode.js
  52. +114 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/search.js
  53. +117 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/searchcursor.js
  54. +16 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/simple-hint.css
  55. +72 −0 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/simple-hint.js
  56. +9 −8 server/src/webtest/java/org/neo4j/server/webadmin/DatabrowserWebIT.java
  57. +19 −13 server/src/webtest/java/org/neo4j/server/webdriver/WebadminWebdriverLibrary.java
  58. +66 −0 server/tools/webadmin-develop
View
19 server/README.md
@@ -12,21 +12,26 @@ Subsequent builds can simply:
mvn clean package
-Finally, run the server using:
+Run the server using:
mvn exec:java
## Webadmin development
-Webadmin builds during the compile and process-classes phases. If you are doing webadmin development work, you can make your changes auto-deploy, so you don't have to restart the server. Run the two commands below in separate consoles.
+The web administration interface, webadmin, can be found in two places of the source tree:
-Start the server (let this get the server started before issuing other commands):
+ # This contains uncompiled coffeescript and haml files, including unit tests (under test/)
+ src/main/coffeescript
- mvn clean compile exec:java -Pneodev
+ # This contains static web resources, such as javascript libraries, css and images
+ src/main/resources/webadmin-html
+
+Webadmin compiles as part of the normal server build.
+If you are doing development work, you can start a server instance that automatically picks up
+changes in the two webadmin source folders, and deploys them into the running server.
-Auto-deploy changes to webadmin files:
-
- mvn compile -Dbrew.watch=true -Pneodev
+ # Run this bash command while standing in the 'server'-folder
+ tools/webadmin-develop
Then go to [http://localhost:7474/webadmin/](http://localhost:7474/webadmin/)
View
23 server/src/main/coffeescript/lib/amd/CodeMirror.coffee
@@ -0,0 +1,23 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define ['order!lib/codemirror2/codemirror'], () ->
+
+ CodeMirror
View
188 server/src/main/coffeescript/neo4j/codemirror/cypher.coffee
@@ -0,0 +1,188 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+# Based off of the MySQL highlighter,
+# still some stuff remaining to clear up from there.
+define ['lib/amd/CodeMirror'], (CodeMirror) ->
+
+ CodeMirror.defineMode "cypher", (config) ->
+ indentUnit = config.indentUnit
+ curPunc = null
+
+ wordRegexp = (words) -> new RegExp("^(?:" + words.join("|") + ")$", "i")
+
+ # TODO: CodeMirror has support for code-completion, there are examples
+ # of modules that add that for a given language which we could use as
+ # a base here.
+
+ # TODO: This should be auto-generated by the same mechanism that
+ # generates syntax highlighting for the manual
+ ops = wordRegexp([
+ 'node','nodes','and','or','in','not',
+ 'all','any','none','single',
+ 'length','id','type','coalesce','head',
+ 'last','extract','filter','tail',
+ 'abs','round','sqrt','sign'])
+ keywords = wordRegexp([
+ 'START','MATCH','RELATE','WHERE','CREATE','RETURN','MATCH',
+ 'LIMIT','ORDER BY','SKIP',
+ 'COUND','SUM','AVG','MAX','MIN','COLLECT','DISCINCT','WITH'])
+ operatorChars = /[*+\-<>=&|]/
+
+ tokenBase = (stream, state) ->
+ ch = stream.next()
+ curPunc = null
+ if ch == "$" or ch == "?"
+ stream.match(/^[\w\d]*/)
+ return "variable-2"
+
+ else if ch == "<" && !stream.match(/^[\s\u00a0=]/, false)
+ stream.match(/^[^\s\u00a0>]*>?/)
+ return "atom"
+
+ else if ch == "\"" || ch == "'"
+ state.tokenize = tokenLiteral(ch)
+ return state.tokenize(stream, state)
+
+ else if ch == "`"
+ state.tokenize = tokenOpLiteral(ch)
+ return state.tokenize(stream, state)
+
+ else if /[{}\(\),\.;\[\]]/.test(ch)
+ curPunc = ch
+ return null
+
+ else if ch is "/"
+ ch2 = stream.next()
+ if(ch2=="/")
+ stream.skipToEnd()
+ return "comment"
+
+ else if (operatorChars.test(ch))
+ stream.eatWhile(operatorChars)
+ return null
+
+ else if (ch == ":")
+ stream.eatWhile(/[\w\d\._\-]/)
+ return "atom"
+
+ else
+ stream.eatWhile(/[_\w\d]/)
+ if (stream.eat(":"))
+ stream.eatWhile(/[\w\d_\-]/)
+ return "atom"
+ word = stream.current()
+ if (ops.test(word))
+ return null
+ else if (keywords.test(word))
+ return "keyword"
+ else
+ return "variable"
+
+ tokenLiteral = (quote) ->
+ return (stream, state) ->
+ escaped = false
+ while ((ch = stream.next()) != null)
+ if (ch == quote && !escaped)
+ state.tokenize = tokenBase
+ break
+ escaped = !escaped && ch == "\\"
+ return "string"
+
+ tokenOpLiteral = (quote) ->
+ return (stream, state) ->
+ escaped = false
+ while ((ch = stream.next()) != null)
+ if (ch == quote && !escaped)
+ state.tokenize = tokenBase
+ break
+ escaped = !escaped && ch == "\\"
+ return "variable-2"
+
+
+ pushContext = (state, type, col) ->
+ state.context = {prev: state.context, indent: state.indent, col: col, type: type}
+
+ popContext = (state) ->
+ state.indent = state.context.indent
+ state.context = state.context.prev
+
+ return {
+ startState: (base) ->
+ return {
+ tokenize: tokenBase,
+ context: null,
+ indent: 0,
+ col: 0
+ }
+
+ token: (stream, state) ->
+ if (stream.sol())
+ if (state.context && state.context.align == null)
+ state.context.align = false
+ state.indent = stream.indentation()
+ if (stream.eatSpace())
+ return null
+ style = state.tokenize(stream, state)
+
+ if (style != "comment" && state.context && state.context.align == null && state.context.type != "pattern")
+ state.context.align = true
+
+ if (curPunc == "(")
+ pushContext(state, ")", stream.column())
+ else if (curPunc == "[")
+ pushContext(state, "]", stream.column())
+ else if (curPunc == "{")
+ pushContext(state, "}", stream.column())
+ else if (/[\]\}\)]/.test(curPunc))
+ while (state.context && state.context.type == "pattern")
+ popContext(state)
+ if (state.context && curPunc == state.context.type)
+ popContext(state)
+ else if (curPunc == "." && state.context && state.context.type == "pattern")
+ popContext(state)
+ else if (/atom|string|variable/.test(style) && state.context)
+ if (/[\}\]]/.test(state.context.type))
+ pushContext(state, "pattern", stream.column())
+ else if (state.context.type == "pattern" && !state.context.align)
+ state.context.align = true
+ state.context.col = stream.column()
+
+ return style
+
+ indent: (state, textAfter) ->
+ firstChar = textAfter && textAfter.charAt(0)
+ context = state.context
+ if (/[\]\}]/.test(firstChar))
+ while (context && context.type == "pattern")
+ context = context.prev
+
+ closing = context && firstChar == context.type
+ if (!context)
+ return 0
+ else if (context.type == "pattern")
+ return context.col
+ else if (context.align)
+ return context.col + (closing ? 0 : 1)
+ else
+ return context.indent + (closing ? 0 : indentUnit)
+ }
+
+ CodeMirror.defineMIME("text/x-cypher", "cypher");
View
6 server/src/main/coffeescript/neo4j/webadmin/ApplicationState.coffee
@@ -20,11 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
define(
['./Settings'
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel'
'ribcage/Model'],
- (Settings,Model) ->
+ (Settings, MainMenuModel, Model) ->
class ApplicationState extends Model
+ getMainMenuModel : ->
+ @mainMenu ?= new MainMenuModel()
+
getServer : ->
@get "server"
View
15 server/src/main/coffeescript/neo4j/webadmin/Bootstrapper.coffee
@@ -38,11 +38,18 @@ define(
timeout : 1000 * 60 * 60 * 6 # Let requests run up to six hours
})
- appState = new ApplicationState
- appState.set server : new neo4js.GraphDatabase(location.protocol + "//" + location.host)
+ @appState = new ApplicationState
+ @appState.set server : new neo4js.GraphDatabase(location.protocol + "//" + location.host)
- jQuery ->
- m.init(appState) for m in modules
+ jQuery =>
+ @_initModule module for module in modules
Backbone.history.start()
+
+ _initModule : (module) ->
+ mainMenu = @appState.getMainMenuModel()
+ module.init(@appState)
+ if module.getMenuItems?
+ for item in module.getMenuItems()
+ mainMenu.addMenuItem(item)
)
View
30 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/BaseUI.coffee
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
define(
['./base',
+ './MenuView',
'ribcage/View',
'lib/amd/jQuery'],
- (template, View, $) ->
+ (template, MenuView, View, $) ->
class BaseView extends View
@@ -30,25 +31,19 @@ define(
init : (@appState) =>
$("body").append(@el)
- @appState.bind 'change:mainView', @mainViewChanged
+ @appState.bind 'change:mainView', @onMainViewChanged
+ @menuView = new MenuView(@appState.getMainMenuModel())
- mainViewChanged : (event) =>
+ onMainViewChanged : (event) =>
if @mainView?
@mainView.detach()
@mainView = event.attributes.mainView
@render()
render : ->
- $(@el).html @template( mainmenu : [
- { label : "Dashboard", subtitle:"Overview",url : "#", current: location.hash is "" }
- { label : "Data browser",subtitle:"Explore and edit",url : "#/data/" , current: location.hash.indexOf("#/data/") is 0 }
- { label : "Console", subtitle:"Power tool",url : "#/console/" , current: location.hash.indexOf("#/console/") is 0 }
- { label : "Server info", subtitle:"Details",url : "#/info/" , current: location.hash is "#/info/" }
- { label : "Index manager", subtitle:"Indexing overview",url : "#/index/" , current: location.hash is "#/index/" } ] )
-
- if @mainView?
- @mainView.attach($("#contents"))
- @mainView.render()
+ $(@el).html( @template() )
+ @_renderMainView()
+ @_renderMenu()
return this
remove : =>
@@ -56,4 +51,13 @@ define(
if @mainView?
@mainView.remove()
super()
+
+ _renderMainView : ->
+ if @mainView?
+ @mainView.attach($("#contents"))
+ @mainView.render()
+
+ _renderMenu : ->
+ @menuView.attach($("#mainmenu"))
+ @menuView.render()
)
View
49 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/MenuView.coffee
@@ -0,0 +1,49 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define(
+ ['./menuTemplate',
+ 'ribcage/View',
+ 'lib/amd/jQuery'],
+ (template, View, $) ->
+
+ class MenuView extends View
+
+ template : template
+ tagName : "ul"
+
+ constructor : (@menuModel) ->
+ @menuModel.bind 'change', @onMenuChange
+ super()
+
+ onMenuChange : (event) =>
+ @render()
+
+ render : =>
+ $(@el).html( @template(
+ menuitems : @menuModel.getMenuItems()
+ current : @menuModel.getCurrentItem()
+ ))
+ return this
+
+ remove : =>
+ @menuModel.unbind 'change', @onMenuChange
+ super()
+)
View
12 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/base.haml
@@ -6,17 +6,7 @@
%li
%a.micro-button(target="_blank" href="http://docs.neo4j.org/chunked/@@neo4j.version@@/") Documentation
%img#logo(src="img/logo.png")
- %ul#mainmenu
- :each item in mainmenu
- %li.title-button
- :if item.current
- %a.current(href="#{htmlEscape(item.url)}")
- %span.subtitle= htmlEscape(item.subtitle)
- %span= htmlEscape(item.label)
- :if !item.current
- %a(href="#{htmlEscape(item.url)}")
- %span.subtitle= htmlEscape(item.subtitle)
- %span= htmlEscape(item.label)
+ #mainmenu
#contents
View
10 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/menuTemplate.haml
@@ -0,0 +1,10 @@
+:each item in menuitems
+ %li.title-button
+ :if item === current
+ %a.current(href="#{htmlEscape(item.getUrl())}")
+ %span.subtitle= htmlEscape(item.getSubtitle())
+ %span= htmlEscape(item.getTitle())
+ :if item !== current
+ %a(href="#{htmlEscape(item.getUrl())}")
+ %span.subtitle= htmlEscape(item.getSubtitle())
+ %span= htmlEscape(item.getTitle())
View
55 server/src/main/coffeescript/neo4j/webadmin/modules/baseui/models/MainMenuModel.coffee
@@ -0,0 +1,55 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define(
+ ['ribcage/Model',
+ 'lib/amd/Underscore'],
+ (Model,_) ->
+
+ class MainMenuModel extends Model
+
+ class @Item extends Model
+
+ getTitle : -> @get "title"
+ getSubtitle : -> @get "subtitle"
+ getUrl : -> @get "url"
+
+ setUrl : (url) -> @set "url":url
+
+ constructor : ->
+ super()
+ @_items = []
+
+ addMenuItem : (item) ->
+ @_items.push(item)
+ @trigger "change:items"
+ item.bind "change", => @trigger "change:items"
+
+ getMenuItems : -> @_items
+
+ getCurrentItem : ->
+ url = location.hash
+ for item in _(@getMenuItems()).sortBy( (i)-> (-i.getUrl().length) )
+ if url.indexOf(item.getUrl()) is 0 or (url.length is 0 and item.getUrl() is "#")
+ return item
+
+ return null
+
+)
View
16 server/src/main/coffeescript/neo4j/webadmin/modules/console/ConsoleRouter.coffee
@@ -24,8 +24,9 @@ define(
'./views/ShellConsoleView'
'./views/GremlinConsoleView'
'./views/HttpConsoleView'
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel'
'ribcage/Router'],
- (Console, HttpConsole, ShellConsoleView, GremlinConsoleView, HttpConsoleView, Router) ->
+ (Console, HttpConsole, ShellConsoleView, GremlinConsoleView, HttpConsoleView, MainMenuModel, Router) ->
class ConsoleRouter extends Router
routes :
@@ -36,6 +37,12 @@ define(
init : (appState) =>
@appState = appState
+
+ @menuItem = new MainMenuModel.Item
+ title : "Console",
+ subtitle:"Power tool",
+ url : "#/console/"
+
@gremlinState = new Console(server:@appState.get("server"), lang:"gremlin")
@shellState = new Console(server:@appState.get("server"), lang:"shell")
@httpState = new HttpConsole(server:@appState.get("server"), lang:"http")
@@ -63,4 +70,11 @@ define(
getConsoleView : (type) =>
@views[type]
+
+ #
+ # Bootstrapper SPI
+ #
+
+ getMenuItems : ->
+ [@menuItem]
)
View
15 server/src/main/coffeescript/neo4j/webadmin/modules/dashboard/DashboardRouter.coffee
@@ -26,8 +26,9 @@ define(
'./models/ServerStatistics'
'./models/DashboardState'
'./models/KernelBean'
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel'
'ribcage/Router'],
- (DashboardView, ServerPrimitives, DiskUsage, CacheUsage, ServerStatistics, DashboardState, KernelBean, Router) ->
+ (DashboardView, ServerPrimitives, DiskUsage, CacheUsage, ServerStatistics, DashboardState, KernelBean, MainMenuModel, Router) ->
class DashboardRouter extends Router
routes :
@@ -35,6 +36,11 @@ define(
init : (appState) =>
@appState = appState
+
+ @menuItem = new MainMenuModel.Item
+ title : "Dashboard",
+ subtitle:"Overview",
+ url : "#"
dashboard : =>
@saveLocation()
@@ -64,4 +70,11 @@ define(
getDashboardState : =>
@dashboardState ?= new DashboardState( server : @appState.getServer() )
+ #
+ # Bootstrapper SPI
+ #
+
+ getMenuItems : ->
+ [@menuItem]
+
)
View
110 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/DataBrowserRouter.coffee
@@ -1,4 +1,5 @@
+# TODO: Split into one Module class and one Router class
define(
['./search/QueuedSearch',
'./views/DataBrowserView',
@@ -6,33 +7,36 @@ define(
'./visualization/views/VisualizationProfileView',
'./models/DataBrowserState',
'./DataBrowserSettings',
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel',
'ribcage/Router'],
- (QueuedSearch, DataBrowserView, VisualizationSettingsView, VisualizationProfileView, DataBrowserState, DataBrowserSettings, Router) ->
+ (QueuedSearch, DataBrowserView, VisualizationSettingsView, VisualizationProfileView, DataBrowserState, DataBrowserSettings, MainMenuModel, Router) ->
class DataBrowserRouter extends Router
routes :
- "/data/" : "base"
- "/data/search/*query" : "search"
"/data/visualization/settings/" : "visualizationSettings"
"/data/visualization/settings/profile/" : "createVisualizationProfile"
"/data/visualization/settings/profile/:id/" : "editVisualizationProfile"
shortcuts :
- "s" : "focusOnSearchField"
- "v" : "switchDataView"
+ "s" : "onEditorFocusShortcut"
+ "v" : "onViewTypeToggleShortcut"
init : (appState) =>
- @appState = appState
- @server = appState.get "server"
- @searcher = new QueuedSearch(@server)
-
- @dataModel = new DataBrowserState( server : @server )
+ # Because we need to be able to match newlines, we need to define
+ # this route with our own regex
+ @route(/data\/search\/([\s\S]*)/i, 'search', @search)
- @dataModel.bind "change:query", @queryChanged
+ @appState = appState
+ @dataModel = new DataBrowserState( server : @appState.getServer() )
+
+ @dataModel.bind "change:query", @onQueryChangedInModel
+ @dataModel.bind "change:data", @onDataChangedInModel
- base : =>
- @queryChanged()
+ @menuItem = new MainMenuModel.Item
+ title : "Data browser",
+ subtitle:"Explore and edit",
+ url : @_getCurrentQueryURI()
search : (query) =>
@saveLocation()
@@ -43,6 +47,9 @@ define(
@dataModel.setQuery query
@appState.set( mainView : @getDataBrowserView() )
+ if @_looksLikeReadOnlyQuery(query)
+ @dataModel.executeCurrentQuery()
+
visualizationSettings : () =>
@saveLocation()
@visualizationSettingsView ?= new VisualizationSettingsView
@@ -64,38 +71,41 @@ define(
v.setProfileToManage profile
@appState.set mainView : v
+ #
+ # Bootstrapper SPI
+ #
+
+ getMenuItems : ->
+ [@menuItem]
+
#
- # Keyboard shortcuts
+ # Event handlers
#
- focusOnSearchField : (ev) =>
- @base()
- setTimeout( () ->
- $("#data-console").focus()
- 1)
+ onEditorFocusShortcut : (ev) =>
+ @search(@dataModel.getQuery())
+ setTimeout( (=> @getDataBrowserView().focusOnEditor()), 1)
- switchDataView : (ev) =>
+ onViewTypeToggleShortcut : (ev) =>
@getDataBrowserView().switchView()
- #
- # Internals
- #
+ onQueryChangedInModel : =>
+ url = @_getCurrentQueryURI()
+
+ @menuItem.setUrl(url)
- queryChanged : =>
- query = @dataModel.get "query"
- if query == null
- return @search("0")
+ #if location.hash != url
+ # location.hash = url
- url = "#/data/search/#{encodeURIComponent(query)}/"
+ onDataChangedInModel : =>
+ url = @_getCurrentQueryURI()
if location.hash != url
location.hash = url
-
- if @dataModel.get "queryOutOfSyncWithData"
- @searcher.exec(@dataModel.get "query").then(@showResult, @showResult)
- showResult : (result) =>
- @dataModel.setData(result)
+ #
+ # Internals
+ #
getDataBrowserView : =>
@view ?= new DataBrowserView
@@ -108,4 +118,38 @@ define(
getDataBrowserSettings : ->
@dataBrowserSettings ?= new DataBrowserSettings @appState.getSettings()
+
+
+ # We only auto-execute read-only queries,
+ # and we determine if a query is read-only here.
+ # Note: Since we execute queries from the current URL,
+ # this is a very real security issue. If modifying queries
+ # slip through here, attackers can redirect an adminstrator
+ # to a webadmin URL with a malicious Cypher query. Please
+ # opt for better-safe-than-sorry when updating this regex.
+ _looksLikeReadOnlyQuery : (query) ->
+ pattern = ///^(
+ # Super basic cypher queries
+ (start
+ \s+
+ [a-z]+=node\(\d+\)
+ \s+
+ return \s+ [a-z]+) | # or
+
+ # Direct node id lookups
+ ((node:)?\d+) | # or
+
+ # Direct rel id lookups
+ (rel:\d+) | # or
+
+ # Direct rel id lookups
+ (rels:\d+)
+ )$
+ ///i
+
+ pattern.test(query)
+
+ _getCurrentQueryURI : ->
+ query = @dataModel.getQuery()
+ return "#/data/search/#{encodeURIComponent(query)}/"
)
View
151 ...r/src/main/coffeescript/neo4j/webadmin/modules/databrowser/models/DataBrowserState.coffee
@@ -19,23 +19,53 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
###
define(
- ['./NodeProxy'
+ ['neo4j/webadmin/modules/databrowser/search/QueuedSearch',
+ './NodeProxy'
'./NodeList'
'./RelationshipProxy'
'./RelationshipList'
+ 'ribcage/time/Timer'
'ribcage/Model'],
- (NodeProxy, NodeList, RelationshipProxy, RelationshipList, Model) ->
+ (QueuedSearch, NodeProxy, NodeList, RelationshipProxy, RelationshipList, Timer, Model) ->
class DataBrowserState extends Model
+
+ @State :
+ ERROR : -1
+ EMPTY : 0
+ NOT_EXECUTED : 1
+ SINGLE_NODE : 2
+ SINGLE_RELATIONSHIP : 3
+ NODE_LIST : 4
+ RELATIONSHIP_LIST : 5
+ CYPHER_RESULT : 6
+
+ class @QueryMetaData extends Model
+
+ defaults :
+ executionTime : 0
+ numberOfRows : 0
+
+ getExecutionTime : -> @get "executionTime"
+ getNumberOfRows : -> @get "numberOfRows"
+
+ setExecutionTime : (t) -> @set executionTime : t
+ setNumberOfRows : (n) -> @set numberOfRows : n
+
defaults :
- type : null
data : null
- query : null
+ query : "START root=node(0) // Start with the reference node\n" +
+ "RETURN root // and return it.\n" +
+ "\n" +
+ "// Hit CTRL+ENTER to execute"
queryOutOfSyncWithData : true
+ state : DataBrowserState.State.NOT_EXECUTED
+ querymeta : new DataBrowserState.QueryMetaData()
initialize : (options) =>
- @server = options.server
+ @searcher = new QueuedSearch(options.server)
+ @_executionTimer = new Timer
getQuery : =>
@get "query"
@@ -43,42 +73,97 @@ define(
getData : =>
@get "data"
- getDataType : =>
- @get "type"
-
- dataIsSingleNode : () =>
- return @get("type") == "node"
-
- dataIsSingleRelationship : () =>
- return @get("type") == "relationship"
+ getState : =>
+ @get "state"
+
+ getQueryMetadata : =>
+ @get "querymeta"
setQuery : (val, isForCurrentData=false, opts={}) =>
- if @get("query") != val or opts.force is true
- @set {"queryOutOfSyncWithData": not isForCurrentData }, opts
- @set {"query" : val }, opts
+ if @getQuery() != val or opts.force is true
+ if not isForCurrentData
+ state = DataBrowserState.State.NOT_EXECUTED
+ else
+ state = @getState()
+
+ @set {"query":val, "state":state, "queryOutOfSyncWithData": not isForCurrentData }, opts
+ if state is DataBrowserState.State.NOT_EXECUTED
+ @set {"data":null}, opts
+
+ executeCurrentQuery : =>
+ @_executionTimer.start()
+ @searcher.exec(@getQuery()).then(@setData,@setData)
setData : (result, basedOnCurrentQuery=true, opts={}) =>
- @set({"data":result, "queryOutOfSyncWithData" : not basedOnCurrentQuery }, {silent:true})
+ @_executionTimer.stop()
+
+ executionTime = @_executionTimer.getTimePassed()
+ originalState = @getState()
+
+ # These to be determined below
+ state = null
+ data = null
+ numberOfRows = null
if result instanceof neo4j.models.Node
- return @set({type:"node","data":new NodeProxy(result)}, opts)
+ state = DataBrowserState.State.SINGLE_NODE
+ data = new NodeProxy(result)
else if result instanceof neo4j.models.Relationship
- return @set({type:"relationship","data":new RelationshipProxy(result)}, opts)
-
- else if _(result).isArray() and result.length > 0
-
- if result.length is 1 # If only showing one item, show it in single-item view
- return @setData(result[0], basedOnCurrentQuery, opts)
+ state = DataBrowserState.State.SINGLE_RELATIONSHIP
+ data = new RelationshipProxy(result)
+
+ else if _(result).isArray() and result.length is 0
+ state = DataBrowserState.State.EMPTY
+
+ else if _(result).isArray() and result.length is 1
+ # If only showing one item, show it in single-item view
+ return @setData(result[0], basedOnCurrentQuery, opts)
+
+ else if _(result).isArray()
+ if result[0] instanceof neo4j.models.Relationship
+ state = DataBrowserState.State.RELATIONSHIP_LIST
+ data = new RelationshipList(result)
+
+ else if result[0] instanceof neo4j.models.Node
+ state = DataBrowserState.State.NODE_LIST
+ data = new NodeList(result)
+
+ else if result instanceof neo4j.cypher.QueryResult and result.size() is 0
+ state = DataBrowserState.State.EMPTY
+
+ else if result instanceof neo4j.cypher.QueryResult
+ state = DataBrowserState.State.CYPHER_RESULT
+ data = result
+
+ else if result instanceof neo4j.exceptions.NotFoundException
+ state = DataBrowserState.State.EMPTY
+
+ else
+ state = DataBrowserState.State.ERROR
+ data = result
+
+ # Query meta data
+ if state isnt DataBrowserState.State.ERROR
+ @_updateQueryMetaData(data,executionTime)
+
+ @set({"state":state, "data":data, "queryOutOfSyncWithData" : not basedOnCurrentQuery }, {silent:true})
+ if not opts.silent
+ @trigger "change:data"
+ @trigger "change:state" if originalState != state
+
+ _updateQueryMetaData : (data, executionTime) ->
+ if data?
+ if data instanceof neo4j.cypher.QueryResult
+ numberOfRows = data.data.length
else
- if result[0] instanceof neo4j.models.Relationship
- return @set({type:"relationshipList", "data":new RelationshipList(result)}, opts)
- else if result[0] instanceof neo4j.models.Node
- return @set({type:"nodeList", "data":new NodeList(result)}, opts)
- else if result instanceof neo4j.cypher.QueryResult and result.size() > 0
- @set({type:"cypher"})
- return @trigger "change:data"
-
- @set({type:"not-found", "data":null}, opts)
+ numberOfRows = if data.length? then data.length else 1
+ else
+ numberOfRows = 0
+ meta = @getQueryMetadata()
+ meta.setNumberOfRows(numberOfRows)
+ meta.setExecutionTime(executionTime)
+
+ @trigger "change:querymeta"
)
View
2  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/search/CypherSearcher.coffee
@@ -27,7 +27,7 @@ define ["neo4j/webadmin/utils/ItemUrlResolver"], (ItemUrlResolver) ->
@urlResolver = new ItemUrlResolver(server)
@pattern = /// ^
(start|cypher|create) # Start with "start", "cypher" or "create"
- (.+) # followed by anything
+ ([\s\S]+) # followed by anything
$
///i
View
130 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/ConsoleView.coffee
@@ -0,0 +1,130 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define(
+ ['ribcage/View'
+ 'neo4j/webadmin/utils/Keys'
+ 'lib/amd/CodeMirror'
+ 'neo4j/codemirror/cypher'
+ './consoleTemplate'
+ 'lib/amd/jQuery.putCursorAtEnd'],
+ (View, Keys, CodeMirror, cypherHighlighting, template, $) ->
+
+ class ConsoleView extends View
+
+ template : template
+
+ events :
+ "paste #data-console" : "onPaste"
+ "click #data-execute-console" : "onSearchClicked"
+
+ initialize : (options)->
+ @dataModel = options.dataModel
+
+ @dataModel.bind("change:query", @onDataModelQueryChanged)
+
+ render : =>
+ $(@el).html template()
+
+ # TODO: Check if there is a way to re-use this
+ @_editor = CodeMirror($("#data-console").get(0),{
+ value: @dataModel.getQuery()
+ onKeyEvent: @onKeyEvent
+ mode: "text/x-cypher"
+ })
+
+ # WebDriver functional tests are unable to enter
+ # text into the editor. Give them a global reference
+ # to use programatically.
+ if document? then document.dataBrowserEditor = @_editor
+
+ @_adjustEditorHeightToNumberOfNewlines()
+ @el
+
+ focusOnEditor : =>
+ if @_editor?
+ @_editor.focus()
+
+ # Select all
+ start = {line:0,ch:0}
+ end = {line:@_editor.lineCount()-1,ch:@_editor.getLine(@_editor.lineCount()-1).length}
+ @_editor.setSelection(start, end)
+
+ # Event handling
+
+ onSearchClicked : (ev) =>
+ @_executeQuery @_getEditorValue()
+
+ onKeyEvent : (editor, ev) =>
+ #ev = jQuery.Event(ev.type)
+ switch ev.type
+ when "keyup" then @onKeyUp(ev)
+ when "keypress" then @onKeyPress(ev)
+
+ onKeyPress : (ev) =>
+ if ev.which is Keys.ENTER and ev.ctrlKey or ev.which is 10 # WebKit
+ ev.stop()
+ @_executeQuery @_getEditorValue()
+
+ onKeyUp : (ev) =>
+ @_adjustEditorHeightToNumberOfNewlines()
+ @_saveCurrentEditorContents()
+
+ onPaste : (ev) =>
+ # We don't have an API to access the text being pasted,
+ # so we work around it by adding this little job to the
+ # end of the js work queue.
+ setTimeout( @_adjustEditorHeightToNumberOfNewlines, 0)
+ setTimeout( @_saveCurrentEditorContents, 0)
+
+ onDataModelQueryChanged : (ev) =>
+ if @dataModel.getQuery() != @_getEditorValue()
+ @render()
+
+ # Internals
+
+ _saveQueryInModel : (query) ->
+ @dataModel.setQuery(query, false)
+
+ _executeQuery : (query) ->
+ @_saveQueryInModel(query)
+ @dataModel.trigger("change:query")
+ @dataModel.executeCurrentQuery()
+
+ _adjustEditorHeightToNumberOfNewlines : =>
+ @_setEditorLines @_newlinesIn(@_getEditorValue()) + 1
+
+ _saveCurrentEditorContents : =>
+ @_saveQueryInModel(@_getEditorValue())
+
+ _setEditorLines : (numberOfLines) ->
+ # TODO: Create single source of truth for line height here
+ # (eg. now it is both here and in style.css)
+ height = 10 + 14 * numberOfLines
+ $(".CodeMirror-scroll",@el).css("height",height)
+ @_editor.refresh()
+
+ _getEditorValue : () -> @_editor.getValue()
+ _setEditorValue : (v) -> @_editor.setValue(v)
+
+ _newlinesIn : (string) ->
+ if string.match(/\n/g) then string.match(/\n/g).length else 0
+
+)
View
7 ...ain/coffeescript/neo4j/webadmin/modules/databrowser/views/CreateRelationshipDialog.coffee
@@ -20,12 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
define(
['neo4j/webadmin/utils/ItemUrlResolver'
+ 'neo4j/webadmin/modules/databrowser/models/DataBrowserState'
'./createRelationship',
'ribcage/View',
'neo4j/webadmin/utils/FormHelper',
'lib/amd/jQuery'],
- (ItemUrlResolver, template, View, FormHelper, $) ->
-
+ (ItemUrlResolver, DataBrowserState, template, View, FormHelper, $) ->
+
class CreateRelationshipDialog extends View
className: "popout"
@@ -47,7 +48,7 @@ define(
@urlResolver = new ItemUrlResolver(@server)
@type = "RELATED_TO"
- if @dataModel.dataIsSingleNode()
+ if @dataModel.getState() is DataBrowserState.State.SINGLE_NODE
@from = @dataModel.getData().getId()
else
@from = ""
View
6 ...er/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/CypherResultView.coffee
@@ -28,13 +28,13 @@ define(
render : =>
$(@el).html(template(
- result : @dataModel.getData(),
+ result : @queryResult,
id : (entity) ->
entity.self.substr(entity.self.lastIndexOf("/")+1)
))
return this
- setDataModel : (dataModel) =>
- @dataModel = dataModel
+ setData : (queryResult) =>
+ @queryResult = queryResult
)
View
87 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/DataBrowserView.coffee
@@ -22,22 +22,27 @@ define(
['neo4j/webadmin/utils/ItemUrlResolver'
'./TabularView'
'./VisualizedView'
+ './ConsoleView'
'./CreateRelationshipDialog'
+ 'neo4j/webadmin/modules/databrowser/models/DataBrowserState'
'ribcage/View'
'./base'
+ './queryMetadataTemplate'
+ './notExecutedTemplate'
+ './errorTemplate'
'lib/amd/jQuery'],
- (ItemUrlResolver, TabularView, VisualizedView, CreateRelationshipDialog, View, template, $) ->
+ (ItemUrlResolver, TabularView, VisualizedView, ConsoleView, CreateRelationshipDialog, DataBrowserState, View, template, queryMetadataTemplate, notExecutedTemplate, errorTemplate, $) ->
+
+ State = DataBrowserState.State
class DataBrowserView extends View
template : template
events :
- "keypress #data-console" : "consoleKeyPressed"
"click #data-create-node" : "createNode"
"click #data-create-relationship" : "createRelationship"
"click #data-switch-view" : "switchView"
- "click #data-execute-console" : "search"
initialize : (options)->
@dataModel = options.dataModel
@@ -45,28 +50,70 @@ define(
@server = options.state.getServer()
@urlResolver = new ItemUrlResolver(@server)
-
- @dataModel.bind("change:query", @queryChanged)
+ @consoleView = new ConsoleView(options)
+
+ @dataModel.bind("change:querymeta", @renderQueryMetadataView)
+ @dataModel.bind("change:state", @renderQueryMetadataView)
+
@switchToTabularView()
+ focusOnEditor : =>
+ if @consoleView?
+ @consoleView.focusOnEditor()
+
render : =>
+ @detachConsoleView()
$(@el).html @template(
- query : @dataModel.getQuery()
- viewType : @viewType
- dataType : @dataModel.getDataType() )
+ viewType : @viewType)
+ @renderConsoleView()
@renderDataView()
+ @renderQueryMetadataView()
+
+ detachConsoleView : =>
+ @consoleView.detach()
+
+ renderConsoleView : =>
+ @consoleView.attach($("#data-console-area", @el))
+ if not @consoleViewRendered
+ @consoleViewRendered = true
+ @consoleView.render()
+ return this
+
+ renderQueryMetadataView : =>
+ metaBar = $("#data-query-metadata", @el)
+ switch @dataModel.getState()
+ when State.NOT_EXECUTED
+ metaBar.html(notExecutedTemplate())
+ return this
+ when State.ERROR
+ @renderError(@dataModel.getData())
+ return this
+ else
+ metaBar.html queryMetadataTemplate(
+ meta : @dataModel.getQueryMetadata())
+ return this
renderDataView : =>
@dataView.attach($("#data-area", @el).empty())
@dataView.render()
return this
- queryChanged : =>
- $("#data-console",@el).val(@dataModel.getQuery())
+ renderError : (error)->
+ title = "Unknown error"
+ description = "An unknown error occurred, was unable to retrieve a result for you."
+ monospaceDescription = null
- search : (ev) =>
- @dataModel.setQuery( $("#data-console",@el).val(), false, { force:true, silent:true})
- @dataModel.trigger("change:query")
+ if error instanceof neo4j.exceptions.HttpException
+ if error.data.exception = "SyntaxException"
+ title = "Invalid query"
+ description = null
+ monospaceDescription = error.data.message
+
+ $("#data-query-metadata", @el).html(errorTemplate(
+ "title":title
+ "description":description
+ "monospaceDescription":monospaceDescription
+ ))
createNode : =>
@server.node({}).then (node) =>
@@ -92,11 +139,6 @@ define(
delete(@createRelationshipDialog)
$("#data-create-relationship").removeClass("selected")
- consoleKeyPressed : (ev) =>
- if ev.which is 13# and ev.ctrlKey # ctrl + enter
- ev.stopPropagation()
- @search()
-
switchView : (ev) =>
if @viewType == "visualized"
$(ev.target).removeClass("tabular") if ev?
@@ -123,19 +165,14 @@ define(
@tabularView ?= new TabularView(dataModel:@dataModel, appState:@appState, server:@server)
@viewType = "tabular"
@dataView = @tabularView
-
- unbind : ->
- @dataModel.unbind("change:query", @queryChanged)
detach : ->
- @unbind()
@hideCreateRelationshipDialog()
- if @dataView?
- @dataView.detach()
+ if @dataView? then @dataView.detach()
+ if @consoleView? then @consoleView.detach()
super()
remove : =>
- @unbind()
@hideCreateRelationshipDialog()
@dataView.remove()
View
6 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/NodeListView.coffee
@@ -28,11 +28,11 @@ define(
render : =>
$(@el).html(template(
- nodeList : @dataModel.getData()
+ nodeList : @list
))
return this
- setDataModel : (dataModel) =>
- @dataModel = dataModel
+ setData : (list) =>
+ @list = list
)
View
3  ...c/main/coffeescript/neo4j/webadmin/modules/databrowser/views/PropertyContainerView.coffee
@@ -119,9 +119,8 @@ define(
getPropertyIdForElement : (element) =>
$(element).closest("ul").find("input.property-id").val()
- setDataModel : (dataModel) =>
+ setData : (@propertyContainer) =>
@unbind()
- @propertyContainer = dataModel.getData()
@propertyContainer.bind "remove:property", @renderProperties
@propertyContainer.bind "add:property", @renderProperties
@propertyContainer.bind "change:status", @updateSaveState
View
6 ...rc/main/coffeescript/neo4j/webadmin/modules/databrowser/views/RelationshipListView.coffee
@@ -28,11 +28,11 @@ define(
render : =>
$(@el).html(template(
- relationshipList : @dataModel.getData()
+ relationshipList : @list
))
return this
- setDataModel : (dataModel) =>
- @dataModel = dataModel
+ setData : (list) =>
+ @list = list
)
View
30 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/TabularView.coffee
@@ -24,11 +24,14 @@ define(
'./RelationshipListView'
'./NodeListView'
'./CypherResultView'
+ 'neo4j/webadmin/modules/databrowser/models/DataBrowserState'
'ribcage/View'
'./notfound'
'lib/amd/jQuery'],
- (NodeView, RelationshipView, RelationshipListView, NodeListView, CypherResultView, View, notFoundTemplate, $) ->
+ (NodeView, RelationshipView, RelationshipListView, NodeListView, CypherResultView, DataBrowserState, View, notFoundTemplate, $) ->
+ State = DataBrowserState.State
+
class SimpleView extends View
initialize : (options)->
@@ -43,26 +46,31 @@ define(
@dataModel.bind("change:data", @render)
render : =>
- type = @dataModel.get("type")
- switch type
- when "node"
+ state = @dataModel.getState()
+ switch state
+ when State.SINGLE_NODE
view = @nodeView
- when "nodeList"
+ when State.NODE_LIST
view = @nodeListView
- when "relationship"
+ when State.SINGLE_RELATIONSHIP
view = @relationshipView
- when "relationshipList"
+ when State.RELATIONSHIP_LIST
view = @relationshipListView
- when "cypher"
+ when State.CYPHER_RESULT
view = @cypherResultView
- else
+ when State.EMPTY
$(@el).html(notFoundTemplate())
return this
- view.setDataModel(@dataModel)
+ when State.NOT_EXECUTED
+ return this
+ when State.ERROR
+ return this
+
+ view.setData(@dataModel.getData())
$(@el).html(view.render().el)
view.delegateEvents()
return this
-
+
remove : =>
@dataModel.unbind("change", @render)
@nodeView.remove()
View
26 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/VisualizedView.coffee
@@ -26,8 +26,11 @@ define(
'ribcage/View'
'ribcage/security/HtmlEscaper'
'./visualization'
- 'ribcage/ui/Dropdown'],
- (VisualGraph, DataBrowserSettings, ItemUrlResolver, VisualizationSettingsDialog, View, HtmlEscaper, template, Dropdown) ->
+ 'ribcage/ui/Dropdown'
+ 'neo4j/webadmin/modules/databrowser/models/DataBrowserState'],
+ (VisualGraph, DataBrowserSettings, ItemUrlResolver, VisualizationSettingsDialog, View, HtmlEscaper, template, Dropdown, DataBrowserState) ->
+
+ State = DataBrowserState.State
class ProfilesDropdown extends Dropdown
@@ -111,15 +114,24 @@ define(
@vizEl = $("#visualization", @el)
@getViz().attach(@vizEl)
- switch @dataModel.get("type")
- when "node"
+ switch @dataModel.getState()
+ when State.SINGLE_NODE
@visualizeFromNode @dataModel.getData().getItem()
- when "nodeList"
+ when State.NODE_LIST
@visualizeFromNodes @dataModel.getData().getRawNodes()
- when "relationship"
+ when State.SINGLE_RELATIONSHIP
@visualizeFromRelationships [@dataModel.getData().getItem()]
- when "relationshipList"
+ when State.RELATIONSHIP_LIST
@visualizeFromRelationships @dataModel.getData().getRawRelationships()
+ when State.CYPHER_RESULT
+ # TODO
+ return this
+ when State.EMPTY
+ return this
+ when State.NOT_EXECUTED
+ return this
+ when State.ERROR
+ return this
return this
View
37 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/base.haml
@@ -1,14 +1,42 @@
.workarea
.controls.pad
.span-half.data-console-wrap
- %input#data-console(value="#{htmlEscape(query)}" type="text")
- #data-execute-console.icon-button(title="Search")
- %span.icon
+ #data-console-area
.form-tooltip
%a(href="#")
%span.form-tooltip-icon
- %span.form-tooltip-text <b>Shortcuts:</b><br /><b>s</b> - Highlight search bar</br><b>v</b> - Toggle between visualizer and tabular view<br /><br /><b>Search bar syntax:</b><br /><b>Cypher:</b> [any cypher query]<br /><b>Get node:</b> [node id]<br /><b>Get relationship:</b> rel:[relationship id]<br /><b>Get relationships for node:</b> rels:[node id]<br /><br /><b>Indexes</b><br /><b>Search nodes</b> node:index:[index]:[query]<br /><i>Example: node:index:myindex:name:*</i><br /><b>Search relationships:</b> rel:index:[index]:[index query]<br /><b>Direct node lookup</b> node:index:[index]:[key]:[value]<br /><b>Direct relationship lookup</b> rel:index:[index]:[key]:[value]
+ %span.form-tooltip-text
+ %b Global shortcuts:
+ %br
+ \<i>s</i> - Highlight query console
+ %br
+ \<i>v</i> - Toggle between visualizer and tabular view
+ %br
+ %br
+ %b Query console special keys:
+ %br
+ \<i>RETURN</i> Add new line
+ %br
+ \<i>CTRL+RETURN</i> Execute current query
+ %br
+ %br
+ \<b>Query console syntax:</b>
+ %br
+ \<b>Cypher:</b> [any cypher query]
+ %br
+ \<b>Node:</b> [node id]
+ %br
+ \<b>Relationship:</b> rel:[relationship id]
+ %br
+ %br
+ \<b>Indexes</b>
+ %br
+ \<b>Nodes:</b> node:index:[index]:[query]
+ %br
+ \<b>Rels:</b> rel:index:[index]:[query]
+ %br
+ \<i>Ex: node:index:myindex:name:*</i>
.span-half.last
%ul.data-toolbar.button-bar
%li
@@ -30,4 +58,5 @@
.break
+ #data-query-metadata.pad
#data-area
View
3  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/consoleTemplate.haml
@@ -0,0 +1,3 @@
+#data-console
+#data-execute-console.icon-button(title="Execute")
+ %span.icon
View
6 server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/errorTemplate.haml
@@ -0,0 +1,6 @@
+%p
+ %b =title
+:if description
+ %p = description
+:if monospaceDescription
+ %pre=monospaceDescription
View
1  ...r/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/notExecutedTemplate.haml
@@ -0,0 +1 @@
+%p <b>Query not executed yet.</b> Press the search button or hit <i>CTRL+ENTER</i> inside the query editor to execute it.
View
2  server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/notfound.haml
@@ -1,3 +1,3 @@
.pad
%h1 Not found
- %p There was no result returned for your query.
+ %p There is no data matching your query in the database.
View
5 ...src/main/coffeescript/neo4j/webadmin/modules/databrowser/views/queryMetadataTemplate.haml
@@ -0,0 +1,5 @@
+%ul.metadata
+
+ %li = "Returned <b>" + meta.getNumberOfRows() + (meta.getNumberOfRows() === 1 ? " row." : " rows.") + "</b>"
+ %li = "Query took <b> " + meta.getExecutionTime() + "ms</b>"
+.break
View
16 server/src/main/coffeescript/neo4j/webadmin/modules/indexmanager/IndexManagerRouter.coffee
@@ -21,8 +21,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
define(
['./views/IndexManagerView'
'./models/IndexManager'
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel'
'ribcage/Router'],
- (IndexManagerView, IndexManager, Router) ->
+ (IndexManagerView, IndexManager, MainMenuModel, Router) ->
class IndexManagerRouter extends Router
routes :
@@ -30,6 +31,12 @@ define(
init : (appState) =>
@appState = appState
+
+ @menuItem = new MainMenuModel.Item
+ title : "Indexes",
+ subtitle:"Add and remove",
+ url : "#/index/"
+
@idxMgr = new IndexManager(server:@appState.get("server"))
idxManager : =>
@@ -40,4 +47,11 @@ define(
@view ?= new IndexManagerView
state : @appState
idxMgr : @idxMgr
+
+ #
+ # Bootstrapper SPI
+ #
+
+ getMenuItems : ->
+ [@menuItem]
)
View
16 server/src/main/coffeescript/neo4j/webadmin/modules/serverinfo/ServerInfoRouter.coffee
@@ -21,8 +21,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
define(
['./models/ServerInfo'
'./views/ServerInfoView'
+ 'neo4j/webadmin/modules/baseui/models/MainMenuModel'
'ribcage/Router'],
- (ServerInfo, ServerInfoView, Router) ->
+ (ServerInfo, ServerInfoView, MainMenuModel, Router) ->
class ServerInfoRouter extends Router
routes :
@@ -30,6 +31,12 @@ define(
"/info/:domain/:name/" : "bean",
init : (@appState) =>
+
+ @menuItem = new MainMenuModel.Item
+ title : "Server info",
+ subtitle:"Details",
+ url : "#/info/"
+
@serverInfo = new ServerInfo { server : @appState.get "server" }
@server = @appState.get "server"
@@ -47,4 +54,11 @@ define(
@view ?= new ServerInfoView
appState:@appState
serverInfo:@serverInfo
+
+ #
+ # Bootstrapper SPI
+ #
+
+ getMenuItems : ->
+ [@menuItem]
)
View
26 server/src/main/coffeescript/neo4j/webadmin/utils/Keys.coffee
@@ -0,0 +1,26 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define [], () ->
+
+ class Keys
+ @BACKSPACE : 8
+ @TAB : 9
+ @ENTER : 13
View
36 server/src/main/coffeescript/ribcage/time/Timer.coffee
@@ -0,0 +1,36 @@
+###
+Copyright (c) 2002-2012 "Neo Technology,"
+Network Engine for Objects in Lund AB [http://neotechnology.com]
+
+This file is part of Neo4j.
+
+Neo4j is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+define(
+ [],
+ () ->
+ class Timer
+
+ start : () ->
+ @_startTime = new Date().getTime()
+
+ stop : ->
+ @_stopTime = new Date().getTime()
+
+ getTimePassed : ->
+ @_stopTime - @_startTime
+
+
+)
View
16 ...rc/main/coffeescript/test/neo4j/webadmin/modules/databrowser/TestDataBrowserRouter.coffee
@@ -0,0 +1,16 @@
+
+
+define ['lib/amd/Backbone',"neo4j/webadmin/modules/databrowser/DataBrowserRouter"], (Backbone, DataBrowserRouter) ->
+
+ describe "DataBrowserRouter", ->
+ it "can pick out read-only queries", ->
+ dbr = new DataBrowserRouter(getServer:()->null)
+
+ expect(dbr._looksLikeReadOnlyQuery "1").toBe(true)
+ expect(dbr._looksLikeReadOnlyQuery "node:1").toBe(true)
+ expect(dbr._looksLikeReadOnlyQuery "start n=node(0) return n").toBe(true)
+
+ expect(dbr._looksLikeReadOnlyQuery "start n=node(0) match n--a return n").toBe(false)
+ expect(dbr._looksLikeReadOnlyQuery "create n return n").toBe(false)
+ expect(dbr._looksLikeReadOnlyQuery "start n=node(0) relate n--a return n").toBe(false)
+ expect(dbr._looksLikeReadOnlyQuery "start n=node(0) set n.name='bob' return n").toBe(false)
View
2  server/src/main/java/org/neo4j/server/rest/repr/ExceptionRepresentation.java
@@ -40,7 +40,7 @@ protected void serialize( MappingSerializer serializer )
{
serializer.putString( "message", message );
}
- serializer.putString( "exception", exception.toString() );
+ serializer.putString( "exception", exception.getClass().getSimpleName() );
StackTraceElement[] trace = exception.getStackTrace();
if ( trace != null )
{
View
6 server/src/main/resources/webadmin-html/css/buttons.css
@@ -54,9 +54,9 @@
*/
.button, .text-icon-button, .icon-button, .micro-button, .bad-button {
- -moz-border-radius: 4px 4px 4px 4px;
- -webkit-border-radius:4px;
- border-radius:4px;
+ -moz-border-radius: 3px 3px 3px 3px;
+ -webkit-border-radius:3px;
+ border-radius:3px;
background: #f2f2f2;
background: -moz-linear-gradient(top, #f2f2f2 1%, #eaeaea 100%);
View
11 server/src/main/resources/webadmin-html/css/forms.css
@@ -7,15 +7,20 @@ label {
font-weight:bold;
}
-input {
+input, textarea {
outline:none;
height:24px;
border: 1px solid #CCCCCC;
- border-radius: 4px 4px 4px 4px;
+ border-radius: 3px 3px 3px 3px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
- padding: 4px 2px;
+ padding: 4px 3px;
+}
+
+textarea
+{
+ padding: 5px;
}
input.small {
View
46 server/src/main/resources/webadmin-html/css/style.css
@@ -7,6 +7,7 @@
@import url(forms.css);
@import url(grid.css);
@import url(../lib/colorpicker/css/colorpicker.css);
+@import url(../js/lib/codemirror2/codemirror.css);
body {
width:100%;
@@ -94,7 +95,7 @@ body {
#header { width:100%; height:50px; background:url("../img/tab_shade.png") repeat-x scroll center bottom #333333; }
#logo { float:left; margin: 4px 99px 0 10px; }
- #mainmenu {
+ #mainmenu, #mainmenu ul {
height: 50px;
list-style: none outside none;
margin: 0;
@@ -194,8 +195,45 @@ body {
#data-console {
color: #444444;
font-size: 14px;
+ font-family:monospace;
width: 85%;
float:left;
+ outline:none;
+ border: 1px solid #CCCCCC;
+ border-radius: 3px 3px 3px 3px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
+ -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.8) inset;
+ padding: 4px 3px;
+}
+
+/** Overide codemirror defaults **/
+.CodeMirror-scroll {
+ overflow: hidden; /* Don't show scroll bars. */
+ /* This is needed to prevent an IE[67] bug where the scrolled content
+ is visible outside of the scrolling box. */
+ position: relative;
+ outline: none;
+}
+.CodeMirror-scrollbar {
+ display:none !important;
+}
+
+#data-query-metadata {
+ font-size:12px;
+ border-bottom:1px solid #ccc;
+ background:#efefef;
+}
+
+#data-query-metadata ul {
+ list-style:none;
+ margin:0;
+ padding:0;
+}
+
+#data-query-metadata ul li {
+ float:left;
+ padding:0.5em 10px 0.5em 0;
}
#data-area {
@@ -204,6 +242,12 @@ body {
border-top: 1px solid #DDD;
}
+.workarea .controls {
+ box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.9) inset;
+ -moz-box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.9) inset;
+ -webkit-box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.9) inset;
+}
+
.data-console-wrap .form-tooltip {
float: left;
padding-left: 25px;
View
168 server/src/main/resources/webadmin-html/js/lib/codemirror2/codemirror.css
@@ -0,0 +1,168 @@
+.CodeMirror {
+ line-height: 1em;
+ font-family: monospace;
+
+ /* Necessary so the scrollbar can be absolutely positioned within the wrapper on Lion. */
+ position: relative;
+ /* This prevents unwanted scrollbars from showing up on the body and wrapper in IE. */
+ overflow: hidden;
+}
+
+.CodeMirror-scroll {
+ overflow-x: auto;
+ overflow-y: hidden;
+ height: 300px;
+ /* This is needed to prevent an IE[67] bug where the scrolled content
+ is visible outside of the scrolling box. */
+ position: relative;
+ outline: none;
+}
+
+/* Vertical scrollbar */
+.CodeMirror-scrollbar {
+ float: right;
+ overflow-x: hidden;
+ overflow-y: scroll;
+
+ /* This corrects for the 1px gap introduced to the left of the scrollbar
+ by the rule for .CodeMirror-scrollbar-inner. */
+ margin-left: -1px;
+}
+.CodeMirror-scrollbar-inner {
+ /* This needs to have a nonzero width in order for the scrollbar to appear
+ in Firefox and IE9. */
+ width: 1px;
+}
+.CodeMirror-scrollbar.cm-sb-overlap {
+ /* Ensure that the scrollbar appears in Lion, and that it overlaps the content
+ rather than sitting to the right of it. */
+ position: absolute;
+ z-index: 1;
+ float: none;
+ right: 0;
+ min-width: 12px;
+}
+.CodeMirror-scrollbar.cm-sb-nonoverlap {
+ min-width: 12px;
+}
+.CodeMirror-scrollbar.cm-sb-ie7 {
+ min-width: 18px;
+}
+
+.CodeMirror-gutter {
+ position: absolute; left: 0; top: 0;
+ z-index: 10;
+ background-color: #f7f7f7;
+ border-right: 1px solid #eee;
+ min-width: 2em;
+ height: 100%;
+}
+.CodeMirror-gutter-text {
+ color: #aaa;
+ text-align: right;
+ padding: .4em .2em .4em .4em;
+ white-space: pre !important;
+}
+.CodeMirror-lines {
+ padding: .4em;
+ white-space: pre;
+ cursor: text;
+}
+.CodeMirror-lines * {
+ /* Necessary for throw-scrolling to decelerate properly on Safari. */
+ pointer-events: none;
+}
+
+.CodeMirror pre {
+ -moz-border-radius: 0;
+ -webkit-border-radius: 0;
+ -o-border-radius: 0;
+ border-radius: 0;
+ border-width: 0; margin: 0; padding: 0; background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ padding: 0; margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+}
+
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+.CodeMirror-wrap .CodeMirror-scroll {
+ overflow-x: hidden;
+}
+
+.CodeMirror textarea {
+ outline: none !important;
+}
+
+.CodeMirror pre.CodeMirror-cursor {
+ z-index: 10;
+ position: absolute;
+ visibility: hidden;
+ border-left: 1px solid black;
+ border-right: none;
+ width: 0;
+}
+.cm-keymap-fat-cursor pre.CodeMirror-cursor {
+ width: auto;
+ border: 0;
+ background: transparent;
+ background: rgba(0, 200, 0, .4);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800);
+}
+/* Kludge to turn off filter in ie9+, which also accepts rgba */
+.cm-keymap-fat-cursor pre.CodeMirror-cursor:not(#nonsense_id) {
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {}
+.CodeMirror-focused pre.CodeMirror-cursor {
+ visibility: visible;
+}
+
+div.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; }
+
+.CodeMirror-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* Default theme */
+
+.cm-s-default span.cm-keyword {color: #708;}
+.cm-s-default span.cm-atom {color: #219;}
+.cm-s-default span.cm-number {color: #164;}
+.cm-s-default span.cm-def {color: #00f;}
+.cm-s-default span.cm-variable {color: black;}
+.cm-s-default span.cm-variable-2 {color: #05a;}
+.cm-s-default span.cm-variable-3 {color: #085;}
+.cm-s-default span.cm-property {color: black;}
+.cm-s-default span.cm-operator {color: black;}
+.cm-s-default span.cm-comment {color: #a50;}
+.cm-s-default span.cm-string {color: #a11;}
+.cm-s-default span.cm-string-2 {color: #f50;}
+.cm-s-default span.cm-meta {color: #555;}
+.cm-s-default span.cm-error {color: #f00;}
+.cm-s-default span.cm-qualifier {color: #555;}
+.cm-s-default span.cm-builtin {color: #30a;}
+.cm-s-default span.cm-bracket {color: #cc7;}
+.cm-s-default span.cm-tag {color: #170;}
+.cm-s-default span.cm-attribute {color: #00c;}
+.cm-s-default span.cm-header {color: blue;}
+.cm-s-default span.cm-quote {color: #090;}
+.cm-s-default span.cm-hr {color: #999;}
+.cm-s-default span.cm-link {color: #00c;}
+
+span.cm-header, span.cm-strong {font-weight: bold;}
+span.cm-em {font-style: italic;}
+span.cm-emstrong {font-style: italic; font-weight: bold;}
+span.cm-link {text-decoration: underline;}
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
View
3,176 server/src/main/resources/webadmin-html/js/lib/codemirror2/codemirror.js
3,176 additions, 0 deletions not shown
View
146 server/src/main/resources/webadmin-html/js/lib/codemirror2/util/closetag.js
@@ -0,0 +1,146 @@
+/**
+ * Tag-closer extension for CodeMirror.
+ *
+ * This extension adds a "closeTag" utility function that can be used with key bindings to
+ * insert a matching end tag after the ">" character of a start tag has been typed. It can
+ * also complete "</" if a matching start tag is found. It will correctly ignore signal
+ * characters for empty tags, comments, CDATA, etc.
+ *
+ * The function depends on internal parser state to identify tags. It is compatible with the
+ * following CodeMirror modes and will ignore all others:
+ * - htmlmixed
+ * - xml
+ *
+ * See demos/closetag.html for a usage example.
+ *
+ * @author Nathan Williams <nathan@nlwillia.net>
+ * Contributed under the same license terms as CodeMirror.
+ */
+(function() {
+ /** Option that allows tag closing behavior to be toggled. Default is true. */
+ CodeMirror.defaults['closeTagEnabled'] = true;