use vue.js for the UI

This commit is contained in:
2018-04-02 09:18:41 +02:00
parent 22c99f8517
commit 5e53e667fe
9 changed files with 11607 additions and 159 deletions

View File

@@ -12,6 +12,10 @@ public class BadRequest extends RuntimeException {
super(message, cause);
}
public BadRequest(final String message) {
super(message);
}
public BadRequest(final Throwable cause) {
super(cause);
}

View File

@@ -1,7 +1,6 @@
package org.lucares.pdbui;
import java.text.Collator;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
@@ -12,6 +11,7 @@ import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import org.lucares.pdb.datastore.Proposal;
import org.lucares.pdb.plot.api.PlotSettings;
import org.lucares.pdbui.domain.AutocompleteProposal;
@@ -28,6 +28,7 @@ import org.lucares.recommind.logs.Plotter;
import org.lucares.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
@@ -51,9 +52,12 @@ public class PdbController implements HardcodedValues {
private final Plotter plotter;
private final PerformanceDb db;
private final ReentrantLock plotterLock = new ReentrantLock();
@Value("${mode.production:true}")
private boolean modeProduction;
public PdbController(final PerformanceDb db, final Plotter plotter) {
this.db = db;
this.plotter = plotter;
@@ -63,8 +67,10 @@ public class PdbController implements HardcodedValues {
public ModelAndView index() {
final String view = "main";
final Map<String, Object> model = new HashMap<>();
model.put("oldestValue", LocalDateTime.now().minusDays(7).format(DATE_FORMAT_BEGIN));
model.put("latestValue", LocalDateTime.now().format(DATE_FORMAT_END));
// model.put("oldestValue",
// LocalDateTime.now().minusDays(7).format(DATE_FORMAT_BEGIN));
// model.put("latestValue", LocalDateTime.now().format(DATE_FORMAT_END));
model.put("isProduction", modeProduction);
return new ModelAndView(view, model);
}
@@ -77,20 +83,21 @@ public class PdbController implements HardcodedValues {
PlotResponse createPlot(@RequestBody final PlotRequest request)
throws InternalPlottingException, InterruptedException {
final PlotSettings plotSettings = PlotSettingsTransformer
.toSettings(request);
final PlotSettings plotSettings = PlotSettingsTransformer.toSettings(request);
if (StringUtils.isBlank(plotSettings.getQuery())) {
throw new BadRequest("The query must not be empty!");
}
// TODO the UI should cancel requests that are in flight before sending a plot request
// TODO the UI should cancel requests that are in flight before sending a plot
// request
if (plotterLock.tryLock()) {
try {
final PlotResult result = plotter.plot(plotSettings);
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/"
+ result.getImageName();
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
LOGGER.trace("image url: {}", imageUrl);
return new PlotResponse(
DataSeries.toMap(result.getDataSeries()), imageUrl);
return new PlotResponse(DataSeries.toMap(result.getDataSeries()), imageUrl);
} catch (final NoDataPointsException e) {
throw new NotFoundException(e);
} finally {
@@ -115,7 +122,7 @@ public class PdbController implements HardcodedValues {
final int zeroBasedCaretIndex = caretIndex - 1;
final List<Proposal> proposals = db.autocomplete(query, zeroBasedCaretIndex);
final List<Proposal> nonEmptyProposals = CollectionUtils.filter(proposals, p -> p.hasResults() );
final List<Proposal> nonEmptyProposals = CollectionUtils.filter(proposals, p -> p.hasResults());
final List<AutocompleteProposal> autocompleteProposals = toAutocompleteProposals(nonEmptyProposals);
Collections.sort(autocompleteProposals, new AutocompleteProposalByValue());

View File

@@ -1,25 +1,22 @@
package org.lucares.pdbui.domain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class PlotResponse {
private List<String> imageUrls = new ArrayList<>();
private String imageUrl = "";
private Map<String, Integer> dataSeries;
public PlotResponse(final Map<String, Integer> dataSeries, final String... imageUrls) {
public PlotResponse(final Map<String, Integer> dataSeries, final String imageUrl) {
this.dataSeries = dataSeries;
this.imageUrls.addAll(Arrays.asList(imageUrls));
this.imageUrl = imageUrl;
}
public List<String> getImageUrls() {
return imageUrls;
public String getImageUrl() {
return imageUrl;
}
public void setImageUrls(final List<String> imageUrls) {
this.imageUrls = imageUrls;
public void setImageUrl(final String imageUrl) {
this.imageUrl = imageUrl;
}
public Map<String, Integer> getDataSeries() {
@@ -32,7 +29,6 @@ public class PlotResponse {
@Override
public String toString() {
return String.valueOf(imageUrls) + " " + dataSeries;
return imageUrl + " " + dataSeries;
}
}

View File

@@ -5,16 +5,9 @@ html {
font-size: 14px;
}
body{
display: grid;
height: 100vh;
margin: 0;
grid:
"search_field" auto
"search" auto
"navigation " auto
"result" 1fr
/ 1fr;
body {
margin:0;
padding:0;
}
@font-face {
@@ -29,6 +22,17 @@ body{
display: inline-block;
}
#app {
display: grid;
height: 100vh;
margin: 0;
grid:
"search" auto
"navigation " auto
"result" 1fr
/ 1fr;
}
#logo {
grid-area: logo;
font-size: 1.2em;
@@ -46,6 +50,11 @@ body{
grid-area: search;
background-color: #aaa;
padding-bottom:3px;
display: grid;
grid:
"search_field" auto
"filter_bar" auto
/ 1fr
}
#navigation {
@@ -55,26 +64,26 @@ body{
justify-content: space-between;
}
.autocomplete .active {
background-color: #AAA;
}
.autocomplete.open{
z-index:2;
#result {
grid-area: result;
background: #eee;
margin: 0;
padding: 0;
overflow: hidden;
position:relative;
}
/* scrollbars are nice, but with them an empty autocomplete box is shown
.autocomplete, #search-input-wrapper .autocomplete.open {
overflow-y: scroll;
#filter-bar {
grid-area: filter_bar;
}
*/
#search-input {
box-sizing: border-box;
border: 0;
width: 100%;
padding: 2px;
}
#search-limit-value {
width: 4em;
}
@@ -96,14 +105,7 @@ body{
margin-right:3px;
}
#result {
grid-area: result;
background: #eee;
margin: 0;
padding: 0;
overflow: hidden;
position:relative;
}
#result-image {
height: 100%;
@@ -134,6 +136,10 @@ body{
right: 0;
}
#result-error-message {
position: absolute;
top: 0;
}
.center
{

View File

@@ -0,0 +1,564 @@
'use strict';
window.onload=function(){
Vue.component('result-view', {
props: ['searchBar', 'resultView'],
methods: {
prev_image: function()
{
var splitBy = data.searchBar.splitBy;
if (splitBy['values'].length > 0)
{
splitBy['index'] = (splitBy['index']+ splitBy['values'].length-1) % splitBy['values'].length;
plotCurrent();
}
},
next_image: function()
{
var splitBy = data.searchBar.splitBy;
if (splitBy['values'].length > 0)
{
splitBy['index'] = (splitBy['index']+1) % splitBy['values'].length;
plotCurrent();
}
}
},
template: `
<div id="result">
<div id="prev_image" v-show="searchBar.splitBy.field" v-on:click="prev_image" title="Previous Plot"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div id="next_image" v-show="searchBar.splitBy.field" v-on:click="next_image" title="Next Plot"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
<div id="result-image"><img v-bind:src="resultView.imageUrl" v-if="resultView.imageUrl"/></div>
<div id="result-error-message" v-if="resultView.errorMessage">{{ resultView.errorMessage }}</div>
</div>`
});
Vue.component('navigation-bar', {
props: [],
methods: {
refresh: function()
{
plotCurrent();
},
zoomIn: function()
{
this.shiftDate(0.25);
this.zoom(0.5);
plotCurrent();
},
zoomOut: function()
{
this.shiftDate(-0.5);
this.zoom(2);
plotCurrent();
},
dateLeftShift: function()
{
this.shiftDate(-1);
plotCurrent();
},
dateHalfLeftShift: function()
{
this.shiftDate(-0.5);
plotCurrent();
},
dateHalfRightShift: function()
{
this.shiftDate(0.5);
plotCurrent();
},
dateRightShift: function()
{
this.shiftDate(1);
plotCurrent();
},
zoom: function(factor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = data.searchBar.dateRange;
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var newValue = value*factor;
while (newValue != Math.round(newValue)){
switch (period) {
case "second":
case "seconds":
if (value == 1) {
// we reached the smallest range
}
else if (value % 2 == 1){
value = value -1;
}
break;
case "minute":
case "minutes":
value = value * 60;
period = "seconds";
break;
case "hour":
case "hours":
value = value * 60;
period = "minutes";
break;
case "day":
case "days":
value = value * 24;
period = "hours";
break;
case "week":
case "weeks":
value = value * 7;
period = "days";
break;
case "month":
case "months":
value = value * 30;
period = "days";
break;
default:
console.log("unhandled value: "+ period);
break;
}
newValue = value*factor
}
data.searchBar.dateRange = newValue + " " + period;
}
}
},
shiftDate: function(directionalFactor)
{
var dateBefore = Date.parse(data.searchBar.dateFrom);
var newDate = this.shiftByInterval(dateBefore, directionalFactor);
data.searchBar.dateFrom = newDate.toString("yyyy-MM-dd HH:mm:ss");
},
shiftByInterval: function(date, directionalFactor)
{
if (!$('#search-date-range').is(":invalid")) {
var dateRange = data.searchBar.dateRange;
var tokens = dateRange.split(/ +/,2);
if(tokens.length == 2)
{
var value = parseInt(tokens[0]);
var period = tokens[1];
var config = {};
value = directionalFactor * value;
switch (period) {
case "second":
case "seconds":
config = { seconds: value };
break;
case "minute":
case "minutes":
config = { minutes: value };
break;
case "hour":
case "hours":
config = { minutes: 60*value };
break;
case "day":
case "days":
config = { days: value };
break;
case "week":
case "weeks":
config = { days: 7*value };
break;
case "month":
case "months":
config = { days: 30*value };
break;
default:
break;
}
var newDate = date.add(config);
return newDate;
}
}
return date;
}
},
template: `
<div id="navigation">
<button id="nav_left" v-on:click="dateLeftShift" title="Show Older Values"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half" v-on:click="dateHalfLeftShift" title="Show Older Values"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<div>
<button id="zoom_in" v-on:click="zoomIn" title="Zoom In"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="refresh" v-on:click="refresh" title="Refresh"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button id="zoom_out" v-on:click="zoomOut" title="Zoom Out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
</div>
<button id="nav_right_half" v-on:click="dateHalfRightShift" title="Show Newer Values"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right" v-on:click="dateRightShift" title="Show Newer Values"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
</div>`
});
Vue.component('group-by-item', {
props: ['groupBy'],
template: `
<select v-model="groupBy.selected" :id="groupBy.id">
<option
v-for="option in groupBy.options"
v-bind:value="option.value"
>{{ option.text }}</option>
</select>`
});
Vue.component('search-bar', {
props: ['searchBar'],
methods: {
plot: function (event) {
var vm = this;
if (this.searchBar.splitByKeys.selected){
this.splitQueries(function (fieldValues) {
data.searchBar.splitBy['values'] = fieldValues;
vm.enableSplitBy(fieldValues);
plotCurrent();
});
}else{
this.disableSplitBy();
plotCurrent();
}
},
dashboard: function (event) {
alert('dashboard');
},
enableSplitBy: function(fieldValues) {
data.searchBar.splitBy['field'] = data.searchBar.splitByKeys.selected;
data.searchBar.splitBy['values'] = fieldValues;
data.searchBar.splitBy['index'] = 0;
data.searchBar.splitBy['query'] = data.searchBar.query;
},
disableSplitBy: function() {
data.searchBar.splitBy['field'] = '';
data.searchBar.splitBy['values'] = [];
data.searchBar.splitBy['index'] = 0;
data.searchBar.splitBy['query'] = '';
},
splitQueries: function(successCallback)
{
var request = {};
request['query'] = data.searchBar.query;
var error = function(e) {
data.resultView.errorMessage = "FAILED: " + JSON.parse(e.responseText).message;
};
var url = "/fields/"+encodeURIComponent(data.searchBar.splitByKeys.selected)+"/values";
getJson(url, request, successCallback, error);
},
},
template: `
<form id="search-bar" v-on:submit.prevent.stop>
<div id="search-input-wrapper">
<input
id="search-input"
v-model="searchBar.query"
placeholder="field=value and anotherField=anotherValue"/>
</div>
<div id="filter-bar">
<label for="search-group-by-0">Group:</label>
<group-by-item
v-for="item in searchBar.groupByKeys"
v-bind:key="item.id"
v-bind:groupBy="item"
></group-by-item>
<!--v-bind="{ id: 'search-group-by-'+item.id, 'selected': item.selected, 'options':item.options }"-->
<label for="split-by">Split:</label>
<select id="split-by" v-model="searchBar.splitByKeys.selected">
<option
v-for="option in searchBar.splitByKeys.options"
v-bind:key="option.value"
v-bind:value="option.value"
>{{ option.text }}</option>
</select>
<div class="group">
<label for="search-limit-by">Limit:</label>
<select id="search-limit-by" v-model="searchBar.limitBy.selected">
<option value="NO_LIMIT" selected="selected">no limit</option>
<option value="MOST_VALUES">most values</option>
<option value="FEWEST_VALUES">fewest values</option>
<option value="MAX_VALUE">max value</option>
<option value="MIN_VALUE">min value</option>
</select>
<input
type="number"
id="search-limit-value"
name="search-limit-value"
min="1"
max="1000"
v-show="searchBar.limitBy.selected != 'NO_LIMIT'"
v-model="searchBar.limitBy.number"/>
</div>
<div class="group">
<label for="search-date-from">From Date:</label>
<input
id="search-date-from"
v-model="searchBar.dateFrom"
class="input_date"
type="text"
required="required"
pattern="\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\\d|3[0-1]) [0-2]\\d:[0-5]\\d:[0-5]\\d" />
</div>
<div class="group">
<label for="search-date-range">Interval:</label>
<input
id="search-date-range"
v-model="searchBar.dateRange"
type="text"
list="ranges"
required="required"
pattern="\\d+ (second|minute|hour|day|week|month)s?">
<datalist id="ranges">
<option value="60 seconds"/>
<option value="5 minutes"/>
<option value="1 hour"/>
<option value="1 day"/>
<option value="1 week"/>
<option value="1 month"/>
</datalist>
</div>
<div class="group">
<label for="search-y-axis-scale">Y-Axis:</label>
<select id="search-y-axis-scale" v-model="searchBar.axisScale">
<option value="LINEAR" selected="selected">linear</option>
<option value="LOG10">log 10</option>
<option value="LOG2">log 2</option>
</select>
</div>
<div class="group">
<label for="plot-type">Type:</label>
<select id="plot-type" v-model="searchBar.plotType">
<option value="SCATTER">Scatter</option>
<option value="PERCENTILES">Percentiles</option>
</select>
</div>
<div class="group" id="group-show-aggregate" v-show="searchBar.plotType == 'SCATTER'">
<label for="show-aggregate">Aggregate:</label>
<select id="show-aggregate" v-model="searchBar.showAggregate">
<option value="NONE">-</option>
<option value="PERCENTILES">percentiles</option>
</select>
</div>
<div class="group">
<input type="checkbox" id="key-outside" v-model="searchBar.keyOutside"/>
<label for="key-outside">Legend outside</label>
</div>
<div class="group">
<button
id="plot-submit"
title="Create Plot"
v-on:click.prevent.stop="plot"
><i class="fa fa-area-chart" aria-hidden="true"></i> Plot</button>
<!--
<button
id="dashboard-submit"
title="Create Dashboard"
v-on:click.prevent.stop="dashboard"
><i class="fa fa-object-group" aria-hidden="true"></i> Dashboard</button>
-->
</div>
</div>
</form>`
});
var rootView = new Vue({
el: '#app',
data: data,
created: function() {
var self = this;
var request = {};
var success = function(response){
var options = [{ text: '', value: '' }];
response.forEach( (item, index) => { options.push({text: item, value: item}); } );
for (var i = 0; i < 3; i++){
self.searchBar.groupByKeys.push({
'id': i,
'selected': '',
'options': options
});
}
self.searchBar.splitByKeys.options = options
};
var error = function(e) {
data.resultView.errorMessage = "FAILED: " + JSON.parse(e.responseText).message;
};
getJson("fields", request, success, error);
}
});
}
var data = {
searchBar: {
query: 'pod=vapfinra01 and method = ViewService.findFieldViewGroup',
groupByKeys: [],
splitByKeys: {
'selected': 'method',
'options': []
},
limitBy: {
'selected': 'NO_LIMIT',
'number': 10
},
dateFrom: '2018-01-05 09:03:00', //Date.now().add({ days: -7 }).toString("yyyy-MM-dd HH:mm:ss"),
dateRange: '1 week',
axisScale: 'LOG10',
plotType: 'SCATTER',
showAggregate: 'NONE',
keyOutside: false,
splitBy: {
field: '',
query: '',
values: [],
index: 0
},
navigation: {
prevNext: {
show: false
}
}
},
resultView: {
imageUrl: '',
errorMessage: ''
}
};
function plotCurrent()
{
//showLoadingIcon();
if (data.searchBar.splitBy['field']) {
var query = createQuery();
sendPlotRequest(query);
}else{
sendPlotRequest(data.searchBar.query);
}
}
function createQuery()
{
var splitBy = data.searchBar.splitBy;
var query = splitBy['query'];
if (query.length > 0) {
query = "("+query+") and "+splitBy['field']+ " = " +splitBy['values'][splitBy['index']];
} else {
query = splitBy['field']+ " = " +splitBy['values'][splitBy['index']];
}
return query;
}
function groupBy()
{
var result = [];
data.searchBar.groupByKeys.forEach(function(item) {
if (item.selected) {
result.push(item.selected);
}
});
return result;
}
function sendPlotRequest(query){
var request = {};
request['query'] = query;
request['height'] = $('#result-image').height();
request['width'] = $('#result-image').width();
request['groupBy'] = groupBy();
request['limitBy'] = data.searchBar.limitBy.selected;
request['limit'] = parseInt(data.searchBar.limitBy.number);
request['dateFrom'] = data.searchBar.dateFrom;
request['dateRange'] = data.searchBar.dateRange;
request['axisScale'] = data.searchBar.axisScale;
request['plotType'] = data.searchBar.plotType;
request['aggregate'] = data.searchBar.showAggregate;
request['keyOutside'] = data.searchBar.keyOutside;
var success = function(response){
data.resultView.imageUrl = response.imageUrl;
data.resultView.errorMessage = '';
};
var error = function(e) {
data.resultView.imageUrl = '';
if (e.status == 404){
data.resultView.errorMessage = "No data points found for query: " + query;
}
else if (e.status == 503){
data.resultView.errorMessage = "Too many parallel requests.";
}
else{
data.resultView.errorMessage = "FAILED: " + JSON.parse(e.responseText).message;
}
};
postJson("plots", request, success, error);
}
function postJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(requestData),
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback)
//.always(pauseInvaders)
;
}
function getJson(url, requestData, successCallback, errorCallback) {
$.ajax({
type: "GET",
url: url,
data: requestData,
contentType: 'application/json'
})
.done(successCallback)
.fail(errorCallback);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -2,10 +2,17 @@
<html>
<head>
<script type="text/javascript" src="js/jquery-3.2.0.min.js"></script>
<script type="text/javascript" src="js/search.js"></script>
<script type="text/javascript" src="js/autocomplete.js"></script>
<!--<script type="text/javascript" src="js/search.js"></script>-->
<script type="text/javascript" src="js/date.js"></script>
<script type="text/javascript" src="js/invaders.js"></script>
{{#isProduction}}
<script type="text/javascript" src="js/vue-2.5.16.min.js"></script>
{{/isProduction}}
{{^isProduction}}
<script type="text/javascript" src="js/vue-2.5.16-dev.js"></script>
{{/isProduction}}
<script type="text/javascript" src="js/ui.js"></script>
<link rel="stylesheet" type="text/css" href="css/typography.css">
<link rel="stylesheet" type="text/css" href="css/design.css">
<link rel="stylesheet" type="text/css" href="css/loading.css">
@@ -14,98 +21,10 @@
<link rel="stylesheet" type="text/css" href="css/invaders.css">
</head>
<body>
<div id="search-input-wrapper">
<input id="search-input" placeholder="field=value and anotherField=anotherValue" data-autocomplete="autocomplete"
data-autocomplete-empty-message="nothing found" value="pod=vapfinra01 and method = ViewService.findFieldViewGroup" />
</div>
<div id="search-bar">
<form>
<div id="search-settings-bar">
<div class="group">
<label for="search-group-by-1">Group:</label>
<select id="search-group-by-1"></select>
<select id="search-group-by-2"></select>
<select id="search-group-by-3"></select>
</div>
<label for="split-by">Split:</label>
<select id="split-by"></select>
<div class="group">
<label for="search-limit-by">Limit:</label>
<select id="search-limit-by">
<option value="NO_LIMIT" selected="selected">no limit</option>
<option value="MOST_VALUES">most values</option>
<option value="FEWEST_VALUES">fewest values</option>
<option value="MAX_VALUE">max value</option>
<option value="MIN_VALUE">min value</option>
</select>
<input type="number" id="search-limit-value" name="search-limit-value" min="1" max="1000" value="10"/>
</div>
<div class="group">
<label for="search-date-from">From Date:</label>
<input id="search-date-from" class="input_date" type="text" value="{{oldestValue}}" required="required" pattern="\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1]) [0-2]\d:[0-5]\d:[0-5]\d">
</div>
<div class="group">
<label for="search-date-range">Interval:</label>
<input id="search-date-range" type="text" list="ranges" required="required" value="1 week" pattern="\d+ (second|minute|hour|day|week|month)s?">
<datalist id="ranges">
<option value="60 seconds">
<option value="5 minutes">
<option value="1 hour">
<option value="1 day">
<option value="1 week">
<option value="1 month">
</datalist>
</div>
<div class="group">
<label for="search-y-axis-scale">Y-Axis:</label>
<select id="search-y-axis-scale">
<option value="LINEAR" selected="selected">linear</option>
<option value="LOG10">log 10</option>
<option value="LOG2">log 2</option>
</select>
</div>
<div class="group">
<label for="plot-type">Type:</label>
<select id="plot-type">
<option value="SCATTER" selected="selected">Scatter</option>
<option value="PERCENTILES">Percentiles</option>
</select>
</div>
<div class="group" id="group-show-aggregate">
<label for="show-aggregate">Aggregate:</label>
<select id="show-aggregate">
<option value="NONE" selected="selected">-</option>
<option value="PERCENTILES">percentiles</option>
</select>
</div>
<div class="group">
<input type="checkbox" id="key-outside" />
<label for="key-outside">Legend outside</label>
</div>
<button id="search-submit" title="Create Plot"><i class="fa fa-area-chart" aria-hidden="true"></i> Plot</button>
</div>
</form>
</div>
<div id="navigation">
<button id="nav_left" title="Show Older Values"><i class="fa fa-angle-double-left" aria-hidden="true"></i></button>
<button id="nav_left_half" title="Show Older Values"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<div>
<button id="zoom_in" title="Zoom In"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button id="refresh" title="Refresh"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<button id="zoom_out" title="Zoom Out"><i class="fa fa-minus-circle" aria-hidden="true"></i></button>
</div>
<button id="nav_right_half" title="Show Newer Values"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
<button id="nav_right" title="Show Newer Values"><i class="fa fa-angle-double-right" aria-hidden="true"></i></button>
</div>
<div id="result">
<div id="prev_image" title="Previous Plot"><i class="fa fa-angle-double-left" aria-hidden="true"></i></div>
<div id="next_image" title="Next Plot"><i class="fa fa-angle-double-right" aria-hidden="true"></i></div>
<div id="result-image"></div>
<div id="app">
<search-bar v-bind="{ 'searchBar': searchBar }"></search-bar>
<navigation-bar v-bind="{ 'searchBar': searchBar }"></navigation-bar>
<result-view v-bind="{ 'searchBar': searchBar, 'resultView': resultView }"></result-view>
</div>
</body>
</html>