Merge pull request #1825 from gitbucket/features/dbviewer
Replace H2 console
@@ -43,25 +43,11 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
}
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||
val context = request.getServletContext.getContextPath
|
||||
val path = httpRequest.getRequestURI.substring(context.length)
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val context = request.getServletContext.getContextPath
|
||||
val path = httpRequest.getRequestURI.substring(context.length)
|
||||
|
||||
if(path.startsWith("/console/")){
|
||||
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
val baseUrl = this.baseUrl(httpRequest)
|
||||
if(account == null){
|
||||
// Redirect to login form
|
||||
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(baseUrl + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
|
||||
if(path.startsWith("/git/") || path.startsWith("/git-lfs/")){
|
||||
// Git repository
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
|
||||
@@ -13,16 +13,22 @@ import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import org.scalatra.forms._
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import scala.collection.JavaConverters._
|
||||
import org.scalatra._
|
||||
import org.json4s.jackson.Serialization
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
case class Table(name: String, columns: Seq[Column])
|
||||
case class Column(name: String, primaryKey: Boolean)
|
||||
|
||||
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
@@ -152,6 +158,71 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
|
||||
get("/admin/dbviewer")(adminOnly {
|
||||
val conn = request2Session(request).conn
|
||||
val meta = conn.getMetaData
|
||||
val tables = ListBuffer[Table]()
|
||||
using(meta.getTables(null, "%", "%", Array("TABLE", "VIEW"))){ rs =>
|
||||
while(rs.next()){
|
||||
val tableName = rs.getString("TABLE_NAME")
|
||||
|
||||
val pkColumns = ListBuffer[String]()
|
||||
using(meta.getPrimaryKeys(null, null, tableName)){ rs =>
|
||||
while(rs.next()){
|
||||
pkColumns += rs.getString("COLUMN_NAME").toUpperCase
|
||||
}
|
||||
}
|
||||
|
||||
val columns = ListBuffer[Column]()
|
||||
using(meta.getColumns(null, "%", tableName, "%")){ rs =>
|
||||
while(rs.next()){
|
||||
val columnName = rs.getString("COLUMN_NAME").toUpperCase
|
||||
columns += Column(columnName, pkColumns.contains(columnName))
|
||||
}
|
||||
}
|
||||
|
||||
tables += Table(tableName.toUpperCase, columns)
|
||||
}
|
||||
}
|
||||
html.dbviewer(tables)
|
||||
})
|
||||
|
||||
post("/admin/dbviewer/_query")(adminOnly {
|
||||
contentType = formats("json")
|
||||
params.get("query").collectFirst { case query if query.trim.nonEmpty =>
|
||||
val trimmedQuery = query.trim
|
||||
if(trimmedQuery.nonEmpty){
|
||||
try {
|
||||
val conn = request2Session(request).conn
|
||||
using(conn.prepareStatement(query)){ stmt =>
|
||||
if(trimmedQuery.toUpperCase.startsWith("SELECT")){
|
||||
using(stmt.executeQuery()){ rs =>
|
||||
val meta = rs.getMetaData
|
||||
val columns = for(i <- 1 to meta.getColumnCount) yield {
|
||||
meta.getColumnName(i)
|
||||
}
|
||||
val result = ListBuffer[Map[String, String]]()
|
||||
while(rs.next()){
|
||||
val row = columns.map { columnName =>
|
||||
columnName -> Option(rs.getObject(columnName)).map(_.toString).getOrElse("<NULL>")
|
||||
}.toMap
|
||||
result += row
|
||||
}
|
||||
Ok(Serialization.write(Map("type" -> "query", "columns" -> columns, "rows" -> result)))
|
||||
}
|
||||
} else {
|
||||
val rows = stmt.executeUpdate()
|
||||
Ok(Serialization.write(Map("type" -> "update", "rows" -> rows)))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Ok(Serialization.write(Map("type" -> "error", "message" -> e.toString)))
|
||||
}
|
||||
}
|
||||
} getOrElse Ok(Serialization.write(Map("type" -> "error", "message" -> "query is empty")))
|
||||
})
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
html.system(flash.get("info"))
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ class CompositeScalatraFilter extends Filter {
|
||||
}
|
||||
|
||||
if(!checkPath.startsWith("/upload/") && !checkPath.startsWith("/git/") && !checkPath.startsWith("/git-lfs/") &&
|
||||
!checkPath.startsWith("/plugin-assets/") && !checkPath.startsWith("/console/")){
|
||||
!checkPath.startsWith("/plugin-assets/")){
|
||||
filters
|
||||
.filter { case (_, path) =>
|
||||
val start = path.replaceFirst("/\\*$", "/")
|
||||
|
||||
@@ -23,7 +23,7 @@ class TransactionFilter extends Filter {
|
||||
|
||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||
val servletPath = req.asInstanceOf[HttpServletRequest].getServletPath()
|
||||
if(servletPath.startsWith("/assets/") || servletPath == "/console" || servletPath == "/git" || servletPath == "/git-lfs"){
|
||||
if(servletPath.startsWith("/assets/") || servletPath == "/git" || servletPath == "/git-lfs"){
|
||||
// assets and git-lfs don't need transaction
|
||||
chain.doFilter(req, res)
|
||||
} else {
|
||||
|
||||
89
src/main/twirl/gitbucket/core/admin/dbviewer.scala.html
Normal file
@@ -0,0 +1,89 @@
|
||||
@(tables: Seq[gitbucket.core.controller.Table])(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.view.helpers
|
||||
@gitbucket.core.html.main("Database viewer") {
|
||||
@gitbucket.core.admin.html.menu("dbviewer") {
|
||||
<div class="container">
|
||||
<div class="col-md-3">
|
||||
<div id="table-tree">
|
||||
<ul>
|
||||
@tables.map { table =>
|
||||
<li data-jstree='{"icon":"@context.path/assets/common/images/table.gif"}'><a href="javascript:void(0);" class="table-link">@table.name</a>
|
||||
<ul>
|
||||
@table.columns.map { column =>
|
||||
<li data-jstree='{"icon":"@context.path/assets/common/images/column.gif"}'>@column.name
|
||||
@if(column.primaryKey){ (PK) }
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div id="editor" style="width: 100%; height: 300px;"></div>
|
||||
<div class="block">
|
||||
<input type="button" value="Run query" id="run-query" class="btn btn-success">
|
||||
</div>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script src="@helpers.assets("/vendors/ace/ace.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="@helpers.assets("/vendors/vakata-jstree-3.3.4/jstree.min.js")" type="text/javascript" charset="utf-8"></script>
|
||||
<link rel="stylesheet" href="@helpers.assets("/vendors/vakata-jstree-3.3.4/themes/default/style.min.css")" />
|
||||
<script>
|
||||
$(function(){
|
||||
$('#editor').text($('#initial').val());
|
||||
var editor = ace.edit("editor");
|
||||
editor.setTheme("ace/theme/monokai");
|
||||
editor.getSession().setMode("ace/mode/sql");
|
||||
|
||||
|
||||
$('#table-tree').jstree();
|
||||
|
||||
$('.table-link').click(function(e){
|
||||
var query = editor.getValue();
|
||||
if(query != ''){
|
||||
query = query + '\n';
|
||||
}
|
||||
console.log(e);
|
||||
editor.setValue(query + 'SELECT * FROM ' + $(e.target).text());
|
||||
});
|
||||
|
||||
$('#run-query').click(function(){
|
||||
console.log(editor.getValue());
|
||||
$.post('@context.path/admin/dbviewer/_query', { query: editor.getValue() }, function(data){
|
||||
if(data.type == "query"){
|
||||
var table = $('<table class="table table-bordered table-hover table-scroll">');
|
||||
|
||||
var header = $('<tr>');
|
||||
$.each(data.columns, function(i, column){
|
||||
header.append($('<th>').text(column));
|
||||
});
|
||||
table.append($('<thead>').append(header));
|
||||
|
||||
var body = $('<tbody>');
|
||||
$.each(data.rows, function(i, rs){
|
||||
var row = $('<tr>');
|
||||
$.each(data.columns, function(i, column){
|
||||
row.append($('<td>').text(rs[column]));
|
||||
});
|
||||
body.append(row);
|
||||
});
|
||||
|
||||
table.append(body);
|
||||
$('#result').empty().append(table);
|
||||
|
||||
} else if(data.type == "update"){
|
||||
$('#result').empty().append($('<span>').text('Updated ' + data.rows + ' rows.'));
|
||||
|
||||
} else if(data.type == "error"){
|
||||
$('#result').empty().append($('<span class="error">').text(data.message));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -25,10 +25,10 @@
|
||||
<span>Data export / import</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item-hover">
|
||||
<a href="@context.path/console/login.jsp" target="_blank">
|
||||
<li class="menu-item-hover @if(active=="dbviewer"){active}">
|
||||
<a href="@context.path/admin/dbviewer">
|
||||
<i class="menu-icon octicon octicon-database"></i>
|
||||
<span>H2 console</span>
|
||||
<span>Database viewer</span>
|
||||
</a>
|
||||
</li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<servlet-class>gitbucket.core.servlet.GitRepositoryServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>GitRepositoryServlet</servlet-name>
|
||||
<url-pattern>/git/*</url-pattern>
|
||||
@@ -70,30 +70,6 @@
|
||||
<url-pattern>/plugin-assets/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- H2 console configuration -->
|
||||
<!-- ===================================================================== -->
|
||||
<servlet>
|
||||
<servlet-name>H2Console</servlet-name>
|
||||
<servlet-class>org.h2.server.web.WebServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>webAllowOthers</param-name>
|
||||
<param-value></param-value>
|
||||
</init-param>
|
||||
<!--
|
||||
<init-param>
|
||||
<param-name>trace</param-name>
|
||||
<param-value></param-value>
|
||||
</init-param>
|
||||
-->
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>H2Console</servlet-name>
|
||||
<url-pattern>/console/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Session timeout -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
@@ -124,6 +124,12 @@ div.content-wrapper {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Global Header */
|
||||
/* ======================================================================== */
|
||||
|
||||
BIN
src/main/webapp/assets/common/images/column.gif
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
src/main/webapp/assets/common/images/table.gif
Normal file
|
After Width: | Height: | Size: 343 B |
8423
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/jstree.js
vendored
Executable file
6
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/jstree.min.js
vendored
Executable file
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default-dark/32px.png
vendored
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default-dark/40px.png
vendored
Executable file
|
After Width: | Height: | Size: 5.6 KiB |
1152
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default-dark/style.css
vendored
Executable file
1
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default-dark/style.min.css
vendored
Executable file
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default-dark/throbber.gif
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default/32px.png
vendored
Executable file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default/40px.png
vendored
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
1108
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default/style.css
vendored
Executable file
1
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default/style.min.css
vendored
Executable file
BIN
src/main/webapp/assets/vendors/vakata-jstree-3.3.4/themes/default/throbber.gif
vendored
Executable file
|
After Width: | Height: | Size: 1.7 KiB |