1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Advanced a lot with "generic model editing"

This commit is contained in:
Adolfo Gómez 2013-11-22 02:22:41 +00:00
parent 004e060aaf
commit de4a9ff291
24 changed files with 2837 additions and 91 deletions

View File

@ -36,7 +36,7 @@ from django.utils.translation import ugettext_lazy as _
from uds.models import Transport from uds.models import Transport
from uds.core.transports import factory from uds.core.transports import factory
from uds.REST import Handler, HandlerError from uds.REST import Handler, NotFound
from uds.REST.mixins import ModelHandlerMixin, ModelTypeHandlerMixin, ModelTableHandlerMixin from uds.REST.mixins import ModelHandlerMixin, ModelTypeHandlerMixin, ModelTableHandlerMixin
import logging import logging
@ -52,10 +52,11 @@ class Transports(ModelHandlerMixin, Handler):
type_ = item.getType() type_ = item.getType()
return { 'id': item.id, return { 'id': item.id,
'name': item.name, 'name': item.name,
'comments': item.comments,
'priority': item.priority, 'priority': item.priority,
'nets_positive': item.nets_positive,
'deployed_count': item.deployedServices.count(), 'deployed_count': item.deployedServices.count(),
'type': type_.type(), 'type': type_.type(),
'comments': item.comments
} }
class Types(ModelTypeHandlerMixin, Handler): class Types(ModelTypeHandlerMixin, Handler):
@ -63,6 +64,13 @@ class Types(ModelTypeHandlerMixin, Handler):
def enum_types(self): def enum_types(self):
return factory().providers().values() return factory().providers().values()
def getGui(self, type_):
try:
return factory().lookup(type_).guiDescription()
except:
raise NotFound('type not found')
class TableInfo(ModelTableHandlerMixin, Handler): class TableInfo(ModelTableHandlerMixin, Handler):
path = 'transports' path = 'transports'

View File

@ -98,7 +98,12 @@ class ModelHandlerMixin(object):
return self.processDetail() return self.processDetail()
try: try:
return list(self.getItems(pk=self._args[0]))[0] val = self.model.objects.get(pk=self._args[0])
res = self.item_as_dict(val)
if hasattr(val, 'getInstance'):
for key, value in val.getInstance().valuesDict().iteritems():
res[key] = value
return res
except: except:
raise NotFound('item not found') raise NotFound('item not found')

View File

