rec(query, callback, data, null);
}
+ this.select = function(field)
+ {
+ this.fields.push(field);
+ }
+
+ this.unselect = function(field)
+ {
+ this.fields = $.grep(this.fields, function(x) { return x != field; });
+ }
+
// we send queries as a json string now
// this.as_POST = function() {
// return {'action': this.action, 'object': this.object, 'timestamp': this.timestamp,
/* Constructor */
if (typeof query == "undefined")
throw "Must pass a query in QueryExt constructor";
- this.query = query
- this.parent_query = (typeof parent_query == "undefined") ? false : parent_query
- this.main_query = (typeof main_query == "undefined") ? false : main_query
+ this.query = query
+ this.parent_query_ext = (typeof parent_query == "undefined") ? false : parent_query
+ this.main_query_ext = (typeof main_query == "undefined") ? false : main_query
this.status = null;
this.results = null;
manifold.query_store.main_queries[query.query_uuid] = query_ext;
// We also need to insert all queries and subqueries from the analyzed_query
+ // XXX We need the root of all subqueries
query.iter_subqueries(function(sq, data, parent_query) {
if (parent_query)
parent_query_ext = manifold.query_store.find_analyzed_query_ext(parent_query.query_uuid);
else
parent_query_ext = null;
+ // XXX parent_query_ext == false
+ // XXX main.subqueries = {} # Normal, we need analyzed_query
sq_ext = new QueryExt(sq, parent_query_ext, query_ext)
manifold.query_store.analyzed_queries[sq.query_uuid] = sq_ext;
});
raise_event: function(query_uuid, event_type, value)
{
+ // Query uuid has been updated with the key of a new element
+ query_ext = manifold.query_store.find_analyzed_query_ext(query_uuid);
+ query = query_ext.query;
+
switch(event_type) {
case SET_ADD:
- // Query uuid has been updated with the key of a new element
- query_ext = manifold.query_store.find_analyzed_query(query_uuid);
-
// update is only possible is the query is not pending, etc
// CHECK status !
// XXX we can only update subqueries of the main query. Check !
// assert query_ext.parent_query == query_ext.main_query
- update_query = query_ext.parent_query.update_query;
+ update_query = query_ext.parent_query_ext.update_query;
// NOTE: update might modify the fields in Get
// NOTE : we have to modify all child queries
case SET_REMOVED:
// Query uuid has been updated with the key of a removed element
break;
+ case FILTER_ADDED:
+ break;
+ case FILTER_REMOVED:
+ break;
+ case FIELD_ADDED:
+ main_query = query_ext.main_query_ext.query;
+ main_update_query = query_ext.main_query_ext.update_query;
+ query.select(value);
+
+ // Here we need the full path through all subqueries
+ path = ""
+ // XXX We might need the query name in the QueryExt structure
+ main_query.select(value);
+
+ // XXX When is an update query associated ?
+ // XXX main_update_query.select(value);
+
+ // We need to inform about changes in these queries to the respective plugins
+ // Note: query, main_query & update_query have the same UUID
+ manifold.raise_query_event(query_uuid, event_type, value);
+ // We are targeting the same object with get and update
+ // The notion of query is bad, we should have a notion of destination, and issue queries on the destination
+ // NOTE: Editing a subquery == editing a local view on the destination
+ break;
+
+ case FIELD_REMOVED:
+ query = query_ext.query;
+ main_query = query_ext.main_query_ext.query;
+ main_update_query = query_ext.main_query_ext.update_query;
+ query.unselect(value);
+ main_query.unselect(value);
+
+ // We need to inform about changes in these queries to the respective plugins
+ // Note: query & main_query have the same UUID
+ manifold.raise_query_event(query_uuid, event_type, value);
+ break;
}
+ // XXX We might need to run the new query again and manage the plugins in the meantime with spinners...
+ // For the time being, we will collect all columns during the first query
},
/* Publish/subscribe channels for internal use */
// Fields
/* Hide/unhide columns to match added/removed fields */
case FIELD_ADDED:
- var object = this;
- $.each(added_fields, function (index, field) {
- var index = object.getColIndex(field,cols);
- if(index != -1)
- object.table.fnSetColumnVis(index, true);
- });
break;
case FIELD_REMOVED:
- var object = this;
- $.each(removed_fields, function (index, field) {
- var index = object.getColIndex(field,cols);
- if(index != -1)
- object.table.fnSetColumnVis(index, false);
- });
break;
case CLEAR_FIELDS:
alert('GoogleMaps::clear_fields() not implemented');
# pass columns as the initial set of columns
# if None then this is taken from the query's fields
# latitude,longitude, zoom : the starting point
- def __init__ (self, query, query_all_uuid = None, latitude=43., longitude=7., zoom=4, **settings):
+ def __init__ (self, query, query_all = None, latitude=43., longitude=7., zoom=4, **settings):
Plugin.__init__ (self, **settings)
self.query=query
- self.query_all_uuid = query_all_uuid
+ self.query_all = query_all
+ self.query_all_uuid = query_all.query_uuid if query_all else None
self.latitude=latitude
self.longitude=longitude
self.zoom=zoom
<div id='main-{{ domid }}'>
-<table class='table table-striped table-bordered dataTable' id='hazelnut-{{ domid }}'>
-<thead><tr> {% for column in columns %}
-<th>{{ column }}</th>{% endfor %} {% if checkboxes %}<th>+/-</th>{% endif %}
-</tr></thead>
-<tbody>
-</tbody>
-<tfoot><tr> {% for column in columns %}
-<th>{{ column }}</th>{% endfor %} {% if checkboxes %}<th>+/-</th>{% endif %}
-</tr></tfoot>
-</table>
+ <table class='table table-striped table-bordered dataTable' id='hazelnut-{{ domid }}'>
+ <thead>
+ <tr>
+ {% for column in columns %}
+ <th>{{ column }}</th>
+ {% endfor %}
+ {% for column in hidden_columns %}
+ <th>{{ column }}</th>
+ {% endfor %}
+ {% if checkboxes %}
+ <th>+/-</th>
+ {% endif %}
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ <tfoot>
+ <tr>
+ {% for column in columns %}
+ <th>{{ column }}</th>
+ {% endfor %}
+ {% for column in hidden_columns %}
+ <th>{{ column }}</th>
+ {% endfor %}
+ {% if checkboxes %}
+ <th>+/-</th>
+ {% endif %}
+ </tr>
+ </tfoot>
+ </table>
</div>
<div class="hazelnut-spacer"></div>
var $this = $(this);
/* An object that will hold private variables and methods */
- var hazelnut = new Hazelnut (options);
- $this.data('Hazelnut', hazelnut);
+ var plugin = new Hazelnut (options);
+ $this.data('Hazelnut', plugin);
/* Events */
$this.on('show.Datatables', methods.show);
// This is the new plugin API meant to replace the weird publish_subscribe mechanism
- $this.set_query_handler(options.query_uuid, hazelnut.query_handler);
- $this.set_record_handler(options.query_uuid, hazelnut.record_handler);
- $this.set_record_handler(options.query_all_uuid, hazelnut.record_handler_all);
+ $this.set_query_handler(options.query_uuid, plugin.query_handler);
+ $this.set_record_handler(options.query_uuid, plugin.record_handler);
+ $this.set_record_handler(options.query_all_uuid, plugin.record_handler_all);
// /* Subscriptions */
// var query_channel = '/query/' + options.query_uuid + '/changed';
var object = this;
- /* Transforms the table into DataTable, and keep a pointer to it */
- actual_options = {
- // Customize the position of Datatables elements (length,filter,button,...)
- // we use a fluid row on top and another on the bottom, making sure we take 12 grid elt's each time
- sDom: "<'row-fluid'<'span5'l><'span1'r><'span6'f>>t<'row-fluid'<'span5'i><'span7'p>>",
- sPaginationType: 'bootstrap',
- // Handle the null values & the error : Datatables warning Requested unknown parameter
- // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2
- aoColumnDefs: [{sDefaultContent: '',aTargets: [ '_all' ]}],
- // WARNING: this one causes tables in a 'tabs' that are not exposed at the time this is run to show up empty
- // sScrollX: '100%', /* Horizontal scrolling */
- bProcessing: true, /* Loading */
- fnDrawCallback: function() { hazelnut_draw_callback.call(object, options); }
- };
- // the intention here is that options.datatables_options as coming from the python object take precedence
- // XXX DISABLED by jordan: was causing errors in datatables.js $.extend(actual_options, options.datatables_options );
- this.table = $('#hazelnut-' + options.plugin_uuid).dataTable(actual_options);
-
- /* Setup the SelectAll button in the dataTable header */
- /* xxx not sure this is still working */
- var oSelectAll = $('#datatableSelectAll-'+ options.plugin_uuid);
- oSelectAll.html("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>Select All");
- oSelectAll.button();
- oSelectAll.css('font-size','11px');
- oSelectAll.css('float','right');
- oSelectAll.css('margin-right','15px');
- oSelectAll.css('margin-bottom','5px');
- oSelectAll.unbind('click');
- oSelectAll.click(selectAll);
-
- /* Add a filtering function to the current table
- * Note: we use closure to get access to the 'options'
- */
- $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) {
- /* No filtering if the table does not match */
- if (oSettings.nTable.id != "hazelnut-" + options.plugin_uuid)
- return true;
- return hazelnut_filter.call(object, oSettings, aData, iDataIndex);
- });
+ this.initialize = function() {
+ /* Transforms the table into DataTable, and keep a pointer to it */
+ actual_options = {
+ // Customize the position of Datatables elements (length,filter,button,...)
+ // we use a fluid row on top and another on the bottom, making sure we take 12 grid elt's each time
+ sDom: "<'row-fluid'<'span5'l><'span1'r><'span6'f>>t<'row-fluid'<'span5'i><'span7'p>>",
+ sPaginationType: 'bootstrap',
+ // Handle the null values & the error : Datatables warning Requested unknown parameter
+ // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2
+ aoColumnDefs: [{sDefaultContent: '',aTargets: [ '_all' ]}],
+ // WARNING: this one causes tables in a 'tabs' that are not exposed at the time this is run to show up empty
+ // sScrollX: '100%', /* Horizontal scrolling */
+ bProcessing: true, /* Loading */
+ fnDrawCallback: function() { hazelnut_draw_callback.call(object, options); }
+ };
+ // the intention here is that options.datatables_options as coming from the python object take precedence
+ // XXX DISABLED by jordan: was causing errors in datatables.js $.extend(actual_options, options.datatables_options );
+ this.table = $('#hazelnut-' + options.plugin_uuid).dataTable(actual_options);
+
+ /* Setup the SelectAll button in the dataTable header */
+ /* xxx not sure this is still working */
+ var oSelectAll = $('#datatableSelectAll-'+ options.plugin_uuid);
+ oSelectAll.html("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>Select All");
+ oSelectAll.button();
+ oSelectAll.css('font-size','11px');
+ oSelectAll.css('float','right');
+ oSelectAll.css('margin-right','15px');
+ oSelectAll.css('margin-bottom','5px');
+ oSelectAll.unbind('click');
+ oSelectAll.click(selectAll);
+
+ /* Add a filtering function to the current table
+ * Note: we use closure to get access to the 'options'
+ */
+ $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) {
+ /* No filtering if the table does not match */
+ if (oSettings.nTable.id != "hazelnut-" + options.plugin_uuid)
+ return true;
+ return hazelnut_filter.call(object, oSettings, aData, iDataIndex);
+ });
+
+ /* Processing hidden_columns */
+ $.each(options.hidden_columns, function(i, field) {
+ object.hide_column(field);
+ });
+ }
/* methods */
}
};
- this.query_handler = function(e, event_type, query)
+ this.show_column = function(field)
+ {
+ var oSettings = object.table.fnSettings();
+ var cols = oSettings.aoColumns;
+ var index = object.getColIndex(field,cols);
+ if (index != -1)
+ object.table.fnSetColumnVis(index, true);
+ }
+
+ this.hide_column = function(field)
+ {
+ var oSettings = object.table.fnSettings();
+ var cols = oSettings.aoColumns;
+ var index = object.getColIndex(field,cols);
+ if (index != -1)
+ object.table.fnSetColumnVis(index, false);
+ }
+
+ this.query_handler = function(e, event_type, data)
{
// This replaces the complex set_query function
// The plugin does not need to remember the query anymore
// Fields
/* Hide/unhide columns to match added/removed fields */
case FIELD_ADDED:
- var object = this;
- $.each(added_fields, function (index, field) {
- var index = object.getColIndex(field,cols);
- if(index != -1)
- object.table.fnSetColumnVis(index, true);
- });
+ object.show_column(data);
break;
case FIELD_REMOVED:
- var object = this;
- $.each(removed_fields, function (index, field) {
- var index = object.getColIndex(field,cols);
- if(index != -1)
- object.table.fnSetColumnVis(index, false);
- });
+ object.hide_column(data);
break;
case CLEAR_FIELDS:
alert('Hazelnut::clear_fields() not implemented');
}
+
+ // Constructor
+ object.initialize();
+
} // constructor
/***************************************************************************
# set checkboxes if a final column with checkboxes is desired
# pass columns as the initial set of columns
# if None then this is taken from the query's fields
- def __init__ (self, query=None, query_all_uuid=None, checkboxes=False, columns=None, datatables_options={}, **settings):
+ def __init__ (self, query=None, query_all=None, checkboxes=False, columns=None, datatables_options={}, **settings):
Plugin.__init__ (self, **settings)
self.query = query
- self.query_all_uuid = query_all_uuid
+ # Until we have a proper way to access queries in Python
+ self.query_all = query_all
+ self.query_all_uuid = query_all.query_uuid if query_all else None
self.checkboxes=checkboxes
+ # XXX We need to have some hidden columns until we properly handle dynamic queries
if columns is not None:
self.columns=columns
+ self.hidden_columns = []
elif self.query:
- self.columns=self.query.fields
+ self.columns = self.query.fields
+ if query_all:
+ # We need a list because sets are not JSON-serilizable
+ self.hidden_columns = list(self.query_all.fields - self.query.fields)
+ else:
+ self.hidden_columns = []
else:
self.columns = []
+ self.hidden_columns = []
self.datatables_options=datatables_options
def template_file (self):
# the list of things passed to the js plugin
def json_settings_list (self):
- return ['plugin_uuid', 'domid', 'query_uuid', 'query_all_uuid', 'checkboxes','datatables_options']
+ return ['plugin_uuid', 'domid', 'query_uuid', 'query_all_uuid', 'checkboxes', 'datatables_options', 'hidden_columns']
return this.each(function() {
var $this = $(this);
- var hazelnut = $this.data('Manifold');
+ var plugin = $this.data('Manifold');
// Unbind all events using namespacing
$(window).unbind(PLUGIN_NAME);
// Remove associated data
- hazelnut.remove();
+ plugin.remove();
$this.removeData('Manifold');
});
}, // destroy
fields = []
metadata = self.page.get_metadata()
md_fields = metadata.details_by_object('resource')
- print "METADATA FIELDS", md_fields
# XXX use django templating system here
for md_field in md_fields['column']:
'resource_type': 'N/A',
'filter_input': filter_input,
'header': None,
+ 'checked': md_field['name'] in self.query.get_select()
})
return { 'fields': fields }
<td class='center'> {{ field.type }}</td>
<td class='center'> {{ field.filter_input }}</td>
<td class='center'>
- <input class='queryeditor-check' id='check_{{ field.name }}' name='{{ field.header }}' type='checkbox' autocomplete='off' value='{{ field.header}}'></input>
+ <input class='queryeditor-check' id='check_{{ field.name }}' name='{{ field.header }}' type='checkbox' autocomplete='off' value='{{ field.name }}' {% if field.checked %} checked {% endif %}></input>
</td>
</tr>
var plugin = new QueryEditor(options);
$this.data('Manifold', plugin);
+ $this.set_query_handler(options.query_uuid, plugin.query_handler);
+ $this.set_record_handler(options.query_uuid, plugin.record_handler);
+
}); // this.each
}, // init
});
jQuery('.queryeditor-check').click(function() {
- manifold.raise_event(object.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
+ manifold.raise_event(object.options.query_uuid, this.checked?FIELD_ADDED:FIELD_REMOVED, this.value);
/*
var column = this.id.substring(6);
query = data.current_query;
// elements in set
switch(event_type) {
case NEW_RECORD:
- /* NOTE in fact we are doing a join here */
- if (object.received_all)
- // update checkbox for record
- object.set_checkbox(record);
- else
- // store for later update of checkboxes
- object.in_set_buffer.push(record);
break;
case CLEAR_RECORDS:
- // nothing to do here
break;
case IN_PROGRESS:
- manifold.spin($(this));
break;
case DONE:
- if (object.received_all)
- manifold.spin($(this), false);
- object.received_set = true;
break;
}
};
// Fields
/* Hide/unhide columns to match added/removed fields */
+ // XXX WRONG IDENTIFIERS
case FIELD_ADDED:
- $('#check_' + data).attr('checked', true);
+ object.check(data);
break;
case FIELD_REMOVED:
- $('#check_' + data).attr('checked', false);
+ object.uncheck(data);
break;
case CLEAR_FIELDS:
alert(PLUGIN_NAME + '::clear_fields() not implemented');
} // switch
+ }
+ this.check = function(field)
+ {
+ $('#check_' + field).attr('checked', true);
+ }
+ this.uncheck = function(field)
+ {
+ $('#check_' + field).attr('checked', false);
}
this.fnFormatDetails = function( metaTable, nTr, div_id ) {
var aData = metaTable.fnGetData( nTr );
*/
(function( $ ){
+ var PLUGIN_NAME = 'ResourcesSelected';
+
// Routing calls
jQuery.fn.ResourcesSelected = function( method ) {
if ( methods[method] ) {
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
- jQuery.error( 'Method ' + method + ' does not exist on jQuery.ResourcesSelected' );
+ jQuery.error( 'Method ' + method + ' does not exist on jQuery.' + PLUGIN_NAME );
}
};
var $this = $(this);
/* An object that will hold private variables and methods */
- var s = new ResourcesSelected(options);
- $(this).data('ResourcesSelected', s);
- var RESULTS_RESOURCES = '/results/' + options.resource_query_uuid + '/changed';
- var UPDATE_RESOURCES = '/update-set/' + options.resource_query_uuid;
-
- $.subscribe(RESULTS_RESOURCES, function(e, resources) { s.set_resources(resources); });
- $.subscribe(UPDATE_RESOURCES, function(e, resources, change) { s.update_resources(resources, change); });
+ var plugin = new ResourcesSelected(options);
+ $(this).data('Manifold', plugin);
+
+ //$this.set_query_handler(options.query_uuid, hazelnut.query_handler);
+ //$this.set_record_handler(options.query_uuid, hazelnut.record_handler);
+
+ //var RESULTS_RESOURCES = '/results/' + options.resource_query_uuid + '/changed';
+ //var UPDATE_RESOURCES = '/update-set/' + options.resource_query_uuid;
+ //$.subscribe(RESULTS_RESOURCES, function(e, resources) { s.set_resources(resources); });
+ //$.subscribe(UPDATE_RESOURCES, function(e, resources, change) { s.update_resources(resources, change); });
}); // this.each
}, // init
*/
destroy : function( ) {
- return this.each(function(){
- var $this = jQuery(this), data = $this.data('ResourcesSelected');
- jQuery(window).unbind('ResourcesSelected');
- data.ResourcesSelected.remove();
- $this.removeData('ResourcesSelected');
- })
+ return this.each(function() {
+ var $this = $(this);
+ var plugin = $this.data('Manifold');
+ // Remove associated data
+ plugin.remove();
+ $this.removeData('Manifold');
+ });
}, // destroy
}; // var methods
/***************************************************************************
- * ResourcesSelected object
+ * Plugin object
***************************************************************************/
function ResourcesSelected(options)
this.options = options;
+ var object = this;
+
/* The resources that are in the slice */
this.current_resources = null;
jQuery('.ResourceSelectedClose').unbind('click');
/* Handle clicks on close span */
/* Reassociate close click every time the table is redrawn */
- $('.ResourceSelectedClose').bind('click',{instance: rs}, close_click);
+ $('.ResourceSelectedClose').bind('click',{instance: rs}, object.close_click);
}
});
} // update_resources
+ this.record_handler = function(e, event_type, record)
+ {
+ // elements in set
+ switch(event_type) {
+ case NEW_RECORD:
+ /* NOTE in fact we are doing a join here */
+ if (object.received_all)
+ // update checkbox for record
+ object.set_checkbox(record);
+ else
+ // store for later update of checkboxes
+ object.in_set_buffer.push(record);
+ break;
+ case CLEAR_RECORDS:
+ // nothing to do here
+ break;
+ case IN_PROGRESS:
+ manifold.spin($(this));
+ break;
+ case DONE:
+ if (object.received_all)
+ manifold.spin($(this), false);
+ object.received_set = true;
+ break;
+ }
+ };
+
+ this.record_handler_all = function(e, event_type, record)
+ {
+ // all elements
+ switch(event_type) {
+ case NEW_RECORD:
+ // Add the record to the table
+ object.new_record(record);
+ break;
+ case CLEAR_RECORDS:
+ object.table.fnClearTable();
+ break;
+ case IN_PROGRESS:
+ manifold.spin($(this));
+ break;
+ case DONE:
+ if (object.received_set) {
+ /* XXX needed ? XXX We uncheck all checkboxes ... */
+ $("[id^='datatables-checkbox-" + object.options.plugin_uuid +"']").attr('checked', false);
+
+ /* ... and check the ones specified in the resource list */
+ $.each(object.in_set_buffer, function(i, record) {
+ object.set_checkbox(record);
+ });
+
+ manifold.spin($(this), false);
+ }
+ object.received_all = true;
+ break;
+ }
+ };
+
+ this.query_handler = function(e, event_type, data)
+ {
+ // This replaces the complex set_query function
+ // The plugin does not need to remember the query anymore
+ switch(event_type) {
+ // Filters
+ case FILTER_ADDED:
+ case FILTER_REMOVED:
+ case CLEAR_FILTERS:
+ // XXX Here we might need to maintain the list of filters !
+ /* Process updates in filters / current_query must be updated before this call for filtering ! */
+ object.table.fnDraw();
+ break;
+
+ // Fields
+ /* Hide/unhide columns to match added/removed fields */
+ case FIELD_ADDED:
+ var field = data;
+ var oSettings = object.table.fnSettings();
+ var cols = oSettings.aoColumns;
+ var index = object.getColIndex(field,cols);
+ if(index != -1)
+ object.table.fnSetColumnVis(index, true);
+ break;
+ case FIELD_REMOVED:
+ var field = data;
+ var oSettings = object.table.fnSettings();
+ var cols = oSettings.aoColumns;
+ var index = object.getColIndex(field,cols);
+ if(index != -1)
+ object.table.fnSetColumnVis(index, false);
+ break;
+ case CLEAR_FIELDS:
+ alert('Hazelnut::clear_fields() not implemented');
+ break;
+ } // switch
+ }
+
} // ResourcesSelected
)
tab_resource_plugins.insert(Hazelnut(
- page = page,
- title = 'List',
- domid = 'checkboxes',
+ page = page,
+ title = 'List',
+ domid = 'checkboxes',
# this is the query at the core of the slice list
- query = sq_resource,
- query_all_uuid = query_resource_all.query_uuid,
- checkboxes = True,
+ query = sq_resource,
+ query_all = query_resource_all,
+ checkboxes = True,
datatables_options = {
# for now we turn off sorting on the checkboxes columns this way
# this of course should be automatic in hazelnut
))
tab_resource_plugins.insert(GoogleMap(
- page = page,
- title = 'Geographic view',
- domid = 'gmap',
+ page = page,
+ title = 'Geographic view',
+ domid = 'gmap',
# tab's sons preferably turn this off
- togglable = False,
- query = sq_resource,
- query_all_uuid = query_resource_all.query_uuid,
- checkboxes = True,
+ togglable = False,
+ query = sq_resource,
+ query_all = query_resource_all,
+ checkboxes = True,
# center on Paris
- latitude = 49.,
- longitude = 2.2,
- zoom = 3,
+ latitude = 49.,
+ longitude = 2.2,
+ zoom = 3,
))
stack_resources.insert(tab_resource_plugins)