diff --git a/.gitignore b/.gitignore
index 5fe629f6a..c924add8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ default.etcd/
*.test
vendor
.protoc-gen-gofast
+.DS_Store
diff --git a/Makefile b/Makefile
index 78c3e71f6..d5f1022d6 100644
--- a/Makefile
+++ b/Makefile
@@ -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)" .
diff --git a/glide.lock b/glide.lock
index c142737ff..a615eac54 100644
--- a/glide.lock
+++ b/glide.lock
@@ -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
diff --git a/glide.yaml b/glide.yaml
index a7c0e98eb..166b2a4dc 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -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
diff --git a/handler.go b/handler.go
index 343127d29..67cc14869 100644
--- a/handler.go
+++ b/handler.go
@@ -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{
diff --git a/statik/.gitignore b/statik/.gitignore
new file mode 100644
index 000000000..514ee40a1
--- /dev/null
+++ b/statik/.gitignore
@@ -0,0 +1 @@
+statik.go
diff --git a/statik/doc.go b/statik/doc.go
new file mode 100644
index 000000000..85edd9e6f
--- /dev/null
+++ b/statik/doc.go
@@ -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
diff --git a/webui/assets/chevron-down.png b/webui/assets/chevron-down.png
new file mode 100644
index 000000000..3312489a2
Binary files /dev/null and b/webui/assets/chevron-down.png differ
diff --git a/webui/assets/main.js b/webui/assets/main.js
new file mode 100644
index 000000000..4e17db19a
--- /dev/null
+++ b/webui/assets/main.js
@@ -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 += `
+
+ Just getting started? Try this:
+ $ curl -XPOST "http://127.0.0.1:10101/index/test" -d '{"options": {"columnLabel": "col"}}' # create index "test"
+ $ curl -XPOST "http://127.0.0.1:10101/index/test/frame/foo" -d '{"options": {"rowLabel": "row"}}' # create frame "foo"
+ # Select "test" in the index dropdown above
+ SetBit(row=0, col=0, frame=foo) # Use PQL to set a bit
+ `
+ }
+ }
+
+
+ var markup =`
+