Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
emulab
emulab-devel
Commits
f4a9a9df
Commit
f4a9a9df
authored
Jul 10, 2017
by
Leigh B Stoller
Browse files
User visible view of the Cluster Status page, which has been improved
upon to handle pre-reserves and reservations in a reasonable manner.
parent
e7fad93e
Changes
6
Hide whitespace changes
Inline
Side-by-side
www/aptui/cluster-status.ajax
View file @
f4a9a9df
<?php
#
# Copyright (c) 2000-201
6
University of Utah and the Flux Group.
# Copyright (c) 2000-201
7
University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
...
...
@@ -31,21 +31,26 @@ include_once("instance_defs.php");
#
function
Do_GetStatus
()
{
global
$ajax_args
,
$geni_response_codes
,
$urn_mapping
,
$TBBASE
;
global
$TBSUEXEC_PATH
;
global
$ajax_args
,
$this_user
,
$geni_response_codes
,
$urn_mapping
;
global
$TBSUEXEC_PATH
,
$TBBASE
,
$TB_PROJECT_CREATEEXPT
,
$OURDOMAIN
;
global
$TB_PROJECT_READINFO
,
$DEFAULT_AGGREGATE_URN
;
$isadmin
=
0
;
$autoextend_maxage
=
TBGetSiteVar
(
"aptui/autoextend_maxage"
);
if
(
!
(
ISADMIN
()
||
ISFOREIGN_ADMIN
()))
{
SPITAJAX_ERROR
(
1
,
"Not enough permission"
);
return
;
# Admins get much more info.
if
(
ISADMIN
()
||
ISFOREIGN_ADMIN
())
{
$isadmin
=
1
;
}
if
(
Instance
::
ValidURN
(
$ajax_args
[
"cluster"
]))
{
$cluster
=
$ajax_args
[
"cluster"
];
}
else
{
$cluster
=
$ajax_args
[
"cluster"
]
.
".cm"
;
SPITAJAX_ERROR
(
1
,
"Not a valid cluster URN"
);
return
;
}
$inuse
=
array
();
$isus
=
(
$cluster
==
$DEFAULT_AGGREGATE_URN
?
1
:
0
);
$cluster
=
escapeshellarg
(
$cluster
);
$fp
=
popen
(
"
$TBSUEXEC_PATH
elabman tbadmin "
.
"webportal_xmlrpc -a
$cluster
InUse"
,
"r"
);
$string
=
""
;
...
...
@@ -53,10 +58,25 @@ function Do_GetStatus()
$string
.
=
fgets
(
$fp
,
1024
);
}
pclose
(
$fp
);
$inuse
=
json_decode
(
$string
);
if
(
gettype
(
$inuse
)
==
"object"
)
{
$inuse
=
$inuse
->
details
;
$results
=
json_decode
(
$string
);
if
(
gettype
(
$results
)
==
"object"
)
{
$inuse
=
$results
->
details
;
if
(
property_exists
(
$results
,
"typeinfo"
))
{
$typeinfo
=
$results
->
typeinfo
;
}
}
else
{
# Backwards compat?
$inuse
=
$results
;
}
#
# Grab local project list for mere users, so we can use it to determine
# if they are in the same project as a reserved_pid, which means it is
# available to the user.
#
$projlist
=
$this_user
->
ProjectAccessList
(
$TB_PROJECT_CREATEEXPT
);
#
# We want to convert slice_urns to local instances so that we
...
...
@@ -65,15 +85,23 @@ function Do_GetStatus()
#
$instances
=
array
();
$maxttls
=
array
();
$projects
=
array
();
$typecounts
=
array
();
$nodeinfo
=
array
();
foreach
(
$inuse
as
$details
)
{
$info
=
array
();
$type
=
$details
->
type
;
if
(
!
array_key_exists
(
$type
,
$typecounts
))
{
$typecounts
[
$type
]
=
array
(
"inuse"
=>
0
,
"preres"
=>
0
,
"free"
=>
0
,
"free_preres"
=>
0
);
if
(
isset
(
$typeinfo
)
&&
property_exists
(
$typeinfo
,
$type
))
{
$typecounts
[
$type
][
"free_reserved"
]
=
$typeinfo
->
{
$type
}
->
freecount
;
}
}
if
(
$details
->
reserved_pid
!=
""
)
{
$typecounts
[
$type
][
"preres"
]
+=
1
;
...
...
@@ -89,6 +117,14 @@ function Do_GetStatus()
$typecounts
[
$type
][
"free"
]
+=
1
;
}
}
$info
[
"type"
]
=
$type
;
$info
[
"node_id"
]
=
$details
->
node_id
;
#
# Find local instance if it exists.
#
$instance
=
null
;
$maxttl
=
$details
->
maxttl
;
if
(
property_exists
(
$details
,
"slice_uuid"
)
&&
$details
->
slice_uuid
!=
""
)
{
...
...
@@ -99,55 +135,149 @@ function Do_GetStatus()
}
else
{
$instance
=
Instance
::
LookupBySlice
(
$details
->
slice_uuid
);
if
(
!
$instance
)
{
continue
;
if
(
!
$instance
)
{
# Avoid not finding more then once.
$instance
=
null
;
}
$instances
[
$details
->
slice_uuid
]
=
$instance
;
# Need the slice for this.
$slice
=
GeniSlice
::
Lookup
(
"sa"
,
$details
->
slice_uuid
);
#
# We can generate the maxttl here, ignoring whatever the
# cluster might think. We need the slice though.
#
$maxttl
=
""
;
if
(
$slice
&&
!
(
$instance
->
admin_lockdown
()
||
$instance
->
user_lockdown
()))
{
$slice_expires
=
strtotime
(
$slice
->
expires
());
$slice_created
=
strtotime
(
$slice
->
created
());
$diff
=
$slice_expires
-
time
();
$cdiff
=
time
()
-
$slice_created
;
if
(
$instance
->
extension_lockout
()
||
(
$cdiff
>
(
3600
*
24
*
$autoextend_maxage
)))
{
$maxttl
=
$slice_expires
-
time
();
}
else
{
#
# This is the amount of free time the user can
# get, plus we add two days, since a denied extension
# automatically grants two days to avoid termination
# before we get a chance to look at it.
#
$maxttl
=
(
$slice_created
+
(
3600
*
24
*
2
)
+
(
3600
*
24
*
$autoextend_maxage
))
-
time
();
if
(
$instance
&&
$slice
)
{
#
# We can generate the maxttl here, ignoring whatever the
# cluster might think. We need the slice though.
#
if
(
$slice
&&
!
(
$instance
->
admin_lockdown
()
||
$instance
->
user_lockdown
()))
{
$slice_expires
=
strtotime
(
$slice
->
expires
());
$slice_created
=
strtotime
(
$slice
->
created
());
$diff
=
$slice_expires
-
time
();
$cdiff
=
time
()
-
$slice_created
;
if
(
$instance
->
extension_lockout
()
||
(
$cdiff
>
(
3600
*
24
*
$autoextend_maxage
)))
{
$maxttl
=
$slice_expires
-
time
();
}
else
{
#
# This is the amount of free time the user can
# get, plus we add two days, since a denied
# extension automatically grants two days to
# avoid termination before we get a chance to look
# at it.
#
$maxttl
=
(
$slice_created
+
(
3600
*
24
*
2
)
+
(
3600
*
24
*
$autoextend_maxage
))
-
time
();
}
}
}
else
{
$maxttl
=
$details
->
maxttl
;
}
$maxttls
[
$details
->
slice_uuid
]
=
$maxttl
;
}
}
#
# Find local project for a pre-reserve.
#
$reserved_project
=
null
;
$lpid
=
null
;
if
(
property_exists
(
$details
,
"reserved_urn"
)
&&
$details
->
reserved_urn
!=
""
&&
Instance
::
ValidURN
(
$details
->
reserved_urn
))
{
list
(
$auth
,
$type
,
$id
)
=
Instance
::
ParseURN
(
$details
->
reserved_urn
);
#
# Subdomain is project id.
#
list
(
$domain
,
$pid
)
=
preg_split
(
'/:/'
,
$auth
);
if
(
$domain
==
$OURDOMAIN
&&
$pid
&&
$pid
!=
""
)
{
$lpid
=
$pid
;
}
}
elseif
(
$details
->
reserved_pid
!=
""
&&
$isus
)
{
# Local (classic) project.
$lpid
=
$details
->
reserved_pid
;
}
if
(
$lpid
)
{
if
(
array_key_exists
(
$lpid
,
$projects
))
{
$reserved_project
=
$projects
[
$lpid
];
}
else
{
$reserved_project
=
Project
::
Lookup
(
$lpid
);
if
(
$reserved_project
)
{
$projects
[
$lpid
]
=
$reserved_project
;
}
else
{
# Avoid repeated failed lookups.
$projects
[
$lpid
]
=
$reserved_project
=
null
;
}
}
}
if
(
$isadmin
)
{
if
(
$instance
)
{
$details
->
pid
=
$instance
->
pid
();
$details
->
instance_uuid
=
$instance
->
uuid
();
$details
->
instance_name
=
$instance
->
name
();
$details
->
uid
=
$instance
->
creator
();
$details
->
maxttl
=
$maxttl
;
$info
[
"pid"
]
=
$instance
->
pid
();
$info
[
"uid"
]
=
$instance
->
creator
();
$info
[
"instance_uuid"
]
=
$instance
->
uuid
();
$info
[
"instance_name"
]
=
$instance
->
name
();
}
else
{
$info
[
"pid"
]
=
$details
->
pid
;
$info
[
"uid"
]
=
$details
->
uid
;
}
$info
[
"eid"
]
=
$details
->
eid
;
$info
[
"ttl"
]
=
$details
->
ttl
;
$info
[
"maxttl"
]
=
$maxttl
;
$info
[
"reserved_pid"
]
=
$details
->
reserved_pid
;
}
else
{
$available
=
1
;
if
(
$details
->
eid
!=
""
)
{
$available
=
0
;
}
elseif
(
$details
->
reserved_pid
!=
""
||
$reserved_project
)
{
#
# Free but a pre-reserve. See if the user is a member of
# local project and thus can use the node.
#
if
(
$reserved_project
)
{
if
(
!
$reserved_project
->
AccessCheck
(
$this_user
,
$TB_PROJECT_READINFO
))
{
$available
=
0
;
}
}
else
{
# Do not know anything, so not available;
$available
=
0
;
}
}
$info
[
"available"
]
=
$available
;
#
# Adjust free counts for this type, see above.
#
if
(
$available
&&
$details
->
pid
==
""
&&
$details
->
reserved_pid
!=
""
)
{
$typecounts
[
$type
][
"free"
]
+=
1
;
# Reservation system ignores prereserved nodes.
$typecounts
[
$type
][
"free_reserved"
]
+=
1
;
}
}
$nodeinfo
[]
=
$info
;
}
$dashboard
[
"totals"
]
=
$typecounts
;
$dashboard
[
"inuse"
]
=
$inuse
;
SPITAJAX_RESPONSE
(
$dashboard
);
#
# If the reservation system says that the number of free nodes is
# different then what we calculated, put that in parens.
#
$blob
=
array
();
$blob
[
"totals"
]
=
$typecounts
;
$blob
[
"inuse"
]
=
$nodeinfo
;
SPITAJAX_RESPONSE
(
$blob
);
}
function
Do_GetPreReservations
()
...
...
www/aptui/cluster-status.php
View file @
f4a9a9df
<?php
#
# Copyright (c) 2000-201
6
University of Utah and the Flux Group.
# Copyright (c) 2000-201
7
University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
...
...
@@ -38,16 +38,13 @@ $this_user = CheckLoginOrRedirect();
$isadmin
=
(
ISADMIN
()
?
1
:
0
);
$isfadmin
=
(
ISFOREIGN_ADMIN
()
?
1
:
0
);
if
(
!
(
ISADMIN
()
||
ISFOREIGN_ADMIN
()))
{
SPITUSERERROR
(
"You do not have permission to view this page!"
);
}
SPITHEADER
(
1
);
#
# The apt_aggregates table should tell us what clusters, but for
# now it is always the local cluster
#
if
(
$TBMAINSITE
&&
!
$IS
EMULAB
)
{
if
(
$TBMAINSITE
&&
$IS
CLOUD
)
{
$aggregates
=
array
(
"Emulab"
=>
"urn:publicid:IDN+emulab.net+authority+cm"
,
"APT"
=>
"urn:publicid:IDN+apt.emulab.net+authority+cm"
,
...
...
www/aptui/js/cluster-status.js
View file @
f4a9a9df
...
...
@@ -14,11 +14,12 @@ $(function ()
function
initialize
()
{
window
.
APT_OPTIONS
.
initialize
(
sup
);
isadmin
=
window
.
ISADMIN
;
isadmin
=
window
.
ISADMIN
||
window
.
ISFADMIN
;
amlist
=
JSON
.
parse
(
_
.
unescape
(
$
(
'
#agglist-json
'
)[
0
].
textContent
));
var
html
=
mainTemplate
({
amlist
:
amlist
,
"
amlist
"
:
amlist
,
"
isadmin
"
:
isadmin
,
});
$
(
'
#page-body
'
).
html
(
html
);
...
...
@@ -36,7 +37,7 @@ $(function ()
{
_
.
each
(
amlist
,
function
(
urn
,
name
)
{
var
callback
=
function
(
json
)
{
//
console.log(json);
console
.
log
(
json
);
if
(
json
.
code
)
{
console
.
log
(
"
Could not get cluster data:
"
+
json
.
value
);
return
;
...
...
@@ -44,57 +45,88 @@ $(function ()
var
inuse
=
json
.
value
.
inuse
;
var
html
=
""
;
inuse
.
forEach
(
function
(
value
,
index
)
{
_
.
each
(
inuse
,
function
(
value
,
name
)
{
var
type
=
""
;
if
(
_
.
has
(
value
,
"
type
"
))
{
type
=
value
.
type
;
}
var
expires
=
""
;
if
(
_
.
has
(
value
,
"
ttl
"
))
{
var
ttl
=
value
.
ttl
;
if
(
ttl
!=
""
)
{
expires
=
moment
().
add
(
ttl
,
'
seconds
'
).
fromNow
();
html
=
html
+
"
<tr>
"
+
"
<td>
"
+
value
.
node_id
+
"
</td>
"
+
"
<td>
"
+
type
+
"
</td>
"
;
if
(
isadmin
)
{
var
expires
=
""
;
if
(
_
.
has
(
value
,
"
ttl
"
))
{
var
ttl
=
value
.
ttl
;
if
(
ttl
!=
""
)
{
expires
=
moment
()
.
add
(
ttl
,
'
seconds
'
).
fromNow
();
}
}
}
var
allowed
=
""
;
if
(
_
.
has
(
value
,
"
maxttl
"
))
{
var
maxttl
=
value
.
maxttl
;
if
(
maxttl
!=
""
)
{
allowed
=
moment
().
add
(
maxttl
,
'
seconds
'
).
fromNow
();
var
allowed
=
""
;
if
(
_
.
has
(
value
,
"
maxttl
"
))
{
var
maxttl
=
value
.
maxttl
;
if
(
maxttl
!=
""
)
{
allowed
=
moment
()
.
add
(
maxttl
,
'
seconds
'
).
fromNow
();
}
}
var
uid
=
""
;
if
(
_
.
has
(
value
,
"
uid
"
))
{
uid
=
value
.
uid
;
}
var
eid
=
""
;
if
(
_
.
has
(
value
,
"
eid
"
))
{
eid
=
value
.
eid
;
if
(
_
.
has
(
value
,
"
instance_uuid
"
))
{
var
uuid
=
value
.
instance_uuid
;
eid
=
"
<a href='status.php?uuid=
"
+
uuid
+
"
' target=_blank>
"
+
value
.
instance_name
+
"
</a>
"
;
}
}
html
=
html
+
"
<td>
"
+
value
.
pid
+
"
</td>
"
+
"
<td>
"
+
eid
+
"
</td>
"
+
"
<td>
"
+
uid
+
"
</td>
"
+
"
<td>
"
+
expires
+
"
</td>
"
+
"
<td>
"
+
allowed
+
"
</td>
"
+
"
<td>
"
+
value
.
reserved_pid
+
"
</td>
"
;
}
var
uid
=
""
;
if
(
_
.
has
(
value
,
"
uid
"
))
{
uid
=
value
.
uid
;
}
var
eid
=
value
.
eid
;
if
(
_
.
has
(
value
,
"
instance_uuid
"
))
{
var
uuid
=
value
.
instance_uuid
;
eid
=
"
<a href='status.php?uuid=
"
+
uuid
+
"
' target=_blank>
"
+
value
.
instance_name
+
"
</a>
"
;
else
{
if
(
value
.
available
)
{
html
=
html
+
"
<td>Yes</td>
"
;
}
else
{
html
=
html
+
"
<td>No</td>
"
;
}
}
html
=
html
+
"
<tr>
"
+
"
<td>
"
+
value
.
node_id
+
"
</td>
"
+
"
<td>
"
+
type
+
"
</td>
"
+
"
<td>
"
+
value
.
pid
+
"
</td>
"
+
"
<td>
"
+
eid
+
"
</td>
"
+
"
<td>
"
+
uid
+
"
</td>
"
+
"
<td>
"
+
expires
+
"
</td>
"
+
"
<td>
"
+
allowed
+
"
</td>
"
+
"
<td>
"
+
value
.
reserved_pid
+
"
</td>
"
+
"
</tr>
"
;
html
=
html
+
"
</tr>
"
;
});
$
(
'
#
'
+
name
+
'
-tbody
'
).
html
(
html
);
InitTable
(
name
);
// These are the totals.
html
=
countsTemplate
({
"
totals
"
:
json
.
value
.
totals
});
html
=
countsTemplate
({
"
totals
"
:
json
.
value
.
totals
,
"
isadmin
"
:
isadmin
});
$
(
'
#counts-panel-
'
+
name
).
html
(
html
);
// This activates the tooltip subsystem.
$
(
'
#counts-panel-
'
+
name
+
'
'
+
'
[data-toggle="tooltip"]
'
).
tooltip
({
delay
:
{
"
hide
"
:
500
,
"
show
"
:
150
},
placement
:
'
auto
'
,
});
// We reference the totals table in InitTable();
InitTable
(
name
);
}
var
xmlthing
=
sup
.
CallServerMethod
(
null
,
"
cluster-status
"
,
"
GetStatus
"
,
{
"
cluster
"
:
urn
});
xmlthing
.
done
(
callback
);
});
if
(
!
isadmin
)
{
return
;
}
_
.
each
(
amlist
,
function
(
urn
,
name
)
{
var
callback
=
function
(
json
)
{
console
.
log
(
json
);
...
...
@@ -195,13 +227,16 @@ $(function ()
// Allows using filter_liveSearch or delayed search &
// pressing escape to cancel the search
$
.
tablesorter
.
filter
.
bindSearch
(
table
,
$
(
searchname
));
$
.
tablesorter
.
filter
.
bindSearch
(
table
,
$
(
'
#inuse-search-all
'
));
$
(
tablename
).
removeClass
(
"
hidden
"
);
if
(
window
.
ISCLOUD
)
{
$
.
tablesorter
.
filter
.
bindSearch
(
table
,
$
(
'
#inuse-search-all
'
));
}
/*
* This is the expand/collapse button for an individual table.
*/
$
(
'
#inuse-collapse-button-
'
+
name
).
click
(
function
()
{
$
(
'
#inuse-collapse-button-
'
+
name
).
click
(
function
(
event
)
{
event
.
preventDefault
();
if
(
$
(
panelname
).
data
(
"
status
"
)
==
"
minimized
"
)
{
$
(
panelname
).
removeClass
(
"
inuse-panel
"
);
$
(
'
#counts-panel-
'
+
name
).
removeClass
(
"
counts-panel
"
);
...
...
@@ -217,6 +252,20 @@ $(function ()
$
(
this
).
addClass
(
"
glyphicon-chevron-right
"
);
}
})
// Only one, expand immediately.
if
(
Object
.
keys
(
amlist
).
length
==
1
)
{
$
(
'
#inuse-collapse-button-
'
+
name
).
click
();
}
$
(
tablename
).
removeClass
(
"
hidden
"
);
// Bind type column in the counts table to initiating search
$
(
'
#counts-panel-
'
+
name
+
'
.counts-type
'
).
click
(
function
(
event
)
{
event
.
preventDefault
();
$
(
tablename
+
'
input.tablesorter-filter.form-control[data-column="1"]
'
)
.
val
(
$
(
event
.
target
).
text
());
table
.
trigger
(
'
search
'
,
false
);
});
}
$
(
document
).
ready
(
initialize
);
...
...
www/aptui/quickvm_sup.php
View file @
f4a9a9df
...
...
@@ -377,7 +377,8 @@ echo "
<li><a href='instantiate.php'>Start Experiment</a></li>
<li><a href='manage_profile.php'>Create Experiment Profile</a></li>
<li><a href='reserve.php'>Reserve Nodes</a></li>
"
;
<li><a href='cluster-status.php'>Cluster Status</a></li>
"
;
echo
" <li class='divider'></li>
<li><a href='user-dashboard.php#experiments'>
My Experiments</a></li>
...
...
www/aptui/template/cluster-status-templates.html
View file @
f4a9a9df
...
...
@@ -5,18 +5,48 @@
<
tr
>
<
th
>
Type
<
/th
>
<
th
>
Inuse
<
/th
>
<
th
>
Pre
<
/th
>
<
th
>
Free
<
/th
>
<%
if
(
isadmin
)
{
%>
<
th
>
Pre
<
a
href
=
'
#
'
class
=
'
btn btn-xs
'
data
-
toggle
=
'
tooltip
'
data
-
container
=
"
body
"
data
-
trigger
=
"
hover
"
title
=
'
Number preserved AND free in parens
'
style
=
"
margin-bottom: 3px;
padding-left: 0px;
padding-right: 0px;
"
>
<
span
class
=
'
glyphicon glyphicon-question-sign
'
><
/span></
a
><
/th
>
<%
}
%>
<
th
>
Free
<
a
href
=
'
#
'
class
=
'
btn btn-xs
'
data
-
toggle
=
'
tooltip
'
data
-
container
=
"
body
"
data
-
trigger
=
"
hover
"
title
=
'
If the reservation system determines that some free
nodes will soon be unavailble, the adjusted number is
shown in parens.
'
style
=
"
margin-bottom: 3px;
padding-left: 0px;
padding-right: 0px;
"
>
<
span
class
=
'
glyphicon glyphicon-question-sign
'
><
/span></
a
><
/th
>
<
/th
>
<
/tr
>
<
/thead
>
<
tbody
>
<%
_
.
each
(
totals
,
function
(
value
,
type
)
{
%>
<
tr
>
<
td
><%-
type
%><
/td
>
<
td
><
a
href
=
"
#
"
class
=
"
counts-type
"
><
%-
type
%><
/
a></
td
>
<
td
><%-
value
.
inuse
%><
/td
>
<
td
><%-
value
.
preres
%><
/td
>
<
td
><%-
value
.
f
re
e
%>
<
%
if
(
isadmin
)
{
%
>
<
td
><%-
value
.
p
re
res
%>
<%
if
(
value
.
free_preres
)
{
%>
(
<%-
value
.
free_preres
%>
)
<%
}
%>
<
/td
>
<%
}
%>
<
td
><%-
value
.
free
%>
<%
if
(
_
.
has
(
value
,
"
free_reserved
"
)
&&
value
.
free
!=
value
.
free_reserved
)
{
%>
(
<%-
value
.
free_reserved
%>
)
<%
}
%>
<
/td
>
<
/tr
>
<%
});
%>
...
...
www/aptui/template/cluster-status.html
View file @
f4a9a9df
...
...
@@ -36,19 +36,31 @@
padding
:
0
!important
;
}
</style>
<div
class=
'row'
style=
"margin-bottom: 5px"
>
<div
class=
'col-sm-8 col-sm-offset-3'
>
<input
class=
'form-control search'
type=
'search'
data-column=
'all'
id=
'inuse-search-all'
placeholder=
'Global Search'
>
<
%
if
(
window.ISCLOUD
)
{
%
>
<div
class=
'row'
style=
"margin-bottom: 5px"
>
<div
<%
if
(
isadmin
)
{
%
>
class='col-sm-8 col-sm-offset-3'
<
%
}
else
{
%
>
class='col-sm-5 col-sm-offset-5'
<
%
}
%
>
>
<input
class=
'form-control search'
type=
'search'
data-column=
'all'
id=
'inuse-search-all'
placeholder=
'Global Search'
>
</div>
</div>
<
/div
>
<
%
}
%
>
<
%
_.each
(
amlist
,
function
(
urn
,
name
)
{
%
>
<div
class=
'row'
>
<div
class=
'col-sm-3'
>
<div
<%
if
(
isadmin
)
{
%
>
class='col-sm-3'
<
%
}
else
{
%
>