422 lines
13 KiB
JavaScript
422 lines
13 KiB
JavaScript
var state = {};
|
|
var localstate = {
|
|
manual: {}, recipe: 0, page: ['General', 'Dashboard'],
|
|
maxpoints: 1000,
|
|
editing: {}
|
|
};
|
|
var menu_items = [
|
|
{name: "General",
|
|
subitems:[
|
|
{name: "Dashboard"},
|
|
{name: "Sensors & Plots"}
|
|
]
|
|
},
|
|
{name: "Recipes",
|
|
subitems:[
|
|
{name: "Recipes Editor", link: 'edit-recipe'},
|
|
{name: "New Recipe"}
|
|
]
|
|
},
|
|
{name: "Configuration",
|
|
subitems:[
|
|
{name: "Settings"}
|
|
]
|
|
}
|
|
];
|
|
|
|
function applyState(newstate) {
|
|
if (newstate !== undefined) {
|
|
state = JSON.parse(JSON.stringify(newstate));
|
|
}
|
|
// preprocess
|
|
state.manual["dismissed"] = localstate.manual["dismissed"];
|
|
let selected_recipe = null;
|
|
for (let i = 0; i < state.recipes.length; i++) {
|
|
state.recipes[i].idx = i;
|
|
if (localstate.recipe == i) selected_recipe = state.recipes[i];
|
|
state.recipes[i].selected = localstate.recipe == i;
|
|
}
|
|
state.selected_recipe = JSON.parse(JSON.stringify(selected_recipe));
|
|
for (let i = 0; i < menu_items.length; i++) {
|
|
let group_match = menu_items[i].name == localstate.page[0];
|
|
for (let sub = 0; sub < menu_items[i].subitems.length; sub++) {
|
|
let sub_match = menu_items[i].subitems[sub].name == localstate.page[1];
|
|
menu_items[i].subitems[sub].current = group_match && sub_match;
|
|
menu_items[i].subitems[sub].path = JSON.stringify([
|
|
menu_items[i].name, menu_items[i].subitems[sub].name]);
|
|
}
|
|
}
|
|
state.menu_items = menu_items;
|
|
|
|
console.log('Apply state '+JSON.stringify(state));
|
|
// apply
|
|
render_sidebar(state);
|
|
manual_modal(state.manual);
|
|
current_recipe(state.recipe);
|
|
render_load_recipe(state);
|
|
render_plot();
|
|
localstate.editing.recipe = state.recipes.findIndex(
|
|
function (el) {return el.name == localstate.editing.recipe_name;});
|
|
}
|
|
|
|
var enable_draw = true;
|
|
var plot_data = {};
|
|
|
|
function add_plot_data(newpoints, render=true) {
|
|
for (var i = 0; i < newpoints.length; ++i) {
|
|
let point = newpoints[i]
|
|
let key = point[0];
|
|
if (!(key in plot_data)) {
|
|
plot_data[key] = {
|
|
x: [],
|
|
y: [],
|
|
type: 'scatter',
|
|
name: key
|
|
}
|
|
}
|
|
plot_data[key].x.push(point[1]);
|
|
plot_data[key].y.push(point[2]);
|
|
if (plot_data[key].x.length > localstate.maxpoints) {
|
|
plot_data[key].x.shift();
|
|
plot_data[key].y.shift();
|
|
}
|
|
}
|
|
if (render) render_plot();
|
|
}
|
|
|
|
function render_plot() {
|
|
if (enable_draw && document.getElementById("data-plot") !== null) {
|
|
Plotly.newPlot('data-plot', Object.values(plot_data));
|
|
}
|
|
}
|
|
|
|
|
|
function render_sensors(sensordata) {
|
|
let html = document.getElementById("sensor-list");
|
|
if (state.sensors === undefined || html === null) return;
|
|
let template = `<div class="content">
|
|
{{#sensors}}
|
|
<h4>{{what}}</h4>
|
|
{{^data}}No sensors found{{/data}}
|
|
<ul>
|
|
{{#data}}
|
|
<li ondblclick="alert('rename')">{{0}} -- {{1}} {{unit}}</li>
|
|
{{/data}}
|
|
</ul>
|
|
{{/sensors}}
|
|
</div>`;
|
|
let data = [];
|
|
for (let i = 0, k = Object.keys(state.sensors.units); i < k.length; ++i) {
|
|
let what = k[i];
|
|
let unit = state.sensors.units[k[i]];
|
|
let sensors = Object.keys(
|
|
Object.keys(state.sensors.found).reduce(function (filtered, key) {
|
|
if (state.sensors.found[key] == what) filtered[key] = state.sensors.found[key];
|
|
return filtered;
|
|
}, {}));
|
|
let req_data = [];
|
|
for (let j = 0; j < sensordata.length; ++j) {
|
|
if (sensors.includes(sensordata[j][0])) {
|
|
req_data.push([sensordata[j][0], sensordata[j][2]]);
|
|
}
|
|
}
|
|
data.push({what: what, unit: unit, data: req_data});
|
|
}
|
|
html.innerHTML = Mustache.render(template, {sensors:data});
|
|
}
|
|
|
|
function navigate(path, link) {
|
|
if (link !== undefined) {
|
|
window.location.href = "/" + link
|
|
return;
|
|
}
|
|
localstate.page = path;
|
|
applyState();
|
|
}
|
|
|
|
function render_sidebar(data) {
|
|
let html = document.getElementById("sidebar");
|
|
let template = `<aside class="menu">
|
|
{{#menu_items}}
|
|
<p class="menu-label">{{name}}</p>
|
|
<ul class="menu-list">
|
|
{{#subitems}}
|
|
<li><a onclick="navigate({{path}}, '{{link}}')"
|
|
class="{{#current}}is-active{{/current}}">{{name}}</a></li>
|
|
{{/subitems}}
|
|
</ul>
|
|
{{/menu_items}}
|
|
</aside>`
|
|
html.innerHTML = Mustache.render(template, data);
|
|
}
|
|
|
|
function respond_manual(response) {
|
|
socket.emit('manual response', response);
|
|
}
|
|
|
|
function select_recipe(idx) {
|
|
localstate.recipe = idx;
|
|
applyState();
|
|
}
|
|
|
|
function load_recipe() {
|
|
socket.emit('load recipe idx', localstate.recipe);
|
|
}
|
|
|
|
function edit_recipe(recipeid) {
|
|
let recipe = recipeid === undefined ? localstate.recipe : recipeid;
|
|
window.location.href = '/edit-recipe/'+recipe;
|
|
}
|
|
|
|
function currently_editing_recipe(name) {
|
|
localstate.editing.recipe_name = name;
|
|
}
|
|
|
|
function update_field(fieldname, newvalue) {
|
|
state.recipes[localstate.editing.recipe].phases[localstate.editing.phase][fieldname] = newvalue;
|
|
console.log(state.recipes[localstate.editing.recipe].phases[localstate.editing.phase]);
|
|
console.log(fieldname, newvalue);
|
|
}
|
|
|
|
function save_phase() {
|
|
let edited_phase = state.recipes[localstate.editing.recipe].phases[
|
|
localstate.editing.phase];
|
|
console.log(edited_phase);
|
|
socket.emit('update recipe phase',
|
|
localstate.editing.recipe, localstate.editing.phase, edited_phase)
|
|
}
|
|
|
|
function edit_phase(phaseid) {
|
|
console.log(localstate);
|
|
let html = document.getElementById("phase-editor");
|
|
if (html === null) return;
|
|
localstate.editing.phase = phaseid;
|
|
data = state.recipes[localstate.editing.recipe].phases[phaseid];
|
|
console.log(data.text);
|
|
let template = `<div class="field">
|
|
<label class="label">Phase Name</label>
|
|
<div class="control">
|
|
<input class="input"
|
|
oninput="update_field('name', this.value)"
|
|
type="text" placeholder="Phase Name" value="{{name}}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">Phase Description</label>
|
|
<div class="control">
|
|
<textarea class="textarea" placeholder="Phase Description"
|
|
oninput="update_field('text', this.value)">{{text}}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">On Load</label>
|
|
<div class="control">
|
|
<textarea
|
|
oninput="update_field('onload', this.value)"
|
|
class="textarea" placeholder="#t">{{onload}}</textarea>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="label">Next Condition</label>
|
|
<div class="control">
|
|
<textarea oninput="update_field('nextcond', this.value)"
|
|
class="textarea" placeholder="#t">{{nextcond}}</textarea>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="label">On Exit</label>
|
|
<div class="control">
|
|
<textarea oninput="update_field('onexit', this.value)"
|
|
class="textarea" placeholder="#t">{{onexit}}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-link"
|
|
onclick="save_phase();">Save</button>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-link is-light"
|
|
onclick="stop_editing_phase();">Cancel</button>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-danger"
|
|
onclick="delete_phase();">Delete</button>
|
|
</div>
|
|
</div>`;
|
|
html.innerHTML = Mustache.render(template, data);
|
|
}
|
|
|
|
function stop_recipe() {
|
|
socket.emit('stop recipe');
|
|
}
|
|
|
|
function next_phase() {
|
|
socket.emit('next phase');
|
|
}
|
|
|
|
function dismiss_modal() {
|
|
applyState(state);
|
|
}
|
|
|
|
function render_load_recipe(data) {
|
|
let html = document.getElementById("load-recipe");
|
|
if (html === null) return;
|
|
let template = `<div class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title">
|
|
Load Recipe
|
|
</p>
|
|
</header>
|
|
<div class="card-content">
|
|
<div class="tabs is-centered">
|
|
<ul>
|
|
{{#recipes}}
|
|
<li class="{{#selected}}is-active{{/selected}}">
|
|
<a onclick="select_recipe({{idx}})">{{name}}</a>
|
|
</li>
|
|
{{/recipes}}
|
|
</ul>
|
|
</div>
|
|
{{#selected_recipe}}
|
|
{{description}}
|
|
<br/><br/>
|
|
Controllers:<br/>
|
|
<ul>
|
|
{{#controllers}}
|
|
<li>{{.}}</li>
|
|
{{/controllers}}
|
|
</ul>
|
|
{{#phases}}
|
|
<br><br>
|
|
<div class="content"><h4>{{name}}</h4></div>
|
|
{{text}}<br>
|
|
On Load: {{onload}}<br>
|
|
Exit When: {{nextcond}}<br>
|
|
On Exit: {{onexit}}<br>
|
|
{{/phases}}
|
|
{{/selected_recipe}}
|
|
</div>
|
|
<footer class="card-footer">
|
|
<a onclick="load_recipe()"
|
|
class="card-footer-item is-primary">Load</a>
|
|
<a onclick="edit_recipe()"
|
|
class="card-footer-item">Edit</a>
|
|
</footer>
|
|
</div>`
|
|
html.innerHTML = Mustache.render(template, data);
|
|
}
|
|
|
|
function current_recipe(data) {
|
|
let html = document.getElementById("current-recipe");
|
|
if (html === null) return;
|
|
if (data === null) {
|
|
html.innerHTML = '';
|
|
return;
|
|
}
|
|
data.current_phase = data.phases.find(function (p) { return p.current; });
|
|
let template = `
|
|
<div class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title">
|
|
Active Recipe: {{name}}
|
|
</p>
|
|
</header>
|
|
<div class="card-content">
|
|
{{description}}
|
|
<div class="tabs is-centered">
|
|
<ul>
|
|
{{#phases}}
|
|
<li class="{{#current}}is-active{{/current}}"><a>{{name}}</a></li>
|
|
{{/phases}}
|
|
</ul>
|
|
</div>
|
|
{{current_phase.text}}
|
|
<br/>
|
|
<br/>
|
|
Next Cond: {{current_phase.nextcond}}
|
|
<br/>
|
|
On Load: {{current_phase.onload}}
|
|
<br/>
|
|
On Exit: {{current_phase.onexit}}
|
|
</div>
|
|
<footer class="card-footer">
|
|
<!-- <a class="card-footer-item is-primary">Pause</a> -->
|
|
<a onclick="next_phase()"
|
|
class="card-footer-item">Next Phase</a>
|
|
<a onclick="stop_recipe()"
|
|
class="card-footer-item">Stop</a>
|
|
</footer>
|
|
</div>`;
|
|
html.innerHTML = Mustache.render(template, data);
|
|
}
|
|
|
|
function manual_modal(data) {
|
|
let modal = document.getElementById("manual-modal");
|
|
let template = `<div class="modal {{^dismissed}}{{#required}}is-active{{/required}}{{/dismissed}}">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head">
|
|
<p class="modal-card-title">Manual Intervention Required</p>
|
|
<button class="delete" aria-label="close" onclick="dismiss_modal()"></button>
|
|
</header>
|
|
<section class="modal-card-body">
|
|
{{label}}
|
|
</section>
|
|
<footer class="modal-card-foot">
|
|
{{#options}}
|
|
<button class="button" onclick="respond_manual('{{value}}')">
|
|
{{tag}}
|
|
</button>
|
|
{{/options}}
|
|
</footer>
|
|
</div>
|
|
</div>`;
|
|
modal.innerHTML = Mustache.render(template, data);
|
|
}
|
|
|
|
|
|
let items = document.querySelectorAll('#items-list > li')
|
|
|
|
|
|
items.forEach(item => {
|
|
$(item).prop('draggable', true)
|
|
item.addEventListener('dragstart', dragStart)
|
|
item.addEventListener('drop', dropped)
|
|
item.addEventListener('dragenter', cancelDefault)
|
|
item.addEventListener('dragover', cancelDefault)
|
|
})
|
|
|
|
function dragStart (e) {
|
|
var index = $(e.target).index()
|
|
e.dataTransfer.setData('text/plain', index)
|
|
}
|
|
|
|
function dropped (e) {
|
|
cancelDefault(e)
|
|
|
|
// get new and old index
|
|
let oldIndex = e.dataTransfer.getData('text/plain')
|
|
let target = $(e.target)
|
|
let newIndex = target.index()
|
|
|
|
// remove dropped items at old place
|
|
let dropped = $(this).parent().children().eq(oldIndex).remove()
|
|
|
|
// insert the dropped items at new place
|
|
if (newIndex < oldIndex) {
|
|
target.before(dropped)
|
|
} else {
|
|
target.after(dropped)
|
|
}
|
|
}
|
|
|
|
function cancelDefault (e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
return false
|
|
}
|