@ -20,7 +20,6 @@ body {
padding: 5px 15px; padding: 5px 15px;
} }
/* Custom */ /* Custom */
.btn3d-tables { .btn3d-tables {
margin-top:0px; margin-top:0px;
@ -37,8 +36,8 @@ body {
} }
/* collapsable && closeable pannels */ /* collapsable && closeable pannels */
.chevron:before { .chevron:before {
content: "\f139"; content: "\f139";
} }
.chevron.collapsed:before { .chevron.collapsed:before {
content: "\f13a"; content: "\f13a";
@ -53,6 +52,12 @@ body {
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
.modal-dialog {
/* new custom width */
width: 60%;
}
/* Edit Below to Customize Widths > 768px */ /* Edit Below to Customize Widths > 768px */
@media (min-width:768px) { @media (min-width:768px) {

View File

@ -1,11 +1,26 @@
/* jshint strict: true */
// ------------------------------- // -------------------------------
// Templates related // Templates related
// Inserted into api // Inserted into api
// for the admin app // for the admin app
// ------------------------------- // -------------------------------
(function(api, $) { (function(api, $) {
api.templates = {}; "use strict";
// Registers Handlebar useful helpers
// Equal comparision (like if helper, but with comparation)
Handlebars.registerHelper('ifequals', function(context1, context2, options) {
console.log('Comparing ', context1, ' with ', context2);
if(context1 == context2) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
api.templates = {};
// Now initialize templates api
api.templates.cache = new api.cache('tmpls'); // Will cache templates locally. If name contains api.templates.cache = new api.cache('tmpls'); // Will cache templates locally. If name contains
// '?', data will not be cached and always // '?', data will not be cached and always
// re-requested. We do not care about lang, because page will reload on language change // re-requested. We do not care about lang, because page will reload on language change
@ -28,7 +43,7 @@
url : api.template_url + name, url : api.template_url + name,
type : "GET", type : "GET",
dataType : "text", dataType : "text",
success : function(data) { success : function(data) {
var cachedId = 'tmpl_' + name; var cachedId = 'tmpl_' + name;
$this.cache.put('_' + cachedId, $this.evaluate(data)); $this.cache.put('_' + cachedId, $this.evaluate(data));
$this.cache.put(name, cachedId); $this.cache.put(name, cachedId);
@ -44,9 +59,8 @@
}); });
}; };
// Simple JavaScript Templating // Simple JavaScript Templating, using HandleBars
// Based on John Resig - http://ejohn.org/ - MIT Licensed api.templates.evaluate = function (str, context) {
api.templates.evaluate = function (str, data) {
// Figure out if we're getting a template, or if we need to // Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result. // load the template - and be sure to cache the result.
var cached; var cached;
@ -55,24 +69,9 @@
if( cached === undefined ) { if( cached === undefined ) {
cached = api.templates.evaluate(document.getElementById(str).innerHTML); cached = api.templates.evaluate(document.getElementById(str).innerHTML);
this.cache.put('_'+str, cached); this.cache.put('_'+str, cached);
} }
} }
// If cached, get cached first var template = cached || Handlebars.compile(str);
var fn = cached || return context ? template(context) : template;
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str.replace(/[\r\t\n]/g, " ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g, "$1\r").replace(
/\t=(.*?)%>/g, "',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join(
"\\'") + "');}return p.join('');");
// Provide some basic currying to the user
return data ? fn(data) : fn;
}; };
}(window.api = window.api || {}, jQuery)); }(window.api = window.api || {}, jQuery));

View File

