Merge pull request #488 from alanbernstein/webui-updates

Webui updates
This commit is contained in:
alanbernstein 2017-04-29 19:06:19 -05:00 committed by GitHub
commit 5593add463
18 changed files with 830 additions and 5 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ default.etcd/
*.test
vendor
.protoc-gen-gofast
.DS_Store

View file

@ -1,6 +1,7 @@
.PHONY: glide vendor-update docker pilosa crossbuild install generate
.PHONY: glide vendor-update docker pilosa crossbuild install generate statik
GLIDE := $(shell command -v glide 2>/dev/null)
STATIK := $(shell command -v statik 2>/dev/null)
PROTOC := $(shell command -v protoc 2>/dev/null)
VERSION := $(shell git describe --tags 2> /dev/null || echo unknown)
IDENTIFIER := $(VERSION)-$(GOOS)-$(GOARCH)
@ -41,14 +42,24 @@ install: vendor
.protoc-gen-gofast: vendor
ifndef PROTOC
$(error "protoc is not available please install protoc from https://github.com/google/protobuf/releases")
$(error "protoc is not available. please install protoc from https://github.com/google/protobuf/releases")
endif
go build -o .protoc-gen-gofast ./vendor/github.com/gogo/protobuf/protoc-gen-gofast
cp ./.protoc-gen-gofast $(GOPATH)/bin/protoc-gen-gofast
generate: .protoc-gen-gofast
generate-protoc: .protoc-gen-gofast
go generate github.com/pilosa/pilosa/internal
generate-statik: statik
go generate github.com/pilosa/pilosa
generate: generate-protoc generate-statik
statik:
ifndef STATIK
go get github.com/rakyll/statik
endif
docker:
docker build -t "pilosa:$(VERSION)" \
--build-arg ldflags="-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" .

8
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: 4bdea17c62dcd469584382515052e7a246fae6deefc2d62da70bf768e18e1f0c
updated: 2017-04-19T10:51:10.409094081-05:00
hash: 5066da70d6f312d032800619798684a693851eb77991b3788f68e89dfaf93925
updated: 2017-04-25T15:31:49.712775923-05:00
imports:
- name: github.com/armon/go-metrics
version: 97c69685293dce4c0a2d0b19535179bbc976e4d2
@ -66,6 +66,10 @@ imports:
version: c37440a7cf42ac63b919c752ca73a85067e05992
- name: github.com/pelletier/go-toml
version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a
- name: github.com/rakyll/statik
version: 89fe3459b5c829c32e89bdff9c43f18aad728f2f
subpackages:
- fs
- name: github.com/satori/go.uuid
version: 879c5887cd475cd7864858769793b2ceb0d44feb
- name: github.com/spf13/afero

View file

@ -31,5 +31,9 @@ import:
- package: github.com/spf13/viper
- package: github.com/gorilla/mux
version: ^1.3.0
- package: github.com/rakyll/statik
version: 89fe3459b5c829c32e89bdff9c43f18aad728f2f
subpackages:
- fs
- package: github.com/hashicorp/memberlist
- package: golang.org/x/sync

