commit
995d52db1b
@ -196,21 +196,28 @@ func HandleQuery(query string, c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
q := c.Request.URL.Query()
|
format := getQueryParam(c, "format")
|
||||||
|
filename := getQueryParam(c, "filename")
|
||||||
|
|
||||||
if len(q["format"]) > 0 && q["format"][0] == "csv" {
|
if filename == "" {
|
||||||
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
|
filename = fmt.Sprintf("pgweb-%v.%v", time.Now().Unix(), format)
|
||||||
if len(q["filename"]) > 0 && q["filename"][0] != "" {
|
|
||||||
filename = q["filename"][0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if format != "" {
|
||||||
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
||||||
c.Data(200, "text/csv", result.CSV())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "csv":
|
||||||
|
c.Data(200, "text/csv", result.CSV())
|
||||||
|
case "json":
|
||||||
|
c.Data(200, "applicaiton/json", result.JSON())
|
||||||
|
case "xml":
|
||||||
|
c.XML(200, result)
|
||||||
|
default:
|
||||||
c.JSON(200, result)
|
c.JSON(200, result)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetBookmarks(c *gin.Context) {
|
func GetBookmarks(c *gin.Context) {
|
||||||
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
|
||||||
|
@ -22,6 +22,17 @@ type Error struct {
|
|||||||
Message string `json:"error"`
|
Message string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQueryParam(c *gin.Context, name string) string {
|
||||||
|
result := ""
|
||||||
|
q := c.Request.URL.Query()
|
||||||
|
|
||||||
|
if len(q[name]) > 0 {
|
||||||
|
result = q[name][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func assetContentType(name string) string {
|
func assetContentType(name string) string {
|
||||||
ext := filepath.Ext(name)
|
ext := filepath.Ext(name)
|
||||||
result := mime.TypeByExtension(ext)
|
result := mime.TypeByExtension(ext)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/csv"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
@ -21,13 +19,6 @@ type Client struct {
|
|||||||
ConnectionString string
|
ConnectionString string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Row []interface{}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
Columns []string `json:"columns"`
|
|
||||||
Rows []Row `json:"rows"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct to hold table rows browsing options
|
// Struct to hold table rows browsing options
|
||||||
type RowsOptions struct {
|
type RowsOptions struct {
|
||||||
Limit int // Number of rows to fetch
|
Limit int // Number of rows to fetch
|
||||||
@ -205,51 +196,6 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Result) Format() []map[string]interface{} {
|
|
||||||
var items []map[string]interface{}
|
|
||||||
|
|
||||||
for _, row := range res.Rows {
|
|
||||||
item := make(map[string]interface{})
|
|
||||||
|
|
||||||
for i, c := range res.Columns {
|
|
||||||
item[c] = row[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *Result) CSV() []byte {
|
|
||||||
buff := &bytes.Buffer{}
|
|
||||||
writer := csv.NewWriter(buff)
|
|
||||||
|
|
||||||
writer.Write(res.Columns)
|
|
||||||
|
|
||||||
for _, row := range res.Rows {
|
|
||||||
record := make([]string, len(res.Columns))
|
|
||||||
|
|
||||||
for i, item := range row {
|
|
||||||
if item != nil {
|
|
||||||
record[i] = fmt.Sprintf("%v", item)
|
|
||||||
} else {
|
|
||||||
record[i] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := writer.Write(record)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Flush()
|
|
||||||
return buff.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close database connection
|
// Close database connection
|
||||||
func (client *Client) Close() error {
|
func (client *Client) Close() error {
|
||||||
if client.db != nil {
|
if client.db != nil {
|
||||||
|
65
pkg/client/result.go
Normal file
65
pkg/client/result.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Row []interface{}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Rows []Row `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) Format() []map[string]interface{} {
|
||||||
|
var items []map[string]interface{}
|
||||||
|
|
||||||
|
for _, row := range res.Rows {
|
||||||
|
item := make(map[string]interface{})
|
||||||
|
|
||||||
|
for i, c := range res.Columns {
|
||||||
|
item[c] = row[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) CSV() []byte {
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
writer := csv.NewWriter(buff)
|
||||||
|
|
||||||
|
writer.Write(res.Columns)
|
||||||
|
|
||||||
|
for _, row := range res.Rows {
|
||||||
|
record := make([]string, len(res.Columns))
|
||||||
|
|
||||||
|
for i, item := range row {
|
||||||
|
if item != nil {
|
||||||
|
record[i] = fmt.Sprintf("%v", item)
|
||||||
|
} else {
|
||||||
|
record[i] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writer.Write(record)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
return buff.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) JSON() []byte {
|
||||||
|
data, _ := json.Marshal(res.Format())
|
||||||
|
return data
|
||||||
|
}
|
46
pkg/client/result_test.go
Normal file
46
pkg/client/result_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_CSV(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Columns: []string{"id", "name", "email"},
|
||||||
|
Rows: []Row{
|
||||||
|
Row{1, "John", "john@example.com"},
|
||||||
|
Row{2, "Bob", "bob@example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "id,name,email\n1,John,john@example.com\n2,Bob,bob@example.com\n"
|
||||||
|
output := string(result.CSV())
|
||||||
|
|
||||||
|
assert.Equal(t, expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_JSON(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Columns: []string{"id", "name", "email"},
|
||||||
|
Rows: []Row{
|
||||||
|
Row{1, "John", "john@example.com"},
|
||||||
|
Row{2, "Bob", "bob@example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output := result.JSON()
|
||||||
|
obj := []map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(output, &obj)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(obj))
|
||||||
|
|
||||||
|
for i, row := range obj {
|
||||||
|
for j, col := range result.Columns {
|
||||||
|
assert.Equal(t, result.Rows[i][j], row[col])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -61,7 +61,8 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" />
|
<input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" />
|
||||||
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
||||||
<input type="button" id="csv" value="Download CSV" class="btn btn-sm btn-default" />
|
<input type="button" id="csv" value="CSV" class="btn btn-sm btn-default" />
|
||||||
|
<input type="button" id="json" value="JSON" class="btn btn-sm btn-default" />
|
||||||
|
|
||||||
<div id="query_progress">Please wait, query is executing...</div>
|
<div id="query_progress">Please wait, query is executing...</div>
|
||||||
</div>
|
</div>
|
||||||
@ -197,7 +198,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tables_context_menu">
|
<div id="tables_context_menu">
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li><a href="#" data-action="export">Export to CSV</a></li>
|
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
|
||||||
|
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
|
||||||
|
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
||||||
<li><a href="#" data-action="delete">Delete table</a></li>
|
<li><a href="#" data-action="delete">Delete table</a></li>
|
||||||
|
@ -84,7 +84,7 @@ function resetTable() {
|
|||||||
removeClass("no-crop");
|
removeClass("no-crop");
|
||||||
}
|
}
|
||||||
|
|
||||||
function performTableAction(table, action) {
|
function performTableAction(table, action, el) {
|
||||||
if (action == "truncate" || action == "delete") {
|
if (action == "truncate" || action == "delete") {
|
||||||
var message = "Are you sure you want to " + action + " table " + table + " ?";
|
var message = "Are you sure you want to " + action + " table " + table + " ?";
|
||||||
if (!confirm(message)) return;
|
if (!confirm(message)) return;
|
||||||
@ -106,9 +106,10 @@ function performTableAction(table, action) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "export":
|
case "export":
|
||||||
var filename = table + ".csv"
|
var format = el.data("format");
|
||||||
|
var filename = table + "." + format;
|
||||||
var query = window.encodeURI("SELECT * FROM " + table);
|
var query = window.encodeURI("SELECT * FROM " + table);
|
||||||
var url = "http://" + window.location.host + "/api/query?format=csv&filename=" + filename + "&query=" + query;
|
var url = "http://" + window.location.host + "/api/query?format=" + format + "&filename=" + filename + "&query=" + query;
|
||||||
var win = window.open(url, "_blank");
|
var win = window.open(url, "_blank");
|
||||||
win.focus();
|
win.focus();
|
||||||
break;
|
break;
|
||||||
@ -315,13 +316,13 @@ function showActivityPanel() {
|
|||||||
function runQuery() {
|
function runQuery() {
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", true);
|
$("#run, #explain, #csv, #json").prop("disabled", true);
|
||||||
$("#query_progress").show();
|
$("#query_progress").show();
|
||||||
|
|
||||||
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -329,7 +330,7 @@ function runQuery() {
|
|||||||
executeQuery(query, function(data) {
|
executeQuery(query, function(data) {
|
||||||
buildTable(data);
|
buildTable(data);
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
$("#input").show();
|
$("#input").show();
|
||||||
$("#output").removeClass("full");
|
$("#output").removeClass("full");
|
||||||
@ -351,13 +352,13 @@ function runQuery() {
|
|||||||
function runExplain() {
|
function runExplain() {
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", true);
|
$("#run, #explain, #csv, #json").prop("disabled", true);
|
||||||
$("#query_progress").show();
|
$("#query_progress").show();
|
||||||
|
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -365,7 +366,7 @@ function runExplain() {
|
|||||||
explainQuery(query, function(data) {
|
explainQuery(query, function(data) {
|
||||||
buildTable(data);
|
buildTable(data);
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
$("#input").show();
|
$("#input").show();
|
||||||
$("#output").removeClass("full");
|
$("#output").removeClass("full");
|
||||||
@ -373,14 +374,14 @@ function runExplain() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportToCSV() {
|
function exportTo(format) {
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://" + window.location.host + "/api/query?format=csv&query=" + encodeQuery(query);
|
var url = "http://" + window.location.host + "/api/query?format=" + format + "&query=" + encodeQuery(query);
|
||||||
var win = window.open(url, '_blank');
|
var win = window.open(url, '_blank');
|
||||||
|
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
@ -523,9 +524,13 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#csv").on("click", function() {
|
$("#csv").on("click", function() {
|
||||||
exportToCSV();
|
exportTo("csv");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#json").on("click", function() {
|
||||||
|
exportTo("json");
|
||||||
|
})
|
||||||
|
|
||||||
$("#results").on("click", "tr", function() {
|
$("#results").on("click", "tr", function() {
|
||||||
$("#results tr.selected").removeClass();
|
$("#results tr.selected").removeClass();
|
||||||
$(this).addClass("selected");
|
$(this).addClass("selected");
|
||||||
@ -583,9 +588,10 @@ $(document).ready(function() {
|
|||||||
target: "#tables_context_menu",
|
target: "#tables_context_menu",
|
||||||
scopes: "li",
|
scopes: "li",
|
||||||
onItem: function(context, e) {
|
onItem: function(context, e) {
|
||||||
|
var el = $(e.target);
|
||||||
var table = $.trim($(context[0]).text());
|
var table = $.trim($(context[0]).text());
|
||||||
var action = $(e.target).data("action");
|
var action = el.data("action");
|
||||||
performTableAction(table, action);
|
performTableAction(table, action, el);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user