@ -33,7 +33,7 @@ gui.providers.link = function(event) {
gui.appendToWorkspace(gui.breadcrumbs(gettext('Service Providers'))); gui.appendToWorkspace(gui.breadcrumbs(gettext('Service Providers')));
var tableId = gui.providers.table({ var tableId = gui.providers.table({
rowSelect : 'multi', rowSelect : 'single',
onEdit: function(value, event, table) { onEdit: function(value, event, table) {
gui.providers.rest.gui(value.type, { gui.providers.rest.gui(value.type, {
success: function(data){ success: function(data){
@ -105,12 +105,7 @@ gui.authenticators.link = function(event) {
gui.authenticators.rest.gui(value.type, { gui.authenticators.rest.gui(value.type, {
success: function(data){ success: function(data){
var form = gui.fields(data); var form = gui.fields(data);
gui.appendToWorkspace(gui.modal('edit_modal', gettext('Edit authenticator'), form)); gui.launchModal(gettext('Edit authenticator')+' '+value.name, form);
$('#edit_modal').modal()
.on('hidden.bs.modal', function () {
$('#edit_modal').remove();
})
;
}, },
}); });
}, },
@ -152,6 +147,23 @@ gui.connectivity.link = function(event) {
rowSelect : 'multi', rowSelect : 'multi',
container : 'transports-placeholder', container : 'transports-placeholder',
buttons : [ 'edit', 'delete', 'xls' ], buttons : [ 'edit', 'delete', 'xls' ],
onEdit: function(value, event, table) {
gui.connectivity.transports.rest.gui(value.type, {
success: function(itemGui){
gui.connectivity.transports.rest.get({
id:value.id,
success: function(item) {
var form = gui.fields(itemGui, item);
gui.launchModal(gettext('Edit transport')+' '+value.name,form, function(form_selector) {
var fields = gui.fields.read(form_selector);
return false;
});
},
});
},
});
},
}); });
gui.connectivity.networks.table({ gui.connectivity.networks.table({
rowSelect : 'multi', rowSelect : 'multi',

View File

@ -3,15 +3,17 @@
"use strict"; "use strict";
// Returns a form that will manage a gui description (new or edit) // Returns a form that will manage a gui description (new or edit)
gui.fields = function(item_description) { gui.fields = function(itemGui, item) {
var form = '<form class="form-horizontal" role="form">'; var editing = item !== undefined; // Locate real Editing
// item_description is expected to have fields sorted by .gui.order (REST api returns them sorted) item = item || {id:''};
$.each(item_description, function(index, f){ var form = '<form class="form-horizontal" role="form">' +
'<input type="hidden" name="id" class="modal_field_data" value="' + item.id + '">';
// itemGui is expected to have fields sorted by .gui.order (REST api returns them sorted)
$.each(itemGui, function(index, f){
gui.doLog(f); gui.doLog(f);
var editing = false; // Locate real Editing gui.doLog(item[f.name]);
form += api.templates.evaluate('tmpl_fld_'+f.gui.type, { form += api.templates.evaluate('tmpl_fld_'+f.gui.type, {
value: f.value || f.gui.value || f.gui.defvalue, // If no value present, use default value value: item[f.name] || f.gui.value || f.gui.defvalue, // If no value present, use default value
values: f.gui.values, values: f.gui.values,
label: f.gui.label, label: f.gui.label,
length: f.gui.length, length: f.gui.length,
@ -21,11 +23,29 @@
tooltip: f.gui.tooltip, tooltip: f.gui.tooltip,
type: f.gui.type, type: f.gui.type,
name: f.name, name: f.name,
css: 'modal_field_data',
}); });
}); });
form += '</form>'; form += '</form>';
return form; return form;
}; };
// Reads fields from a form
gui.fields.read = function(formSelector) {
var res = {};
$(formSelector + ' .modal_field_data').each(function(i, field) {
var $field = $(field);
if( $field.attr('name') ) { // Is a valid field
if( $field.attr('type') == 'checkbox') {
res[$field.attr('name')] = $field.is(':checked');
} else {
res[$field.attr('name')] = $field.val();
}
}
});
gui.doLog(res);
return res;
};
}(window.gui = window.gui || {}, jQuery)); }(window.gui = window.gui || {}, jQuery));

View File

@ -105,6 +105,36 @@
}); });
}; };
gui.launchModal = function(title, content, onSuccess) {
var id = Math.random().toString().split('.')[1];
gui.appendToWorkspace(gui.modal(id, title, content));
id = '#' + id; // for jQuery
// For "beauty" switches, initialize them now
$(id + ' .make-switch').bootstrapSwitch();
// Activate "cool" selects
$(id + ' .selectpicker').selectpicker();
// TEST: cooller on mobile devices
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ) {
$(id + ' .selectpicker').selectpicker('mobile');
}
// Activate tooltips
$(id + ' [data-toggle="tooltip"]').tooltip({delay: 500, placement: 'auto right'});
// And catch "accept" (default is "Save" in fact) button click
$(id + ' .button-accept').click(function(){
if( onSuccess ) {
if( onSuccess(id + ' form') === false ) // Some error may have ocurred, do not close dialog
return;
}
$(id).modal('hide');
});
// Launch modal
$(id).modal()
.on('hidden.bs.modal', function () {
$(id).remove();
});
};
gui.clearWorkspace = function() { gui.clearWorkspace = function() {
$('#content').empty(); $('#content').empty();
$('#minimized').empty(); $('#minimized').empty();

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
<link href="{% get_static_prefix %}css/font-awesome.min.css" rel="stylesheet" media="screen"> <link href="{% get_static_prefix %}css/font-awesome.min.css" rel="stylesheet" media="screen">
<link href="{% get_static_prefix %}css/bootstrap-formhelpers.min.css" rel="stylesheet" media="screen"> <link href="{% get_static_prefix %}css/bootstrap-formhelpers.min.css" rel="stylesheet" media="screen">
<link href="{% get_static_prefix %}css/bootstrap-switch.css" rel="stylesheet" media="screen"> <link href="{% get_static_prefix %}css/bootstrap-switch.css" rel="stylesheet" media="screen">
<link href="{% get_static_prefix %}css/bootstrap-select.min.css" rel="stylesheet" media="screen">
<link href="{% get_static_prefix %}adm/css/jquery.dataTables.css" rel="stylesheet" media="screen"> <link href="{% get_static_prefix %}adm/css/jquery.dataTables.css" rel="stylesheet" media="screen">
<link href="{% get_static_prefix %}adm/css/TableTools.css" rel="stylesheet" media="screen"> <link href="{% get_static_prefix %}adm/css/TableTools.css" rel="stylesheet" media="screen">
@ -33,29 +34,32 @@
{% block menu %}{% include 'uds/admin/snippets/navbar.html' %}{% endblock %} {% block menu %}{% include 'uds/admin/snippets/navbar.html' %}{% endblock %}
<!-- End of menu --> <!-- End of menu -->
<!-- Content --> <!-- Content -->
<div id="page-wrapper"> <div id="page-wrapper">
<div id="content"> <div id="content">
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div>
<div class="row">
<div id="minimized" class="col-lg-12">
</div>
</div>
</div> </div>
<!-- End of content --> <div class="row">
</div> <div id="minimized" class="col-lg-12">
</div> </div>
</div>
</div>
</div>
<script src="{% url 'uds.web.views.jsCatalog' LANGUAGE_CODE %}"></script> <script src="{% url 'uds.web.views.jsCatalog' LANGUAGE_CODE %}"></script>
<script src="{% get_static_prefix %}js/jquery-1.10.2.min.js"></script> <script src="{% get_static_prefix %}js/jquery-1.10.2.min.js"></script>
<script src="{% get_static_prefix %}js/jquery.cookie.js"></script> <script src="{% get_static_prefix %}js/jquery.cookie.js"></script>
<script src="{% get_static_prefix %}js/bootstrap.min.js"></script> <script src="{% get_static_prefix %}js/bootstrap.min.js"></script>
<script src="{% get_static_prefix %}js/bootstrap-switch.min.js"></script> <script src="{% get_static_prefix %}js/bootstrap-switch.min.js"></script>
<script src="{% get_static_prefix %}js/bootstrap-select.min.js"></script>
<script src="{% get_static_prefix %}adm/js/jquery.blockUI.js"></script> <script src="{% get_static_prefix %}adm/js/jquery.blockUI.js"></script>
<script src="{% get_static_prefix %}adm/js/jquery.dataTables.min.js"></script> <script src="{% get_static_prefix %}adm/js/jquery.dataTables.min.js"></script>
<script src="{% get_static_prefix %}adm/js/TableTools.min.js"></script> <script src="{% get_static_prefix %}adm/js/TableTools.min.js"></script>
<!-- template engine -->
<script src="{% get_static_prefix %}adm/js/handlebars-v1.1.2.js"></script>
<!-- for "save" from javascript --> <!-- for "save" from javascript -->
<script src="{% get_static_prefix %}adm/js/Blob.js"></script> <script src="{% get_static_prefix %}adm/js/Blob.js"></script>

View File

@ -1,19 +1,21 @@
{% load i18n html5 static %} {% load i18n %}
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<h1>{% trans 'Authenticators' %} <small>{% trans 'administration of authenticators' %}</small></h1> <h1>{% trans 'Authenticators' %}</h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a class="lnk-dashboard" href="#"><i class="fa fa-dashboard"></i> Dashboard</a></li> <li><a class="lnk-dashboard" href="#"><i class="fa fa-dashboard"></i> Dashboard</a></li>
<li>otra cosa</li> <li>otra cosa</li>
</ol> </ol>
</div> </div>
</div><!-- /.row --> </div><!-- /.row -->
{% verbatim %}
<div class="row"> <div class="row">
<div id="<%= auths %>" class="col-xs-12"></div> <div id="{{ auths }}" class="col-xs-12"></div>
</div> </div>
<div class="row"> <div class="row">
<div id="<%= groups %>" class="col-xs-12"></div> <div id="{{ groups }}" class="col-xs-12"></div>
</div> </div>
<div class="row"> <div class="row">
<div id="<%= users %>" class="col-xs-12"></div> <div id="{{ users }}" class="col-xs-12"></div>
</div> </div>
{% endverbatim %}

View File

@ -1,4 +1,4 @@
{% load i18n html5 static %} {% load i18n %}
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<h1>{% trans 'Connectivity' %} <small>{% trans 'overview' %}</small></h1> <h1>{% trans 'Connectivity' %} <small>{% trans 'overview' %}</small></h1>
@ -7,10 +7,11 @@
</ol> </ol>
</div> </div>
</div><!-- /.row --> </div><!-- /.row -->
{% verbatim %}
<div class="row"> <div class="row">
<div class="col-lg-6" id="<%= transports %>"> <div class="col-lg-6" id="{{ transports }}">
</div> </div>
<div class="col-lg-6" id="<%= networks %>"> <div class="col-lg-6" id="{{ networks }}">
</div> </div>
</div> </div>
{% endverbatim %}

View File

@ -0,0 +1,9 @@
{% extends "uds/admin/tmpl/fld/form-group.html" %}
{% load i18n %}
{% block field %}
<div class="make-switch" data-on-label="{% trans 'Yes' %}" data-off-label="{% trans 'No' %}">
{% verbatim %}
<input type="checkbox" class="{{ css }}" name="{{ name }}" id="{{ name }}_field" {{# ifequals value true }}checked{{/ ifequals }}>
</div>
{% endverbatim %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "uds/admin/tmpl/fld/form-group.html" %}
{% load i18n %}
{% block field %}
{% verbatim %}
<select class="selectpicker show-menu-arrow {{ css }}" name="{{ name }}" id="{{ name }}_field">
{{#each values }}
<option value="{{ id }}"{{# ifequals id ../value }}selected{{/ ifequals }}>{{ text }}</option>
{{/each}}
</select>
{% endverbatim %}
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "uds/admin/tmpl/fld/form-group.html" %}
{% load i18n %}
{% verbatim %}
{% endverbatim %}

View File

@ -0,0 +1,9 @@
{% load i18n %}
{% verbatim %}
<div class="form-group">
<label for="{{ name }}_field" class="col-sm-3 control-label" data-toggle="tooltip" data-title="{{ tooltip }}">{{ label }}</label>
<div class="col-sm-9">
{% endverbatim %}
{% block field %}EMPTY!!!{% endblock %}
</div>
</div>

View File

@ -0,0 +1,3 @@
{% verbatim %}
<input type="hidden" class="{{ css }}" name="{{ name }}}" value="{{ value }}">
{% endverbatim %}

View File

@ -0,0 +1,5 @@
{% extends "uds/admin/tmpl/fld/form-group.html" %}
{% load i18n %}
{% verbatim %}
{% endverbatim %}

View File

@ -1,6 +1,7 @@
<div class="form-group"> {% extends "uds/admin/tmpl/fld/form-group.html" %}
<label for="<%= name %>_field" class="col-sm-3 control-label"><%= label %></label> {% load i18n %}
<div class="col-sm-9"> {% block field %}
<input type="numeric" class="form-control" id="<%= name %>_field" placeholder="<%= tooltip %>"> {% verbatim %}
</div> <input type="numeric" name="{{ name }}" class="form-control {{ css }}" id="{{ name }}_field" placeholder="{{ tooltip }}" value="{{ value }}">
</div> {% endverbatim %}
{% endblock %}

View File

@ -0,0 +1,2 @@
{% verbatim %}
{% endverbatim %}

View File

@ -1,6 +1,7 @@
<div class="form-group"> {% extends "uds/admin/tmpl/fld/form-group.html" %}
<label for="<%= name %>_field" class="col-sm-3 control-label"><%= label %></label> {% load i18n %}
<div class="col-sm-9"> {% block field %}
<input type="text" class="form-control" id="<%= name %>_field" placeholder="<%= tooltip %>"> {% verbatim %}
</div> <input type="text" class="form-control {{ css }}" name="{{ name }}" id="{{ name }}_field" placeholder="{{ tooltip }}" value="{{ value }}">
</div> {% endverbatim %}
{% endblock %}

View File

@ -0,0 +1,2 @@
{% verbatim %}
{% endverbatim %}

View File

@ -1,17 +1,30 @@
{% load i18n %} {% load i18n %}
<div id="<%= id %>" class="modal fade"> {% verbatim %}
<div id="{{ id }}" class="modal fade">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><%= title %></h4> <h4 class="modal-title">{{ title }}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<%= content %> {{{ content }}}
{{# if button1 }}
{{{ button1 }}}
{{ else }}
{% endverbatim %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button>
<button type="button" class="btn btn-primary">{% trans 'Save' %}</button> {% verbatim %}
{{/ if }}
{{# if button2 }}
{{{ button1 }}}
{{ else }}
{% endverbatim %}
<button type="button" class="btn btn-primary button-accept">{% trans 'Save' %}</button>
{% verbatim %}
{{/ if }}
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
{% endverbatim %}

View File

@ -1,3 +1,4 @@
{% verbatim %}
<?xml version="1.0"?> <?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?> <?mso-application progid="Excel.Sheet"?>
<Workbook <Workbook
@ -9,7 +10,7 @@
<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office"> <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
<Author>UDS Administration</Author> <Author>UDS Administration</Author>
<LastAuthor>UDS Administration Interface</LastAuthor> <LastAuthor>UDS Administration Interface</LastAuthor>
<Created><%= creation_date %></Created> <Created>{{ creation_date }}</Created>
<Company>UDS</Company> <Company>UDS</Company>
<Version>1.0</Version> <Version>1.0</Version>
</DocumentProperties> </DocumentProperties>
@ -34,10 +35,10 @@
<Font x:Family="Verdana" ss:Bold="1" /> <Font x:Family="Verdana" ss:Bold="1" />
</Style> </Style>
</Styles> </Styles>
<Worksheet ss:Name="<%= worksheet %>"> <Worksheet ss:Name="{{ worksheet }}">
<Table ss:ExpandedColumnCount="<%= columns_count %>" ss:ExpandedRowCount="<%= rows_count %>" <Table ss:ExpandedColumnCount="{{ columns_count }}" ss:ExpandedRowCount="{{ rows_count }}"
x:FullColumns="1" x:FullRows="1"> x:FullColumns="1" x:FullRows="1">
<%= rows %> {{{ rows }}}
</Table> </Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"> <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<Print> <Print>
@ -58,3 +59,4 @@
</WorksheetOptions> </WorksheetOptions>
</Worksheet> </Worksheet>
</Workbook> </Workbook>
{% endverbatim %}

View File

@ -1,15 +1,17 @@
<div class="panel panel-primary" id="<%= panelId %>" data-minimized="<%= title %>"> {% verbatim %}
<div class="panel panel-primary" id="{{ panelId }}" data-minimized="{{ title }}">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"><span class="fa fa-<%= icon %>"></span> <%= title %> <h3 class="panel-title"><span class="fa fa-{{ icon }}"></span> {{ title }}
<span class="panel-icon fa fa-dot-circle-o pull-right" <span class="panel-icon fa fa-dot-circle-o pull-right"
onclick="gui.minimizePanel('#<%= panelId %>');"> </span> onclick="gui.minimizePanel('#{{ panelId }}');"> </span>
<span class="panel-icon fa chevron pull-right" data-toggle="collapse" <span class="panel-icon fa chevron pull-right" data-toggle="collapse"
data-target="#<%= panelId %> > div.panel-body"> </span> data-target="#{{ panelId }} > div.panel-body"> </span>
<span class="panel-icon fa fa-refresh pull-right"> </span> <span class="panel-icon fa fa-refresh pull-right"> </span>
</h3> </h3>
</div> </div>
<div class="panel-body collapse in"> <div class="panel-body collapse in">
<table class="table table-striped table-bordered table-hover" id="<%= table_id %>"> <table class="table table-striped table-bordered table-hover" id="{{ table_id }}">
</table> </table>
</div> </div>
</div> </div>
{% endverbatim %}