View file

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:generate statik -src=./webui
package pilosa
import (
@ -37,6 +39,9 @@ import (
"github.com/gorilla/mux"
"github.com/pilosa/pilosa/internal"
"github.com/pilosa/pilosa/pql"
_ "github.com/pilosa/pilosa/statik"
"github.com/rakyll/statik/fs"
)
// Handler represents an HTTP handler.
@ -75,6 +80,8 @@ func NewHandler() *Handler {
// NewRouter creates a Gorilla Mux http router.
func NewRouter(handler *Handler) *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/", handler.handleWebUI).Methods("GET")
router.HandleFunc("/assets/{file}", handler.handleWebUI).Methods("GET")
router.HandleFunc("/index", handler.handleGetIndexes).Methods("GET")
router.HandleFunc("/index/{index}", handler.handleGetIndex).Methods("GET")
router.HandleFunc("/index/{index}", handler.handlePostIndex).Methods("POST")
@ -122,6 +129,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Router.ServeHTTP(w, r)
}
func (h *Handler) handleWebUI(w http.ResponseWriter, r *http.Request) {
// If user is using curl, don't chuck HTML at them
if strings.HasPrefix(r.UserAgent(), "curl") {
http.Error(w, "Welcome. Pilosa is running. Visit https://www.pilosa.com/docs/ for more information or try the WebUI by visiting this URL in your browser.", http.StatusNotFound)
return
}
statikFS, err := fs.New()
if err != nil {
h.writeQueryResponse(w, r, &QueryResponse{Err: err})
fmt.Println("Pilosa WebUI is not available. Please run `make generate-statik` before building Pilosa with `make install`.")
return
}
http.FileServer(statikFS).ServeHTTP(w, r)
}
// handleGetSchema handles GET /schema requests.
func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(getSchemaResponse{

1
statik/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
statik.go

3
statik/doc.go Normal file
View file

@ -0,0 +1,3 @@
// Package statik contains static assets for the Web UI. `go generate` will
// produce statik.go, which is ignored by git.
package statik

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

390
webui/assets/main.js Normal file
View file

@ -0,0 +1,390 @@
class REPL {
constructor(input, output, button) {
this.input = input
this.output = output
this.button = button
this.history = []
this.history_index = 0
this.history_buffer = ''
this.result_number = 0
}
bind_events() {
var repl = this
var keys = {
TAB: 9,
ENTER: 13,
UP_ARROW: 38,
DOWN_ARROW: 40
}
var keywords = {
// keyword: length of substring that comes after cursor
"SetBit()": 1,
"ClearBit()": 1,
"SetBitmapAttrs()": 1,
"Bitmap()": 1,
"Union()": 1,
"Intersect()": 1,
"Difference()": 1,
"Count()": 1,
"Range()": 1,
"TopN()": 1,
"frame=": 0,
}
this.input.addEventListener("keydown", function(e) {
if (e.keyCode == keys.UP_ARROW) {
e.preventDefault()
if (repl.input.value.substring(0, repl.input.selectionStart).indexOf('\n') == '-1') {
if (repl.history_index == 0) {
return
} else {
if (repl.history_index == repl.history.length) {
repl.history_buffer = repl.input.value
}
repl.history_index--
repl.input.value = repl.history[repl.history_index]
repl.input.setSelectionRange(repl.input.value.length, repl.input.value.length)
}
}
}
if (e.keyCode == keys.DOWN_ARROW) {
e.preventDefault()
if (repl.input.value.substring(repl.input.selectionEnd, repl.input.length).indexOf('\n') == '-1') {
if (repl.history_index == repl.history.length) {
return
} else {
repl.history_index++
if (repl.history_index == repl.history.length) {
repl.input.value = repl.history_buffer
} else {
repl.input.value = repl.history[repl.history_index]
}
repl.input.setSelectionRange(repl.input.value.length, repl.input.value.length)
}
}
}
if (e.keyCode == keys.ENTER && !e.shiftKey) {
e.preventDefault()
repl.submit();
}
if (e.keyCode == keys.TAB) {
e.preventDefault()
// extract word fragment ending at cursor. a word fragment:
// - starts with last nonalpha character before cursor (or beginning of string)
// - ends at cursor
var word_start = repl.input.selectionEnd-1
while(word_start>0) {
var c = repl.input.value.charCodeAt(word_start)
if(!((c>64 && c<91) || (c>96 && c<123))) {
word_start++
break
}
word_start--
}
var input_word = repl.input.value.substring(word_start, repl.input.selectionEnd)
// check for keyword match and insert
// this just stops at the first match
for(var keyword in keywords) {
if(keyword.startsWith(input_word)){
var cursor_pos = repl.input.selectionEnd
var completion = keyword.substring(input_word.length)
var before = repl.input.value.substring(0, cursor_pos)
var after = repl.input.value.substring(cursor_pos)
repl.input.value = before + completion + after
var new_pos = cursor_pos + completion.length - keywords[keyword]
repl.input.setSelectionRange(new_pos, new_pos)
break
}
}
}
})
repl.button.onclick = function() {
repl.submit();
};
}
submit() {
this.history_buffer = ''
this.history_index = this.history.length
this.history[this.history_index] = this.input.value
this.history_index++
this.process_query(this.input.value)
this.input.value = ""
}
process_query(query) {
var xhr = new XMLHttpRequest();
var e = document.getElementById("index-dropdown");
var indexname = e.options[e.selectedIndex].text;
xhr.open('POST', '/index/' + indexname + '/query');
xhr.setRequestHeader('Content-Type', 'application/text');
var repl = this
var start_time = new Date().getTime();
xhr.send(query)
xhr.onload = function() {
var end_time = new Date().getTime()
repl.result_number++
repl.createSingleOutput({
"input": query,
"output": xhr.responseText,
"indexname": indexname,
"querytime_ms": end_time - start_time,
})
}
}
createSingleOutput(res) {
var node = document.createElement("div");
node.classList.add('output');
var output_string = res['output']
var output_json = JSON.parse(output_string)
var result_class = "result-output"
var getting_started_errors = [
'index not found',
'frame not found',
]
if("error" in output_json) {
result_class = "result-error"
if(getting_started_errors.indexOf(output_json['error']) >= 0) {
output_string += `<br />
<br />
Just getting started? Try this:<br />
$ curl -XPOST "http://127.0.0.1:10101/index/test" -d '{"options": {"columnLabel": "col"}}' # create index "test"<br />
$ curl -XPOST "http://127.0.0.1:10101/index/test/frame/foo" -d '{"options": {"rowLabel": "row"}}' # create frame "foo"<br />
# Select "test" in the index dropdown above<br />
SetBit(row=0, col=0, frame=foo) # Use PQL to set a bit
`
}
}
var markup =`
<div class="panes">
<div class="pane active">
<div class="result-io">
<div class="result-io-header">
<h5>Input</h5>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<em>Source: ${res.indexname}</em>
</div>
<div class="result-input">
${res.input}
</div>
</div>
<div class="result-io">
<div class="result-io-header">
<h5>output</h5>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<em>${res.querytime_ms} ms</em>
</div>
<div class="${result_class}">
${output_string}
</div>
</div>
</div>
<div class="pane">
<div class="raw">
<a href="${res.url}">Export raw .csv</a>
</div>
</div>
</div>
`
node.innerHTML = markup;
this.output.insertBefore(node, this.output.firstChild)
}
populate_index_dropdown() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/schema')
var select = document.getElementById('index-dropdown')
xhr.onload = function() {
var schema = JSON.parse(xhr.responseText)
for(var i=0; i<schema['indexes'].length; i++) {
var opt = document.createElement('option')
opt.value = i+1
opt.innerHTML = schema['indexes'][i]['name']
select.appendChild(opt)
}
// set the active option to one of the populated options, instead of invalid "Index"
if(i > 0) {
select.value = 1;
}
}
xhr.send(null)
}
}
function populate_version() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/version')
var node = document.getElementById('server-version')
xhr.onload = function() {
version = JSON.parse(xhr.responseText)['version']
node.innerHTML = version
}
xhr.send(null)
}
function handle_nav_click(e) {
// e.id = "nav-xxx"
name = e.id.substring(4)
set_active_pane_by_name(name)
window.location.hash = name
}
function set_active_pane_by_name(name) {
// toggle the nav buttons
document.getElementsByClassName("nav-active")[0].classList.remove("nav-active")
document.getElementById("nav-" + name).classList.add("nav-active")
// toggle the main interface content divs
document.getElementsByClassName("interface-active")[0].classList.remove("interface-active")
document.getElementById('interface-' + name).classList.add("interface-active")
// hack hack
switch(name) {
case "cluster":
update_cluster_status()
break
case "documentation":
open_external_docs()
break
}
}
function update_cluster_status() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/status')
status_node = document.getElementById('status')
xhr.onload = function() {
var status = JSON.parse(xhr.responseText)
render_status(status)
}
xhr.send(null)
}
function render_status(status) {
// render node table
var nodes_div = document.getElementById("status-nodes")
while (nodes_div.firstChild) {
nodes_div.removeChild(nodes_div.firstChild);
}
var nodes = status["status"]["Nodes"]
table = document.createElement("table")
tbody = document.createElement("tbody")
table.appendChild(tbody)
var caption = document.createElement("caption")
caption.innerHTML = "(" + nodes.length + ")"
table.appendChild(caption)
var header = document.createElement('tr')
markup = `<th>Host</th>
<th>State</th>`
header.innerHTML = markup
tbody.appendChild(header)
for(var n=0; n<nodes.length; n++) {
var row = document.createElement("tr")
markup = `<td>${nodes[n]["Host"]}</td>
<td>${nodes[n]["State"]}</td>`
row.innerHTML = markup
tbody.appendChild(row)
}
nodes_div.appendChild(table)
// render index tables
var indexes_div = document.getElementById("status-indexes")
while (indexes_div.firstChild) {
indexes_div.removeChild(indexes_div.firstChild);
}
var indexes = nodes[0]["Indexes"] // TODO currently comes from only node 0
for(var n=0; n<indexes.length; n++) {
table = document.createElement("table")
tbody = document.createElement("tbody")
table.appendChild(tbody)
var caption = document.createElement("caption")
caption.innerHTML = indexes[n]["Name"] + " (Column Label: " + indexes[n]["Meta"]["ColumnLabel"] + ")"
table.appendChild(caption)
var header = document.createElement('tr')
markup = `<th>Name</th>
<th>Row Label</th>
<th>Cache Type</th>
<th>Cache Size</th>`
header.innerHTML = markup
tbody.appendChild(header)
var frames = indexes[n]["Frames"]
if(frames) {
for(var m=0; m<frames.length; m++) {
var row = document.createElement("tr")
row.innerHTML = `<td>${frames[m]["Name"]}</td>
<td>${frames[m]["Meta"]["RowLabel"]}</td>
<td>${frames[m]["Meta"]["CacheType"]}</td>
<td>${frames[m]["Meta"]["CacheSize"]}</td>`
tbody.appendChild(row)
}
}
indexes_div.appendChild(table)
}
// render slice tables
// TODO enable when Slices element is present in status response
/*
var slices_div = document.getElementById("status-slices")
data = ""
for(var n=0; n<nodes.length; n++) {
var indexes = nodes[n]["Indexes"]
for(var m=0; m<indexes.length; m++) {
data += nodes[n]["Host"] + "/" + indexes[m]["Name"] + ": " + indexes[m]["Slices"] + "<br />"
}
}
slices_div.innerHTML = data
*/
}
function open_external_docs() {
window.open("https://www.pilosa.com/docs");
}
function check_anchor_uri() {
var pane_names = {"console": 0, "cluster": 0, "documentation": 0}
var anchor = window.location.hash.substr(1);
if(anchor in pane_names) {
set_active_pane_by_name(anchor)
}
}
Date.prototype.today = function () {
return this.getFullYear() +"/"+ (((this.getMonth()+1) < 10)?"0":"") + (this.getMonth()+1) +"/"+ ((this.getDate() < 10)?"0":"") + this.getDate();
}
Date.prototype.timeNow = function () {
return ((this.getHours() < 10)?"0":"") + this.getHours() +":"+ ((this.getMinutes() < 10)?"0":"") + this.getMinutes() +":"+ ((this.getSeconds() < 10)?"0":"") + this.getSeconds();
}
populate_version()
var input = document.getElementById('query')
var output = document.getElementById('outputs')
var button = document.getElementById('query-btn')
repl = new REPL(input, output, button)
repl.populate_index_dropdown()
repl.bind_events()
input.focus()
check_anchor_uri()

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100.75 36.67"><defs><style>.cls-1{fill:#1db598;}.cls-2{fill:none;stroke:#3c5f8d;stroke-miterlimit:10;stroke-width:5px;}</style></defs><title>nav_cluster_1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><circle class="cls-1" cx="55.14" cy="18.24" r="9.61"/><circle class="cls-1" cx="18.33" cy="18.33" r="9.61"/><circle class="cls-1" cx="91.14" cy="18.24" r="9.61"/><circle class="cls-2" cx="18.33" cy="18.33" r="15.83"/></g></g></svg>

