1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-03 01:17:56 +03:00

Added fast navigation to administration interface

This commit is contained in:
Adolfo Gómez García 2016-04-20 11:37:04 +02:00
parent f7fa92e6c1
commit 203e2fcdd0
12 changed files with 158 additions and 33 deletions

View File

@ -77,7 +77,7 @@ class ServicesPools(ModelHandler):
{'parent': {'title': _('Parent Service')}}, {'parent': {'title': _('Parent Service')}},
{'state': {'title': _('status'), 'type': 'dict', 'dict': State.dictionary()}}, {'state': {'title': _('status'), 'type': 'dict', 'dict': State.dictionary()}},
{'show_transports': {'title': _('Shows transports'), 'type': 'callback'}}, {'show_transports': {'title': _('Shows transports'), 'type': 'callback'}},
{'servicesPoolGroup': {'title': _('Pool Group')}}, {'pool_group_name': {'title': _('Pool Group')}},
{'tags': {'title': _('tags'), 'visible': False}}, {'tags': {'title': _('tags'), 'visible': False}},
] ]
# Field from where to get "class" and prefix for that class, so this will generate "row-state-A, row-state-X, .... # Field from where to get "class" and prefix for that class, so this will generate "row-state-A, row-state-X, ....
@ -89,6 +89,14 @@ class ServicesPools(ModelHandler):
def item_as_dict(self, item): def item_as_dict(self, item):
# if item does not have an associated service, hide it (the case, for example, for a removed service) # if item does not have an associated service, hide it (the case, for example, for a removed service)
# Access from dict will raise an exception, and item will be skipped # Access from dict will raise an exception, and item will be skipped
poolGroupId = None
poolGroupName = _('Default')
poolGroupThumb = DEFAULT_THUMB_BASE64
if item.servicesPoolGroup is not None:
poolGroupId = item.servicesPoolGroup.uuid
poolGroupName = item.servicesPoolGroup.name
if item.servicesPoolGroup.image is not None:
poolGroupThumb = item.servicesPoolGroup.image.thumb64
val = { val = {
'id': item.uuid, 'id': item.uuid,
'name': item.name, 'name': item.name,
@ -101,8 +109,9 @@ class ServicesPools(ModelHandler):
'service_id': item.service.uuid, 'service_id': item.service.uuid,
'provider_id': item.service.provider.uuid, 'provider_id': item.service.provider.uuid,
'image_id': item.image.uuid if item.image is not None else None, 'image_id': item.image.uuid if item.image is not None else None,
'servicesPoolGroup_id': item.servicesPoolGroup.uuid if item.servicesPoolGroup is not None else None, 'pool_group_id': poolGroupId,
'servicesPoolGroup': item.servicesPoolGroup.name if item.servicesPoolGroup is not None else _('Default'), 'pool_group_name': poolGroupName,
'pool_group_thumb': poolGroupThumb,
'initial_srvs': item.initial_srvs, 'initial_srvs': item.initial_srvs,
'cache_l1_srvs': item.cache_l1_srvs, 'cache_l1_srvs': item.cache_l1_srvs,
'cache_l2_srvs': item.cache_l2_srvs, 'cache_l2_srvs': item.cache_l2_srvs,

View File

@ -86,6 +86,10 @@ class AssignedService(DetailHandler):
else: else:
val.update({ val.update({
'owner': item.user.manager.name + "-" + item.user.name, 'owner': item.user.manager.name + "-" + item.user.name,
'owner_info': {
'auth_id': item.user.manager.uuid,
'user_id': item.user.uuid
},
'in_use': item.in_use, 'in_use': item.in_use,
'in_use_date': item.in_use_date, 'in_use_date': item.in_use_date,
'source_host': item.src_hostname, 'source_host': item.src_hostname,
@ -204,6 +208,7 @@ class Groups(DetailHandler):
def getItems(self, parent, item): def getItems(self, parent, item):
return [{ return [{
'id': i.uuid, 'id': i.uuid,
'auth_id': i.manager.uuid,
'name': i.name, 'name': i.name,
'comments': i.comments, 'comments': i.comments,
'state': i.state, 'state': i.state,

View File

@ -1,14 +1,14 @@
# jshint strict: true # jshint strict: true
gui.authenticators = new GuiElement(api.authenticators, "auth") gui.authenticators = new GuiElement(api.authenticators, "auth")
gui.authenticators.link = (event) -> gui.authenticators.link = (event) ->
"use strict" "use strict"
# Button definition to trigger "Test" action # Button definition to trigger "Test" action
testButton = testButton: testButton = testButton:
text: gettext("Test") text: gettext("Test")
css: "btn-info" css: "btn-info"
# Clears the log of the detail, in this case, the log of "users" # Clears the log of the detail, in this case, the log of "users"
# Memory saver :-) # Memory saver :-)
detailLogTable = null detailLogTable = null
@ -21,7 +21,7 @@ gui.authenticators.link = (event) ->
$("#users-log-placeholder").empty() $("#users-log-placeholder").empty()
return return
# Clears the details # Clears the details
# Memory saver :-) # Memory saver :-)
prevTables = [] prevTables = []
@ -41,7 +41,7 @@ gui.authenticators.link = (event) ->
prevTables = [] prevTables = []
return return
# Search button event generator for user/group # Search button event generator for user/group
searchForm = (parentModalId, type, id, title, searchLabel, resultsLabel) -> searchForm = (parentModalId, type, id, title, searchLabel, resultsLabel) ->
errorModal = gui.failRequestModalFnc(gettext("Search error")) errorModal = gui.failRequestModalFnc(gettext("Search error"))
@ -108,7 +108,7 @@ gui.authenticators.link = (event) ->
logs: "logs-placeholder" logs: "logs-placeholder"
) )
gui.setLinksEvents() gui.setLinksEvents()
# Append tabs click events # Append tabs click events
$(".bottom_tabs").on "click", (event) -> $(".bottom_tabs").on "click", (event) ->
gui.doLog event.target gui.doLog event.target
@ -130,6 +130,21 @@ gui.authenticators.link = (event) ->
"permissions" "permissions"
] ]
onFoundUuid: (item) ->
# Invoked if our table has found a "desirable" item (uuid)
if gui.lookup2Uuid?
type = gui.lookup2Uuid[0]
gui.lookupUuid = gui.lookup2Uuid.substr(1)
gui.lookup2Uuid = null
setTimeout( () ->
if type == 'g'
$('a[href="#groups-placeholder"]').tab('show')
$("#groups-placeholder span.fa-refresh").click()
else
$('a[href="#users-placeholder_tab"]').tab('show')
$("#users-placeholder_tab span.fa-refresh").click()
, 500)
onRefresh: (tbl) -> onRefresh: (tbl) ->
gui.doLog 'Refresh called for authenticators' gui.doLog 'Refresh called for authenticators'
clearDetails() clearDetails()
@ -141,10 +156,10 @@ gui.authenticators.link = (event) ->
onRowSelect: (selected) -> onRowSelect: (selected) ->
clearDetails() clearDetails()
if selected.length > 1 if selected.length > 1
return return
# We can have lots of users, so memory can grow up rapidly if we do not keep thins clean # We can have lots of users, so memory can grow up rapidly if we do not keep thins clean
# To do so, we empty previous table contents before storing new table contents # To do so, we empty previous table contents before storing new table contents
# Anyway, TabletTools will keep "leaking" memory, but we can handle a little "leak" that will be fixed as soon as we change the section # Anyway, TabletTools will keep "leaking" memory, but we can handle a little "leak" that will be fixed as soon as we change the section
@ -185,7 +200,7 @@ gui.authenticators.link = (event) ->
modalId = gui.launchModal(gettext("Edit group") + " <b>" + item.name + "</b>", api.templates.evaluate(tmpl, modalId = gui.launchModal(gettext("Edit group") + " <b>" + item.name + "</b>", api.templates.evaluate(tmpl,
id: item.id id: item.id
type: item.type type: item.type
meta_if_any: item.meta_if_any meta_if_any: item.meta_if_any
groupname: item.name groupname: item.name
groupname_label: type.groupNameLabel groupname_label: type.groupNameLabel
comments: item.comments comments: item.comments
@ -215,7 +230,7 @@ gui.authenticators.link = (event) ->
return return
if value.type is "meta" if value.type is "meta"
# Meta will get all groups # Meta will get all groups
group.rest.overview (groups) -> group.rest.overview (groups) ->
exec groups exec groups
@ -273,7 +288,7 @@ gui.authenticators.link = (event) ->
onDelete: gui.methods.del(group, gettext("Delete group"), gettext("Group deletion error")) onDelete: gui.methods.del(group, gettext("Delete group"), gettext("Group deletion error"))
) )
tmpLogTable = null tmpLogTable = null
# New button will only be shown on authenticators that can create new users # New button will only be shown on authenticators that can create new users
usrButtons = [ usrButtons = [
"edit" "edit"
@ -322,7 +337,7 @@ gui.authenticators.link = (event) ->
api.templates.get "user", (tmpl) -> # Get form template api.templates.get "user", (tmpl) -> # Get form template
group.rest.overview (groups) -> # Get groups group.rest.overview (groups) -> # Get groups
user.rest.item value.id, (item) -> # Get item to edit user.rest.item value.id, (item) -> # Get item to edit
# Creates modal # Creates modal
modalId = gui.launchModal(gettext("Edit user") + " <b>" + value.name + "</b>", api.templates.evaluate(tmpl, modalId = gui.launchModal(gettext("Edit user") + " <b>" + value.name + "</b>", api.templates.evaluate(tmpl,
id: item.id id: item.id
@ -345,7 +360,7 @@ gui.authenticators.link = (event) ->
gui.tools.unblockUI() gui.tools.unblockUI()
$(modalId + " .button-accept").click -> $(modalId + " .button-accept").click ->
fields = gui.forms.read(modalId) fields = gui.forms.read(modalId)
# If needs password, and password has changed # If needs password, and password has changed
gui.doLog "passwords", type.needsPassword, password, fields.password gui.doLog "passwords", type.needsPassword, password, fields.password
delete fields.password if fields.password is password if type.needsPassword delete fields.password if fields.password is password if type.needsPassword
@ -385,7 +400,7 @@ gui.authenticators.link = (event) ->
searchForm modalId, "user", id, gettext("Search users"), gettext("User"), gettext("Users found") # Enable search button click, if it exist ofc searchForm modalId, "user", id, gettext("Search users"), gettext("User"), gettext("Users found") # Enable search button click, if it exist ofc
$(modalId + " .button-accept").click -> $(modalId + " .button-accept").click ->
fields = gui.forms.read(modalId) fields = gui.forms.read(modalId)
# If needs password, and password has changed # If needs password, and password has changed
gui.doLog "Fields", fields gui.doLog "Fields", fields
user.rest.create fields, ((data) -> # Success on put user.rest.create fields, ((data) -> # Success on put
@ -408,7 +423,7 @@ gui.authenticators.link = (event) ->
container: "logs-placeholder" container: "logs-placeholder"
doNotLoadData: true doNotLoadData: true
) )
# So we can destroy the tables beforing adding new ones # So we can destroy the tables beforing adding new ones
prevTables.push grpTable prevTables.push grpTable
prevTables.push usrTable prevTables.push usrTable
@ -421,4 +436,4 @@ gui.authenticators.link = (event) ->
) )
return return
false false

View File

@ -50,8 +50,16 @@ gui.providers.link = (event) ->
onCheck: (check, items) -> # Check if item can be deleted onCheck: (check, items) -> # Check if item can be deleted
true true
onFoundUuid: (item) ->
# Invoked if our table has found a "desirable" item (uuid)
setTimeout( () ->
$('a[href="#services-placeholder_tab"]').tab('show')
$("#services-placeholder_tab span.fa-refresh").click()
, 500)
gui.lookupUuid = gui.lookup2Uuid
gui.lookup2Uuid = null
onRefresh: (tbl) -> onRefresh: (tbl) ->
gui.doLog 'Invoked onRefresh for a provider'
clearDetails() clearDetails()
return return
@ -116,7 +124,8 @@ gui.providers.link = (event) ->
api.templates.get "service-info", (tmpl) -> api.templates.get "service-info", (tmpl) ->
content = api.templates.evaluate(tmpl, content = api.templates.evaluate(tmpl,
id: 'information', id: 'information',
pools: pools pools: pools,
goClass: 'goLink'
) )
modalId = gui.launchModal(gettext('Service information'), content, modalId = gui.launchModal(gettext('Service information'), content,
actionButton: " " actionButton: " "
@ -151,6 +160,16 @@ gui.providers.link = (event) ->
language: gui.config.dataTablesLanguage language: gui.config.dataTablesLanguage
) )
$('.goLink').on('click', (event) ->
$this = $(this);
event.preventDefault();
gui.lookupUuid = $this.attr('href').substr(1)
$(modalId).modal('hide')
setTimeout( ->
$(".lnk-deployed_services").click();
, 500);
)
return return
select: (vals, value, btn, tbl, refreshFnc) -> select: (vals, value, btn, tbl, refreshFnc) ->

View File

@ -42,7 +42,7 @@ gui.servicesPools.actionsCalendars = (servPool, info) ->
click: (val, value, btn, tbl, refreshFnc) -> click: (val, value, btn, tbl, refreshFnc) ->
if val.length != 1 if val.length != 1
return return
gui.doLog val, val[0] gui.doLog val, val[0]
gui.forms.confirmModal gettext("Execute action"), gettext("Launch action execution right now?"), gui.forms.confirmModal gettext("Execute action"), gettext("Launch action execution right now?"),
onYes: -> onYes: ->
@ -80,6 +80,7 @@ gui.servicesPools.actionsCalendars = (servPool, info) ->
$.each data, (index, value) -> $.each data, (index, value) ->
value.params = ( k + "=" + value.params[k] for k in Object.keys(value.params)).toString() value.params = ( k + "=" + value.params[k] for k in Object.keys(value.params)).toString()
value.atStart = if value.atStart then gettext('Beginning') else gettext('Ending') value.atStart = if value.atStart then gettext('Beginning') else gettext('Ending')
value.calendar = gui.fastLink(value.calendar, value.calendarId, 'gui.servicesPools.fastLink', 'goCalendarLink')
onNew: (value, table, refreshFnc) -> onNew: (value, table, refreshFnc) ->

View File

@ -23,6 +23,9 @@ gui.servicesPools.accessCalendars = (servPool, info) ->
return true return true
onData: (data) -> onData: (data) ->
$.each data, (index, value) ->
# value.calendar = "<a href='##{value.calendarId}' class='goCalendarLink'>#{value.calendar}</a>"
value.calendar = gui.fastLink(value.calendar, value.calendarId, 'gui.servicesPools.fastLink', 'goCalendarLink')
data.push data.push
id: -1, id: -1,
calendar: '-', calendar: '-',

View File

@ -52,7 +52,7 @@ gui.servicesPools.transports = (servPool, info) ->
$.each data, (undefined_, value) -> $.each data, (undefined_, value) ->
style = "display:inline-block; background: url(data:image/png;base64," + value.type.icon + "); ; background-size: 16px 16px; background-repeat: no-repeat; width: 16px; height: 16px; vertical-align: middle;" style = "display:inline-block; background: url(data:image/png;base64," + value.type.icon + "); ; background-size: 16px 16px; background-repeat: no-repeat; width: 16px; height: 16px; vertical-align: middle;"
value.trans_type = value.type.name value.trans_type = value.type.name
value.name = "<span style=\"" + style + "\"></span> " + value.name value.name = gui.fastLink("<span style=\"" + style + "\"></span> #{value.name}", value.id, 'gui.servicesPools.fastLink', 'goTransportLink')
return return
return return

View File

@ -1,5 +1,47 @@
# jshint strict: true # jshint strict: true
gui.servicesPools = new GuiElement(api.servicesPools, "servicespools") gui.servicesPools = new GuiElement(api.servicesPools, "servicespools")
# To allow fast admin navigation
gui.servicesPools.fastLink = (event, obj) ->
gui.doLog 'FastLink clicked', obj
event.preventDefault();
event.stopPropagation();
$obj = $(obj);
if $obj.hasClass('goServiceLink')
vals = $obj.attr('href').substr(1).split(',')
gui.lookupUuid = vals[0]
gui.lookup2Uuid = vals[1]
setTimeout( ->
$(".lnk-service_providers").click();
, 50
)
else if $obj.hasClass('goPoolGroupLink')
gui.lookupUuid = $obj.attr('href').substr(1)
setTimeout( ->
$(".lnk-spoolsgroup").click();
, 50
)
else if $obj.hasClass('goAuthLink')
vals = $obj.attr('href').substr(1).split(',')
gui.lookupUuid = vals[0]
gui.lookup2Uuid = vals[1]
setTimeout( ->
$(".lnk-authenticators").click();
, 50)
else if $obj.hasClass('goTransportLink')
gui.lookupUuid = $obj.attr('href').substr(1)
setTimeout( ->
$(".lnk-connectivity").click();
, 50)
else if $obj.hasClass('goCalendarLink')
gui.lookupUuid = $obj.attr('href').substr(1)
setTimeout( ->
$(".lnk-calendars").click();
, 50)
gui.servicesPools.link = (event) -> gui.servicesPools.link = (event) ->
"use strict" "use strict"
gui.clearWorkspace() gui.clearWorkspace()
@ -306,13 +348,14 @@ gui.servicesPools.link = (event) ->
return return
onDelete: gui.methods.del(groups, gettext("Remove group"), gettext("Group removal error"))
onData: (data) -> onData: (data) ->
$.each data, (undefined_, value) -> $.each data, (undefined_, value) ->
value.group_name = "<b>" + value.auth_name + "</b>\\" + value.name value.group_name = gui.fastLink(value.auth_name, "#{value.auth_id},g#{value.id}", 'gui.servicesPools.fastLink', 'goAuthLink')
return return
return return
onDelete: gui.methods.del(groups, gettext("Remove group"), gettext("Group removal error"))
) )
prevTables.push groupsTable prevTables.push groupsTable
else else
@ -344,6 +387,7 @@ gui.servicesPools.link = (event) ->
value.in_use = gettext('Yes') value.in_use = gettext('Yes')
else else
value.in_use = gettext('No') value.in_use = gettext('No')
value.owner = gui.fastLink(value.owner, "#{value.owner_info.auth_id},u#{value.owner_info.user_id}", 'gui.servicesPools.fastLink', 'goAuthLink')
return return
@ -401,15 +445,17 @@ gui.servicesPools.link = (event) ->
prevTables.push logTable prevTables.push logTable
return return
# Pre-process data received to add "icon" to deployed service # Pre-process data received to add "icon" to deployed service
onData: (data) -> onData: (data) ->
gui.doLog "onData", data gui.doLog "onData for services pools", data
$.each data, (index, value) -> $.each data, (index, value) ->
gui.doLog value.thumb
try try
style = "display:inline-block; background: url(data:image/png;base64," + value.thumb + "); background-size: 16px 16px; background-repeat: no-repeat; width: 16px; height: 16px; vertical-align: middle;" style = "display:inline-block; background: url(data:image/png;base64," + value.thumb + "); background-size: 16px 16px; background-repeat: no-repeat; width: 16px; height: 16px; vertical-align: middle;"
gui.doLog style style_grp = "display:inline-block; background: url(data:image/png;base64," + value.pool_group_thumb + "); background-size: 16px 16px; background-repeat: no-repeat; width: 16px; height: 16px; vertical-align: middle;"
value.parent = gui.fastLink(value.parent, "#{value.provider_id},#{value.service_id}", 'gui.servicesPools.fastLink', 'goServiceLink')
value.pool_group_name = "<span style='#{style_grp}'></span> #{value.pool_group_name}"
if value.pool_group_id?
value.pool_group_name = gui.fastLink(value.pool_group_name, value.pool_group_id, 'gui.servicesPools.fastLink', 'goPoolGroupLink')
if value.restrained if value.restrained
value.name = "<span class=\"fa fa-exclamation text-danger\"></span> " + value.name value.name = "<span class=\"fa fa-exclamation text-danger\"></span> " + value.name
value.state = gettext("Restrained") value.state = gettext("Restrained")

View File

@ -214,6 +214,23 @@
columns.push column columns.push column
return return
lookupUuid = (dTable) ->
if gui.lookupUuid?
gui.doLog "Looking up #{gui.lookupUuid}"
dTable.rows().every( (rowIdx, tableLoop, rowLoop) ->
# rowLoop holds the position in sorted table
try
if this.data().id == gui.lookupUuid
gui.doLog "Found: #{this.data()}"
gui.lookupUuid = null
page = Math.floor(rowLoop / dTable.page.info().length)
dTable.page(page).draw(false)
this.select()
if tblParams.onFoundUuid?
tblParams.onFoundUuid(this)
catch error
;
)
# Responsive style for tables, using tables.css and this code generates the "titles" for vertical display on small sizes # Responsive style for tables, using tables.css and this code generates the "titles" for vertical display on small sizes
initTable = (data) -> initTable = (data) ->
@ -249,6 +266,7 @@
selCallback null, tbl, null, null selCallback null, tbl, null, null
gui.doLog "onRefresh", tblParams.onRefresh gui.doLog "onRefresh", tblParams.onRefresh
tblParams.onRefresh self tblParams.onRefresh self
lookupUuid(tbl)
gui.tools.unblockUI()), gui.failRequestModalFnc(gettext("Refresh operation failed")) gui.tools.unblockUI()), gui.failRequestModalFnc(gettext("Refresh operation failed"))
) )
return return
@ -559,6 +577,10 @@
tableTop = $("#" + tableId).offset().top tableTop = $("#" + tableId).offset().top
$("html, body").scrollTop tableTop $("html, body").scrollTop tableTop
gui.test = dTable
# Try to locate gui.lookupUuid as last action
lookupUuid(dTable)
# if table rendered event # if table rendered event
tblParams.onLoad self if tblParams.onLoad tblParams.onLoad self if tblParams.onLoad
return return

View File

@ -5,6 +5,10 @@
# Public attributes # Public attributes
gui.debug = on gui.debug = on
# Used for lookup items on some circustances
gui.lookupUuid = null
gui.lookup2Uuid = null # Used for going to second level of tables. Take into account that this is used by some guis
# "public" methods # "public" methods
gui.doLog = (args...)-> gui.doLog = (args...)->
if gui.debug if gui.debug
@ -162,6 +166,9 @@
return return
gui.fastLink = (text, href, onClick, clas) ->
"<span>#{text}</span><span style='float:right;'><a href='##{href}' onclick='#{onClick}(event, this);' class='#{clas}'><i class='fa fa-external-link'> </i></a></span>"
gui.setLinksEvents = -> gui.setLinksEvents = ->
sidebarLinks = [ sidebarLinks = [
{ {

View File

@ -59,8 +59,6 @@
{% compress js %} {% compress js %}
<!-- minified js from: 'jquery', 'jquery.cookie', 'bootstrap.min', 'bootstrap-switch.min', 'bootstrap-select.min', 'jquery.validate.min', 'jquery.blockUI', 'flot',
'jquery.dataTables.min', 'TableTools.min', 'Blob', 'FileSaver', 'ZeroClipboard', 'dataTables.bootstrap', 'handlebars-v1.1.2', UDS admin JS's -->
<script src="{% get_static_prefix %}adm/js/jquery-2.1.3.min.js"></script> <script src="{% get_static_prefix %}adm/js/jquery-2.1.3.min.js"></script>
<script src="{% get_static_prefix %}adm/js/datatables.min.js"></script> <script src="{% get_static_prefix %}adm/js/datatables.min.js"></script>
<script src="{% get_static_prefix %}adm/js/jquery.cookie.js"></script> <script src="{% get_static_prefix %}adm/js/jquery.cookie.js"></script>

View File

@ -21,13 +21,13 @@
<th>{% endverbatim %}{% trans 'Pool' %}{% verbatim %}</th> <th>{% endverbatim %}{% trans 'Pool' %}{% verbatim %}</th>
<th>{% endverbatim %}{% trans 'State' %}{% verbatim %}</th> <th>{% endverbatim %}{% trans 'State' %}{% verbatim %}</th>
<th>{% endverbatim %}{% trans 'Image' %}{% verbatim %}</th> <th>{% endverbatim %}{% trans 'Image' %}{% verbatim %}</th>
<th>{% endverbatim %}{% trans 'User Services' %}{% verbatim %}</th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each pools }} {{#each pools }}
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}<span style="float: right;"><a href="#{{ id }}" class="{{ ../goClass }}"><i class="fa fa-external-link"> </i></a></span></td>
<td>{{ state }}</td> <td>{{ state }}</td>
<td><img src="data:image/png;base64,{{ thumb }}" style="width: 32px; height: auto;"/></td> <td><img src="data:image/png;base64,{{ thumb }}" style="width: 32px; height: auto;"/></td>
<td>{{ user_services_count }}</td> <td>{{ user_services_count }}</td>