From: Loic Baron Date: Fri, 28 Mar 2014 18:37:27 +0000 (+0100) Subject: ColumnsEditor: based on query_editor plugin, simplified, without autocomplete filters X-Git-Tag: myslice-1.1~161^2~5 X-Git-Url: http://git.onelab.eu/?a=commitdiff_plain;h=1ddfdc56b0611c9af2e2659084f0a98328783a67;hp=97bbf7ec1d09ef9837b9b2471fdfa8bf8bd4d959;p=myslice.git ColumnsEditor: based on query_editor plugin, simplified, without autocomplete filters --- diff --git a/plugins/columns_editor/__init__.py b/plugins/columns_editor/__init__.py new file mode 100644 index 00000000..3cc48a9d --- /dev/null +++ b/plugins/columns_editor/__init__.py @@ -0,0 +1,91 @@ +from unfold.plugin import Plugin + +from django.template.loader import render_to_string + +# XXX We need naming helpers in the python Plugin class also, used in template + +class ColumnsEditor(Plugin): + def __init__ (self, query, query_all = None, **settings): + Plugin.__init__ (self, **settings) + self.query=query + self.query_uuid = query.query_uuid + self.query_all = query_all + self.query_all_uuid = query_all.query_uuid if query_all else None + + def template_file(self): + return "columns_editor.html" + + def requirements (self): + reqs = { + 'js_files' : [ + # XXX datatables + 'js/columns_editor.js', + ] , + 'css_files': [ + 'css/query_editor.css', + 'css/jquery-ui.css', + ] + } + return reqs + + def export_json_settings (self): + return True + + def template_env(self, request): + fields = [] + #hidden_columns = self.hidden_columns + metadata = self.page.get_metadata() + md_fields = metadata.details_by_object('resource') + + # XXX use django templating system here + for md_field in md_fields['column']: + if md_field['type'] == 'string': + if 'allowed_values' in md_field: + allowed_values = md_field['allowed_values'].split(',') + + options = [] + for v in allowed_values: + v_desc = v.split('-') + options.append(v_desc[0]) + + env = { + 'domid': self.domid, + 'options': options + } + filter_input = render_to_string('filter_input_string_values.html', env) + else: + env = { + 'domid': self.domid, + 'field': md_field['name'] + } + filter_input = render_to_string('filter_input_string.html', env) + + elif md_field['type'] == 'int': + allowed_values = md_field.get('allowed_values', '0,0').split(',') + env = { + 'domid': self.domid, + 'field': md_field['name'], + 'min' : allowed_values[0], + 'max' : allowed_values[1] + } + filter_input = render_to_string('filter_input_integer.html', env) + else: + env = { + 'domid': self.domid, + 'field': md_field['name'] + } + filter_input = render_to_string('filter_input_others.html', env) + + fields.append({ + 'name': md_field['name'], + 'type': md_field['type'], + 'resource_type': 'N/A', + 'filter_input': filter_input, + 'header': None, + 'checked': md_field['name'] in self.query.get_select() + }) + #return { 'fields': fields, 'hidden_columns': hidden_columns } + #return { 'fields': fields , 'query_uuid': self.query_uuid, 'query_all_uuid': self.query_all_uuid } + return { 'fields': fields } + + def json_settings_list (self): return ['plugin_uuid', 'domid', 'query_uuid', 'query_all_uuid', ] diff --git a/plugins/columns_editor/static/css/query_editor.css b/plugins/columns_editor/static/css/query_editor.css new file mode 100644 index 00000000..4061d0f8 --- /dev/null +++ b/plugins/columns_editor/static/css/query_editor.css @@ -0,0 +1,360 @@ +div.query-editor-spacer { + padding: 15px 5px; +} + +table.query-editor { + width: 95%; + padding-bottom: 10px; +} + +/* Add a scrollbar to autocomplete fields */ +.ui-autocomplete { + max-height: 100px; + overflow-y: auto; + /* prevent horizontal scrollbar */ + overflow-x: hidden; + /* add padding to account for vertical scrollbar */ + padding-right: 20px; + + /* NEED TO BE IMPROVED LATER... */ + /* How to use properties from content class in /templates/myslice/css/myslice.css ? */ + /* How to factorize this ? Maybe applied differently in other plugins ? */ + font-size: 11px; +} +.queryeditor-auto-filter{ + width:200px; +} +/* IE 6 doesn't support max-height + * we use height instead, but this forces the menu to always be this tall + */ +* html .ui-autocomplete { + height: 100px; +} + +table.query-editor { + margin: 0 auto; + clear: both; + /* width: 80%;*/ + width: 300px; +} + +table.query-editor input { + font: normal 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; +} + +table.query-editor thead th { + padding: 3px 18px 3px 3px; + border-bottom: 1px solid black; + font-weight: bold; + cursor: pointer; + * cursor: hand; +} + +table.query-editor tfoot th { + padding: 3px 18px 3px 10px; + border-top: 1px solid black; + font-weight: bold; +} + +table.query-editor td { + padding: 2px 5px; + font: normal 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; +} + +table.query-editor td.center, table.query-editor th.center { + text-align: center; +} + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * DataTables row classes + */ +table.query-editor tr.odd.gradeA { + background-color: #ddffdd; +} + +table.query-editor tr.even.gradeA { + background-color: #eeffee; +} + +table.query-editor tr.odd.gradeC { + background-color: #ddddff; +} + +table.query-editor tr.even.gradeC { + background-color: #eeeeff; +} + +table.query-editor tr.odd.gradeX { + background-color: #ffdddd; +} + +table.query-editor tr.even.gradeX { + background-color: #ffeeee; +} + +table.query-editor tr.odd.gradeU { + background-color: #ddd; +} + +table.query-editor tr.even.gradeU { + background-color: #eee; +} + +/* change color: T / even -> odd +1 +5 -3*/ +table.query-editor tr.odd.row_sliver td { + background-color: #9FAFD1; +} + +table.query-editor tr.even.row_added td { + background-color: #b1d19f; +} +table.query-editor tr.odd.row_added td { + background-color: #a3c98f; +} + +table.query-editor tr.even.row_removed td { + background-color: #d9b0b0; +} + +table.query-editor tr.odd.row_removed td { + background-color: #d1a09f; +} + +table.query-editor tr.gradeA { + background-color: #eeffee; +} + +table.query-editor tr.gradeC { + background-color: #ddddff; +} + +table.query-editor tr.gradeX { + background-color: #ffdddd; +} + +table.query-editor tr.gradeU { + background-color: #ddd; +} + + + + +div.selected{background-color:gray; color:black} + +/* icons */ +.myslice-icon-timestamp { + background-image: url('images/myslice-icon-timestamp.png') !important; +} +.myslice-icon-filter { + background-image: url('images/myslice-icon-filter.png') !important; +} +.myslice-icon-fields { + background-image: url('images/myslice-icon-fields.png') !important; +} +.myslice-icon-groups { + background-image: url('images/myslice-icon-groups.png') !important; +} +.myslice-icon-summary { + background-image: url('images/myslice-icon-summary.png') !important; +} +.myslice-icon-resources { + background-image: url('images/myslice-icon-resources.png') !important; +} +.myslice-icon-users { + background-image: url('images/myslice-icon-users.png') !important; +} + +a.source-url{ + font-weight: bold; +} + +span.bold { + font-weight: bold; +} + +div#selectdescr { + padding-top:2em; + color: #555555; +} + +span.short { + height:10px; +} + +span.column-title { + font-size: 15px; + font-weight: bold; +} + +span.column-detail { + font-size: 11px; + font-style: italic; +} + +span.group_info { + font-size: 11px; + color: green; + font-weight: bold; +} + +span.filter_info { + color: red; + font-weight: bold; +} + + +/* column configuation style */ + +OPTION.out{ + background-color:white; + color:black; +} +OPTION.in{ + background-color:#CAE8EA; + color:#4f6b72; +} + +/* jordan disabled + div.out{background-color:white; color:black} + div.in{background-color:#CAE8EA; color:#4f6b72} + div.selected{background-color:gray; color:black} + div.invisible{display:none} + */ + +div.note-div { + padding: 4px; + background-color: #cae8ea; + width: 800px; + margin-left:auto; + margin-right:auto; +} + +div#scrolldiv_old { + border : solid 2px grey; + padding:4px; + width:300px; + height:180px; + overflow:auto; +} + +th,td.top { + vertical-align: top; + text-align: left; + padding:10px; +} + +tr.hidden { + display:none; +} + +td.smallright { + text-align: right; + width:20px; +} + +table.center { + margin-left:auto; + margin-right:auto; +} + +table.columnlist { + width:270px; +} + +table.query-editor td.header { + background-color: #CAE8EA; + text-align: center; + width:30px; +} + +span.header { + font-weight: bold; + color: #3399CC; +} + +a.source-url{ + font-weight: bold; +} + +span.menubig { + font-size: 16px; + font-weight: bold; +} + +span.menusmall { + font-size: 14px; + font-weight: bold; +} + +span.menuright { + font-weight: bold; + float: right; +} + +span.simpleright { + float: right; +} + +span.gray{ + color: #555555; +} + +span.short { + height:10px; +} + +span.column-title { + font-size: 13px; + font-weight: bold; +} + +span.column-detail { + font-size: 11px; + font-style: italic; +} + +span.myslice_small { + font-size: 11px; +} + +span#username { + font-weight: bold; + font-size: 1.3em; +} + +.filter_popup{ + position:relative; /*this is the key*/ + float: right; + z-index:24; + background: url('images/myslice-icon-filter.png') no-repeat; + /*background-color:#ccc;*/ + width: 200px; + height: 300px; + color:#000; + text-decoration:none; + clear: both; +} + +.filter_popup:hover{ + z-index:25; +/*background-color:#ff0*/ +} + +.filter_popup span{ + display: none; +} + +.filter_popup:hover span{ /*the span will display just on :hover state*/ + display:block; + position:absolute; + /*top:1em;*/ + left:-19em; + width: 20em; + font-size: 8pt; + border:1px solid #ccdddd; + background-color:#ddeeee; + color:#000; + text-align: left; + padding: 0em 0em 0em 1em; +} diff --git a/plugins/columns_editor/static/img/details_close.png b/plugins/columns_editor/static/img/details_close.png new file mode 100644 index 00000000..fcc23c63 Binary files /dev/null and b/plugins/columns_editor/static/img/details_close.png differ diff --git a/plugins/columns_editor/static/img/myslice-icon-filter.png b/plugins/columns_editor/static/img/myslice-icon-filter.png new file mode 100644 index 00000000..b2f4ec7b Binary files /dev/null and b/plugins/columns_editor/static/img/myslice-icon-filter.png differ diff --git a/plugins/columns_editor/static/js/columns_editor.js b/plugins/columns_editor/static/js/columns_editor.js new file mode 100644 index 00000000..4ab9b7d7 --- /dev/null +++ b/plugins/columns_editor/static/js/columns_editor.js @@ -0,0 +1,352 @@ +/** + * Description: ColumnsEditor plugin + * Copyright (c) 2012-2013 UPMC Sorbonne Universite + * License: GPLv3 + */ + +// XXX TODO This plugin will be interested in changes in metadata +// What if we remove a filter, is it removed in the right min/max field ??? +// -> no on_filter_removed is not yet implemented +// XXX if a plugin has not declared a handler, it might become inconsistent, +// and the interface should either reset or disable it +(function($){ + + var ColumnsEditor = Plugin.extend({ + + event_filter_added: function(op, suffix) { + suffix = (typeof suffix === 'undefined') ? '' : manifold.separator + suffix; + var self = this; + return function(e, ui) { + var array = self.array_from_id(e.target.id); + var key = self.field_from_id(array); // No need to remove suffix... + + // using autocomplete ui + if(typeof(ui) != "undefined"){ + var value = ui.item.value; + }else{ + var value = e.target.value; + } + + if (value) { + // XXX This should be handled by manifold + //manifold.raise_event(object.options.query_uuid, FILTER_UPDATED, [key, op, value]); + manifold.raise_event(self.options.query_uuid, FILTER_ADDED, [key, op, value]); + } else { + // XXX This should be handled by manifold + manifold.raise_event(self.options.query_uuid, FILTER_REMOVED, [key, op]); + } + }; + }, + + init: function(options, element) { + this._super(options, element); + this.listen_query(options.query_uuid); + // this one is the complete list of resources + // and will be bound to callbacks like on_all_new_record + this.listen_query(options.query_all_uuid, 'all'); + + + this.elts('queryeditor-auto-filter').change(this.event_filter_added('=')); + this.elts('queryeditor-filter').change(this.event_filter_added('=')); + this.elts('queryeditor-filter-min').change(this.event_filter_added('>')); + this.elts('queryeditor-filter-max').change(this.event_filter_added('<')); + + var self = this; + this.elts('queryeditor-check').click(function() { + manifold.raise_event(self.options.query_uuid, this.checked?FIELD_ADDED:FIELD_REMOVED, this.value); + }); + + /* The following code adds an expandable column for the table + // XXX Why isn't it done statically ? + var nCloneTh = document.createElement( 'th' ); + var nCloneTd = document.createElement( 'td' ); + nCloneTd.innerHTML = ""; + //nCloneTd.innerHTML = ''; + nCloneTh.innerHTML = 'Info'; + nCloneTd.className = "center"; + nCloneTh.className = "center"; + this.elmt('table thead tr').each(function() { + this.insertBefore(nCloneTh, this.childNodes[0]); + }); + this.elmt('table tbody tr').each(function() { + this.insertBefore(nCloneTd.cloneNode( true ), this.childNodes[0]); + }); + */ + + // We are currently using a DataTable display, but another browsing component could be better + //jQuery('#'+this.options.plugin_uuid+'-table').dataTable... + /* + var metaTable = this.elmt('table').dataTable({ +// Thierry : I'm turning off all the dataTables options for now, so that +// the table displays more properly again, might need more tuning though +// bFilter : false, +// bPaginate : false, +// bInfo : false, +// sScrollX : '100%', // Horizontal scrolling +// sScrollY : '200px', +// //bJQueryUI : true, // Use jQuery UI +// bProcessing : true, // Loading +// aaSorting : [[ 1, "asc" ]], // sort by column fields on load +// aoColumnDefs: [ +// { 'bSortable': false, 'aTargets': [ 0 ]}, +// { 'sWidth': '8px', 'aTargets': [ 0 ] }, +// { 'sWidth': '8px', 'aTargets': [ 4 ] } // XXX NB OF COLS +// ] + }); + this.table = metaTable; + */ + this.table = this.elmt('table'); + + // Actions on the newly added fields + this.elmt('table tbody td span').on('click', function() { + var nTr = this.parentNode.parentNode; + // use jQuery UI instead of images to keep a common UI + // class="glyphicon glyphicon-chevron-down treeclick tree-minus" + // East oriented Triangle class="glyphicon-chevron-right" + // South oriented Triangle class="glyphicon-chevron-down" + + if (this.hasClass("glyphicon-chevron-right")) { + this.removeClass("glyphicon-chevron-right").addClass("glyphicon-chevron-down"); + // XXX ?????? + metaTable.fnOpen(nTr, this.fnFormatDetails(metaTable, nTr, self.options.plugin_uuid+'_div'), 'details' ); + } else { + this.removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-right"); + metaTable.fnClose(nTr); + } + }); + + this.elmt('table_wrapper').css({ + 'padding-top' : '0em', + 'padding-bottom': '0em' + }); + + // autocomplete list of tags + this.availableTags = {}; + + }, // init + + /* UI management */ + + check_field: function(field) + { + this.elmt('check', field).attr('checked', true); + }, + + uncheck_field: function(field) + { + this.elmt('check', field).attr('checked', false); + }, + + update_filter_value: function(filter, removed) + { + removed = !(typeof removed === 'undefined'); // default = False + + var key = filter[0]; + var op = filter[1]; + var value = filter[2]; + + var id = this.id_from_field(key); + + if (op == '=') { + var element = this.elmt(id); + } else { + var suffix; + if (op == '<') { + this.elmt(id, 'max').val(value); + } else if (op == '>') { + this.elmt(id, 'min').val(value); + } else { + return; + } + var element = this.elmt(id, suffix); + } + + element.val(removed?null:value); + + }, + + /* Events */ + + on_filter_added: function(filter) + { + this.update_filter_value(filter); + }, + + on_filter_removed: function(filter) + { + this.update_filter_value(filter, true); + }, + + on_field_added: function(field) + { + this.check_field(field); + }, + + on_field_removed: function(field) + { + this.uncheck_field(field); + }, + + /* RECORD HANDLERS */ + on_query_done: function() + { + //console.log("Query_Editor: query_done!"); + //console.log(this.availableTags); + }, + /* Autocomplete based on query_all to get all the fields, where query get only the fields selected */ + on_all_new_record: function(record) + { + /* + availableTags = this.availableTags; + jQuery.each(record,function(key,value){ + value = unfold.get_value(value); + if(!availableTags.hasOwnProperty(key)){availableTags[key]=new Array();} + //availableTags[key].push(value); + var currentArray = availableTags[key]; + if(value!=null){ + if(jQuery.inArray(value,currentArray)==-1){availableTags[key].push(value);} + } + }); + this.availableTags = availableTags; + this.update_autocomplete(availableTags); + */ + }, + + /* Former code not used at the moment */ + + print_field_description: function(field_header, div_id) + { + //var selected = all_headers[field_header]; + var selected = getMetadata_field('resource',field_header); + + field_header = div_id+"_"+field_header; + + var output = "
"; + + output += "
"; + output += '