After

Width:  |  Height:  |  Size: 521 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100.75 36.67"><defs><style>.cls-1{fill:#1db598;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:5px;}</style></defs><title>nav_cluster_1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><circle class="cls-1" cx="55.14" cy="18.24" r="9.61"/><circle class="cls-1" cx="18.33" cy="18.33" r="9.61"/><circle class="cls-1" cx="91.14" cy="18.24" r="9.61"/><circle class="cls-2" cx="18.33" cy="18.33" r="15.83"/></g></g></svg>

After

Width:  |  Height:  |  Size: 517 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78.47 48.95"><defs><style>.cls-1{fill:none;stroke:#3c5f8d;stroke-miterlimit:10;stroke-width:6px;}.cls-2{fill:#1db598;}</style></defs><title>nav_console</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" x="3" y="3" width="72.47" height="42.95"/><circle class="cls-2" cx="39.25" cy="25.92" r="9.61"/></g></g></svg>

After

Width:  |  Height:  |  Size: 416 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78.47 48.95"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:6px;}.cls-2{fill:#1db598;}</style></defs><title>nav_console</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" x="3" y="3" width="72.47" height="42.95"/><circle class="cls-2" cx="39.25" cy="25.92" r="9.61"/></g></g></svg>

After

Width:  |  Height:  |  Size: 414 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 44.5"><defs><style>.cls-1{fill:none;stroke:#3c5f8d;stroke-miterlimit:10;stroke-width:5px;}.cls-2{fill:#1db598;}</style></defs><title>documentation</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><line class="cls-1" y1="2.5" x2="70" y2="2.5"/><line class="cls-1" y1="20" x2="70" y2="20"/><line class="cls-1" y1="38.5" x2="52.82" y2="38.5"/><circle class="cls-2" cx="63.5" cy="38.5" r="6"/></g></g></svg>

