This adds a calendar view where users can see a timeline of resources, showing when each resource is reserved. Implements: blueprint calendar-view Change-Id: I4f2649d6c9538037dff4747ef4a8210da3666354changes/62/805462/7
parent
eda400f1ec
commit
d6398f5e7a
@ -0,0 +1,23 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
host_reservation = (
|
||||
getattr(settings, 'OPENSTACK_BLAZAR_HOST_RESERVATION', {
|
||||
'enabled': True,
|
||||
'calendar_attribute': 'hypervisor_hostname',
|
||||
}))
|
||||
|
||||
floatingip_reservation = (
|
||||
getattr(settings, 'OPENSTACK_BLAZAR_FLOATINGIP_RESERVATION', {
|
||||
'enabled': False, }))
|
@ -0,0 +1,36 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Leases" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=calendar_title %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<form class="form-inline" name="blazar-calendar-controls">
|
||||
<div class="form-group calendar-group">
|
||||
<button class="btn btn-sm btn-default calendar-quickdays" data-calendar-days="1">1 {% trans "day" %}</button>
|
||||
<button class="btn btn-sm btn-default calendar-quickdays" data-calendar-days="7">7 {% trans "days" %}</button>
|
||||
<button class="btn btn-sm btn-default calendar-quickdays" data-calendar-days="30">30 {% trans "days" %}</button>
|
||||
</div>
|
||||
<div class="form-group calendar-group">
|
||||
<label for="dateStart">{% trans "Start" %}</label>
|
||||
<input class="form-control calendar-datebox-date" type="text" name="dateStart" id="dateStart" placeholder="MM/DD/YYYY">
|
||||
<input class="form-control calendar-datebox-hour" type="number" min="00" max="23" name="timeStartHours" id="timeStartHours" placeholder="hh">
|
||||
:
|
||||
00
|
||||
</div>
|
||||
<div class="form-group calendar-group">
|
||||
<label for="dateEnd">{% trans "End" %}</label>
|
||||
<input class="form-control calendar-datebox-date" type="text" name="dateEnd" id="dateEnd" placeholder="MM/DD/YYYY">
|
||||
<input class="form-control calendar-datebox-hour" type="number" min="00" max="23" name="timeEndHours" id="timeEndHours" placeholder="hh">
|
||||
:
|
||||
00
|
||||
</div>
|
||||
</form>
|
||||
<div class="blazar-calendar" id="blazar-calendar-{{resource_type}}">
|
||||
<div class="text-center">
|
||||
<h2>{% trans "Loading Reservations" %}<br><i class="fa fa-spinner fa-spin"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,155 @@
|
||||
(function(window, horizon, $, undefined) {
|
||||
'use strict';
|
||||
|
||||
const CHART_TITLE_HEIGHT = 68;
|
||||
const ROW_HEIGHT = 60;
|
||||
|
||||
const selector = '#blazar-calendar-host';
|
||||
const pluralResourceType = gettext("Hosts");
|
||||
if ($(selector).length < 1) return;
|
||||
const calendarElement = $(selector);
|
||||
const form = $('form[name="blazar-calendar-controls"]');
|
||||
|
||||
function init() {
|
||||
calendarElement.addClass('loaded');
|
||||
$.getJSON("resources.json")
|
||||
.done(function(resp) {
|
||||
const rowAttr = resp.row_attr;
|
||||
// For this row shows up at all, we need at least 1 data point.
|
||||
const reservationsById = {}
|
||||
resp.reservations.forEach(function(reservation){
|
||||
if(!(reservation.reservation_id in reservationsById)){
|
||||
reservationsById[reservation.reservation_id] = reservation
|
||||
reservation.name = reservation.reservation_id
|
||||
reservation.data = []
|
||||
}
|
||||
const newReservation = {
|
||||
'start_date': new Date(reservation.start_date),
|
||||
'end_date': new Date(reservation.end_date),
|
||||
'x': reservation[rowAttr],
|
||||
'y': [
|
||||
new Date(reservation.start_date).getTime(),
|
||||
new Date(reservation.end_date).getTime()
|
||||
],
|
||||
}
|
||||
reservationsById[reservation.reservation_id].data.push(newReservation)
|
||||
})
|
||||
reservationsById["0"] = {"name": "0", "data": []}
|
||||
resp.resources.forEach(function(resource){
|
||||
reservationsById["0"].data.push({x: resource[rowAttr], y: [0, 0]})
|
||||
})
|
||||
const allReservations = Object.values(reservationsById)
|
||||
constructCalendar(allReservations, computeTimeDomain(7), resp.resources)
|
||||
})
|
||||
.fail(function() {
|
||||
calendarElement.html(`<div class="alert alert-danger">${gettext("Unable to load reservations")}.</div>`);
|
||||
});
|
||||
|
||||
function constructCalendar(rows, timeDomain, resources){
|
||||
calendarElement.empty();
|
||||
const options = {
|
||||
series: rows,
|
||||
chart: {
|
||||
type: 'rangeBar',
|
||||
toolbar: {show: false},
|
||||
zoom: {enabled: false, type: 'xy'},
|
||||
height: ROW_HEIGHT * resources.length + CHART_TITLE_HEIGHT,
|
||||
width: "100%",
|
||||
},
|
||||
plotOptions: { bar: {horizontal: true, rangeBarGroupRows: true}},
|
||||
xaxis: { type: 'datetime' },
|
||||
legend: { show: false },
|
||||
tooltip: {
|
||||
custom: function({series, seriesIndex, dataPointIndex, w}) {
|
||||
const datum = rows[seriesIndex];
|
||||
const resourcesReserved = datum.data.map(function(el){ return el.x }).join("<br>");
|
||||
const project_dt = "";
|
||||
if(datum.project_id){
|
||||
project_dt = `<dt>${gettext("Project")}</dt>
|
||||
<dd>${datum.project_id}</dd>`;
|
||||
}
|
||||
return `<div class='tooltip-content'><dl>
|
||||
${project_dt}
|
||||
<dt>${pluralResourceType}</dt>
|
||||
<dd>${resourcesReserved}</dd>
|
||||
<dt>${gettext("Reserved")}</dt>
|
||||
<dd>${datum.start_date} <strong>${gettext("to")}</strong> ${datum.end_date}</dd>
|
||||
</dl></div>`;
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
xaxis: [
|
||||
{
|
||||
x: new Date().getTime(),
|
||||
borderColor: '#00E396',
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
const chart = new ApexCharts(document.querySelector(selector), options);
|
||||
chart.render();
|
||||
|
||||
setTimeDomain(timeDomain, chart); // Also sets the yaxis limits
|
||||
|
||||
$('input[data-datepicker]', form).datepicker({
|
||||
dateFormat: 'mm/dd/yyyy'
|
||||
});
|
||||
|
||||
$('input', form).on('change', function() {
|
||||
if (form.hasClass('time-domain-processed')) {
|
||||
const timeDomain = getTimeDomain();
|
||||
// If invalid ordering is chosen, set period to 1 day
|
||||
if (timeDomain[0] >= timeDomain[1]) {
|
||||
timeDomain[1] = d3.time.day.offset(timeDomain[0], +1);
|
||||
}
|
||||
setTimeDomain(timeDomain, chart);
|
||||
}
|
||||
});
|
||||
|
||||
$('.calendar-quickdays').click(function() {
|
||||
const days = parseInt($(this).data("calendar-days"));
|
||||
if (!isNaN(days)) {
|
||||
const timeDomain = computeTimeDomain(days);
|
||||
setTimeDomain(timeDomain, chart);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function computeTimeDomain(days) {
|
||||
const padFraction = 1/8; // chart default is 3 hours for 1 day
|
||||
return [
|
||||
d3.time.day.offset(Date.now(), -days * padFraction),
|
||||
d3.time.day.offset(Date.now(), days * (1 + padFraction))
|
||||
];
|
||||
}
|
||||
|
||||
function setTimeDomain(timeDomain, chart) {
|
||||
// Set the input elements
|
||||
form.removeClass('time-domain-processed');
|
||||
$('#dateStart').datepicker('setDate', timeDomain[0]);
|
||||
$('#timeStartHours').val(timeDomain[0].getHours());
|
||||
$('#dateEnd').datepicker('setDate', timeDomain[1]);
|
||||
$('#timeEndHours').val(timeDomain[1].getHours());
|
||||
form.addClass('time-domain-processed');
|
||||
const options = { yaxis: {min: timeDomain[0].getTime(), max: timeDomain[1].getTime()}}
|
||||
chart.updateOptions(options)
|
||||
}
|
||||
|
||||
function getTimeDomain() {
|
||||
const timeDomain = [
|
||||
$('#dateStart').datepicker('getDate'),
|
||||
$('#dateEnd').datepicker('getDate')
|
||||
];
|
||||
|
||||
timeDomain[0].setHours($('#timeStartHours').val());
|
||||
timeDomain[0].setMinutes(0);
|
||||
timeDomain[1].setHours($('#timeEndHours').val());
|
||||
timeDomain[1].setMinutes(0);
|
||||
|
||||
return timeDomain;
|
||||
}
|
||||
}
|
||||
|
||||
horizon.addInitFunction(init);
|
||||
|
||||
})(window, horizon, jQuery);
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,55 @@
|
||||
.blazar-calendar {
|
||||
width: 100%;
|
||||
min-height: 800px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blazar-calendar .chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 300px;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.apexcharts-series {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.apexcharts-series.apexcharts-active {
|
||||
stroke: yellow;
|
||||
}
|
||||
|
||||
.apexcharts-xaxis-annotations line {
|
||||
stroke: #A8DE52;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.apexcharts-yaxis-label {
|
||||
stroke: rgba(#000, 0.25);
|
||||
}
|
||||
|
||||
.axis line,
|
||||
.axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-width: 2;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.calendar-group {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.form-control.calendar-datebox-date {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.form-control.calendar-datebox-hour {
|
||||
width: 55px;
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
.inline-groups > .control-group {
|
||||
display: inline-block;
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.control-sublabel {
|
||||
color: #999;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
features:
|
||||
- |
|
||||
A calendar view of host availability is added. Any reservable host will
|
||||
be shown in its own row, over a period of time. Allocations for the host
|
||||
are indicated along the horizontal axis.
|
Loading…
Reference in new issue