'+selected['title']+'

'; + output += '

'+selected['description']+'

'; + + var period_select = ""; + + if (selected['value_type'] == 'string') { + + var values_select = "

"; + } + else + output+='

Unit: '+selected['unit']; + + output+= '

'; + + output += '

Source: '+selected['platform']+''; + + //if (selected['via'] != '') + //output += ' via '+selected['via']+''; + + output += '

'; + output += "
"; + + /* + output += "
"; + output += "

Group resources with the same value "; + output += "

Select aggregator : "; + output += "

"; + output += "
"; + output += "

Select timestamp : "; + output += period_select; + output += "

"; + */ + output += "
"; + + return output; + }, + + update_autocomplete: function(availableTags) + { + var self = this; + var domid = this.options.plugin_uuid; + + jQuery.each(availableTags, function(key, value){ + value.sort(); + // using dataTables's $ to search also in nodes that are not currently displayed + var element = self.table.$("#"+domid+"__field__"+key); + + element.autocomplete({ + source: value, + selectFirst: true, + minLength: 0, // allows to browse items with no value typed in + select: self.event_filter_added('=') + }); + }); + }, // update_autocomplete + +/* + update_autocomplete: function(e, rows, current_query) + { + var d = data; + d.current_query = current_query; + var availableTags={}; + jQuery.each (rows, function(index, obj) { + jQuery.each(obj,function(key,value){ + value = unfold.get_value(value); + if(!availableTags.hasOwnProperty(key)){availableTags[key]=new Array();} + //availableTags[key].push(value); + var currentArray=availableTags[key]; + if(value!=null){ + if(jQuery.inArray(value,currentArray)==-1){availableTags[key].push(value);} + } + }); + }); + jQuery.each(availableTags, function(key, value){ + value.sort(); + jQuery("#"+options.plugin_uuid+"-filter-"+key).autocomplete({ + source: value, + selectFirst: true, + minLength: 0, // allows to browse items with no value typed in + select: function(event, ui) { + var key=getKeySplitId(this.id,"-"); + var op='='; + var val=ui.item.value; + + query=d.current_query; + query.update_filter(key,op,val); + // Publish the query changed, the other plugins with subscribe will get the changes + jQuery.publish('/query/' + query.uuid + '/changed', query); + //add_ActiveFilter(this.id,'=',ui.item.value,d); + } + }); + }); + }, // update_autocomplete +*/ + fnFormatDetails: function( metaTable, nTr, div_id ) + { + var aData = metaTable.fnGetData( nTr ); + var sOut = '
'; + //sOut += prepare_tab_description(aData[1].substr(21, aData[1].length-21-7), div_id); + sOut += this.print_field_description(aData[1].substring(3, aData[1].length-4), div_id); + sOut += '
'; + + return sOut; + } + }); + + $.plugin('ColumnsEditor', ColumnsEditor); + +})(jQuery); diff --git a/plugins/columns_editor/templates/columns_editor.html b/plugins/columns_editor/templates/columns_editor.html new file mode 100644 index 00000000..1d66e982 --- /dev/null +++ b/plugins/columns_editor/templates/columns_editor.html @@ -0,0 +1,45 @@ + +
+ + + + + + + + + + + + {# Loop through metadata and display related information #} + {% for field in fields %} + + + + + + + + + {% endfor %} + + +
InfoField+/-
{{ field.name }} + +
+
diff --git a/plugins/columns_editor/templates/filter_input_integer.html b/plugins/columns_editor/templates/filter_input_integer.html new file mode 100644 index 00000000..bdbacb69 --- /dev/null +++ b/plugins/columns_editor/templates/filter_input_integer.html @@ -0,0 +1,5 @@ +
+ + - + +
diff --git a/plugins/columns_editor/templates/filter_input_others.html b/plugins/columns_editor/templates/filter_input_others.html new file mode 100644 index 00000000..14e60335 --- /dev/null +++ b/plugins/columns_editor/templates/filter_input_others.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/plugins/columns_editor/templates/filter_input_string.html b/plugins/columns_editor/templates/filter_input_string.html new file mode 100644 index 00000000..110f16d8 --- /dev/null +++ b/plugins/columns_editor/templates/filter_input_string.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/plugins/columns_editor/templates/filter_input_string_values.html b/plugins/columns_editor/templates/filter_input_string_values.html new file mode 100644 index 00000000..6a8aecaa --- /dev/null +++ b/plugins/columns_editor/templates/filter_input_string_values.html @@ -0,0 +1,6 @@ + diff --git a/portal/sliceresourceview.py b/portal/sliceresourceview.py index 7d4ab089..254e2fde 100644 --- a/portal/sliceresourceview.py +++ b/portal/sliceresourceview.py @@ -18,6 +18,7 @@ from plugins.googlemap import GoogleMap from plugins.queryupdater import QueryUpdater from plugins.testbeds import TestbedsPlugin from plugins.scheduler2 import Scheduler2 +from plugins.columns_editor import ColumnsEditor from myslice.theme import ThemeView @@ -70,10 +71,8 @@ class SliceResourceView (LoginRequiredView, ThemeView): query_all_lease = Query.get('lease').select(lease_fields) page.enqueue_query(query_all_lease) - print "!!!!!!!!!! query leases = ",query_all_lease - # -------------------------------------------------------------------------- - # RESOURCES LIST + # ALL RESOURCES LIST # resources as a list using datatable plugin list_resources = QueryTable( @@ -91,6 +90,39 @@ class SliceResourceView (LoginRequiredView, ThemeView): }, ) + + # -------------------------------------------------------------------------- + # RESERVED RESOURCES LIST + # resources as a list using datatable plugin + + list_reserved_resources = QueryTable( + page = page, + domid = 'resources-reserved-list', + title = 'List view', + query = sq_resource, + query_all = sq_resource, + init_key = "urn", + checkboxes = True, + datatables_options = { + 'iDisplayLength': 25, + 'bLengthChange' : True, + 'bAutoWidth' : True, + }, + ) + + # -------------------------------------------------------------------------- + # COLUMNS EDITOR + # list of fields to be applied on the query + # this will add/remove columns in QueryTable plugin + + filter_column_editor = ColumnsEditor( + page = page, + query = sq_resource, + query_all = query_resource_all, + title = "Select Columns", + domid = 'select-columns', + ) + # -------------------------------------------------------------------------- # RESOURCES MAP # the resources part is made of a Tabs (Geographic, List), @@ -170,6 +202,10 @@ class SliceResourceView (LoginRequiredView, ThemeView): template_env = {} template_env['list_resources'] = list_resources.render(self.request) + template_env['list_reserved_resources'] = list_reserved_resources.render(self.request) + + template_env['columns_editor'] = filter_column_editor.render(self.request) + template_env['filter_testbeds'] = filter_testbeds.render(self.request) template_env['map_resources'] = map_resources.render(self.request) template_env['scheduler'] = resources_as_scheduler2.render(self.request) diff --git a/portal/templates/slice-resource-view.html b/portal/templates/slice-resource-view.html index 11a20179..89849ff1 100644 --- a/portal/templates/slice-resource-view.html +++ b/portal/templates/slice-resource-view.html @@ -17,7 +17,7 @@
Resources All - Reserved + Reserved Pending
@@ -52,11 +52,17 @@
  • Scheduler
  • +
    {{list_resources}}
    +