Merge pull request #110 from sosedoff/filter-and-pagination

Filter and pagination
This commit is contained in:
Dan Sosedoff 2016-01-08 15:44:41 -06:00
commit 73cb979721
9 changed files with 428 additions and 44 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
bindata.go -diff

View File

@ -4,7 +4,6 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@ -109,32 +108,54 @@ func GetTable(c *gin.Context) {
} }
func GetTableRows(c *gin.Context) { func GetTableRows(c *gin.Context) {
limit := 1000 // Number of rows to fetch offset, err := parseIntFormValue(c, "offset", 0)
limitVal := c.Request.FormValue("limit") if err != nil {
c.JSON(400, NewError(err))
return
}
if limitVal != "" { limit, err := parseIntFormValue(c, "limit", 100)
num, err := strconv.Atoi(limitVal) if err != nil {
c.JSON(400, NewError(err))
if err != nil { return
c.JSON(400, Error{"Invalid limit value"})
return
}
if num <= 0 {
c.JSON(400, Error{"Limit should be greater than 0"})
return
}
limit = num
} }
opts := client.RowsOptions{ opts := client.RowsOptions{
Limit: limit, Limit: limit,
Offset: offset,
SortColumn: c.Request.FormValue("sort_column"), SortColumn: c.Request.FormValue("sort_column"),
SortOrder: c.Request.FormValue("sort_order"), SortOrder: c.Request.FormValue("sort_order"),
Where: c.Request.FormValue("where"),
} }
res, err := DbClient.TableRows(c.Params.ByName("table"), opts) res, err := DbClient.TableRows(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
return
}
countRes, err := DbClient.TableRowsCount(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
return
}
numFetch := int64(opts.Limit)
numOffset := int64(opts.Offset)
numRows := countRes.Rows[0][0].(int64)
numPages := numRows / numFetch
if numPages*numFetch < numRows {
numPages++
}
res.Pagination = &client.Pagination{
Rows: numRows,
Page: (numOffset / numFetch) + 1,
Pages: numPages,
PerPage: numFetch,
}
serveResult(res, err, c) serveResult(res, err, c)
} }

View File