After

Width:  |  Height:  |  Size: 492 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 44.5"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:5px;}.cls-2{fill:#1db598;}</style></defs><title>documentation</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><line class="cls-1" y1="2.5" x2="70" y2="2.5"/><line class="cls-1" y1="20" x2="70" y2="20"/><line class="cls-1" y1="38.5" x2="52.82" y2="38.5"/><circle class="cls-2" cx="63.5" cy="38.5" r="6"/></g></g></svg>

After

Width:  |  Height:  |  Size: 488 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.47 45.95"><defs><style>.cls-1{fill:none;stroke:#3c5f8d;stroke-miterlimit:10;stroke-width:3px;}.cls-2{fill:#1db598;}</style></defs><title>nav_item1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" x="1.5" y="1.5" width="72.47" height="42.95"/><circle class="cls-2" cx="37.75" cy="24.42" r="9.61"/></g></g></svg>

After

Width:  |  Height:  |  Size: 418 B

293
webui/assets/style.css Normal file
View file

@ -0,0 +1,293 @@
*{
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body{
font-family: sans-serif;
background-color: #fbfcfd;
margin: 0;
color: #102445;
}
h2{
margin-bottom: 30px;
}
h5{
text-transform: uppercase;
letter-spacing: 2px;
line-height: 1.21;
margin: 0;
}
a{
line-height: 1.38;
letter-spacing: 0.2px;
text-decoration: none;
color: #102445;
}
a:hover{
color: #1db598;
}
textarea{
width: 100%;
margin-bottom: 10px;
border-radius: 2px;
background-color: #fbfcfd;
border: solid 1.5px #e4eff4;
font-family: monospace;
font-size: 16px;
line-height: 1.5;
letter-spacing: 1.1px;
outline: none;
padding: 30px;
}
select{
/*-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: url("img/chevron-down.png") no-repeat calc(100% - 10px) !important;*/
border-radius: 3px;
background-color: #fbfcfd;
width: 187px;
height: 50px;
border: solid 1.5px #e4eff4;
font-size: 18px;
font-weight: bold;
line-height: 1.39;
letter-spacing: 0.2px;
color: #102445;
padding: 10.5px;
}
button{
width: 165px;
height: 50px;
border-radius: 3px;
background-color: #1db598;
outline: none;
border: none;
font-size: 16px;
color: white;
}
em{
font-style: normal;
opacity: 0.5;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.2px;
color: #102445;
}
.header{
height: 92px;
display: flex;
align-items: center;
justify-content: space-between;
width: 90%;
margin: auto;
}
.container{
display: flex;
height:100%;
min-height: 100vh;
}
.nav{
color: white;
display: flex;
flex-direction: column;
width: 150px;
background: #3c5f8d;
}
.nav-item{
height:150px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border-bottom: 3px solid #2a4871;
cursor: pointer;
}
.nav-active{
background: #f2f7f9;
font-weight: bold;
color: #1db598;
}
.nav-item > .nav-image {
display: flex;
}
.nav-item > .nav-image-active {
display: none;
}
.nav-active > .nav-image {
display: none;
}
.nav-active > .nav-image-active {
display: flex;
}
.interface{
display: none;
flex: 1;
flex-direction: column;
align-items: center;
background: #f2f7f9;
}
.interface-active{
display: flex;
}
.query{
margin-bottom: 30px;
}
.query,
.output-container,
.status-container{
width: 75%;
}
.output{
margin-bottom: 30px;
}
.input-controls{
display: flex;
justify-content: flex-end;
}
.tabs{
display: flex;
background: #eaf2f6;
}
.active-tab{
background: white;
font-weight: bold;
color: #1db598;
}
.tab{
height:60px;
width: 100px;
border-top-right-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
visibility: visible;
cursor: pointer;
}
.pane{
background: white;
padding: 30px;
display: none;
}
.active{
display: block;
}
.result-io-header{
display: flex;
align-items: center;
margin-bottom: 15px;
}
.result-input,
.result-output,
.result-error{
border-radius: 2px;
background-color: #fafafa;
border: solid 1.5px #e4eff4;
font-family: monospace;
font-size: 16px;
line-height: 1.5;
letter-spacing: 1.1px;
color: #102445;
padding: 15px;
margin-bottom: 15px;
}
.result-output{
background-color: #edf9f7;
border-left: solid 4px #1db598;
}
.result-error{
background-color: #fbf1f0;
border-left: solid 4px #fa3035;
color: #fa3035;
}
.raw{
height: 253px;
display: flex;
align-items: center;
justify-content: center;
}
.result-table > table {
border-left: solid 4px #1db598;
}
table{
border: solid 0.5px #e0e0e0;
width: 100%;
margin-bottom: 30px;
/*color:#3c5f8d;*/
}
caption{
text-align:left;
font-size: 16px;
font-weight: bold;
line-height: 1.21;
letter-spacing: 2px;
text-align: left;
}
th{
font-size: 14px;
font-weight: bold;
line-height: 1.21;
letter-spacing: 2px;
color: #102445;
text-transform: uppercase;
text-align: left;
padding: 21px 30px;
background-color: white;
}
tr{
border: solid 0.5px #e0e0e0;
background-color: white;
}
tr:nth-child(even) {
background-color: #f2f7f9;
}
td{
padding: 21px 30px;
}
.string { color: green; }
.number { color: darkorange; }
.boolean { color: blue; }
.null { color: magenta; }
.key { color: red; }

89
webui/index.html Normal file
View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/style.css">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Pilosa Console</title>
<link rel="icon" type="image/png" href="https://dc3kpxyuw05cb.cloudfront.net/img/favicon.png">
</head>
<body>
<div class="header">
<img src="https://dc3kpxyuw05cb.cloudfront.net/img/logo.svg" width="110" alt="">
<div id="server-version" class=""></div>
</div>
<div class="container">
<div class="nav">
<div class="nav-item nav-active" id="nav-console" onclick="handle_nav_click(this)">
<img style='margin-bottom:10px;' height='40px' src="/assets/nav-console.svg" alt="" class="nav-image">
<img style='margin-bottom:10px;' height='40px' src="/assets/nav-console-active.svg" alt="" class="nav-image-active">
Console
</div>
<div class="nav-item" id="nav-cluster" onclick="handle_nav_click(this)">
<img style='margin-bottom:10px;' height='20px' src="/assets/nav-cluster.svg" alt="" class="nav-image">
<img style='margin-bottom:10px;' height='20px' src="/assets/nav-cluster-active.svg" alt="" class="nav-image-active">
Cluster Admin
</div>
<div class="nav-item" id="nav-documentation" onclick="handle_nav_click(this)">
<img style='margin-bottom:10px;' height='30px' src="/assets/nav-documentation.svg" alt="" class="nav-image">
<img style='margin-bottom:10px;' height='30px' src="/assets/nav-documentation-active.svg" alt="" class="nav-image-active">
Documentation
</div>
</div>
<div class="interface interface-active" id="interface-console">
<div class="query">
<h2>Query</h2>
<textarea name="name" id='query' rows="5" required></textarea>
<div class='input-controls'>
<select class="" name="" id="index-dropdown">
<option value="0">Select index</option>
</select>
&nbsp;&nbsp;&nbsp;
<button id='query-btn'>Run Query</button>
</div>
</div>
<div class="output-container">
<h2>Output</h2>
<div id="outputs">
<!-- results -->
</div>
</div>
</div>
<div class="interface" id="interface-cluster">
<div class="status-container">
<h2>Nodes</h2>
<div class="status-table-container" id="status-nodes">
</div>
</div>
<div class="status-container">
<h2>Indexes</h2>
<div class="status-table-container" id="status-indexes">
</div>
</div>
<div class="status-container">
<h2>Slice Distribution</h2>
<div class="status-table-container" id="status-slices">
...
</div>
</div>
</div>
<div class="interface" id="interface-documentation">
docs!
</div>
</div>
<script type="text/javascript" src="/assets/main.js"></script>
</body>
</html>