Merge pull request #107 from sosedoff/export

Export to JSON / XML
This commit is contained in:
Dan Sosedoff 2016-01-04 18:40:43 -06:00
commit 995d52db1b
8 changed files with 184 additions and 100 deletions

View File

@ -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())

View File

@ -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)

View File

@ -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
View 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
View 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

View File

@ -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>

View File

@ -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);
} }
}); });