@ -1,9 +1,11 @@
package api package api
import ( import (
"fmt"
"log" "log"
"mime" "mime"
"path/filepath" "path/filepath"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/data" "github.com/sosedoff/pgweb/pkg/data"
@ -33,6 +35,25 @@ func getQueryParam(c *gin.Context, name string) string {
return result return result
} }
func parseIntFormValue(c *gin.Context, name string, defValue int) (int, error) {
val := c.Request.FormValue(name)
if val == "" {
return defValue, nil
}
num, err := strconv.Atoi(val)
if err != nil {
return defValue, fmt.Errorf("%s must be a number", name)
}
if num < 1 && defValue != 0 {
return defValue, fmt.Errorf("%s must be greated than 0", name)
}
return num, nil
}
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

@ -21,6 +21,8 @@ type Client struct {
// Struct to hold table rows browsing options // Struct to hold table rows browsing options
type RowsOptions struct { type RowsOptions struct {
Where string // Custom filter
Offset int // Number of rows to skip
Limit int // Number of rows to fetch Limit int // Number of rows to fetch
SortColumn string // Column to sort by SortColumn string // Column to sort by
SortOrder string // Sort direction (ASC, DESC) SortOrder string // Sort direction (ASC, DESC)
@ -98,6 +100,10 @@ func (client *Client) Table(table string) (*Result, error) {
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) { func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
sql := fmt.Sprintf(`SELECT * FROM "%s"`, table) sql := fmt.Sprintf(`SELECT * FROM "%s"`, table)
if opts.Where != "" {
sql += fmt.Sprintf(" WHERE %s", opts.Where)
}
if opts.SortColumn != "" { if opts.SortColumn != "" {
if opts.SortOrder == "" { if opts.SortOrder == "" {
opts.SortOrder = "ASC" opts.SortOrder = "ASC"
@ -110,6 +116,20 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
sql += fmt.Sprintf(" LIMIT %d", opts.Limit) sql += fmt.Sprintf(" LIMIT %d", opts.Limit)
} }
if opts.Offset > 0 {
sql += fmt.Sprintf(" OFFSET %d", opts.Offset)
}
return client.query(sql)
}
func (client *Client) TableRowsCount(table string, opts RowsOptions) (*Result, error) {
sql := fmt.Sprintf(`SELECT COUNT(1) FROM "%s"`, table)
if opts.Where != "" {
sql += fmt.Sprintf(" WHERE %s", opts.Where)
}
return client.query(sql) return client.query(sql)
} }

View File

@ -11,9 +11,17 @@ import (
type Row []interface{} type Row []interface{}
type Pagination struct {
Rows int64 `json:"rows_count"`
Page int64 `json:"page"`
Pages int64 `json:"pages_count"`
PerPage int64 `json:"per_page"`
}
type Result struct { type Result struct {
Columns []string `json:"columns"` Pagination *Pagination `json:"pagination,omitempty"`
Rows []Row `json:"rows"` Columns []string `json:"columns"`
Rows []Row `json:"rows"`
} }
// Due to big int number limitations in javascript, numbers should be encoded // Due to big int number limitations in javascript, numbers should be encoded

File diff suppressed because one or more lines are too long

View File

@ -263,7 +263,6 @@
border-color: #64903e; border-color: #64903e;
} }
#input .actions #query_progress { #input .actions #query_progress {
display: none; display: none;
float: left; float: left;
@ -300,8 +299,90 @@
overflow: auto; overflow: auto;
} }
#output.full { #pagination {
display: none;
position: absolute;
width: 100%;
height: 50px;
padding: 10px;
top: 0px; top: 0px;
left: 0px;
background: #fff;
border-bottom: 1px solid #eee;
box-shadow: 0 1px 3px 0px #f1f1f1;
}
#pagination .pager-container {
float: right;
}
#pagination .filters {
float: left;
font-size: 12px;
}
#pagination .filters span {
display: inline-block;
float: left;
font-weight: bold;
line-height: 32px;
height: 32px;
margin: 0px 8px;
color: #999;
}
#pagination .filters select {
font-size: 12px;
width: 100px;
float: left;
line-height: 30px;
height: 30px;
margin-right: 8px;
outline: none;
}
#pagination .filters select.column {
width: 150px;
}
#pagination .filters select.filter {
width: 100px;
}
#pagination .filters input {
float: left;
width: 200px;
height: 30px;
line-height: 30px;
margin-right: 8px;
font-size: 12px;
}
#pagination .filters .btn-primary {
border-color: #7eb54e;
color: #7eb54e;
background: #fff;
outline: none;
float: left;
margin-right: 8px;
}
#pagination .filters .btn-default {
float: left;
outline: none;
}
#pagination .btn-group {
float: right;
}
#pagination .current-page {
float: right;
font-size: 12px;
margin-right: 12px;
color: #999;
line-height: 32px;
height: 32px;
} }
#results { #results {
@ -394,6 +475,20 @@
max-width: none; max-width: none;
} }
.full #output {
top: 0px !important;
}
.with-pagination #output {
top: 50px !important;
}
.with-pagination #pagination {
display: block;
}
/* -------------------------------------------------------------------------- */
#custom_query { #custom_query {
height: 193px; height: 193px;
margin-top: 12px; margin-top: 12px;

View File

