Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
7
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Open sidebar
emulab
emulab-devel
Commits
3ead4566
Commit
3ead4566
authored
Oct 23, 2017
by
Leigh B Stoller
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement time series graphs on the reserve page, as per issue
#334
.
parent
195b5d18
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
787 additions
and
72 deletions
+787
-72
apt/manage_reservations.in
apt/manage_reservations.in
+3
-3
www/aptui/aggregate_defs.php
www/aptui/aggregate_defs.php
+2
-1
www/aptui/js/list-reservations.js
www/aptui/js/list-reservations.js
+2
-2
www/aptui/js/reserve.js
www/aptui/js/reserve.js
+113
-50
www/aptui/js/resgraphs.js
www/aptui/js/resgraphs.js
+228
-0
www/aptui/js/resinfo.js
www/aptui/js/resinfo.js
+94
-0
www/aptui/reserve.ajax
www/aptui/reserve.ajax
+32
-8
www/aptui/reserve.php
www/aptui/reserve.php
+7
-3
www/aptui/resinfo.php
www/aptui/resinfo.php
+105
-0
www/aptui/template/reservation-graph.html
www/aptui/template/reservation-graph.html
+60
-0
www/aptui/template/reservation-list.html
www/aptui/template/reservation-list.html
+11
-0
www/aptui/template/reserve-request.html
www/aptui/template/reserve-request.html
+87
-5
www/aptui/template/resgraph-modal.html
www/aptui/template/resgraph-modal.html
+22
-0
www/aptui/template/resinfo.html
www/aptui/template/resinfo.html
+21
-0
No files found.
apt/manage_reservations.in
View file @
3ead4566
...
...
@@ -500,14 +500,14 @@ sub DoList()
}
$tmp
->
{
$key
}
=
$details
;
}
$
list
=
$tmp
;
$
response
->
value
()
->
{'
reservations
'}
=
$tmp
;
}
if
(
defined
(
$webtask
))
{
$webtask
->
value
(
$
list
);
$webtask
->
value
(
$
response
->
value
()
);
$webtask
->
Exited
(
0
);
}
else
{
print
Dumper
(
$
list
);
print
Dumper
(
$
response
->
value
()
);
}
exit
(
0
);
}
...
...
www/aptui/aggregate_defs.php
View file @
3ead4566
...
...
@@ -189,7 +189,8 @@ class Aggregate
$query_result
=
DBQueryFatal
(
"select urn from apt_aggregates "
.
"where disabled=0 and reservations=1 and "
.
" FIND_IN_SET('
$PORTAL_GENESIS
', portals)"
);
" FIND_IN_SET('
$PORTAL_GENESIS
', portals)"
.
"order by isfederate,name"
);
while
(
$row
=
mysql_fetch_array
(
$query_result
))
{
$urn
=
$row
[
"urn"
];
...
...
www/aptui/js/list-reservations.js
View file @
3ead4566
...
...
@@ -33,7 +33,7 @@ $(function ()
_
.
each
(
amlist
,
function
(
urn
,
name
)
{
var
callback
=
function
(
json
)
{
console
.
log
(
json
);
console
.
log
(
"
LoadData
"
,
json
);
// Kill the spinner.
amcount
--
;
...
...
@@ -45,7 +45,7 @@ $(function ()
name
+
"
:
"
+
json
.
value
);
return
;
}
var
reservations
=
json
.
value
;
var
reservations
=
json
.
value
.
reservations
;
rescount
+=
reservations
.
length
;
if
(
reservations
.
length
==
0
)
{
...
...
www/aptui/js/reserve.js
View file @
3ead4566
...
...
@@ -2,17 +2,17 @@ $(function ()
{
'
use strict
'
;
var
template_list
=
[
"
reserve-request
"
,
"
reserve-faq
"
,
"
reservation-list
"
,
"
oops-modal
"
,
"
waitwait-modal
"
];
var
template_list
=
[
"
reserve-request
"
,
"
reserve-faq
"
,
"
reservation-graph
"
,
"
oops-modal
"
,
"
waitwait-modal
"
];
var
templates
=
APT_OPTIONS
.
fetchTemplateList
(
template_list
);
var
mainString
=
templates
[
"
reserve-request
"
];
var
oopsString
=
templates
[
"
oops-modal
"
];
var
waitwaitString
=
templates
[
"
waitwait-modal
"
];
var
mainTemplate
=
_
.
template
(
mainString
);
var
list
Template
=
_
.
template
(
templates
[
"
reservation-
list
"
]);
var
mainTemplate
=
_
.
template
(
templates
[
"
reserve-request
"
]
);
var
graph
Template
=
_
.
template
(
templates
[
"
reservation-
graph
"
]);
var
fields
=
null
;
var
projlist
=
null
;
var
amlist
=
null
;
var
amorder
=
[];
var
isadmin
=
false
;
var
editing
=
false
;
var
buttonstate
=
"
check
"
;
...
...
@@ -44,6 +44,7 @@ $(function ()
Delete
();
});
}
LoadReservations
();
}
//
...
...
@@ -63,7 +64,16 @@ $(function ()
html
=
aptforms
.
FormatFormFieldsHorizontal
(
html
);
$
(
'
#main-body
'
).
html
(
html
);
$
(
'
.faq-contents
'
).
html
(
templates
[
"
reserve-faq
"
]);
// Graph list.
$
(
'
#reservation-lists .reservation-div
'
)
.
html
(
graphTemplate
({
"
amlist
"
:
amlist
,
"
showcontrols
"
:
true
}));
// Handler for the Help button
$
(
'
#reservation-help-button
'
).
click
(
function
(
event
)
{
event
.
preventDefault
();
sup
.
ShowModal
(
'
#reservation-help-modal
'
);
});
// Handler for the FAQ link.
$
(
'
#reservation-faq-button
'
).
click
(
function
(
event
)
{
event
.
preventDefault
();
...
...
@@ -75,11 +85,22 @@ $(function ()
// Set the manual link since the FAQ is not a template.
$
(
'
#reservation-manual
'
).
attr
(
"
href
"
,
window
.
MANUAL
);
// Handler for the Reservation Graph Help button
$
(
'
.resgraph-help-button
'
).
click
(
function
(
event
)
{
event
.
preventDefault
();
sup
.
ShowModal
(
'
#resgraph-help-modal
'
);
});
// This activates the popover subsystem.
$
(
'
[data-toggle="popover"]
'
).
popover
({
trigger
:
'
hover
'
,
container
:
'
body
'
});
// This activates the tooltip subsystem.
$
(
'
[data-toggle="tooltip"]
'
).
tooltip
({
placement
:
'
auto
'
});
// Handler for cluster change to show the type list.
$
(
'
#reserve-request-form #cluster
'
).
change
(
function
(
event
)
{
$
(
"
#reserve-request-form #cluster option:selected
"
).
...
...
@@ -132,7 +153,6 @@ $(function ()
aptforms
.
EnableUnsavedWarning
(
'
#reserve-request-form
'
,
modified_callback
);
LoadReservations
();
}
/*
...
...
@@ -230,16 +250,59 @@ $(function ()
"
Validate
"
,
checkonly_callback
);
}
/*
* Load anonymized reservations from each am in the list and generate tables.
*/
// Call back from the graphs to change the dates on a blank form
function
SetDates
(
when
)
{
//console.info("dates", when);
// Bump to next hour. Will be confusing at midnight.
when
.
setHours
(
when
.
getHours
()
+
1
);
if
(
!
editing
)
{
$
(
"
#reserve-request-form #start_day
"
).
datepicker
(
"
setDate
"
,
when
);
//$("#reserve-request-form #end_day").datepicker("setDate", when);
$
(
"
#reserve-request-form [name=start_hour]
"
).
val
(
when
.
getHours
());
//$("#reserve-request-form [name=end_hour]").val(when.getHours());
aptforms
.
MarkFormUnsaved
();
}
}
// Set the cluster after clicking on a graph.
function
SetCluster
(
nickname
,
urn
)
{
$
(
'
#reserve-request-form [name=cluster] option[value="
'
+
urn
+
'
"]
'
)
.
prop
(
"
selected
"
,
"
selected
"
);
if
(
$
(
'
#reservation-lists :first-child
'
).
attr
(
"
id
"
)
!=
nickname
)
{
$
(
'
#
'
+
nickname
).
fadeOut
(
"
fast
"
,
function
()
{
if
(
$
(
window
).
scrollTop
())
{
$
(
'
html, body
'
).
animate
({
scrollTop
:
'
0px
'
},
500
,
"
swing
"
,
function
()
{
$
(
'
#reservation-lists
'
)
.
prepend
(
$
(
'
#
'
+
nickname
));
$
(
'
#
'
+
nickname
)
.
fadeIn
(
"
fast
"
);
});
}
else
{
$
(
'
#reservation-lists
'
).
prepend
(
$
(
'
#
'
+
nickname
));
$
(
'
#
'
+
nickname
).
fadeIn
(
"
fast
"
);
}
});
}
aptforms
.
MarkFormUnsaved
();
}
/*
* Load anonymized reservations from each am in the list and
* generate tables.
*/
function
LoadReservations
()
{
var
count
=
Object
.
keys
(
amlist
).
length
;
_
.
each
(
amlist
,
function
(
details
,
urn
)
{
var
callback
=
function
(
json
)
{
//
console.log(json);
console
.
log
(
"
LoadReservations
"
,
json
);
// Kill the spinner.
count
--
;
...
...
@@ -251,48 +314,40 @@ $(function ()
details
.
name
+
"
:
"
+
json
.
value
);
return
;
}
var
reservations
=
json
.
value
;
if
(
reservations
.
length
==
0
)
return
;
$
(
'
#reservation-lists #
'
+
details
.
nickname
)
.
removeClass
(
"
hidden
"
);
// Generate the main template.
var
html
=
listTemplate
({
"
reservations
"
:
reservations
,
"
showidx
"
:
false
,
"
showproject
"
:
false
,
"
showuser
"
:
false
,
"
showusing
"
:
false
,
"
anonymous
"
:
true
,
"
name
"
:
details
.
name
,
});
html
=
"
<div class='row' id='
"
+
details
.
nickname
+
"
'>
"
+
"
<div class='col-xs-12 col-xs-offset-0'>
"
+
html
+
"
</div>
"
+
"
</div>
"
;
$
(
'
#reservation-lists
'
).
prepend
(
html
);
// Format dates with moment before display.
$
(
'
#
'
+
details
.
nickname
+
'
.format-date
'
).
each
(
function
()
{
var
date
=
$
.
trim
(
$
(
this
).
html
());
if
(
date
!=
""
)
{
$
(
this
).
html
(
moment
(
$
(
this
).
html
()).
format
(
"
lll
"
));
}
});
$
(
'
#
'
+
details
.
nickname
+
'
.tablesorter
'
)
.
tablesorter
({
theme
:
'
green
'
,
// initialize zebra
widgets
:
[
"
zebra
"
],
// When clicking on a graph, make it the current cluster.
if
(
!
editing
)
{
$
(
'
#
'
+
details
.
nickname
+
'
.panel-body
'
)
.
click
(
function
(
event
)
{
SetCluster
(
details
.
nickname
,
urn
);
});
}
ShowResGraph
({
"
forecast
"
:
json
.
value
.
forecast
,
"
selector
"
:
details
.
nickname
+
"
.timeseries-graph-panel
"
,
"
click_callback
"
:
SetDates
});
$
(
'
#
'
+
details
.
nickname
+
'
.resgraph-fullscreen
'
)
.
click
(
function
(
event
)
{
event
.
preventDefault
();
// Panel title in the modal.
$
(
'
#resgraph-modal .cluster-name
'
)
.
html
(
details
.
nickname
);
$
(
'
#resgraph-modal
'
).
on
(
'
shown.bs.modal
'
,
function
()
{
ShowResGraph
({
"
forecast
"
:
json
.
value
.
forecast
,
"
selector
"
:
"
resgraph-modal
"
,
"
click_callback
"
:
SetDates
});
});
sup
.
ShowModal
(
'
#resgraph-modal
'
,
function
()
{
$
(
'
#resgraph-modal
'
).
off
(
'
shown.bs.modal
'
);
});
});
// This activates the tooltip subsystem.
$
(
'
[data-toggle="tooltip"]
'
).
tooltip
({
placement
:
'
auto
'
,
});
}
var
xmlthing
=
sup
.
CallServerMethod
(
null
,
"
reserve
"
,
"
List
Reservation
s
"
,
"
Reservation
Info
"
,
{
"
cluster
"
:
details
.
nickname
,
"
anonymous
"
:
1
});
xmlthing
.
done
(
callback
);
...
...
@@ -385,7 +440,7 @@ $(function ()
function
PopulateReservation
()
{
var
callback
=
function
(
json
)
{
console
.
log
(
json
);
console
.
log
(
"
PopulateReservation
"
,
json
);
sup
.
HideWaitWait
();
if
(
json
.
code
)
{
sup
.
SpitOops
(
"
oops
"
,
json
.
value
);
...
...
@@ -502,6 +557,7 @@ $(function ()
*/
var
options
=
""
;
var
typelist
=
amlist
[
selected_cluster
].
typeinfo
;
var
nickname
=
amlist
[
selected_cluster
].
nickname
;
_
.
each
(
typelist
,
function
(
details
,
type
)
{
var
count
=
details
.
count
;
...
...
@@ -512,6 +568,13 @@ $(function ()
});
$
(
"
#reserve-request-form #type
"
)
.
html
(
"
<option value=''>Please Select</option>
"
+
options
);
if
(
$
(
'
#reservation-lists :first-child
'
).
attr
(
"
id
"
)
!=
nickname
)
{
$
(
'
#
'
+
nickname
).
fadeOut
(
"
fast
"
,
function
()
{
$
(
'
#reservation-lists
'
).
prepend
(
$
(
'
#
'
+
nickname
));
$
(
'
#
'
+
nickname
).
fadeIn
(
"
fast
"
);
});
}
}
// Toggle the button between check and submit.
...
...
www/aptui/js/resgraphs.js
0 → 100644
View file @
3ead4566
//
// Reservation timeline graph.
//
$
(
function
()
{
window
.
ShowResGraph
=
(
function
()
{
'
use strict
'
;
function
ProcessData
(
forecast
)
{
var
index
=
0
;
var
datums
=
[];
/*
* For the interactive tooltip to work, every has data set has to
* have same set of x axis values (timestamps). So we are first
* going to create a hash of hashes; the keys are the time stamps
* and the value is a hash of type => free for that timestamp.
*/
var
stamps
=
{};
// Each node type
for
(
var
type
in
forecast
)
{
// This is an array of objects.
var
array
=
forecast
[
type
];
if
(
array
.
length
==
1
)
{
if
(
parseInt
(
array
[
0
].
free
)
==
0
)
{
continue
;
}
array
.
push
(
$
.
extend
({},
array
[
0
]));
array
[
1
].
t
=
parseInt
(
array
[
1
].
t
)
+
(
30
*
3600
*
24
);
}
/*
* Hmm, Gary says there can be duplicate entries for the same
* time stamp, and we want the last one. So have to splice those
* out before we process. Yuck.
*/
if
(
array
.
length
>
1
)
{
var
temp
=
[];
for
(
var
i
=
0
;
i
<
array
.
length
-
1
;
i
++
)
{
var
data
=
array
[
i
];
var
nextdata
=
array
[
i
+
1
];
if
(
data
.
t
==
nextdata
.
t
)
{
continue
;
}
temp
.
push
(
data
);
}
// Tack on last one.
if
(
temp
[
temp
.
length
-
1
].
t
!=
array
[
array
.
length
-
1
].
t
)
{
temp
.
push
(
array
[
array
.
length
-
1
]);
}
array
=
temp
;
}
for
(
var
i
=
0
;
i
<
array
.
length
;
i
++
)
{
var
data
=
array
[
i
];
var
stamp
=
data
.
t
;
var
free
=
parseInt
(
data
.
free
);
if
(
!
_
.
has
(
stamps
,
stamp
))
{
stamps
[
stamp
]
=
{};
}
stamps
[
stamp
][
type
]
=
free
;
/*
* We want the changes to look like step functions not
* slopes, so each time we change add another entry for
* the previous second with the old free count.
*/
if
(
i
>
0
)
{
var
lastfree
=
parseInt
(
array
[
i
-
1
].
free
);
var
prevstamp
=
stamp
-
1
;
if
(
!
_
.
has
(
stamps
,
prevstamp
))
{
stamps
[
prevstamp
]
=
{};
}
stamps
[
prevstamp
][
type
]
=
lastfree
;
}
}
}
/*
* Well, this can happen; no datapoints cause no reservations
* and no experiments.
*/
if
(
Object
.
keys
(
stamps
).
length
==
0
)
{
return
null
;
}
/*
* Create a sorted (by timestamp) array of the per-stamp hashes.
*/
var
array
=
Object
.
keys
(
stamps
).
map
(
function
(
key
)
{
return
{
stamp
:
key
,
counts
:
stamps
[
key
]};
});
array
=
array
.
sort
(
function
(
obj1
,
obj2
)
{
// Ascending: first stamp less than the previous
return
obj1
.
stamp
-
obj2
.
stamp
;
});
/*
* Nuts, the first timestamp does not always include all the
* types. It should ... so fill those in with the first count
* we find in the ordered array.
*/
for
(
var
i
=
1
;
i
<
array
.
length
;
i
++
)
{
var
counts
=
array
[
i
].
counts
;
// Each node type
for
(
var
type
in
counts
)
{
if
(
!
_
.
has
(
array
[
0
].
counts
,
type
))
{
array
[
0
].
counts
[
type
]
=
counts
[
type
];
}
}
}
// The first array element now has all the types we want to graph.
var
types
=
Object
.
keys
(
array
[
0
].
counts
);
/*
* Okay, since each time stamp has to have data points for every
* type, go through each stamp and fill in the missing values from
* the immediately preceeding stamp. All of this to make the
* fancy tooltip work right! Sheesh.
*/
for
(
var
i
=
1
;
i
<
array
.
length
;
i
++
)
{
var
counts
=
array
[
i
].
counts
;
// Each node type
for
(
var
t
=
0
;
t
<
types
.
length
;
t
++
)
{
var
type
=
types
[
t
];
if
(
!
_
.
has
(
counts
,
type
))
{
counts
[
type
]
=
array
[
i
-
1
].
counts
[
type
];
}
}
}
//console.info(array);
/*
* Finally, create the series data for NVD3.
*/
for
(
var
t
=
0
;
t
<
types
.
length
;
t
++
)
{
var
type
=
types
[
t
];
var
values
=
[];
datums
[
index
++
]
=
{
"
key
"
:
type
,
"
area
"
:
0
,
"
values
"
:
values
,
};
for
(
var
i
=
0
;
i
<
array
.
length
;
i
++
)
{
var
stamp
=
array
[
i
].
stamp
;
var
counts
=
array
[
i
].
counts
;
values
[
i
]
=
{
// convert seconds to milliseconds.
"
x
"
:
stamp
*
1000
,
"
y
"
:
counts
[
type
],
};
}
}
return
datums
;
}
function
CreateGraph
(
datums
,
selector
,
click_callback
)
{
var
id
=
'
#
'
+
selector
;
$
(
id
).
removeClass
(
"
hidden
"
);
$
(
id
+
'
svg
'
).
html
(
""
);
window
.
nv
.
addGraph
(
function
()
{
var
chart
=
window
.
nv
.
models
.
lineChart
();
var
ylabel
=
"
Free Nodes
"
;
chart
.
margin
({
"
left
"
:
25
,
"
right
"
:
15
,
"
top
"
:
20
,
"
bottom
"
:
20
});
chart
.
xAxis
.
tickFormat
(
function
(
d
)
{
return
d3
.
time
.
format
(
'
%m/%d
'
)(
new
Date
(
d
))
});
//chart.yAxis.axisLabel(ylabel);
var
intformater
=
d3
.
format
(
'
,.0f
'
);
var
formatter
=
function
(
d
)
{
return
intformater
(
d
);
};
chart
.
yAxis
.
tickFormat
(
formatter
);
chart
.
useInteractiveGuideline
(
true
);
d3
.
select
(
id
+
'
svg
'
)
.
datum
(
datums
)
.
call
(
chart
);
// set up the tooltip to display full dates
var
tsFormat
=
d3
.
time
.
format
(
'
%b %-d, %I:%M%p
'
);
var
tooltip
=
chart
.
interactiveLayer
.
tooltip
;
tooltip
.
headerFormatter
(
function
(
d
)
{
return
tsFormat
(
new
Date
(
d
));
});
/*
* When user clicks in the graph, send the timestamp back
* to the caller for changing the form.
*/
if
(
click_callback
)
{
chart
.
lines
.
dispatch
.
on
(
"
elementClick
"
,
function
(
e
)
{
console
.
info
(
e
);
click_callback
(
new
Date
(
e
[
0
].
point
.
x
));
});
}
window
.
nv
.
utils
.
windowResize
(
chart
.
update
);
});
}
// Pass in forecast info for a single aggregate.
return
function
(
args
)
{
console
.
info
(
"
ShowResGraph
"
,
args
);
var
datums
=
ProcessData
(
args
.
forecast
);
if
(
datums
==
null
)
{
return
;
}
console
.
info
(
"
datums
"
,
datums
);
CreateGraph
(
datums
,
args
.
selector
,
args
.
click_callback
);
};
}
)();
});
www/aptui/js/resinfo.js
0 → 100644
View file @
3ead4566
$
(
function
()
{
'
use strict
'
;
var
template_list
=
[
"
resinfo
"
,
"
reservation-graph
"
,
"
oops-modal
"
,
"
waitwait-modal
"
];
var
templates
=
APT_OPTIONS
.
fetchTemplateList
(
template_list
);
var
oopsString
=
templates
[
"
oops-modal
"
];
var
waitwaitString
=
templates
[
"
waitwait-modal
"
];
var
mainTemplate
=
_
.
template
(
templates
[
"
resinfo
"
]);
var
graphTemplate
=
_
.
template
(
templates
[
"
reservation-graph
"
]);
var
amlist
=
null
;
var
isadmin
=
false
;
function
initialize
()
{
window
.
APT_OPTIONS
.
initialize
(
sup
);
isadmin
=
window
.
ISADMIN
;
amlist
=
JSON
.
parse
(
_
.
unescape
(
$
(
'
#amlist-json
'
)[
0
].
textContent
));
GeneratePageBody
();
// Now we can do this.
$
(
'
#oops_div
'
).
html
(
oopsString
);
$
(
'
#waitwait_div
'
).
html
(
waitwaitString
);
LoadReservations
();
}
//
function
GeneratePageBody
()
{
// Generate the template.
var
html
=
mainTemplate
({
amlist
:
amlist
,
isadmin
:
isadmin
,
});
$
(
'
#main-body
'
).
html
(
html
);
// Graph list.
$
(
'
#reservation-lists
'
)
.
html
(
graphTemplate
({
"
amlist
"
:
amlist
,
"
showcontrols
"
:
false
}));
// This activates the popover subsystem.
$
(
'
[data-toggle="popover"]
'
).
popover
({
trigger
:
'
hover
'
,
container
:
'
body
'
});
// This activates the tooltip subsystem.
$
(
'
[data-toggle="tooltip"]
'
).
tooltip
({
placement
:
'
auto
'
});
}
/*
* Load anonymized reservations from each am in the list and
* generate tables.
*/
function
LoadReservations
()
{
var
count
=
Object
.
keys
(
amlist
).
length
;
_
.
each
(
amlist
,
function
(
details
,
urn
)
{
var
callback
=
function
(
json
)
{
console
.
log
(
"
LoadReservations
"
,
json
);
// Kill the spinner.
count
--
;
if
(
count
<=
0
)
{
$
(
'
#spinner
'
).
addClass
(
"
hidden
"
);
}
if
(
json
.
code
)
{
console
.
log
(
"
Could not get reservation data for
"
+
details
.
name
+
"
:
"
+
json
.
value
);
return
;
}
$
(
'
#reservation-lists #
'
+
details
.
nickname
)
.
removeClass
(
"
hidden
"
);
ShowResGraph
({
"
forecast
"
:
json
.
value
.
forecast
,
"
selector
"
:
details
.
nickname
+