Implement table item context menu
- Add ability to export table contents into CSV - Add ability to truncate table - Add ability to delete table
This commit is contained in:
parent
c2290acae3
commit
651b65a882
@ -185,6 +185,10 @@ func HandleQuery(query string, c *gin.Context) {
|
||||
|
||||
if len(q["format"]) > 0 && q["format"][0] == "csv" {
|
||||
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
|
||||
if len(q["filename"]) > 0 && q["filename"][0] != "" {
|
||||
filename = q["filename"][0]
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
||||
c.Data(200, "text/csv", result.CSV())
|
||||
return
|
||||
|
File diff suppressed because one or more lines are too long
@ -449,4 +449,5 @@
|
||||
|
||||
.connection-ssh-group {
|
||||
display: none;
|
||||
}
|
||||
z-index: 1000;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
<script type="text/javascript" src="/static/js/jquery.js"></script>
|
||||
<script type="text/javascript" src="/static/js/ace.js"></script>
|
||||
<script type="text/javascript" src="/static/js/ace-pgsql.js"></script>
|
||||
<script type="text/javascript" src="/static/js/bootstrap-contextmenu.js"></script>
|
||||
<script type="text/javascript" src="/static/js/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@ -81,7 +82,6 @@
|
||||
<div class="btn-group btn-group-sm connection-group-switch">
|
||||
<button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button>
|
||||
<button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button>
|
||||
<!--<button type="button" data="ssh" class="btn btn-default">SSH</button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -193,5 +193,13 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tables_context_menu">
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#" data-action="export">Export to CSV</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
||||
<li><a href="#" data-action="delete">Delete table</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -37,7 +37,7 @@ function loadTables() {
|
||||
|
||||
getTables(function(data) {
|
||||
data.forEach(function(item) {
|
||||
$("<li><span><i class='fa fa-table'></i> " + item + "</span></li>").appendTo("#tables");
|
||||
$("<li><span><i class='fa fa-table'></i> " + item + " </span></li>").appendTo("#tables");
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -68,15 +68,45 @@ function resetTable() {
|
||||
removeClass("no-crop");
|
||||
}
|
||||
|
||||
function performTableAction(table, action) {
|
||||
if (action == "truncate" || action == "delete") {
|
||||
var message = "Are you sure you want to " + action + " table " + table + " ?";
|
||||
if (!confirm(message)) return;
|
||||
}
|
||||
|
||||
switch(action) {
|
||||
case "truncate":
|
||||
executeQuery("TRUNCATE TABLE " + table, function(data) {
|
||||
if (data.error) alert(data.error);
|
||||
resetTable();
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
executeQuery("DROP TABLE " + table, function(data) {
|
||||
if (data.error) alert(data.error);
|
||||
loadTables();
|
||||
resetTable();
|
||||
});
|
||||
break;
|
||||
case "export":
|
||||
var filename = table + ".csv"
|
||||
var query = window.encodeURI("SELECT * FROM " + table);
|
||||
var url = "http://" + window.location.host + "/api/query?format=csv&filename=" + filename + "&query=" + query;
|
||||
var win = window.open(url, "_blank");
|
||||
win.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function sortArrow(direction) {
|
||||
switch (direction) {
|
||||
case "ASC":
|
||||
return "▲";
|
||||
case "DESC":
|
||||
return "▼";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
switch (direction) {
|
||||
case "ASC":
|
||||
return "▲";
|
||||
case "DESC":
|
||||
return "▼";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildTable(results, sortColumn, sortOrder) {
|
||||
@ -496,6 +526,16 @@ $(document).ready(function() {
|
||||
showTableInfo();
|
||||
});
|
||||
|
||||
$("#tables").contextmenu({
|
||||
target: "#tables_context_menu",
|
||||
scopes: "li",
|
||||
onItem: function(context, e) {
|
||||
var table = $.trim($(context[0]).text());
|
||||
var action = $(e.target).data("action");
|
||||
performTableAction(table, action);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh_tables").on("click", function() {
|
||||
loadTables();
|
||||
});
|
||||
|
205
static/js/bootstrap-contextmenu.js
vendored
Normal file
205
static/js/bootstrap-contextmenu.js
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
/*!
|
||||
* Bootstrap Context Menu
|
||||
* Author: @sydcanem
|
||||
* https://github.com/sydcanem/bootstrap-contextmenu
|
||||
*
|
||||
* Inspired by Bootstrap's dropdown plugin.
|
||||
* Bootstrap (http://getbootstrap.com).
|
||||
*
|
||||
* Licensed under MIT
|
||||
* ========================================================= */
|
||||
|
||||
;(function($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/* CONTEXTMENU CLASS DEFINITION
|
||||
* ============================ */
|
||||
var toggle = '[data-toggle="context"]';
|
||||
|
||||
var ContextMenu = function (element, options) {
|
||||
this.$element = $(element);
|
||||
|
||||
this.before = options.before || this.before;
|
||||
this.onItem = options.onItem || this.onItem;
|
||||
this.scopes = options.scopes || null;
|
||||
|
||||
if (options.target) {
|
||||
this.$element.data('target', options.target);
|
||||
}
|
||||
|
||||
this.listen();
|
||||
};
|
||||
|
||||
ContextMenu.prototype = {
|
||||
|
||||
constructor: ContextMenu
|
||||
,show: function(e) {
|
||||
|
||||
var $menu
|
||||
, evt
|
||||
, tp
|
||||
, items
|
||||
, relatedTarget = { relatedTarget: this, target: e.currentTarget };
|
||||
|
||||
if (this.isDisabled()) return;
|
||||
|
||||
this.closemenu();
|
||||
|
||||
if (this.before.call(this,e,$(e.currentTarget)) === false) return;
|
||||
|
||||
$menu = this.getMenu();
|
||||
$menu.trigger(evt = $.Event('show.bs.context', relatedTarget));
|
||||
|
||||
tp = this.getPosition(e, $menu);
|
||||
items = 'li:not(.divider)';
|
||||
$menu.attr('style', '')
|
||||
.css(tp)
|
||||
.addClass('open')
|
||||
.on('click.context.data-api', items, $.proxy(this.onItem, this, $(e.currentTarget)))
|
||||
.trigger('shown.bs.context', relatedTarget);
|
||||
|
||||
// Delegating the `closemenu` only on the currently opened menu.
|
||||
// This prevents other opened menus from closing.
|
||||
$('html')
|
||||
.on('click.context.data-api', $menu.selector, $.proxy(this.closemenu, this));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
,closemenu: function(e) {
|
||||
var $menu
|
||||
, evt
|
||||
, items
|
||||
, relatedTarget;
|
||||
|
||||
$menu = this.getMenu();
|
||||
|
||||
if(!$menu.hasClass('open')) return;
|
||||
|
||||
relatedTarget = { relatedTarget: this };
|
||||
$menu.trigger(evt = $.Event('hide.bs.context', relatedTarget));
|
||||
|
||||
items = 'li:not(.divider)';
|
||||
$menu.removeClass('open')
|
||||
.off('click.context.data-api', items)
|
||||
.trigger('hidden.bs.context', relatedTarget);
|
||||
|
||||
$('html')
|
||||
.off('click.context.data-api', $menu.selector);
|
||||
// Don't propagate click event so other currently
|
||||
// opened menus won't close.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
,keydown: function(e) {
|
||||
if (e.which == 27) this.closemenu(e);
|
||||
}
|
||||
|
||||
,before: function(e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
,onItem: function(e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
,listen: function () {
|
||||
this.$element.on('contextmenu.context.data-api', this.scopes, $.proxy(this.show, this));
|
||||
$('html').on('click.context.data-api', $.proxy(this.closemenu, this));
|
||||
$('html').on('keydown.context.data-api', $.proxy(this.keydown, this));
|
||||
}
|
||||
|
||||
,destroy: function() {
|
||||
this.$element.off('.context.data-api').removeData('context');
|
||||
$('html').off('.context.data-api');
|
||||
}
|
||||
|
||||
,isDisabled: function() {
|
||||
return this.$element.hasClass('disabled') ||
|
||||
this.$element.attr('disabled');
|
||||
}
|
||||
|
||||
,getMenu: function () {
|
||||
var selector = this.$element.data('target')
|
||||
, $menu;
|
||||
|
||||
if (!selector) {
|
||||
selector = this.$element.attr('href');
|
||||
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7
|
||||
}
|
||||
|
||||
$menu = $(selector);
|
||||
|
||||
return $menu && $menu.length ? $menu : this.$element.find(selector);
|
||||
}
|
||||
|
||||
,getPosition: function(e, $menu) {
|
||||
var mouseX = e.clientX
|
||||
, mouseY = e.clientY
|
||||
, boundsX = $(window).width()
|
||||
, boundsY = $(window).height()
|
||||
, menuWidth = $menu.find('.dropdown-menu').outerWidth()
|
||||
, menuHeight = $menu.find('.dropdown-menu').outerHeight()
|
||||
, tp = {"position":"absolute","z-index":9999}
|
||||
, Y, X, parentOffset;
|
||||
|
||||
if (mouseY + menuHeight > boundsY) {
|
||||
Y = {"top": mouseY - menuHeight + $(window).scrollTop()};
|
||||
} else {
|
||||
Y = {"top": mouseY + $(window).scrollTop()};
|
||||
}
|
||||
|
||||
if ((mouseX + menuWidth > boundsX) && ((mouseX - menuWidth) > 0)) {
|
||||
X = {"left": mouseX - menuWidth + $(window).scrollLeft()};
|
||||
} else {
|
||||
X = {"left": mouseX + $(window).scrollLeft()};
|
||||
}
|
||||
|
||||
// If context-menu's parent is positioned using absolute or relative positioning,
|
||||
// the calculated mouse position will be incorrect.
|
||||
// Adjust the position of the menu by its offset parent position.
|
||||
parentOffset = $menu.offsetParent().offset();
|
||||
X.left = X.left - parentOffset.left;
|
||||
Y.top = Y.top - parentOffset.top;
|
||||
|
||||
return $.extend(tp, Y, X);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/* CONTEXT MENU PLUGIN DEFINITION
|
||||
* ========================== */
|
||||
|
||||
$.fn.contextmenu = function (option,e) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
, data = $this.data('context')
|
||||
, options = (typeof option == 'object') && option;
|
||||
|
||||
if (!data) $this.data('context', (data = new ContextMenu($this, options)));
|
||||
if (typeof option == 'string') data[option].call(data, e);
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.contextmenu.Constructor = ContextMenu;
|
||||
|
||||
/* APPLY TO STANDARD CONTEXT MENU ELEMENTS
|
||||
* =================================== */
|
||||
|
||||
$(document)
|
||||
.on('contextmenu.context.data-api', function() {
|
||||
$(toggle).each(function () {
|
||||
var data = $(this).data('context');
|
||||
if (!data) return;
|
||||
data.closemenu();
|
||||
});
|
||||
})
|
||||
.on('contextmenu.context.data-api', toggle, function(e) {
|
||||
$(this).contextmenu('show', e);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
}(jQuery));
|
Loading…
x
Reference in New Issue
Block a user