@ -75,6 +75,36 @@
<table id="results" class="table"></table> <table id="results" class="table"></table>
</div> </div>
</div> </div>
<div id="pagination">
<form class="filters" action="#" id="rows_filter">
<span>Search</span>
<select class="column form-control"></select>
<select class="filter form-control">
<option value="">Select filter</option>
<option value="equal">=</option>
<option value="not_equal">&ne;</option>
<option value="greater">&gt;</option>
<option value="greater_eq">&ge;</option>
<option value="less">&lt;</option>
<option value="less_eq">&le;</option>
<option value="like">LIKE</option>
<option value="ilike">ILIKE</option>
<option value="null">IS NULL</option>
<option value="not_null">NOT NULL</option>
</select>
<input type="text" class="form-control" placeholder="Search query" />
<button class="btn btn-primary btn-sm apply-filters" type="submit">Apply</button>
<button class="btn btn-default btn-sm reset-filters"><i class="fa fa-times"></i></button>
</form>
<div class="btn-group">
<button type="button" class="btn btn-default btn-sm prev-page" disabled="disabled"><i class="fa fa-angle-left"></i></button>
<button type="button" class="btn btn-default btn-sm page change-limit" title="Click to change row limit"></button>
<button type="button" class="btn btn-default btn-sm next-page"><i class="fa fa-angle-right"></i></button>
</div>
<div class="current-page" data-page="1" data-pages="1">
<span id="total_records"></span> rows
</div>
</div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,45 @@
var editor; var editor;
var connected = false; var connected = false;
var bookmarks = {}; var bookmarks = {};
var default_rows_limit = 100;
var filterOptions = {
"equal": "= 'DATA'",
"not_equal": "!= 'DATA'",
"greater": "> 'DATA'" ,
"greater_eq": ">= 'DATA'",
"less": "< 'DATA'",
"less_eq": "<= 'DATA'",
"like": "LIKE 'DATA'",
"ilike": "ILIKE 'DATA'",
"null": "IS NULL",
"not_null": "IS NOT NULL"
};
function setRowsLimit(num) {
localStorage.setItem("rows_limit", num);
}
function getRowsLimit() {
return parseInt(localStorage.getItem("rows_limit") || default_rows_limit);
}
function getPaginationOffset() {
var page = $(".current-page").data("page");
var limit = getRowsLimit();
return (page - 1) * limit;
}
function getPagesCount(rowsCount) {
var limit = getRowsLimit();
var num = parseInt(rowsCount / limit);
if ((num * limit) < rowsCount) {
num++;
}
return num;
}
function apiCall(method, path, params, cb) { function apiCall(method, path, params, cb) {
$.ajax({ $.ajax({
@ -180,7 +219,7 @@ function showQueryHistory() {
setCurrentTab("table_history"); setCurrentTab("table_history");
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").prop("class", "full");
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
} }
@ -198,7 +237,7 @@ function showTableIndexes() {
buildTable(data); buildTable(data);
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").prop("class", "full");
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
} }
@ -216,7 +255,7 @@ function showTableConstraints() {
buildTable(data); buildTable(data);
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").prop("class", "full");
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
} }
@ -237,6 +276,39 @@ function showTableInfo() {
$("#table_rows_count").text(data.rows_count); $("#table_rows_count").text(data.rows_count);
$("#table_encoding").text("Unknown"); $("#table_encoding").text("Unknown");
}); });
buildTableFilters(name);
}
function updatePaginator(pagination) {
if (!pagination) {
$(".current-page").data("page", 1).data("pages", 1);
$("button.page").text("1 of 1");
$(".prev-page, .next-page").prop("disabled", "disabled");
return;
}
$(".current-page").
data("page", pagination.page).
data("pages", pagination.pages_count);
if (pagination.page > 1) {
$(".prev-page").prop("disabled", "");
}
else {
$(".prev-page").prop("disabled", "disabled");
}
if (pagination.pages_count > 1 && pagination.page < pagination.pages_count) {
$(".next-page").prop("disabled", "");
}
else {
$(".next-page").prop("disabled", "disabled");
}
$("#total_records").text(pagination.rows_count);
if (pagination.pages_count == 0) pagination.pages_count = 1;
$("button.page").text(pagination.page + " of " + pagination.pages_count);
} }
function showTableContent(sortColumn, sortOrder) { function showTableContent(sortColumn, sortOrder) {
@ -247,13 +319,37 @@ function showTableContent(sortColumn, sortOrder) {
return; return;
} }
getTableRows(name, { limit: 100, sort_column: sortColumn, sort_order: sortOrder }, function(data) { var opts = {
buildTable(data, sortColumn, sortOrder); limit: getRowsLimit(),
setCurrentTab("table_content"); offset: getPaginationOffset(),
sort_column: sortColumn,
sort_order: sortOrder
};
var filter = {
column: $(".filters select.column").val(),
op: $(".filters select.filter").val(),
input: $(".filters input").val()
};
// Apply filtering only if column is selected
if (filter.column && filter.op) {
var where = [
filter.column,
filterOptions[filter.op].replace("DATA", filter.input)
].join(" ");
opts["where"] = where;
}
getTableRows(name, opts, function(data) {
$("#results").attr("data-mode", "browse"); $("#results").attr("data-mode", "browse");
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").prop("class", "with-pagination");
buildTable(data, sortColumn, sortOrder);
setCurrentTab("table_content");
updatePaginator(data.pagination);
}); });
} }
@ -265,12 +361,13 @@ function showTableStructure() {
return; return;
} }
getTableStructure(name, function(data) { setCurrentTab("table_structure");
setCurrentTab("table_structure");
buildTable(data);
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").prop("class", "full");
getTableStructure(name, function(data) {
buildTable(data);
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
} }
@ -280,7 +377,7 @@ function showQueryPanel() {
editor.focus(); editor.focus();
$("#input").show(); $("#input").show();
$("#output").removeClass("full"); $("#body").prop("class", "")
} }
function showConnectionPanel() { function showConnectionPanel() {
@ -299,7 +396,7 @@ function showConnectionPanel() {
}); });
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").addClass("full");
}); });
} }
@ -309,7 +406,7 @@ function showActivityPanel() {
apiCall("get", "/activity", {}, function(data) { apiCall("get", "/activity", {}, function(data) {
buildTable(data); buildTable(data);
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#body").addClass("full");
}); });
} }
@ -333,7 +430,7 @@ function runQuery() {
$("#run, #explain, #csv, #json, #xml").prop("disabled", false); $("#run, #explain, #csv, #json, #xml").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
$("#input").show(); $("#input").show();
$("#output").removeClass("full"); $("#body").removeClass("full");
if (query.toLowerCase().indexOf("explain") != -1) { if (query.toLowerCase().indexOf("explain") != -1) {
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
@ -369,7 +466,7 @@ function runExplain() {
$("#run, #explain, #csv, #json, #xml").prop("disabled", false); $("#run, #explain, #csv, #json, #xml").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
$("#input").show(); $("#input").show();
$("#output").removeClass("full"); $("#body").removeClass("full");
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
} }
@ -388,6 +485,21 @@ function exportTo(format) {
win.focus(); win.focus();
} }
function buildTableFilters(name) {
getTableStructure(name, function(data) {
if (data.rows.length == 0) {
$("#pagination .filters").hide();
}
$("#pagination select.column").html("<option value='' selected>Select column</option>");
for (row of data.rows) {
var el = $("<option/>").attr("value", row[0]).text(row[0]);
$("#pagination select.column").append(el);
}
});
}
function initEditor() { function initEditor() {
var writeQueryTimeout = null; var writeQueryTimeout = null;
editor = ace.edit("custom_query"); editor = ace.edit("custom_query");
@ -586,6 +698,8 @@ $(document).ready(function() {
$("#sequences li.selected").removeClass("selected"); $("#sequences li.selected").removeClass("selected");
$(this).addClass("selected"); $(this).addClass("selected");
$("#tables").attr("data-current", $.trim($(this).text())); $("#tables").attr("data-current", $.trim($(this).text()));
$(".current-page").data("page", 1);
$(".filters select, .filters input").val("");
showTableContent(); showTableContent();
showTableInfo(); showTableInfo();
@ -618,6 +732,80 @@ $(document).ready(function() {
loadSequences(); loadSequences();
}); });
$("#rows_filter").on("submit", function(e) {
e.preventDefault();
$(".current-page").data("page", 1);
var column = $(this).find("select.column").val();
var filter = $(this).find("select.filter").val();
var query = $.trim($(this).find("input").val());
if (filter && filterOptions[filter].indexOf("DATA") > 0 && query == "") {
alert("Please specify filter query");
return
}
showTableContent();
});
$(".change-limit").on("click", function() {
var limit = prompt("Please specify a new rows limit", getRowsLimit());
if (limit && limit >= 1) {
$(".current-page").data("page", 1);
setRowsLimit(limit);
showTableContent();
}
});
$("select.filter").on("change", function(e) {
var val = $(this).val();
if (["null", "not_null"].indexOf(val) >= 0) {
$(".filters input").hide().val("");
}
else {
$(".filters input").show();
}
});
$("button.reset-filters").on("click", function() {
$(".filters select, .filters input").val("");
showTableContent();
});
$("#pagination .next-page").on("click", function() {
var current = $(".current-page").data("page");
var total = $(".current-page").data("pages");
if (total > current) {
$(".current-page").data("page", current + 1);
showTableContent();
if (current + 1 == total) {
$(this).prop("disabled", "disabled");
}
}
if (current > 1) {
$(".prev-page").prop("disabled", "");
}
});
$("#pagination .prev-page").on("click", function() {
var current = $(".current-page").data("page");
if (current > 1) {
$(".current-page").data("page", current - 1);
$(".next-page").prop("disabled", "");
showTableContent();
}
if (current == 1) {
$(this).prop("disabled", "disabled");
}
});
$("#edit_connection").on("click", function() { $("#edit_connection").on("click", function() {
if (connected) { if (connected) {
$("#close_connection_window").show(); $("#close_connection_window").show();