mirror of https://github.com/status-im/consul.git
commit
db12be5b6c
|
@ -30,3 +30,9 @@ website/build/
|
|||
website/npm-debug.log
|
||||
*.old
|
||||
*.attr
|
||||
|
||||
ui/.sass-cache
|
||||
ui/static/base.css
|
||||
|
||||
ui/static/application.min.js
|
||||
ui/dist/
|
||||
|
|
|
@ -49,6 +49,7 @@ func (c *Command) readConfig() *Config {
|
|||
cmdFlags.StringVar(&cmdConfig.NodeName, "node", "", "node name")
|
||||
cmdFlags.StringVar(&cmdConfig.Datacenter, "dc", "", "node datacenter")
|
||||
cmdFlags.StringVar(&cmdConfig.DataDir, "data-dir", "", "path to the data directory")
|
||||
cmdFlags.StringVar(&cmdConfig.UiDir, "ui-dir", "", "path to the web UI directory")
|
||||
|
||||
cmdFlags.BoolVar(&cmdConfig.Server, "server", false, "run agent as server")
|
||||
cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode")
|
||||
|
@ -179,7 +180,7 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log
|
|||
return err
|
||||
}
|
||||
|
||||
server, err := NewHTTPServer(agent, config.EnableDebug, logOutput, httpAddr.String())
|
||||
server, err := NewHTTPServer(agent, config.UiDir, config.EnableDebug, logOutput, httpAddr.String())
|
||||
if err != nil {
|
||||
agent.Shutdown()
|
||||
c.Ui.Error(fmt.Sprintf("Error starting http server: %s", err))
|
||||
|
@ -483,6 +484,7 @@ Options:
|
|||
-node=hostname Name of this node. Must be unique in the cluster
|
||||
-protocol=N Sets the protocol version. Defaults to latest.
|
||||
-server Switches agent to server mode.
|
||||
-ui-dir=path Path to directory containing the Web UI resources
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
|
|
|
@ -124,6 +124,10 @@ type Config struct {
|
|||
// addresses, then the agent will error and exit.
|
||||
StartJoin []string `mapstructure:"start_join"`
|
||||
|
||||
// UiDir is the directory containing the Web UI resources.
|
||||
// If provided, the UI endpoints will be enabled.
|
||||
UiDir string `mapstructure:"ui_dir"`
|
||||
|
||||
// AEInterval controls the anti-entropy interval. This is how often
|
||||
// the agent attempts to reconcile it's local state with the server'
|
||||
// representation of our state. Defaults to every 60s.
|
||||
|
@ -416,6 +420,9 @@ func MergeConfig(a, b *Config) *Config {
|
|||
if b.Ports.Server != 0 {
|
||||
result.Ports.Server = b.Ports.Server
|
||||
}
|
||||
if b.UiDir != "" {
|
||||
result.UiDir = b.UiDir
|
||||
}
|
||||
|
||||
// Copy the start join addresses
|
||||
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
|
||||
|
|
|
@ -246,6 +246,17 @@ func TestDecodeConfig(t *testing.T) {
|
|||
if config.StartJoin[1] != "2.2.2.2" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
|
||||
// UI Dir
|
||||
input = `{"ui_dir": "/opt/consul-ui"}`
|
||||
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if config.UiDir != "/opt/consul-ui" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeConfig_Service(t *testing.T) {
|
||||
|
@ -377,6 +388,7 @@ func TestMergeConfig(t *testing.T) {
|
|||
Checks: []*CheckDefinition{nil},
|
||||
Services: []*ServiceDefinition{nil},
|
||||
StartJoin: []string{"1.1.1.1"},
|
||||
UiDir: "/opt/consul-ui",
|
||||
}
|
||||
|
||||
c := MergeConfig(a, b)
|
||||
|
|
|
@ -21,11 +21,12 @@ type HTTPServer struct {
|
|||
mux *http.ServeMux
|
||||
listener net.Listener
|
||||
logger *log.Logger
|
||||
uiDir string
|
||||
}
|
||||
|
||||
// NewHTTPServer starts a new HTTP server to provide an interface to
|
||||
// the agent.
|
||||
func NewHTTPServer(agent *Agent, enableDebug bool, logOutput io.Writer, bind string) (*HTTPServer, error) {
|
||||
func NewHTTPServer(agent *Agent, uiDir string, enableDebug bool, logOutput io.Writer, bind string) (*HTTPServer, error) {
|
||||
// Create the mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
@ -41,6 +42,7 @@ func NewHTTPServer(agent *Agent, enableDebug bool, logOutput io.Writer, bind str
|
|||
mux: mux,
|
||||
listener: list,
|
||||
logger: log.New(logOutput, "", log.LstdFlags),
|
||||
uiDir: uiDir,
|
||||
}
|
||||
srv.registerHandlers(enableDebug)
|
||||
|
||||
|
@ -97,6 +99,17 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
|||
s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
}
|
||||
|
||||
// Enable the UI + special endpoints
|
||||
if s.uiDir != "" {
|
||||
// Static file serving done from /ui/
|
||||
s.mux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(http.Dir(s.uiDir))))
|
||||
|
||||
// API's are under /internal/ui/ to avoid conflict
|
||||
s.mux.HandleFunc("/v1/internal/ui/nodes", s.wrap(s.UINodes))
|
||||
s.mux.HandleFunc("/v1/internal/ui/node/", s.wrap(s.UINodeInfo))
|
||||
s.mux.HandleFunc("/v1/internal/ui/services", s.wrap(s.UIServices))
|
||||
}
|
||||
}
|
||||
|
||||
// wrap is used to wrap functions to make them more convenient
|
||||
|
@ -134,11 +147,20 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
|
|||
|
||||
// Renders a simple index page
|
||||
func (s *HTTPServer) Index(resp http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path == "/" {
|
||||
resp.Write([]byte("Consul Agent"))
|
||||
} else {
|
||||
// Check if this is a non-index path
|
||||
if req.URL.Path != "/" {
|
||||
resp.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have no UI configured
|
||||
if s.uiDir == "" {
|
||||
resp.Write([]byte("Consul Agent"))
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the UI endpoint
|
||||
http.Redirect(resp, req, "/ui/", 301)
|
||||
}
|
||||
|
||||
// decodeBody is used to decode a JSON request body
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -17,8 +18,12 @@ import (
|
|||
func makeHTTPServer(t *testing.T) (string, *HTTPServer) {
|
||||
conf := nextConfig()
|
||||
dir, agent := makeAgent(t, conf)
|
||||
uiDir := filepath.Join(dir, "ui")
|
||||
if err := os.Mkdir(uiDir, 755); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
addr, _ := agent.config.ClientListener(agent.config.Ports.HTTP)
|
||||
server, err := NewHTTPServer(agent, true, agent.logOutput, addr.String())
|
||||
server, err := NewHTTPServer(agent, uiDir, true, agent.logOutput, addr.String())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
|
|
@ -19,10 +19,21 @@ func (s *HTTPServer) KVSEndpoint(resp http.ResponseWriter, req *http.Request) (i
|
|||
// Pull out the key name, validation left to each sub-handler
|
||||
args.Key = strings.TrimPrefix(req.URL.Path, "/v1/kv/")
|
||||
|
||||
// Check for a key list
|
||||
keyList := false
|
||||
params := req.URL.Query()
|
||||
if _, ok := params["keys"]; ok {
|
||||
keyList = true
|
||||
}
|
||||
|
||||
// Switch on the method
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return s.KVSGet(resp, req, &args)
|
||||
if keyList {
|
||||
return s.KVSGetKeys(resp, req, &args)
|
||||
} else {
|
||||
return s.KVSGet(resp, req, &args)
|
||||
}
|
||||
case "PUT":
|
||||
return s.KVSPut(resp, req, &args)
|
||||
case "DELETE":
|
||||
|
@ -60,6 +71,44 @@ func (s *HTTPServer) KVSGet(resp http.ResponseWriter, req *http.Request, args *s
|
|||
return out.Entries, nil
|
||||
}
|
||||
|
||||
// KVSGetKeys handles a GET request for keys
|
||||
func (s *HTTPServer) KVSGetKeys(resp http.ResponseWriter, req *http.Request, args *structs.KeyRequest) (interface{}, error) {
|
||||
// Check for a seperator
|
||||
var sep string
|
||||
params := req.URL.Query()
|
||||
if _, ok := params["seperator"]; ok {
|
||||
sep = params.Get("seperator")
|
||||
}
|
||||
|
||||
// Construct the args
|
||||
listArgs := structs.KeyListRequest{
|
||||
Datacenter: args.Datacenter,
|
||||
Prefix: args.Key,
|
||||
Seperator: sep,
|
||||
QueryOptions: args.QueryOptions,
|
||||
}
|
||||
|
||||
// Make the RPC
|
||||
var out structs.IndexedKeyList
|
||||
if err := s.agent.RPC("KVS.ListKeys", &listArgs, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setMeta(resp, &out.QueryMeta)
|
||||
|
||||
// Check if we get a not found. We do not generate
|
||||
// not found for the root, but just provide the empty list
|
||||
if len(out.Keys) == 0 && listArgs.Prefix != "" {
|
||||
resp.WriteHeader(404)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Use empty list instead of null
|
||||
if out.Keys == nil {
|
||||
out.Keys = []string{}
|
||||
}
|
||||
return out.Keys, nil
|
||||
}
|
||||
|
||||
// KVSPut handles a PUT request
|
||||
func (s *HTTPServer) KVSPut(resp http.ResponseWriter, req *http.Request, args *structs.KeyRequest) (interface{}, error) {
|
||||
if missingKey(resp, args) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -281,3 +282,64 @@ func TestKVSEndpoint_CAS(t *testing.T) {
|
|||
t.Fatalf("bad: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
||||
dir, srv := makeHTTPServer(t)
|
||||
defer os.RemoveAll(dir)
|
||||
defer srv.Shutdown()
|
||||
defer srv.agent.Shutdown()
|
||||
|
||||
// Wait for a leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
keys := []string{
|
||||
"bar",
|
||||
"baz",
|
||||
"foo/sub1",
|
||||
"foo/sub2",
|
||||
"zip",
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
buf := bytes.NewBuffer([]byte("test"))
|
||||
req, err := http.NewRequest("PUT", "/v1/kv/"+key, buf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.KVSEndpoint(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if res := obj.(bool); !res {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Get all the keys
|
||||
req, err := http.NewRequest("GET", "/v1/kv/?keys&seperator=/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.KVSEndpoint(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assertIndex(t, resp)
|
||||
|
||||
res, ok := obj.([]string)
|
||||
if !ok {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
|
||||
expect := []string{"bar", "baz", "foo/", "zip"}
|
||||
if !reflect.DeepEqual(res, expect) {
|
||||
t.Fatalf("bad: %v", res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServiceSummary is used to summarize a service
|
||||
type ServiceSummary struct {
|
||||
Name string
|
||||
Nodes []string
|
||||
ChecksPassing int
|
||||
ChecksWarning int
|
||||
ChecksCritical int
|
||||
}
|
||||
|
||||
// UINodes is used to list the nodes in a given datacenter. We return a
|
||||
// NodeDump which provides overview information for all the nodes
|
||||
func (s *HTTPServer) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Get the datacenter
|
||||
var dc string
|
||||
s.parseDC(req, &dc)
|
||||
|
||||
// Try to ge ta node dump
|
||||
var dump structs.NodeDump
|
||||
if err := s.getNodeDump(resp, dc, "", &dump); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dump, nil
|
||||
}
|
||||
|
||||
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
||||
// NodeInfo which provides overview information for the node
|
||||
func (s *HTTPServer) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Get the datacenter
|
||||
var dc string
|
||||
s.parseDC(req, &dc)
|
||||
|
||||
// Verify we have some DC, or use the default
|
||||
node := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
|
||||
if node == "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte("Missing node name"))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Try to get a node dump
|
||||
var dump structs.NodeDump
|
||||
if err := s.getNodeDump(resp, dc, node, &dump); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return only the first entry
|
||||
if len(dump) > 0 {
|
||||
return dump[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getNodeDump is used to get a dump of all node data. We make a best effort by
|
||||
// reading stale data in the case of an availability outage.
|
||||
func (s *HTTPServer) getNodeDump(resp http.ResponseWriter, dc, node string, dump *structs.NodeDump) error {
|
||||
var args interface{}
|
||||
var method string
|
||||
var allowStale *bool
|
||||
|
||||
if node == "" {
|
||||
raw := structs.DCSpecificRequest{Datacenter: dc}
|
||||
method = "Internal.NodeDump"
|
||||
allowStale = &raw.AllowStale
|
||||
args = &raw
|
||||
} else {
|
||||
raw := &structs.NodeSpecificRequest{Datacenter: dc, Node: node}
|
||||
method = "Internal.NodeInfo"
|
||||
allowStale = &raw.AllowStale
|
||||
args = &raw
|
||||
}
|
||||
var out structs.IndexedNodeDump
|
||||
defer setMeta(resp, &out.QueryMeta)
|
||||
|
||||
START:
|
||||
if err := s.agent.RPC(method, args, &out); err != nil {
|
||||
// Retry the request allowing stale data if no leader. The UI should continue
|
||||
// to function even during an outage
|
||||
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !*allowStale {
|
||||
*allowStale = true
|
||||
goto START
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the result
|
||||
*dump = out.Dump
|
||||
return nil
|
||||
}
|
||||
|
||||
// UIServices is used to list the services in a given datacenter. We return a
|
||||
// ServiceSummary which provides overview information for the service
|
||||
func (s *HTTPServer) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Get the datacenter
|
||||
var dc string
|
||||
s.parseDC(req, &dc)
|
||||
|
||||
// Get the full node dump...
|
||||
var dump structs.NodeDump
|
||||
if err := s.getNodeDump(resp, dc, "", &dump); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate the summary
|
||||
return summarizeServices(dump), nil
|
||||
}
|
||||
|
||||
func summarizeServices(dump structs.NodeDump) []*ServiceSummary {
|
||||
// Collect the summary information
|
||||
var services []string
|
||||
summary := make(map[string]*ServiceSummary)
|
||||
getService := func(service string) *ServiceSummary {
|
||||
serv, ok := summary[service]
|
||||
if !ok {
|
||||
serv = &ServiceSummary{Name: service}
|
||||
summary[service] = serv
|
||||
services = append(services, service)
|
||||
}
|
||||
return serv
|
||||
}
|
||||
|
||||
// Aggregate all the node information
|
||||
for _, node := range dump {
|
||||
nodeServices := make([]*ServiceSummary, len(node.Services))
|
||||
for idx, service := range node.Services {
|
||||
sum := getService(service.Service)
|
||||
sum.Nodes = append(sum.Nodes, node.Node)
|
||||
nodeServices[idx] = sum
|
||||
}
|
||||
for _, check := range node.Checks {
|
||||
var services []*ServiceSummary
|
||||
if check.ServiceName == "" {
|
||||
services = nodeServices
|
||||
} else {
|
||||
services = []*ServiceSummary{getService(check.ServiceName)}
|
||||
}
|
||||
for _, sum := range services {
|
||||
switch check.Status {
|
||||
case structs.HealthPassing:
|
||||
sum.ChecksPassing++
|
||||
case structs.HealthWarning:
|
||||
sum.ChecksWarning++
|
||||
case structs.HealthCritical:
|
||||
sum.ChecksCritical++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the services in sorted order
|
||||
sort.Strings(services)
|
||||
output := make([]*ServiceSummary, len(summary))
|
||||
for idx, service := range services {
|
||||
// Sort the nodes
|
||||
sum := summary[service]
|
||||
sort.Strings(sum.Nodes)
|
||||
output[idx] = sum
|
||||
}
|
||||
return output
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUiIndex(t *testing.T) {
|
||||
dir, srv := makeHTTPServer(t)
|
||||
defer os.RemoveAll(dir)
|
||||
defer srv.Shutdown()
|
||||
defer srv.agent.Shutdown()
|
||||
|
||||
// Create file
|
||||
path := filepath.Join(srv.uiDir, "my-file")
|
||||
if err := ioutil.WriteFile(path, []byte("test"), 777); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Register node
|
||||
req, err := http.NewRequest("GET", "/ui/my-file", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = srv.listener.Addr().String()
|
||||
|
||||
// Make the request
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Verify teh response
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("bad: %v", resp)
|
||||
}
|
||||
|
||||
// Verify the body
|
||||
out := bytes.NewBuffer(nil)
|
||||
io.Copy(out, resp.Body)
|
||||
if string(out.Bytes()) != "test" {
|
||||
t.Fatalf("bad: %s", out.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUiNodes(t *testing.T) {
|
||||
dir, srv := makeHTTPServer(t)
|
||||
defer os.RemoveAll(dir)
|
||||
defer srv.Shutdown()
|
||||
defer srv.agent.Shutdown()
|
||||
|
||||
// Wait for leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
req, err := http.NewRequest("GET", "/v1/internal/ui/nodes/dc1", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.UINodes(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assertIndex(t, resp)
|
||||
|
||||
// Should be 1 node for the server
|
||||
nodes := obj.(structs.NodeDump)
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("bad: %v", obj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUiNodeInfo(t *testing.T) {
|
||||
dir, srv := makeHTTPServer(t)
|
||||
defer os.RemoveAll(dir)
|
||||
defer srv.Shutdown()
|
||||
defer srv.agent.Shutdown()
|
||||
|
||||
// Wait for leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
fmt.Sprintf("/v1/internal/ui/node/%s", srv.agent.config.NodeName), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.UINodeInfo(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
assertIndex(t, resp)
|
||||
|
||||
// Should be 1 node for the server
|
||||
node := obj.(*structs.NodeInfo)
|
||||
if node.Node != srv.agent.config.NodeName {
|
||||
t.Fatalf("bad: %v", node)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeServices(t *testing.T) {
|
||||
dump := structs.NodeDump{
|
||||
&structs.NodeInfo{
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Services: []*structs.NodeService{
|
||||
&structs.NodeService{
|
||||
Service: "api",
|
||||
},
|
||||
&structs.NodeService{
|
||||
Service: "web",
|
||||
},
|
||||
},
|
||||
Checks: []*structs.HealthCheck{
|
||||
&structs.HealthCheck{
|
||||
Status: structs.HealthPassing,
|
||||
ServiceName: "",
|
||||
},
|
||||
&structs.HealthCheck{
|
||||
Status: structs.HealthPassing,
|
||||
ServiceName: "web",
|
||||
},
|
||||
&structs.HealthCheck{
|
||||
Status: structs.HealthWarning,
|
||||
ServiceName: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
&structs.NodeInfo{
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Services: []*structs.NodeService{
|
||||
&structs.NodeService{
|
||||
Service: "web",
|
||||
},
|
||||
},
|
||||
Checks: []*structs.HealthCheck{
|
||||
&structs.HealthCheck{
|
||||
Status: structs.HealthCritical,
|
||||
ServiceName: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
&structs.NodeInfo{
|
||||
Node: "zip",
|
||||
Address: "127.0.0.3",
|
||||
Services: []*structs.NodeService{
|
||||
&structs.NodeService{
|
||||
Service: "cache",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
summary := summarizeServices(dump)
|
||||
if len(summary) != 3 {
|
||||
t.Fatalf("bad: %v", summary)
|
||||
}
|
||||
|
||||
expectAPI := &ServiceSummary{
|
||||
Name: "api",
|
||||
Nodes: []string{"foo"},
|
||||
ChecksPassing: 1,
|
||||
ChecksWarning: 1,
|
||||
ChecksCritical: 0,
|
||||
}
|
||||
if !reflect.DeepEqual(summary[0], expectAPI) {
|
||||
t.Fatalf("bad: %v", summary[0])
|
||||
}
|
||||
|
||||
expectCache := &ServiceSummary{
|
||||
Name: "cache",
|
||||
Nodes: []string{"zip"},
|
||||
ChecksPassing: 0,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
}
|
||||
if !reflect.DeepEqual(summary[1], expectCache) {
|
||||
t.Fatalf("bad: %v", summary[1])
|
||||
}
|
||||
|
||||
expectWeb := &ServiceSummary{
|
||||
Name: "web",
|
||||
Nodes: []string{"bar", "foo"},
|
||||
ChecksPassing: 2,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 1,
|
||||
}
|
||||
if !reflect.DeepEqual(summary[2], expectWeb) {
|
||||
t.Fatalf("bad: %v", summary[2])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
)
|
||||
|
||||
// Internal endpoint is used to query the miscellaneous info that
|
||||
// does not necessarily fit into the other systems. It is also
|
||||
// used to hold undocumented APIs that users should not rely on.
|
||||
type Internal struct {
|
||||
srv *Server
|
||||
}
|
||||
|
||||
// ChecksInState is used to get all the checks in a given state
|
||||
func (m *Internal) NodeInfo(args *structs.NodeSpecificRequest,
|
||||
reply *structs.IndexedNodeDump) error {
|
||||
if done, err := m.srv.forward("Internal.NodeInfo", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the state specific checks
|
||||
state := m.srv.fsm.State()
|
||||
return m.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("NodeInfo"),
|
||||
func() error {
|
||||
reply.Index, reply.Dump = state.NodeInfo(args.Node)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ChecksInState is used to get all the checks in a given state
|
||||
func (m *Internal) NodeDump(args *structs.DCSpecificRequest,
|
||||
reply *structs.IndexedNodeDump) error {
|
||||
if done, err := m.srv.forward("Internal.NodeDump", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the state specific checks
|
||||
state := m.srv.fsm.State()
|
||||
return m.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("NodeDump"),
|
||||
func() error {
|
||||
reply.Index, reply.Dump = state.NodeDump()
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInternal_NodeInfo(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
// Wait for leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"master"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: structs.HealthPassing,
|
||||
ServiceID: "db",
|
||||
},
|
||||
}
|
||||
var out struct{}
|
||||
if err := client.Call("Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.NodeSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
}
|
||||
if err := client.Call("Internal.NodeInfo", &req, &out2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
nodes := out2.Dump
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("Bad: %v", nodes)
|
||||
}
|
||||
if nodes[0].Node != "foo" {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if !strContains(nodes[0].Services[0].Tags, "master") {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if nodes[0].Checks[0].Status != structs.HealthPassing {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternal_NodeDump(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
// Wait for leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"master"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: structs.HealthPassing,
|
||||
ServiceID: "db",
|
||||
},
|
||||
}
|
||||
var out struct{}
|
||||
if err := client.Call("Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
arg = structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"slave"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: structs.HealthWarning,
|
||||
ServiceID: "db",
|
||||
},
|
||||
}
|
||||
if err := client.Call("Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
if err := client.Call("Internal.NodeDump", &req, &out2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
nodes := out2.Dump
|
||||
if len(nodes) != 3 {
|
||||
t.Fatalf("Bad: %v", nodes)
|
||||
}
|
||||
|
||||
var foundFoo, foundBar bool
|
||||
for _, node := range nodes {
|
||||
switch node.Node {
|
||||
case "foo":
|
||||
foundFoo = true
|
||||
if !strContains(node.Services[0].Tags, "master") {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if node.Checks[0].Status != structs.HealthPassing {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
|
||||
case "bar":
|
||||
foundBar = true
|
||||
if !strContains(node.Services[0].Tags, "slave") {
|
||||
t.Fatalf("Bad: %v", nodes[1])
|
||||
}
|
||||
if node.Checks[0].Status != structs.HealthWarning {
|
||||
t.Fatalf("Bad: %v", nodes[1])
|
||||
}
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !foundFoo || !foundBar {
|
||||
t.Fatalf("missing foo or bar")
|
||||
}
|
||||
}
|
|
@ -115,3 +115,21 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ListKeys is used to list all keys with a given prefix to a seperator
|
||||
func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyList) error {
|
||||
if done, err := k.srv.forward("KVS.ListKeys", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := k.srv.fsm.State()
|
||||
return k.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("KVSListKeys"),
|
||||
func() error {
|
||||
var err error
|
||||
reply.Index, reply.Keys, err = state.KVSListKeys(args.Prefix, args.Seperator)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
|
@ -171,3 +171,62 @@ func TestKVSEndpoint_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
// Wait for leader
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
keys := []string{
|
||||
"/test/key1",
|
||||
"/test/key2",
|
||||
"/test/sub/key3",
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
arg := structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSSet,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: key,
|
||||
Flags: 1,
|
||||
},
|
||||
}
|
||||
var out bool
|
||||
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
getR := structs.KeyListRequest{
|
||||
Datacenter: "dc1",
|
||||
Prefix: "/test/",
|
||||
Seperator: "/",
|
||||
}
|
||||
var dirent structs.IndexedKeyList
|
||||
if err := client.Call("KVS.ListKeys", &getR, &dirent); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if dirent.Index == 0 {
|
||||
t.Fatalf("Bad: %v", dirent)
|
||||
}
|
||||
if len(dirent.Keys) != 3 {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
if dirent.Keys[0] != "/test/key1" {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
if dirent.Keys[1] != "/test/key2" {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
if dirent.Keys[2] != "/test/sub/" {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -107,11 +107,12 @@ type Server struct {
|
|||
|
||||
// Holds the RPC endpoints
|
||||
type endpoints struct {
|
||||
Catalog *Catalog
|
||||
Health *Health
|
||||
Raft *Raft
|
||||
Status *Status
|
||||
KVS *KVS
|
||||
Catalog *Catalog
|
||||
Health *Health
|
||||
Raft *Raft
|
||||
Status *Status
|
||||
KVS *KVS
|
||||
Internal *Internal
|
||||
}
|
||||
|
||||
// NewServer is used to construct a new Consul server from the
|
||||
|
@ -311,6 +312,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
|
|||
s.endpoints.Catalog = &Catalog{s}
|
||||
s.endpoints.Health = &Health{s}
|
||||
s.endpoints.KVS = &KVS{s}
|
||||
s.endpoints.Internal = &Internal{s}
|
||||
|
||||
// Register the handlers
|
||||
s.rpcServer.Register(s.endpoints.Status)
|
||||
|
@ -318,6 +320,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
|
|||
s.rpcServer.Register(s.endpoints.Catalog)
|
||||
s.rpcServer.Register(s.endpoints.Health)
|
||||
s.rpcServer.Register(s.endpoints.KVS)
|
||||
s.rpcServer.Register(s.endpoints.Internal)
|
||||
|
||||
list, err := net.ListenTCP("tcp", s.config.RPCAddr)
|
||||
if err != nil {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -241,8 +242,11 @@ func (s *StateStore) initialize() error {
|
|||
"NodeChecks": MDBTables{s.checkTable},
|
||||
"ServiceChecks": MDBTables{s.checkTable},
|
||||
"CheckServiceNodes": MDBTables{s.nodeTable, s.serviceTable, s.checkTable},
|
||||
"NodeInfo": MDBTables{s.nodeTable, s.serviceTable, s.checkTable},
|
||||
"NodeDump": MDBTables{s.nodeTable, s.serviceTable, s.checkTable},
|
||||
"KVSGet": MDBTables{s.kvsTable},
|
||||
"KVSList": MDBTables{s.kvsTable},
|
||||
"KVSListKeys": MDBTables{s.kvsTable},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -743,6 +747,94 @@ func (s *StateStore) parseCheckServiceNodes(tx *MDBTxn, res []interface{}, err e
|
|||
return nodes
|
||||
}
|
||||
|
||||
// NodeInfo is used to generate the full info about a node.
|
||||
func (s *StateStore) NodeInfo(node string) (uint64, structs.NodeDump) {
|
||||
tables := s.queryTables["NodeInfo"]
|
||||
tx, err := tables.StartTxn(true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to start txn: %v", err))
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
idx, err := tables.LastIndexTxn(tx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to get last index: %v", err))
|
||||
}
|
||||
|
||||
res, err := s.nodeTable.GetTxn(tx, "id", node)
|
||||
return idx, s.parseNodeInfo(tx, res, err)
|
||||
}
|
||||
|
||||
// NodeDump is used to generate the NodeInfo for all nodes. This is very expensive,
|
||||
// and should generally be avoided for programatic access.
|
||||
func (s *StateStore) NodeDump() (uint64, structs.NodeDump) {
|
||||
tables := s.queryTables["NodeDump"]
|
||||
tx, err := tables.StartTxn(true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to start txn: %v", err))
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
idx, err := tables.LastIndexTxn(tx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to get last index: %v", err))
|
||||
}
|
||||
|
||||
res, err := s.nodeTable.GetTxn(tx, "id")
|
||||
return idx, s.parseNodeInfo(tx, res, err)
|
||||
}
|
||||
|
||||
// parseNodeInfo is used to scan over the results of a node
|
||||
// iteration and generate a NodeDump
|
||||
func (s *StateStore) parseNodeInfo(tx *MDBTxn, res []interface{}, err error) structs.NodeDump {
|
||||
dump := make(structs.NodeDump, 0, len(res))
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERR] consul.state: Failed to get nodes: %v", err)
|
||||
return dump
|
||||
}
|
||||
|
||||
for _, r := range res {
|
||||
// Copy the address and node
|
||||
node := r.(*structs.Node)
|
||||
info := &structs.NodeInfo{
|
||||
Node: node.Node,
|
||||
Address: node.Address,
|
||||
}
|
||||
|
||||
// Get any services of the node
|
||||
res, err = s.serviceTable.GetTxn(tx, "id", node.Node)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERR] consul.state: Failed to get node services: %v", err)
|
||||
}
|
||||
info.Services = make([]*structs.NodeService, 0, len(res))
|
||||
for _, r := range res {
|
||||
service := r.(*structs.ServiceNode)
|
||||
srv := &structs.NodeService{
|
||||
ID: service.ServiceID,
|
||||
Service: service.ServiceName,
|
||||
Tags: service.ServiceTags,
|
||||
Port: service.ServicePort,
|
||||
}
|
||||
info.Services = append(info.Services, srv)
|
||||
}
|
||||
|
||||
// Get any checks of the node
|
||||
res, err = s.checkTable.GetTxn(tx, "node", node.Node)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERR] consul.state: Failed to get node checks: %v", err)
|
||||
}
|
||||
info.Checks = make([]*structs.HealthCheck, 0, len(res))
|
||||
for _, r := range res {
|
||||
chk := r.(*structs.HealthCheck)
|
||||
info.Checks = append(info.Checks, chk)
|
||||
}
|
||||
|
||||
// Add the node info
|
||||
dump = append(dump, info)
|
||||
}
|
||||
return dump
|
||||
}
|
||||
|
||||
// KVSSet is used to create or update a KV entry
|
||||
func (s *StateStore) KVSSet(index uint64, d *structs.DirEntry) error {
|
||||
// Start a new txn
|
||||
|
@ -812,6 +904,57 @@ func (s *StateStore) KVSList(prefix string) (uint64, structs.DirEntries, error)
|
|||
return idx, ents, err
|
||||
}
|
||||
|
||||
// KVSListKeys is used to list keys with a prefix, and up to a given seperator
|
||||
func (s *StateStore) KVSListKeys(prefix, seperator string) (uint64, []string, error) {
|
||||
tx, err := s.kvsTable.StartTxn(true, nil)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
idx, err := s.kvsTable.LastIndexTxn(tx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// Aggregate the stream
|
||||
stream := make(chan interface{}, 128)
|
||||
done := make(chan struct{})
|
||||
var keys []string
|
||||
go func() {
|
||||
prefixLen := len(prefix)
|
||||
sepLen := len(seperator)
|
||||
last := ""
|
||||
for raw := range stream {
|
||||
ent := raw.(*structs.DirEntry)
|
||||
after := ent.Key[prefixLen:]
|
||||
|
||||
// If there is no seperator, always accumulate
|
||||
if sepLen == 0 {
|
||||
keys = append(keys, ent.Key)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for the seperator
|
||||
if idx := strings.Index(after, seperator); idx >= 0 {
|
||||
toSep := ent.Key[:prefixLen+idx+sepLen]
|
||||
if last != toSep {
|
||||
keys = append(keys, toSep)
|
||||
last = toSep
|
||||
}
|
||||
} else {
|
||||
keys = append(keys, ent.Key)
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Start the stream, and wait for completion
|
||||
err = s.kvsTable.StreamTxn(stream, tx, "id_prefix", prefix)
|
||||
<-done
|
||||
return idx, keys, err
|
||||
}
|
||||
|
||||
// KVSDelete is used to delete a KVS entry
|
||||
func (s *StateStore) KVSDelete(index uint64, key string) error {
|
||||
return s.kvsDeleteWithIndex(index, "id", key)
|
||||
|
|
|
@ -1068,6 +1068,109 @@ func TestSS_Register_Deregister_Query(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNodeInfo(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.EnsureNode(1, structs.Node{"foo", "127.0.0.1"}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil {
|
||||
t.Fatalf("err: %v")
|
||||
}
|
||||
check := &structs.HealthCheck{
|
||||
Node: "foo",
|
||||
CheckID: "db",
|
||||
Name: "Can connect",
|
||||
Status: structs.HealthPassing,
|
||||
ServiceID: "db1",
|
||||
}
|
||||
if err := store.EnsureCheck(3, check); err != nil {
|
||||
t.Fatalf("err: %v")
|
||||
}
|
||||
check = &structs.HealthCheck{
|
||||
Node: "foo",
|
||||
CheckID: SerfCheckID,
|
||||
Name: SerfCheckName,
|
||||
Status: structs.HealthPassing,
|
||||
}
|
||||
if err := store.EnsureCheck(4, check); err != nil {
|
||||
t.Fatalf("err: %v")
|
||||
}
|
||||
|
||||
idx, dump := store.NodeInfo("foo")
|
||||
if idx != 4 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(dump) != 1 {
|
||||
t.Fatalf("Bad: %v", dump)
|
||||
}
|
||||
|
||||
info := dump[0]
|
||||
if info.Node != "foo" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if info.Services[0].ID != "db1" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if len(info.Checks) != 2 {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if info.Checks[0].CheckID != "db" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if info.Checks[1].CheckID != SerfCheckID {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeDump(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.EnsureNode(1, structs.Node{"foo", "127.0.0.1"}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil {
|
||||
t.Fatalf("err: %v")
|
||||
}
|
||||
if err := store.EnsureNode(3, structs.Node{"baz", "127.0.0.2"}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.EnsureService(4, "baz", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil {
|
||||
t.Fatalf("err: %v")
|
||||
}
|
||||
|
||||
idx, dump := store.NodeDump()
|
||||
if idx != 4 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(dump) != 2 {
|
||||
t.Fatalf("Bad: %v", dump)
|
||||
}
|
||||
|
||||
info := dump[0]
|
||||
if info.Node != "baz" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if info.Services[0].ID != "db1" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
info = dump[1]
|
||||
if info.Node != "foo" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
if info.Services[0].ID != "db1" {
|
||||
t.Fatalf("Bad: %v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSSet_Get(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
|
@ -1298,6 +1401,121 @@ func TestKVS_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKVS_ListKeys(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Should not exist
|
||||
idx, keys, err := store.KVSListKeys("", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 0 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 0 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Create the entries
|
||||
d := &structs.DirEntry{Key: "/web/a", Flags: 42, Value: []byte("test")}
|
||||
if err := store.KVSSet(1000, d); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
d = &structs.DirEntry{Key: "/web/b", Flags: 42, Value: []byte("test")}
|
||||
if err := store.KVSSet(1001, d); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
d = &structs.DirEntry{Key: "/web/sub/c", Flags: 42, Value: []byte("test")}
|
||||
if err := store.KVSSet(1002, d); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should list
|
||||
idx, keys, err = store.KVSListKeys("", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 1002 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Should list just web
|
||||
idx, keys, err = store.KVSListKeys("/", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 1002 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "/web/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Should list a, b, sub/
|
||||
idx, keys, err = store.KVSListKeys("/web/", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 1002 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "/web/a" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[1] != "/web/b" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[2] != "/web/sub/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Should list c
|
||||
idx, keys, err = store.KVSListKeys("/web/sub/", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 1002 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "/web/sub/c" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Should list all
|
||||
idx, keys, err = store.KVSListKeys("/web/", "")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 1002 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[2] != "/web/sub/c" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSDeleteTree(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
|
|
|
@ -220,6 +220,21 @@ type CheckServiceNode struct {
|
|||
}
|
||||
type CheckServiceNodes []CheckServiceNode
|
||||
|
||||
// NodeInfo is used to dump all associated information about
|
||||
// a node. This is currently used for the UI only, as it is
|
||||
// rather expensive to generate.
|
||||
type NodeInfo struct {
|
||||
Node string
|
||||
Address string
|
||||
Services []*NodeService
|
||||
Checks []*HealthCheck
|
||||
}
|
||||
|
||||
// NodeDump is used to dump all the nodes with all their
|
||||
// associated data. This is currently used for the UI only,
|
||||
// as it is rather expensive to generate.
|
||||
type NodeDump []*NodeInfo
|
||||
|
||||
type IndexedNodes struct {
|
||||
Nodes Nodes
|
||||
QueryMeta
|
||||
|
@ -250,6 +265,11 @@ type IndexedCheckServiceNodes struct {
|
|||
QueryMeta
|
||||
}
|
||||
|
||||
type IndexedNodeDump struct {
|
||||
Dump NodeDump
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// DirEntry is used to represent a directory entry. This is
|
||||
// used for values in our Key-Value store.
|
||||
type DirEntry struct {
|
||||
|
@ -293,11 +313,28 @@ func (r *KeyRequest) RequestDatacenter() string {
|
|||
return r.Datacenter
|
||||
}
|
||||
|
||||
// KeyListRequest is used to list keys
|
||||
type KeyListRequest struct {
|
||||
Datacenter string
|
||||
Prefix string
|
||||
Seperator string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
func (r *KeyListRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
type IndexedDirEntries struct {
|
||||
Entries DirEntries
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
type IndexedKeyList struct {
|
||||
Keys []string
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// Decode is used to decode a MsgPack encoded object
|
||||
func Decode(buf []byte, out interface{}) error {
|
||||
var handle codec.MsgpackHandle
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# A sample Gemfile
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "uglifier"
|
||||
gem "sass"
|
||||
gem "therubyracer"
|
|
@ -0,0 +1,22 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
execjs (2.0.2)
|
||||
json (1.8.1)
|
||||
libv8 (3.16.14.3)
|
||||
ref (1.0.5)
|
||||
sass (3.3.6)
|
||||
therubyracer (0.12.1)
|
||||
libv8 (~> 3.16.14.0)
|
||||
ref
|
||||
uglifier (2.5.0)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
sass
|
||||
therubyracer
|
||||
uglifier
|
|
@ -0,0 +1,17 @@
|
|||
server:
|
||||
python -m SimpleHTTPServer
|
||||
|
||||
watch:
|
||||
sass styles:static --watch
|
||||
|
||||
dist:
|
||||
@echo clean dist
|
||||
@rm -rf dist/index.html
|
||||
@rm -rf dist/static
|
||||
@echo "compile styles/*.scss"
|
||||
@sass styles/base.scss static/base.css
|
||||
@ruby scripts/compile.rb
|
||||
cp -R ./static dist/static/
|
||||
cp index.html dist/index.html
|
||||
|
||||
.PHONY: server watch dist
|
|
@ -0,0 +1,55 @@
|
|||
## Consul Web UI
|
||||
|
||||
This directory contains the Consul Web UI. Consul contains a built-in
|
||||
HTTP server that serves this directoy, but any common HTTP server
|
||||
is capable of serving it.
|
||||
|
||||
It uses JavaScript and [Ember](http://emberjs.com) to communicate with
|
||||
the [Consul API](http://www.consul.io/docs/agent/http.html). The basic
|
||||
features it provides are:
|
||||
|
||||
- Service view. A list of your registered services, their
|
||||
health and the nodes they run on.
|
||||
- Node view. A list of your registered nodes, the services running
|
||||
on each and the health of the node.
|
||||
- Key/value view and update
|
||||
|
||||
It's aware of multiple data centers, so you can get a quick global
|
||||
overview before drilling into specific data-centers for detailed
|
||||
views.
|
||||
|
||||
The UI uses some internal undocumented HTTP APIs to optimize
|
||||
performance and usability.
|
||||
|
||||
### Development
|
||||
|
||||
Improvements and bug fixes are welcome and encouraged for the Web UI.
|
||||
|
||||
You'll need sass to compile CSS stylesheets. Install that with
|
||||
bundler:
|
||||
|
||||
cd ui/
|
||||
bundle
|
||||
|
||||
Reloading compilation for development:
|
||||
|
||||
make watch
|
||||
|
||||
Consul ships with an HTTP server for the API and UI. By default, when
|
||||
you run the agent, it is off. However, if you pass a `-ui-dir` flag
|
||||
with a path to this directoy, you'll be able to access the UI via the
|
||||
Consul HTTP server address, which defaults to `localhost:8500/ui`.
|
||||
|
||||
An example of this command, from inside the `ui/` directory, would be:
|
||||
|
||||
consul agent -bootstrap -server -data-dir /tmp/ -ui-dir .
|
||||
|
||||
|
||||
### Releasing
|
||||
|
||||
These steps are slightly manual at the moment.
|
||||
|
||||
1. Build with `make dist`
|
||||
2. In `dist/index.html`, replace the JS files between `<!-- ASSETS -->` tags with:
|
||||
|
||||
<script src="static/application.min.js"></script>
|
|
@ -0,0 +1,441 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.5">
|
||||
<title>Consul</title>
|
||||
<link rel="stylesheet" href="static/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<div id="app">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/x-handlebars">
|
||||
{{outlet}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="error">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
{{#if controller.model.statusText }}
|
||||
<p class="bold">HTTP error code from Consul: <code>{{controller.model.status}} {{controller.model.statusText}}</code></p>
|
||||
{{/if}}
|
||||
<p>This is an error page for the Consul web UI. You may have visited a URL that is loading an
|
||||
unknown resource, so you can try going back to the <a href="#">root</a>.</p>
|
||||
<p>Otherwise, please report any unexpected
|
||||
issues on the <a href="https://github.com/hashicorp/consul">GitHub page</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="loading">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<img src="static/loading-cylon-purple.svg" width="384" height="48">
|
||||
<p><small>Loading...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="dc">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12 topbar">
|
||||
|
||||
<div class="col-md-1 col-sm-2 col-xs-10 col-sm-offset-0 col-xs-offset-1">
|
||||
<a href="#"><div class="top-brand"></div></a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-10 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'services' class='btn btn-default col-xs-12'}}Services{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-10 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'nodes' class='btn btn-default col-xs-12'}}Nodes{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-10 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'kv' class='btn btn-default col-xs-12'}}Key/Value{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-md-offset-1 col-sm-3 col-sm-offset-5 col-xs-10 col-xs-offset-1">
|
||||
{{#link-to 'services' tagName="div" href=false}}<a {{bind-attr class=":col-xs-12 :btn hasFailingChecks:btn-warning:btn-success"}}>{{ checkMessage }}</a>{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-10 col-sm-offset-0 col-xs-offset-1">
|
||||
<a {{bind-attr class=":col-xs-12 :btn isDropDownVisible:btn-primary:btn-default"}} {{action "toggle"}}> {{model}} <span class="caret"></span> </a>
|
||||
|
||||
{{#if isDropdownVisible}}
|
||||
<ul class="dropdown-menu col-xs-8" style="display:block;">
|
||||
{{#each dc in dcs}}
|
||||
<li {{action "hideDrop"}}>{{#link-to 'services' dc}}{{dc}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="kv/show">
|
||||
<div class="row">
|
||||
<h4 class="breadcrumbs"><a href="" {{action 'linkToKey' grandParentKey }}>{{parentKey}}</a></h4>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
|
||||
{{#each item in model }}
|
||||
|
||||
{{#link-to item.linkToRoute item.urlSafeKey href=false tagName="div" class="panel panel-link panel-short"}}
|
||||
<div {{bind-attr class=":panel-bar item.isFolder:bg-gray:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{item.keyWithoutParent}}
|
||||
</h3>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Create Key
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class="form">
|
||||
<div {{ bind-attr class=":form-group newKey.keyValid:valid" }}>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">{{parentKey}}</span>
|
||||
{{ input value=newKey.Key class="form-control" required=true }}
|
||||
</div>
|
||||
<span class="help-block">To create a folder, end the key with <code>/</code></span>
|
||||
</div>
|
||||
|
||||
{{#if newKey.isFolder }}
|
||||
<p>No value needed for nested keys.</p>
|
||||
{{else}}
|
||||
<div class="form-group">
|
||||
{{ textarea value=newKey.Value class="form-control"}}
|
||||
<span class="help-block">Value can be any format and length</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<button {{ action "createKey"}} {{bind-attr disabled=newKey.isInvalid }} {{ bind-attr class=":btn newKey.isValid:btn-success:btn-default" }}>Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="kv/edit">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
<h4 class="breadcrumbs"><a href="" {{action 'linkToKey' grandParentKey }}>{{parentKey}}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="row">
|
||||
{{#each item in siblings }}
|
||||
{{#link-to item.linkToRoute item.urlSafeKey href=false tagName="div" class="panel panel-link panel-short"}}
|
||||
<div {{bind-attr class=":panel-bar item.isFolder:bg-gray:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{item.keyWithoutParent}}
|
||||
</h3>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-green" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{model.Key}}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
<form class="form">
|
||||
<div class="form-group">
|
||||
{{ textarea value=model.valueDecoded class="form-control"}}
|
||||
</div>
|
||||
<button {{ action "updateKey"}} {{bind-attr disabled=isLoading }} {{ bind-attr class=":btn isLoading:btn-warning:btn-success" }}>Update</button>
|
||||
<button {{ action "deleteKey"}} {{bind-attr disabled=isLoading }} {{ bind-attr class=":btn :pull-right isLoading:btn-warning:btn-danger" }}>Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="item/loading">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<img src="static/loading-cylon-purple.svg" width="384" height="48">
|
||||
<p><small>Loading...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="services">
|
||||
|
||||
<div {{ bind-attr class=":col-md-5" }}>
|
||||
|
||||
{{#each service in services}}
|
||||
|
||||
<div class="row">
|
||||
{{#link-to 'services.show' service.Name tagName="div" href=false class="list-group-item list-link" }}
|
||||
<div {{bind-attr class="service.hasFailingChecks:bg-orange:bg-green :list-bar"}}></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
{{#link-to 'services.show' service.Name class='subtle'}}{{service.Name}}{{/link-to}}
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">{{service.checkMessage}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
{{#each node in service.Nodes }}
|
||||
<li class="bold">{{node}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visible-xs visible-sm">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="service">
|
||||
<h2 class="no-margin">{{ model.0.Service.Service }}</h2>
|
||||
<hr>
|
||||
|
||||
<h5>Nodes</h5>
|
||||
|
||||
{{#each node in model }}
|
||||
|
||||
{{#link-to 'nodes.show' node.Node.Node tagName="div" href=false class="panel panel-link" }}
|
||||
<div {{ bind-attr class=":panel-bar node.hasFailingChecks:bg-orange:bg-green" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{node.Node.Node}}
|
||||
<small>{{node.Node.Address}}</small>
|
||||
<span class="panel-note">{{node.checkMessage}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<ul class="list-unstyled list-broken">
|
||||
{{#each check in node.Checks }}
|
||||
<li>
|
||||
<h4 class="check">{{ check.Name }} <small>{{ check.CheckID }}</small> <span class="pull-right"><small>{{check.Status}}</small></h4>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
{{#link-to "services" class="btn btn-default col-xs-12 visible-xs" }}All Services{{/link-to}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="nodes">
|
||||
<div class="col-md-5">
|
||||
|
||||
{{#each node in nodes}}
|
||||
<div class="row">
|
||||
{{#link-to 'nodes.show' node.Node tagName="div" href=false class="list-group-item list-link" }}
|
||||
<div {{bind-attr class="node.hasFailingChecks:bg-orange:bg-green :list-bar"}}></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
{{node.Node}}
|
||||
<small>{{node.Address}}</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">{{node.checkMessage}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
{{#each service in node.Services }}
|
||||
<li class="bold">{{service.Service}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visible-xs visible-sm">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="node">
|
||||
<h2 class="no-margin">{{ model.Node }} <small> {{ model.Address }}</small></h2>
|
||||
<hr>
|
||||
|
||||
<h5>Services</h5>
|
||||
|
||||
{{#each service in model.Services }}
|
||||
|
||||
{{#link-to 'services.show' service.Service }}
|
||||
<div class="panel panel-link panel-short">
|
||||
<div class="panel-bar bg-light-gray"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{service.Service}}
|
||||
<small>{{sevice.ID}}</small>
|
||||
<span class="panel-note">:{{service.Port}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
|
||||
<h5>Checks</h5>
|
||||
|
||||
{{#each check in model.Checks }}
|
||||
|
||||
<div class="panel">
|
||||
{{ panelBar check.Status }}
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{check.Name}}
|
||||
<small>{{check.CheckID}}</small>
|
||||
<span class="panel-note">{{check.Status}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<h5>Notes</h5>
|
||||
<p>{{ check.Notes }}</p>
|
||||
<h5>Output</h5>
|
||||
<pre>{{check.Output}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/each}}
|
||||
|
||||
{{#link-to "nodes" class="btn btn-default col-xs-12 visible-xs" }}All Nodes{{/link-to}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="index">
|
||||
<div class="col-md-8 col-md-offset-2 col-xs-offset-0 col-sm-offset-0 col-xs-12 col-sm-12 vertical-center">
|
||||
<h5>Select a datacenter</h5>
|
||||
{{#each item in model}}
|
||||
{{#link-to 'services' item }}
|
||||
<div class="panel panel-link panel-short">
|
||||
<div class="panel-bar bg-light-gray"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{item}}
|
||||
<span class="panel-note"></span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- ASSETS -->
|
||||
<script src="javascripts/libs/jquery-1.10.2.js"></script>
|
||||
<script src="javascripts/libs/handlebars-1.1.2.js"></script>
|
||||
<script src="javascripts/libs/ember-1.5.1.js"></script>
|
||||
<script src="javascripts/libs/ember-validations.js"></script>
|
||||
<script src="javascripts/fixtures.js"></script>
|
||||
<script src="javascripts/app/router.js"></script>
|
||||
<script src="javascripts/app/routes.js"></script>
|
||||
<script src="javascripts/app/models.js"></script>
|
||||
<script src="javascripts/app/views.js"></script>
|
||||
<script src="javascripts/app/controllers.js"></script>
|
||||
<script src="javascripts/app/helpers.js"></script>
|
||||
<!-- to activate the test runner, add the "?test" query string parameter -->
|
||||
<script src="tests/runner.js"></script>
|
||||
<!-- <script src="static/application.min.js"></script> -->
|
||||
<!-- /ASSETS -->
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,172 @@
|
|||
App.DcController = Ember.Controller.extend({
|
||||
// Whether or not the dropdown menu can be seen
|
||||
isDropdownVisible: false,
|
||||
|
||||
datacenter: function() {
|
||||
return this.get('content')
|
||||
}.property('Content'),
|
||||
|
||||
checks: function() {
|
||||
var nodes = this.get('nodes');
|
||||
var checks = Ember.A()
|
||||
|
||||
// Combine the checks from all of our nodes
|
||||
// into one.
|
||||
nodes.forEach(function(item) {
|
||||
checks = checks.concat(item.Checks)
|
||||
});
|
||||
|
||||
return checks
|
||||
}.property('nodes'),
|
||||
|
||||
// Returns the total number of failing checks.
|
||||
//
|
||||
// We treat any non-passing checks as failing
|
||||
//
|
||||
totalChecksFailing: function() {
|
||||
var checks = this.get('checks')
|
||||
return (checks.filterBy('Status', 'critical').get('length') +
|
||||
checks.filterBy('Status', 'warning').get('length'))
|
||||
}.property('nodes'),
|
||||
|
||||
//
|
||||
// Returns the human formatted message for the button state
|
||||
//
|
||||
checkMessage: function() {
|
||||
var checks = this.get('checks')
|
||||
var failingChecks = this.get('totalChecksFailing');
|
||||
var passingChecks = checks.filterBy('Status', 'passing').get('length');
|
||||
|
||||
if (this.get('hasFailingChecks') == true) {
|
||||
return failingChecks + ' checks failing';
|
||||
} else {
|
||||
return passingChecks + ' checks passing';
|
||||
}
|
||||
|
||||
}.property('nodes'),
|
||||
|
||||
//
|
||||
// Boolean if the datacenter has any failing checks.
|
||||
//
|
||||
hasFailingChecks: function() {
|
||||
var failingChecks = this.get('totalChecksFailing')
|
||||
return (failingChecks > 0);
|
||||
}.property('nodes'),
|
||||
|
||||
actions: {
|
||||
// Hide and show the dropdown menu
|
||||
toggle: function(item){
|
||||
this.toggleProperty('isDropdownVisible');
|
||||
},
|
||||
// Just hide the dropdown menu
|
||||
hideDrop: function(item){
|
||||
this.set('isDropdownVisible', false);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Add mixins
|
||||
App.KvShowController = Ember.ObjectController.extend(Ember.Validations.Mixin);
|
||||
|
||||
App.KvShowController.reopen({
|
||||
needs: ["dc"],
|
||||
dc: Ember.computed.alias("controllers.dc"),
|
||||
isLoading: false,
|
||||
|
||||
actions: {
|
||||
// Creates the key from the newKey model
|
||||
// set on the route.
|
||||
createKey: function() {
|
||||
this.set('isLoading', true);
|
||||
|
||||
var newKey = this.get('newKey');
|
||||
var parentKey = this.get('parentKey');
|
||||
var grandParentKey = this.get('grandParentKey');
|
||||
var controller = this;
|
||||
var dc = this.get('dc').get('datacenter');
|
||||
|
||||
// If we don't have a previous model to base
|
||||
// on our parent, or we're not at the root level,
|
||||
// add the prefix
|
||||
if (parentKey != undefined && parentKey != "/") {
|
||||
newKey.set('Key', (parentKey + newKey.get('Key')));
|
||||
}
|
||||
|
||||
// Put the Key and the Value retrieved from the form
|
||||
Ember.$.ajax({
|
||||
url: ("/v1/kv/" + newKey.get('Key') + '?dc=' + dc),
|
||||
type: 'PUT',
|
||||
data: newKey.get('Value')
|
||||
}).then(function(response) {
|
||||
// transition to the right place
|
||||
if (newKey.get('isFolder') == true) {
|
||||
controller.transitionToRoute('kv.show', newKey.get('urlSafeKey'));
|
||||
} else {
|
||||
controller.transitionToRoute('kv.edit', newKey.get('urlSafeKey'));
|
||||
}
|
||||
controller.set('isLoading', false)
|
||||
}).fail(function(response) {
|
||||
// Render the error message on the form if the request failed
|
||||
controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App.KvEditController = Ember.Controller.extend({
|
||||
isLoading: false,
|
||||
needs: ["dc"],
|
||||
dc: Ember.computed.alias("controllers.dc"),
|
||||
|
||||
actions: {
|
||||
// Updates the key set as the model on the route.
|
||||
updateKey: function() {
|
||||
this.set('isLoading', true);
|
||||
|
||||
var dc = this.get('dc').get('datacenter');
|
||||
var key = this.get("model");
|
||||
var controller = this;
|
||||
|
||||
// Put the key and the decoded (plain text) value
|
||||
// from the form.
|
||||
Ember.$.ajax({
|
||||
url: ("/v1/kv/" + key.get('Key') + '?dc=' + dc),
|
||||
type: 'PUT',
|
||||
data: key.get('valueDecoded')
|
||||
}).then(function(response) {
|
||||
// If success, just reset the loading state.
|
||||
controller.set('isLoading', false)
|
||||
}).fail(function(response) {
|
||||
// Render the error message on the form if the request failed
|
||||
controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
|
||||
})
|
||||
},
|
||||
|
||||
deleteKey: function() {
|
||||
this.set('isLoading', true);
|
||||
var key = this.get("model");
|
||||
var controller = this;
|
||||
var dc = this.get('dc').get('datacenter');
|
||||
|
||||
// Get the parent for the transition back up a level
|
||||
// after the delete
|
||||
var parent = key.get('urlSafeParentKey');
|
||||
|
||||
// Delete the key
|
||||
Ember.$.ajax({
|
||||
url: ("/v1/kv/" + key.get('Key') + '?dc=' + dc),
|
||||
type: 'DELETE'
|
||||
}).then(function(response) {
|
||||
// Tranisiton back up a level
|
||||
controller.transitionToRoute('kv.show', parent);
|
||||
controller.set('isLoading', false);
|
||||
}).fail(function(response) {
|
||||
// Render the error message on the form if the request failed
|
||||
controller.set('errorMessage', 'Received error while processing: ' + response.statusText)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
Ember.Handlebars.helper('panelBar', function(status) {
|
||||
var highlightClass;
|
||||
|
||||
if (status == "passing") {
|
||||
highlightClass = "bg-green";
|
||||
} else {
|
||||
highlightClass = "bg-orange";
|
||||
}
|
||||
return new Handlebars.SafeString('<div class="panel-bar ' + highlightClass + '"></div>');
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
//
|
||||
// A Consul service.
|
||||
//
|
||||
App.Service = Ember.Object.extend({
|
||||
//
|
||||
// The number of failing checks within the service.
|
||||
//
|
||||
failingChecks: function() {
|
||||
// If the service was returned from `/v1/internal/ui/services`
|
||||
// then we have a aggregated value which we can just grab
|
||||
if (this.get('ChecksCritical') != undefined) {
|
||||
return (this.get('ChecksCritical') + this.get('ChecksWarning'))
|
||||
// Otherwise, we need to filter the child checks by both failing
|
||||
// states
|
||||
} else {
|
||||
return (checks.filterBy('Status', 'critical').get('length') +
|
||||
checks.filterBy('Status', 'warning').get('length'))
|
||||
}
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// The number of passing checks within the service.
|
||||
//
|
||||
passingChecks: function() {
|
||||
// If the service was returned from `/v1/internal/ui/services`
|
||||
// then we have a aggregated value which we can just grab
|
||||
if (this.get('ChecksPassing') != undefined) {
|
||||
return this.get('ChecksPassing')
|
||||
// Otherwise, we need to filter the child checks by both failing
|
||||
// states
|
||||
} else {
|
||||
return this.get('Checks').filterBy('Status', 'passing').get('length');
|
||||
}
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// The formatted message returned for the user which represents the
|
||||
// number of checks failing or passing. Returns `1 passing` or `2 failing`
|
||||
//
|
||||
checkMessage: function() {
|
||||
if (this.get('hasFailingChecks') === false) {
|
||||
return this.get('passingChecks') + ' passing';
|
||||
} else {
|
||||
return this.get('failingChecks') + ' failing';
|
||||
}
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// Boolean of whether or not there are failing checks in the service.
|
||||
// This is used to set color backgrounds and so on.
|
||||
//
|
||||
hasFailingChecks: function() {
|
||||
return (this.get('failingChecks') > 0);
|
||||
}.property('Checks')
|
||||
});
|
||||
|
||||
//
|
||||
// A Consul Node
|
||||
//
|
||||
App.Node = Ember.Object.extend({
|
||||
//
|
||||
// The number of failing checks within the service.
|
||||
//
|
||||
failingChecks: function() {
|
||||
var checks = this.get('Checks');
|
||||
// We view both warning and critical as failing
|
||||
return (checks.filterBy('Status', 'critical').get('length') +
|
||||
checks.filterBy('Status', 'warning').get('length'))
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// The number of passing checks within the service.
|
||||
//
|
||||
passingChecks: function() {
|
||||
return this.get('Checks').filterBy('Status', 'passing').get('length');
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// The formatted message returned for the user which represents the
|
||||
// number of checks failing or passing. Returns `1 passing` or `2 failing`
|
||||
//
|
||||
checkMessage: function() {
|
||||
if (this.get('hasFailingChecks') === false) {
|
||||
return this.get('passingChecks') + ' passing';
|
||||
} else {
|
||||
return this.get('failingChecks') + ' failing';
|
||||
}
|
||||
}.property('Checks'),
|
||||
|
||||
//
|
||||
// Boolean of whether or not there are failing checks in the service.
|
||||
// This is used to set color backgrounds and so on.
|
||||
//
|
||||
hasFailingChecks: function() {
|
||||
return (this.get('failingChecks') > 0);
|
||||
}.property('Checks')
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// A key/value object
|
||||
//
|
||||
App.Key = Ember.Object.extend(Ember.Validations.Mixin, {
|
||||
// Validates using the Ember.Valdiations library
|
||||
validations: {
|
||||
Key: { presence: true }
|
||||
},
|
||||
|
||||
// Boolean if the key is valid
|
||||
keyValid: Ember.computed.empty('errors.Key'),
|
||||
// Boolean if the value is valid
|
||||
valueValid: Ember.computed.empty('errors.Value'),
|
||||
|
||||
// The key with the parent removed.
|
||||
// This is only for display purposes, and used for
|
||||
// showing the key name inside of a nested key.
|
||||
keyWithoutParent: function() {
|
||||
return (this.get('Key').replace(this.get('parentKey'), ''));
|
||||
}.property('Key'),
|
||||
|
||||
// Boolean if the key is a "folder" or not, i.e is a nested key
|
||||
// that feels like a folder. Used for UI
|
||||
isFolder: function() {
|
||||
if (this.get('Key') === undefined) {
|
||||
return false;
|
||||
};
|
||||
return (this.get('Key').slice(-1) == "/")
|
||||
}.property('Key'),
|
||||
|
||||
// The dasherized URL safe version of the key for routing
|
||||
urlSafeKey: function() {
|
||||
return this.get('Key').replace(/\//g, "-")
|
||||
}.property('Key'),
|
||||
|
||||
// The dasherized URL safe version of the parent key for routing
|
||||
urlSafeParentKey: function() {
|
||||
return this.get('parentKey').replace(/\//g, "-")
|
||||
}.property('Key'),
|
||||
|
||||
// Determines what route to link to. If it's a folder,
|
||||
// it will link to kv.show. Otherwise, kv.edit
|
||||
linkToRoute: function() {
|
||||
var key = this.get('urlSafeKey')
|
||||
|
||||
// If the key ends in - it's a folder
|
||||
if (key.slice(-1) === "-") {
|
||||
return 'kv.show'
|
||||
} else {
|
||||
return 'kv.edit'
|
||||
}
|
||||
}.property('Key'),
|
||||
|
||||
// The base64 decoded value of the key.
|
||||
// if you set on this key, it will update
|
||||
// the key.Value
|
||||
valueDecoded: function(key, value) {
|
||||
|
||||
// setter
|
||||
if (arguments.length > 1) {
|
||||
this.set('Value', value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// getter
|
||||
|
||||
// If the value is null, we don't
|
||||
// want to try and base64 decode it, so just return
|
||||
if (this.get('Value') === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// base64 decode the value
|
||||
return window.atob(this.get('Value'));
|
||||
}.property('Value'),
|
||||
|
||||
|
||||
// An array of the key broken up by the /
|
||||
keyParts: function() {
|
||||
var key = this.get('Key');
|
||||
|
||||
// If the key is a folder, remove the last
|
||||
// slash to split properly
|
||||
if (key.slice(-1) == "/") {
|
||||
key = key.substring(0, key.length - 1);
|
||||
}
|
||||
|
||||
return key.split('/');
|
||||
}.property('Key'),
|
||||
|
||||
// The parent Key is the key one level above this.Key
|
||||
// key: baz/bar/foobar/
|
||||
// grandParent: baz/bar/
|
||||
parentKey: function() {
|
||||
var parts = this.get('keyParts').toArray();
|
||||
|
||||
// Remove the last item, essentially going up a level
|
||||
// in hiearchy
|
||||
parts.pop();
|
||||
|
||||
return parts.join("/") + "/";
|
||||
}.property('Key'),
|
||||
|
||||
// The grandParent Key is the key two levels above this.Key
|
||||
// key: baz/bar/foobar/
|
||||
// grandParent: baz/
|
||||
grandParentKey: function() {
|
||||
var parts = this.get('keyParts').toArray();
|
||||
|
||||
// Remove the last two items, jumping two levels back
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
|
||||
return parts.join("/") + "/";
|
||||
}.property('Key')
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
window.App = Ember.Application.create({
|
||||
rootElement: "#app"
|
||||
});
|
||||
|
||||
|
||||
App.Router.map(function() {
|
||||
// Our parent datacenter resource sets the namespace
|
||||
// for the entire application
|
||||
this.resource("dc", {path: "/:dc"}, function() {
|
||||
// Services represent a consul service
|
||||
this.resource("services", { path: "/services" }, function(){
|
||||
// Show an individual service
|
||||
this.route("show", { path: "/:name" });
|
||||
});
|
||||
// Nodes represent a consul node
|
||||
this.resource("nodes", { path: "/nodes" }, function() {
|
||||
// Show an individual node
|
||||
this.route("show", { path: "/:name" });
|
||||
});
|
||||
// Key/Value
|
||||
this.resource("kv", { path: "/kv" }, function(){
|
||||
// This route just redirects to /-
|
||||
this.route("index", { path: "/" });
|
||||
// List keys. This is more like an index
|
||||
this.route("show", { path: "/:key" });
|
||||
// Edit a specific key
|
||||
this.route("edit", { path: "/:key/edit" });
|
||||
})
|
||||
});
|
||||
|
||||
// Shows a datacenter picker. If you only have one
|
||||
// it just redirects you through.
|
||||
this.route("index", { path: "/" });
|
||||
});
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
//
|
||||
// Superclass to be used by all of the main routes below.
|
||||
//
|
||||
App.BaseRoute = Ember.Route.extend({
|
||||
getParentAndGrandparent: function(key) {
|
||||
var parentKey, grandParentKey, isFolder;
|
||||
|
||||
parts = key.split('/');
|
||||
|
||||
// If we are the root, set the parent and grandparent to the
|
||||
// root.
|
||||
if (key == "/") {
|
||||
parentKey = "/";
|
||||
grandParentKey ="/"
|
||||
} else {
|
||||
// Go one level up
|
||||
parts.pop();
|
||||
parentKey = parts.join("/") + "/";
|
||||
|
||||
// Go two levels up
|
||||
parts.pop();
|
||||
grandParentKey = parts.join("/") + "/";
|
||||
}
|
||||
|
||||
return {grandParent: grandParentKey, parent: parentKey}
|
||||
},
|
||||
|
||||
removeDuplicateKeys: function(keys, matcher) {
|
||||
// Loop over the keys
|
||||
keys.forEach(function(item, index) {
|
||||
if (item.get('Key') == matcher) {
|
||||
// If we are in a nested folder and the folder
|
||||
// name matches our position, remove it
|
||||
keys.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Used to link to keys that are not objects,
|
||||
// like parents and grandParents
|
||||
linkToKey: function(key) {
|
||||
key = key.replace(/\//g, "-")
|
||||
|
||||
if (key.slice(-1) === "-") {
|
||||
this.transitionTo('kv.show', key)
|
||||
} else {
|
||||
this.transitionTo('kv.edit', key)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// The route for choosing datacenters, typically the first route loaded.
|
||||
//
|
||||
App.IndexRoute = App.BaseRoute.extend({
|
||||
// Retrieve the list of datacenters
|
||||
model: function(params) {
|
||||
return Ember.$.getJSON('/v1/catalog/datacenters').then(function(data) {
|
||||
return data
|
||||
})
|
||||
},
|
||||
|
||||
afterModel: function(model, transition) {
|
||||
// If we only have one datacenter, jump
|
||||
// straight to it and bypass the global
|
||||
// view
|
||||
if (model.get('length') === 1) {
|
||||
this.transitionTo('services', model[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The parent route for all resources. This keeps the top bar
|
||||
// functioning, as well as the per-dc requests.
|
||||
App.DcRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
// Return a promise hash to retreieve the
|
||||
// dcs and nodes used in the header
|
||||
return Ember.RSVP.hash({
|
||||
dc: params.dc,
|
||||
dcs: Ember.$.getJSON('/v1/catalog/datacenters'),
|
||||
nodes: Ember.$.getJSON('/v1/internal/ui/nodes?dc=' + params.dc).then(function(data) {
|
||||
objs = [];
|
||||
|
||||
// Merge the nodes into a list and create objects out of them
|
||||
data.map(function(obj){
|
||||
objs.push(App.Node.create(obj));
|
||||
});
|
||||
|
||||
return objs;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
setupController: function(controller, models) {
|
||||
controller.set('content', models.dc);
|
||||
controller.set('nodes', models.nodes);
|
||||
controller.set('dcs', models.dcs);
|
||||
controller.set('isDropdownVisible', false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
App.KvIndexRoute = App.BaseRoute.extend({
|
||||
// If they hit /kv we want to just move them to /kv/-
|
||||
beforeModel: function() {
|
||||
this.transitionTo('kv.show', '-')
|
||||
}
|
||||
});
|
||||
|
||||
App.KvShowRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
// Convert the key back to the format consul understands
|
||||
var key = params.key.replace(/-/g, "/")
|
||||
var dc = this.modelFor('dc').dc;
|
||||
|
||||
// Return a promise has with the ?keys for that namespace
|
||||
// and the original key requested in params
|
||||
return Ember.RSVP.hash({
|
||||
key: key,
|
||||
keys: Ember.$.getJSON('/v1/kv/' + key + '?keys&seperator=' + '/&dc=' + dc).then(function(data) {
|
||||
objs = [];
|
||||
data.map(function(obj){
|
||||
objs.push(App.Key.create({Key: obj}));
|
||||
});
|
||||
return objs;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
setupController: function(controller, models) {
|
||||
var key = models.key;
|
||||
var parentKeys = this.getParentAndGrandparent(key);
|
||||
models.keys = this.removeDuplicateKeys(models.keys, models.key);
|
||||
|
||||
controller.set('content', models.keys);
|
||||
controller.set('parentKey', parentKeys.parent);
|
||||
controller.set('grandParentKey', parentKeys.grandParent);
|
||||
controller.set('newKey', App.Key.create());
|
||||
}
|
||||
});
|
||||
|
||||
App.KvEditRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
var key = params.key.replace(/-/g, "/");
|
||||
var dc = this.modelFor('dc').dc;
|
||||
var parentKeys = this.getParentAndGrandparent(key)
|
||||
|
||||
// Return a promise hash to get the data for both columns
|
||||
return Ember.RSVP.hash({
|
||||
key: Ember.$.getJSON('/v1/kv/' + key + '?dc=' + dc).then(function(data) {
|
||||
// Convert the returned data to a Key
|
||||
return App.Key.create().setProperties(data[0]);
|
||||
}),
|
||||
keys: keysPromise = Ember.$.getJSON('/v1/kv/' + parentKeys.parent + '?keys&seperator=' + '/' + '&dc=' + dc).then(function(data) {
|
||||
objs = [];
|
||||
data.map(function(obj){
|
||||
objs.push(App.Key.create({Key: obj}));
|
||||
});
|
||||
return objs;
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
setupController: function(controller, models) {
|
||||
var key = models.key;
|
||||
var parentKeys = this.getParentAndGrandparent(key.get('Key'));
|
||||
models.keys = this.removeDuplicateKeys(models.keys, parentKeys.parent);
|
||||
|
||||
controller.set('content', models.key);
|
||||
controller.set('parentKey', parentKeys.parent);
|
||||
controller.set('grandParentKey', parentKeys.grandParent);
|
||||
controller.set('siblings', models.keys);
|
||||
}
|
||||
});
|
||||
|
||||
App.ServicesRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
var dc = this.modelFor('dc').dc
|
||||
// Return a promise to retrieve all of the services
|
||||
return Ember.$.getJSON('/v1/internal/ui/services?dc=' + dc).then(function(data) {
|
||||
objs = [];
|
||||
data.map(function(obj){
|
||||
objs.push(App.Service.create(obj));
|
||||
});
|
||||
return objs
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.set('services', model);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
App.ServicesShowRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
var dc = this.modelFor('dc').dc
|
||||
// Here we just use the built-in health endpoint, as it gives us everything
|
||||
// we need.
|
||||
return Ember.$.getJSON('/v1/health/service/' + params.name + '?dc=' + dc).then(function(data) {
|
||||
objs = [];
|
||||
data.map(function(obj){
|
||||
objs.push(App.Node.create(obj));
|
||||
});
|
||||
return objs;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
App.NodesShowRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
var dc = this.modelFor('dc').dc
|
||||
// Return a promise hash of the node and nodes
|
||||
return Ember.RSVP.hash({
|
||||
node: Ember.$.getJSON('/v1/internal/ui/node/' + params.name + '?dc=' + dc).then(function(data) {
|
||||
return App.Node.create(data)
|
||||
}),
|
||||
nodes: Ember.$.getJSON('/v1/internal/ui/node/' + params.name + '?dc=' + dc).then(function(data) {
|
||||
return App.Node.create(data)
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
setupController: function(controller, models) {
|
||||
controller.set('content', models.node);
|
||||
//
|
||||
// Since we have 2 column layout, we need to also display the
|
||||
// list of nodes on the left. Hence setting the attribute
|
||||
// {{nodes}} on the controller.
|
||||
//
|
||||
controller.set('nodes', models.nodes);
|
||||
}
|
||||
});
|
||||
|
||||
App.NodesRoute = App.BaseRoute.extend({
|
||||
model: function(params) {
|
||||
var dc = this.modelFor('dc').dc
|
||||
// Return a promise containing the nodes
|
||||
return Ember.$.getJSON('/v1/internal/ui/nodes?dc=' + dc).then(function(data) {
|
||||
objs = [];
|
||||
data.map(function(obj){
|
||||
objs.push(App.Node.create(obj));
|
||||
});
|
||||
return objs
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.set('nodes', model);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
//
|
||||
// DC
|
||||
//
|
||||
|
||||
App.DcView = Ember.View.extend({
|
||||
templateName: 'dc',
|
||||
classNames: 'dropdowns',
|
||||
|
||||
click: function(e){
|
||||
if ($(e.target).is('.dropdowns')){
|
||||
$('ul.dropdown-menu').hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//
|
||||
// Services
|
||||
//
|
||||
App.ServicesView = Ember.View.extend({
|
||||
templateName: 'services',
|
||||
})
|
||||
|
||||
App.ServicesShowView = Ember.View.extend({
|
||||
templateName: 'service'
|
||||
})
|
||||
|
||||
App.ServicesLoadingView = Ember.View.extend({
|
||||
templateName: 'item/loading'
|
||||
})
|
||||
|
||||
//
|
||||
// Nodes
|
||||
//
|
||||
|
||||
App.NodesView = Ember.View.extend({
|
||||
templateName: 'nodes'
|
||||
})
|
||||
|
||||
App.NodesShowView = Ember.View.extend({
|
||||
templateName: 'node'
|
||||
})
|
||||
|
||||
App.NodesLoadingView = Ember.View.extend({
|
||||
templateName: 'item/loading'
|
||||
})
|
||||
|
||||
App.KvListView = Ember.View.extend({
|
||||
templateName: 'kv'
|
||||
})
|
|
@ -0,0 +1,317 @@
|
|||
//
|
||||
// I intentionally am not using ember-data and the fixture
|
||||
// adapter. I'm not confident the Consul UI API will be compatible
|
||||
// without a bunch of wrangling, and it's really not enough updating
|
||||
// of the models to justify the use of such a big component. getJSON
|
||||
// *should* be enough.
|
||||
//
|
||||
|
||||
window.fixtures = {}
|
||||
|
||||
//
|
||||
// The array route, i.e /ui/<dc>/services, should return _all_ services
|
||||
// in the DC
|
||||
//
|
||||
fixtures.services = [
|
||||
{
|
||||
"Name": "vagrant-cloud-http",
|
||||
"Checks": [
|
||||
{
|
||||
"Name": "serfHealth",
|
||||
"Status": "passing"
|
||||
},
|
||||
{
|
||||
"Name": "fooHealth",
|
||||
"Status": "critical"
|
||||
},
|
||||
{
|
||||
"Name": "bazHealth",
|
||||
"Status": "passing"
|
||||
}
|
||||
],
|
||||
"Nodes": [
|
||||
"node-10-0-1-109",
|
||||
"node-10-0-1-102"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "vagrant-share-mux",
|
||||
"Checks": [
|
||||
{
|
||||
"Name": "serfHealth",
|
||||
"Status": "critical"
|
||||
},
|
||||
{
|
||||
"Name": "fooHealth",
|
||||
"Status": "passing"
|
||||
},
|
||||
{
|
||||
"Name": "bazHealth",
|
||||
"Status": "passing"
|
||||
}
|
||||
],
|
||||
"Nodes": [
|
||||
"node-10-0-1-109",
|
||||
"node-10-0-1-102"
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
//
|
||||
// This one is slightly more complicated to allow more UI interaction.
|
||||
// It represents the route /ui/<dc>/services/<service> BUT it's what is
|
||||
// BELOW the top-level key.
|
||||
//
|
||||
// So, what is actually returned should be similar to the /catalog/service/<service>
|
||||
// endpoint.
|
||||
fixtures.services_full = {
|
||||
"vagrant-cloud-http":
|
||||
// This array is what is actually expected from the API.
|
||||
[
|
||||
{
|
||||
"ServicePort": 80,
|
||||
"ServiceTags": null,
|
||||
"ServiceName": "vagrant-cloud-http",
|
||||
"ServiceID": "vagrant-cloud-http",
|
||||
"Address": "10.0.1.109",
|
||||
"Node": "node-10-0-1-109",
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "",
|
||||
"Status": "critical",
|
||||
"Name": "Serf Health Status",
|
||||
"CheckID": "serfHealth",
|
||||
"Node": "node-10-0-1-109"
|
||||
}
|
||||
]
|
||||
},
|
||||
// A node
|
||||
{
|
||||
"ServicePort": 80,
|
||||
"ServiceTags": null,
|
||||
"ServiceName": "vagrant-cloud-http",
|
||||
"ServiceID": "vagrant-cloud-http",
|
||||
"Address": "10.0.1.102",
|
||||
"Node": "node-10-0-1-102",
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "",
|
||||
"Status": "passing",
|
||||
"Name": "Serf Health Status",
|
||||
"CheckID": "serfHealth",
|
||||
"Node": "node-10-0-1-102"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"vagrant-share-mux": [
|
||||
// A node
|
||||
{
|
||||
"ServicePort": 80,
|
||||
"ServiceTags": null,
|
||||
"ServiceName": "vagrant-share-mux",
|
||||
"ServiceID": "vagrant-share-mux",
|
||||
"Address": "10.0.1.102",
|
||||
"Node": "node-10-0-1-102",
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "vagrant-share-mux",
|
||||
"ServiceID": "vagrant-share-mux",
|
||||
"Notes": "",
|
||||
"Output": "200 ok",
|
||||
"Status": "passing",
|
||||
"Name": "Foo Heathly",
|
||||
"CheckID": "fooHealth",
|
||||
"Node": "node-10-0-1-102"
|
||||
}
|
||||
]
|
||||
},
|
||||
// A node
|
||||
{
|
||||
"ServicePort": 80,
|
||||
"ServiceTags": null,
|
||||
"ServiceName": "vagrant-share-mux",
|
||||
"ServiceID": "vagrant-share-mux",
|
||||
"Address": "10.0.1.109",
|
||||
"Node": "node-10-0-1-109",
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "",
|
||||
"Output": "foobar baz",
|
||||
"Status": "passing",
|
||||
"Name": "Baz Status",
|
||||
"CheckID": "bazHealth",
|
||||
"Node": "node-10-0-1-109"
|
||||
},
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "",
|
||||
"Output": "foobar baz",
|
||||
"Status": "critical",
|
||||
"Name": "Serf Health Status",
|
||||
"CheckID": "serfHealth",
|
||||
"Node": "node-10-0-1-109"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
//
|
||||
// /ui/<dc>/nodes
|
||||
// all the nodes
|
||||
//
|
||||
fixtures.nodes = [
|
||||
{
|
||||
"Address": "10.0.1.109",
|
||||
"Name": "node-10-0-1-109",
|
||||
"Services": [
|
||||
"vagrant-share-mux",
|
||||
"vagrant-cloud-http"
|
||||
],
|
||||
"Checks": [
|
||||
{
|
||||
"Name": "serfHealth",
|
||||
"Status": "critical"
|
||||
},
|
||||
{
|
||||
"Name": "bazHealth",
|
||||
"Status": "passing"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Address": "10.0.1.102",
|
||||
"Name": "node-10-0-1-102",
|
||||
"Services": [
|
||||
"vagrant-share-mux",
|
||||
"vagrant-cloud-http"
|
||||
],
|
||||
"Checks": [
|
||||
{
|
||||
"Name": "fooHealth",
|
||||
"Status": "passing"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
// These are for retrieving individual nodes. Same story as services,
|
||||
// the top level key is just for the demo.
|
||||
fixtures.nodes_full = {
|
||||
"node-10-0-1-109":
|
||||
// This is what would be returned.
|
||||
{
|
||||
"Services": [
|
||||
{
|
||||
"Port": 0,
|
||||
"Tags": null,
|
||||
"Service": "vagrant-share-mux",
|
||||
"ID": "vagrant-share-mux"
|
||||
},
|
||||
{
|
||||
"Port": 80,
|
||||
"Tags": null,
|
||||
"Service": "vagrant-cloud-http",
|
||||
"ID": "vagrant-cloud-http"
|
||||
}
|
||||
],
|
||||
"Node": {
|
||||
"Address": "10.0.1.109",
|
||||
"Node": "node-10-0-1-109"
|
||||
},
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "Checks the status of the serf agent",
|
||||
"Status": "critical",
|
||||
"Name": "Serf Health Status",
|
||||
"CheckID": "serfHealth",
|
||||
"Node": "node-10-0-1-109"
|
||||
},
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "",
|
||||
"Output": "foobar baz",
|
||||
"Status": "passing",
|
||||
"Name": "Baz Status",
|
||||
"CheckID": "bazHealth",
|
||||
"Node": "node-10-0-1-109"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node-10-0-1-102": {
|
||||
"Services": [
|
||||
{
|
||||
"Port": 0,
|
||||
"Tags": null,
|
||||
"Service": "vagrant-share-mux",
|
||||
"ID": "vagrant-share-mux"
|
||||
},
|
||||
{
|
||||
"Port": 80,
|
||||
"Tags": null,
|
||||
"Service": "vagrant-cloud-http",
|
||||
"ID": "vagrant-cloud-http"
|
||||
}
|
||||
],
|
||||
"Node": {
|
||||
"Address": "10.0.1.102",
|
||||
"Node": "node-10-0-1-102"
|
||||
},
|
||||
"Checks": [
|
||||
{
|
||||
"ServiceName": "",
|
||||
"ServiceID": "",
|
||||
"Notes": "Checks if the food is healthy",
|
||||
"Output": "foobar baz",
|
||||
"Status": "passing",
|
||||
"Name": "Foo Healthy",
|
||||
"CheckID": "fooStatus",
|
||||
"Node": "node-10-0-1-102"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fixtures.dcs = ['nyc1', 'sf1', 'sg1']
|
||||
|
||||
fixtures.keys_full = {
|
||||
"/": [
|
||||
'foobar',
|
||||
'application',
|
||||
'web/'
|
||||
],
|
||||
"application": {
|
||||
'key': 'application',
|
||||
'value': 'foobarz'
|
||||
},
|
||||
"foobar": {
|
||||
'key': 'foobar',
|
||||
'value': 'baz'
|
||||
},
|
||||
"web/foo/bar": {
|
||||
'key': 'web/foo/bar',
|
||||
'value': 'baz'
|
||||
},
|
||||
"web/foo/baz": {
|
||||
'key': 'web/foo/baz',
|
||||
'value': 'test'
|
||||
},
|
||||
"web/": [
|
||||
"web/foo/"
|
||||
],
|
||||
"web/foo/": [
|
||||
"web/foo/bar",
|
||||
"web/foo/baz"
|
||||
]
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,778 @@
|
|||
// ==========================================================================
|
||||
// Project: Ember Validations
|
||||
// Copyright: Copyright 2013 DockYard, LLC. and contributors.
|
||||
// License: Licensed under MIT license (see license.js)
|
||||
// ==========================================================================
|
||||
|
||||
|
||||
// Version: 1.0.0.beta.2
|
||||
|
||||
(function() {
|
||||
Ember.Validations = Ember.Namespace.create({
|
||||
VERSION: '1.0.0.beta.2'
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.messages = {
|
||||
render: function(attribute, context) {
|
||||
if (Ember.I18n) {
|
||||
return Ember.I18n.t('errors.' + attribute, context);
|
||||
} else {
|
||||
var regex = new RegExp("{{(.*?)}}"),
|
||||
attributeName = "";
|
||||
if (regex.test(this.defaults[attribute])) {
|
||||
attributeName = regex.exec(this.defaults[attribute])[1];
|
||||
}
|
||||
return this.defaults[attribute].replace(regex, context[attributeName]);
|
||||
}
|
||||
},
|
||||
defaults: {
|
||||
inclusion: "is not included in the list",
|
||||
exclusion: "is reserved",
|
||||
invalid: "is invalid",
|
||||
confirmation: "doesn't match {{attribute}}",
|
||||
accepted: "must be accepted",
|
||||
empty: "can't be empty",
|
||||
blank: "can't be blank",
|
||||
present: "must be blank",
|
||||
tooLong: "is too long (maximum is {{count}} characters)",
|
||||
tooShort: "is too short (minimum is {{count}} characters)",
|
||||
wrongLength: "is the wrong length (should be {{count}} characters)",
|
||||
notANumber: "is not a number",
|
||||
notAnInteger: "must be an integer",
|
||||
greaterThan: "must be greater than {{count}}",
|
||||
greaterThanOrEqualTo: "must be greater than or equal to {{count}}",
|
||||
equalTo: "must be equal to {{count}}",
|
||||
lessThan: "must be less than {{count}}",
|
||||
lessThanOrEqualTo: "must be less than or equal to {{count}}",
|
||||
otherThan: "must be other than {{count}}",
|
||||
odd: "must be odd",
|
||||
even: "must be even",
|
||||
url: "is not a valid URL"
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.Errors = Ember.Object.extend({
|
||||
unknownProperty: function(property) {
|
||||
this.set(property, Ember.makeArray());
|
||||
return this.get(property);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
var setValidityMixin = Ember.Mixin.create({
|
||||
isValid: function() {
|
||||
return this.get('validators').compact().filterBy('isValid', false).get('length') === 0;
|
||||
}.property('validators.@each.isValid'),
|
||||
isInvalid: Ember.computed.not('isValid')
|
||||
});
|
||||
|
||||
var pushValidatableObject = function(model, property) {
|
||||
var content = model.get(property);
|
||||
|
||||
model.removeObserver(property, pushValidatableObject);
|
||||
if (Ember.isArray(content)) {
|
||||
model.validators.pushObject(ArrayValidatorProxy.create({model: model, property: property, contentBinding: 'model.' + property}));
|
||||
} else {
|
||||
model.validators.pushObject(content);
|
||||
}
|
||||
};
|
||||
|
||||
var findValidator = function(validator) {
|
||||
var klass = validator.classify();
|
||||
return Ember.Validations.validators.local[klass] || Ember.Validations.validators.remote[klass];
|
||||
};
|
||||
|
||||
var ArrayValidatorProxy = Ember.ArrayProxy.extend(setValidityMixin, {
|
||||
validate: function() {
|
||||
return this._validate();
|
||||
},
|
||||
_validate: function() {
|
||||
var promises = this.get('content').invoke('_validate').without(undefined);
|
||||
return Ember.RSVP.all(promises);
|
||||
}.on('init'),
|
||||
validators: Ember.computed.alias('content')
|
||||
});
|
||||
|
||||
Ember.Validations.Mixin = Ember.Mixin.create(setValidityMixin, {
|
||||
init: function() {
|
||||
this._super();
|
||||
this.errors = Ember.Validations.Errors.create();
|
||||
this._dependentValidationKeys = {};
|
||||
this.validators = Ember.makeArray();
|
||||
if (this.get('validations') === undefined) {
|
||||
this.validations = {};
|
||||
}
|
||||
this.buildValidators();
|
||||
this.validators.forEach(function(validator) {
|
||||
validator.addObserver('errors.[]', this, function(sender, key, value, context, rev) {
|
||||
var errors = Ember.makeArray();
|
||||
this.validators.forEach(function(validator) {
|
||||
if (validator.property === sender.property) {
|
||||
errors = errors.concat(validator.errors);
|
||||
}
|
||||
}, this);
|
||||
this.set('errors.' + sender.property, errors);
|
||||
});
|
||||
}, this);
|
||||
},
|
||||
buildValidators: function() {
|
||||
var property, validator;
|
||||
|
||||
for (property in this.validations) {
|
||||
if (this.validations[property].constructor === Object) {
|
||||
this.buildRuleValidator(property);
|
||||
} else {
|
||||
this.buildObjectValidator(property);
|
||||
}
|
||||
}
|
||||
},
|
||||
buildRuleValidator: function(property) {
|
||||
var validator;
|
||||
for (validator in this.validations[property]) {
|
||||
if (this.validations[property].hasOwnProperty(validator)) {
|
||||
this.validators.pushObject(findValidator(validator).create({model: this, property: property, options: this.validations[property][validator]}));
|
||||
}
|
||||
}
|
||||
},
|
||||
buildObjectValidator: function(property) {
|
||||
if (Ember.isNone(this.get(property))) {
|
||||
this.addObserver(property, this, pushValidatableObject);
|
||||
} else {
|
||||
pushValidatableObject(this, property);
|
||||
}
|
||||
},
|
||||
validate: function() {
|
||||
var self = this;
|
||||
return this._validate().then(function(vals) {
|
||||
var errors = self.get('errors');
|
||||
if (vals.contains(false)) {
|
||||
return Ember.RSVP.reject(errors);
|
||||
}
|
||||
return errors;
|
||||
});
|
||||
},
|
||||
_validate: function() {
|
||||
var promises = this.validators.invoke('_validate').without(undefined);
|
||||
return Ember.RSVP.all(promises);
|
||||
}.on('init')
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.patterns = Ember.Namespace.create({
|
||||
numericality: /^(-|\+)?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d*)?$/,
|
||||
blank: /^\s*$/
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators = Ember.Namespace.create();
|
||||
Ember.Validations.validators.local = Ember.Namespace.create();
|
||||
Ember.Validations.validators.remote = Ember.Namespace.create();
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.Base = Ember.Object.extend({
|
||||
init: function() {
|
||||
this.set('errors', Ember.makeArray());
|
||||
this._dependentValidationKeys = Ember.makeArray();
|
||||
this.conditionals = {
|
||||
'if': this.get('options.if'),
|
||||
unless: this.get('options.unless')
|
||||
};
|
||||
this.model.addObserver(this.property, this, this._validate);
|
||||
},
|
||||
addObserversForDependentValidationKeys: function() {
|
||||
this._dependentValidationKeys.forEach(function(key) {
|
||||
this.model.addObserver(key, this, this._validate);
|
||||
}, this);
|
||||
}.on('init'),
|
||||
pushDependentValidationKeyToModel: function() {
|
||||
var model = this.get('model');
|
||||
if (model._dependentValidationKeys[this.property] === undefined) {
|
||||
model._dependentValidationKeys[this.property] = Ember.makeArray();
|
||||
}
|
||||
model._dependentValidationKeys[this.property].addObjects(this._dependentValidationKeys);
|
||||
}.on('init'),
|
||||
call: function () {
|
||||
throw 'Not implemented!';
|
||||
},
|
||||
unknownProperty: function(key) {
|
||||
var model = this.get('model');
|
||||
if (model) {
|
||||
return model.get(key);
|
||||
}
|
||||
},
|
||||
isValid: Ember.computed.empty('errors.[]'),
|
||||
validate: function() {
|
||||
var self = this;
|
||||
return this._validate().then(function(success) {
|
||||
// Convert validation failures to rejects.
|
||||
var errors = self.get('model.errors');
|
||||
if (success) {
|
||||
return errors;
|
||||
} else {
|
||||
return Ember.RSVP.reject(errors);
|
||||
}
|
||||
});
|
||||
},
|
||||
_validate: function() {
|
||||
this.errors.clear();
|
||||
if (this.canValidate()) {
|
||||
this.call();
|
||||
}
|
||||
if (this.get('isValid')) {
|
||||
return Ember.RSVP.resolve(true);
|
||||
} else {
|
||||
return Ember.RSVP.resolve(false);
|
||||
}
|
||||
}.on('init'),
|
||||
canValidate: function() {
|
||||
if (typeof(this.conditionals) === 'object') {
|
||||
if (this.conditionals['if']) {
|
||||
if (typeof(this.conditionals['if']) === 'function') {
|
||||
return this.conditionals['if'](this.model, this.property);
|
||||
} else if (typeof(this.conditionals['if']) === 'string') {
|
||||
if (typeof(this.model[this.conditionals['if']]) === 'function') {
|
||||
return this.model[this.conditionals['if']]();
|
||||
} else {
|
||||
return this.model.get(this.conditionals['if']);
|
||||
}
|
||||
}
|
||||
} else if (this.conditionals.unless) {
|
||||
if (typeof(this.conditionals.unless) === 'function') {
|
||||
return !this.conditionals.unless(this.model, this.property);
|
||||
} else if (typeof(this.conditionals.unless) === 'string') {
|
||||
if (typeof(this.model[this.conditionals.unless]) === 'function') {
|
||||
return !this.model[this.conditionals.unless]();
|
||||
} else {
|
||||
return !this.model.get(this.conditionals.unless);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Absence = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
/*jshint expr:true*/
|
||||
if (this.options === true) {
|
||||
this.set('options', {});
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('present', this.options));
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
if (!Ember.isEmpty(this.model.get(this.property))) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Acceptance = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
/*jshint expr:true*/
|
||||
if (this.options === true) {
|
||||
this.set('options', {});
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('accepted', this.options));
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
if (this.options.accept) {
|
||||
if (this.model.get(this.property) !== this.options.accept) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.model.get(this.property) !== '1' && this.model.get(this.property) !== 1 && this.model.get(this.property) !== true) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Confirmation = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this.originalProperty = this.property;
|
||||
this.property = this.property + 'Confirmation';
|
||||
this._super();
|
||||
this._dependentValidationKeys.pushObject(this.originalProperty);
|
||||
/*jshint expr:true*/
|
||||
if (this.options === true) {
|
||||
this.set('options', { attribute: this.originalProperty });
|
||||
this.set('options', { message: Ember.Validations.messages.render('confirmation', this.options) });
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
if (this.model.get(this.originalProperty) !== this.model.get(this.property)) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Exclusion = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
if (this.options.constructor === Array) {
|
||||
this.set('options', { 'in': this.options });
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('exclusion', this.options));
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
/*jshint expr:true*/
|
||||
var message, lower, upper;
|
||||
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
if (this.options.allowBlank === undefined) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.options['in']) {
|
||||
if (Ember.$.inArray(this.model.get(this.property), this.options['in']) !== -1) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.options.range) {
|
||||
lower = this.options.range[0];
|
||||
upper = this.options.range[1];
|
||||
|
||||
if (this.model.get(this.property) >= lower && this.model.get(this.property) <= upper) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Format = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
if (this.options.constructor === RegExp) {
|
||||
this.set('options', { 'with': this.options });
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('invalid', this.options));
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
if (this.options.allowBlank === undefined) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.options['with'] && !this.options['with'].test(this.model.get(this.property))) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
} else if (this.options.without && this.options.without.test(this.model.get(this.property))) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Inclusion = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
if (this.options.constructor === Array) {
|
||||
this.set('options', { 'in': this.options });
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('inclusion', this.options));
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
var message, lower, upper;
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
if (this.options.allowBlank === undefined) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.options['in']) {
|
||||
if (Ember.$.inArray(this.model.get(this.property), this.options['in']) === -1) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
} else if (this.options.range) {
|
||||
lower = this.options.range[0];
|
||||
upper = this.options.range[1];
|
||||
|
||||
if (this.model.get(this.property) < lower || this.model.get(this.property) > upper) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Length = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
var index, key;
|
||||
this._super();
|
||||
/*jshint expr:true*/
|
||||
if (typeof(this.options) === 'number') {
|
||||
this.set('options', { 'is': this.options });
|
||||
}
|
||||
|
||||
if (this.options.messages === undefined) {
|
||||
this.set('options.messages', {});
|
||||
}
|
||||
|
||||
for (index = 0; index < this.messageKeys().length; index++) {
|
||||
key = this.messageKeys()[index];
|
||||
if (this.options[key] !== undefined && this.options[key].constructor === String) {
|
||||
this.model.addObserver(this.options[key], this, this._validate);
|
||||
}
|
||||
}
|
||||
|
||||
this.options.tokenizer = this.options.tokenizer || function(value) { return value.split(''); };
|
||||
// if (typeof(this.options.tokenizer) === 'function') {
|
||||
// debugger;
|
||||
// // this.tokenizedLength = new Function('value', 'return '
|
||||
// } else {
|
||||
// this.tokenizedLength = new Function('value', 'return (value || "").' + (this.options.tokenizer || 'split("")') + '.length');
|
||||
// }
|
||||
},
|
||||
CHECKS: {
|
||||
'is' : '==',
|
||||
'minimum' : '>=',
|
||||
'maximum' : '<='
|
||||
},
|
||||
MESSAGES: {
|
||||
'is' : 'wrongLength',
|
||||
'minimum' : 'tooShort',
|
||||
'maximum' : 'tooLong'
|
||||
},
|
||||
getValue: function(key) {
|
||||
if (this.options[key].constructor === String) {
|
||||
return this.model.get(this.options[key]) || 0;
|
||||
} else {
|
||||
return this.options[key];
|
||||
}
|
||||
},
|
||||
messageKeys: function() {
|
||||
return Ember.keys(this.MESSAGES);
|
||||
},
|
||||
checkKeys: function() {
|
||||
return Ember.keys(this.CHECKS);
|
||||
},
|
||||
renderMessageFor: function(key) {
|
||||
var options = {count: this.getValue(key)}, _key;
|
||||
for (_key in this.options) {
|
||||
options[_key] = this.options[_key];
|
||||
}
|
||||
|
||||
return this.options.messages[this.MESSAGES[key]] || Ember.Validations.messages.render(this.MESSAGES[key], options);
|
||||
},
|
||||
renderBlankMessage: function() {
|
||||
if (this.options.is) {
|
||||
return this.renderMessageFor('is');
|
||||
} else if (this.options.minimum) {
|
||||
return this.renderMessageFor('minimum');
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
var check, fn, message, operator, key;
|
||||
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
if (this.options.allowBlank === undefined && (this.options.is || this.options.minimum)) {
|
||||
this.errors.pushObject(this.renderBlankMessage());
|
||||
}
|
||||
} else {
|
||||
for (key in this.CHECKS) {
|
||||
operator = this.CHECKS[key];
|
||||
if (!this.options[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fn = new Function('return ' + this.options.tokenizer(this.model.get(this.property)).length + ' ' + operator + ' ' + this.getValue(key));
|
||||
if (!fn()) {
|
||||
this.errors.pushObject(this.renderMessageFor(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Numericality = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
/*jshint expr:true*/
|
||||
var index, keys, key;
|
||||
this._super();
|
||||
|
||||
if (this.options === true) {
|
||||
this.options = {};
|
||||
} else if (this.options.constructor === String) {
|
||||
key = this.options;
|
||||
this.options = {};
|
||||
this.options[key] = true;
|
||||
}
|
||||
|
||||
if (this.options.messages === undefined || this.options.messages.numericality === undefined) {
|
||||
this.options.messages = this.options.messages || {};
|
||||
this.options.messages = { numericality: Ember.Validations.messages.render('notANumber', this.options) };
|
||||
}
|
||||
|
||||
if (this.options.onlyInteger !== undefined && this.options.messages.onlyInteger === undefined) {
|
||||
this.options.messages.onlyInteger = Ember.Validations.messages.render('notAnInteger', this.options);
|
||||
}
|
||||
|
||||
keys = Ember.keys(this.CHECKS).concat(['odd', 'even']);
|
||||
for(index = 0; index < keys.length; index++) {
|
||||
key = keys[index];
|
||||
|
||||
if (isNaN(this.options[key])) {
|
||||
this.model.addObserver(this.options[key], this, this._validate);
|
||||
}
|
||||
|
||||
if (this.options[key] !== undefined && this.options.messages[key] === undefined) {
|
||||
if (Ember.$.inArray(key, Ember.keys(this.CHECKS)) !== -1) {
|
||||
this.options.count = this.options[key];
|
||||
}
|
||||
this.options.messages[key] = Ember.Validations.messages.render(key, this.options);
|
||||
if (this.options.count !== undefined) {
|
||||
delete this.options.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
CHECKS: {
|
||||
equalTo :'===',
|
||||
greaterThan : '>',
|
||||
greaterThanOrEqualTo : '>=',
|
||||
lessThan : '<',
|
||||
lessThanOrEqualTo : '<='
|
||||
},
|
||||
call: function() {
|
||||
var check, checkValue, fn, form, operator, val;
|
||||
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
if (this.options.allowBlank === undefined) {
|
||||
this.errors.pushObject(this.options.messages.numericality);
|
||||
}
|
||||
} else if (!Ember.Validations.patterns.numericality.test(this.model.get(this.property))) {
|
||||
this.errors.pushObject(this.options.messages.numericality);
|
||||
} else if (this.options.onlyInteger === true && !(/^[+\-]?\d+$/.test(this.model.get(this.property)))) {
|
||||
this.errors.pushObject(this.options.messages.onlyInteger);
|
||||
} else if (this.options.odd && parseInt(this.model.get(this.property), 10) % 2 === 0) {
|
||||
this.errors.pushObject(this.options.messages.odd);
|
||||
} else if (this.options.even && parseInt(this.model.get(this.property), 10) % 2 !== 0) {
|
||||
this.errors.pushObject(this.options.messages.even);
|
||||
} else {
|
||||
for (check in this.CHECKS) {
|
||||
operator = this.CHECKS[check];
|
||||
|
||||
if (this.options[check] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isNaN(parseFloat(this.options[check])) && isFinite(this.options[check])) {
|
||||
checkValue = this.options[check];
|
||||
} else if (this.model.get(this.options[check]) !== undefined) {
|
||||
checkValue = this.model.get(this.options[check]);
|
||||
}
|
||||
|
||||
fn = new Function('return ' + this.model.get(this.property) + ' ' + operator + ' ' + checkValue);
|
||||
|
||||
if (!fn()) {
|
||||
this.errors.pushObject(this.options.messages[check]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Presence = Ember.Validations.validators.Base.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
/*jshint expr:true*/
|
||||
if (this.options === true) {
|
||||
this.options = {};
|
||||
}
|
||||
|
||||
if (this.options.message === undefined) {
|
||||
this.options.message = Ember.Validations.messages.render('blank', this.options);
|
||||
}
|
||||
},
|
||||
call: function() {
|
||||
if (Ember.isEmpty(this.model.get(this.property))) {
|
||||
this.errors.pushObject(this.options.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
Ember.Validations.validators.local.Url = Ember.Validations.validators.Base.extend({
|
||||
regexp: null,
|
||||
regexp_ip: null,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
|
||||
if (this.get('options.message') === undefined) {
|
||||
this.set('options.message', Ember.Validations.messages.render('url', this.options));
|
||||
}
|
||||
|
||||
if (this.get('options.protocols') === undefined) {
|
||||
this.set('options.protocols', ['http', 'https']);
|
||||
}
|
||||
|
||||
// Regular Expression Parts
|
||||
var dec_octet = '(25[0-5]|2[0-4][0-9]|[0-1][0-9][0-9]|[1-9][0-9]|[0-9])'; // 0-255
|
||||
var ipaddress = '(' + dec_octet + '(\\.' + dec_octet + '){3})';
|
||||
var hostname = '([a-zA-Z0-9\\-]+\\.)+([a-zA-Z]{2,})';
|
||||
var encoded = '%[0-9a-fA-F]{2}';
|
||||
var characters = 'a-zA-Z0-9$\\-_.+!*\'(),;:@&=';
|
||||
var segment = '([' + characters + ']|' + encoded + ')*';
|
||||
|
||||
// Build Regular Expression
|
||||
var regex_str = '^';
|
||||
|
||||
if (this.get('options.domainOnly') === true) {
|
||||
regex_str += hostname;
|
||||
} else {
|
||||
regex_str += '(' + this.get('options.protocols').join('|') + '):\\/\\/'; // Protocol
|
||||
|
||||
// Username and password
|
||||
if (this.get('options.allowUserPass') === true) {
|
||||
regex_str += '(([a-zA-Z0-9$\\-_.+!*\'(),;:&=]|' + encoded + ')+@)?'; // Username & passwords
|
||||
}
|
||||
|
||||
// IP Addresses?
|
||||
if (this.get('options.allowIp') === true) {
|
||||
regex_str += '(' + hostname + '|' + ipaddress + ')'; // Hostname OR IP
|
||||
} else {
|
||||
regex_str += '(' + hostname + ')'; // Hostname only
|
||||
}
|
||||
|
||||
// Ports
|
||||
if (this.get('options.allowPort') === true) {
|
||||
regex_str += '(:[0-9]+)?'; // Port
|
||||
}
|
||||
|
||||
regex_str += '(\\/';
|
||||
regex_str += '(' + segment + '(\\/' + segment + ')*)?'; // Path
|
||||
regex_str += '(\\?' + '([' + characters + '/?]|' + encoded + ')*)?'; // Query
|
||||
regex_str += '(\\#' + '([' + characters + '/?]|' + encoded + ')*)?'; // Anchor
|
||||
regex_str += ')?';
|
||||
}
|
||||
|
||||
regex_str += '$';
|
||||
|
||||
// RegExp
|
||||
this.regexp = new RegExp(regex_str);
|
||||
this.regexp_ip = new RegExp(ipaddress);
|
||||
},
|
||||
call: function() {
|
||||
var url = this.model.get(this.property);
|
||||
|
||||
if (Ember.isEmpty(url)) {
|
||||
if (this.get('options.allowBlank') !== true) {
|
||||
this.errors.pushObject(this.get('options.message'));
|
||||
}
|
||||
} else {
|
||||
if (this.get('options.allowIp') !== true) {
|
||||
if (this.regexp_ip.test(url)) {
|
||||
this.errors.pushObject(this.get('options.message'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.regexp.test(url)) {
|
||||
this.errors.pushObject(this.get('options.message'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
})();
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
|||
require 'uglifier'
|
||||
|
||||
File.open("static/application.min.js", "w") {|file| file.truncate(0) }
|
||||
|
||||
libs = [
|
||||
"javascripts/libs/jquery-1.10.2.js",
|
||||
"javascripts/libs/handlebars-1.1.2.js",
|
||||
"javascripts/libs/ember-1.5.1.js",
|
||||
"javascripts/libs/ember-validations.js",
|
||||
]
|
||||
|
||||
app = [
|
||||
"javascripts/app/router.js",
|
||||
"javascripts/app/models.js",
|
||||
"javascripts/app/routes.js",
|
||||
"javascripts/app/controllers.js",
|
||||
"javascripts/app/views.js",
|
||||
"javascripts/app/helpers.js",
|
||||
]
|
||||
|
||||
libs.each do |js_file|
|
||||
File.open("static/application.min.js", "a") do |f|
|
||||
puts "compile #{js_file}"
|
||||
f << Uglifier.compile(File.read(js_file))
|
||||
end
|
||||
end
|
||||
|
||||
app.each do |js_file|
|
||||
File.open("static/application.min.js", "a") do |f|
|
||||
puts "compile #{js_file}"
|
||||
f << Uglifier.compile(File.read(js_file))
|
||||
end
|
||||
end
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 14 32 18" width="32" height="4" fill="#9e84c5" preserveAspectRatio="none">
|
||||
<path opacity="0.8" transform="translate(0 0)" d="M2 14 V18 H6 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
<path opacity="0.5" transform="translate(0 0)" d="M0 14 V18 H8 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0.1s" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
<path opacity="0.25" transform="translate(0 0)" d="M0 14 V18 H8 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0.2s" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 983 B |
|
@ -0,0 +1,320 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Consul Web UI Style Guide</title>
|
||||
<link rel="stylesheet" href="static/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/base.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Consul Web UI Style Guide</h2>
|
||||
<p>This is style guide for the <a href="http://www.consul.io">Consul</a> Web UI. When possible,
|
||||
it's best to follow this guide modifying the UI.</p>
|
||||
<p>Some reasoning behind choices:
|
||||
<ul>
|
||||
<li>Colors. Bright colors were chosen to allow for easy
|
||||
"scanning" of information.</li>
|
||||
<li>Icons will accompany most "actions", those are still
|
||||
pending</li>
|
||||
<li>Layout. The layout will be primarily 2 columns with the
|
||||
header at the top for navigation.</li>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Header</h2>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 topbar">
|
||||
|
||||
<div class="col-md-1 col-sm-2">
|
||||
<a href="#"><div class="top-brand"></div></a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<a class="btn btn-primary" href="#">Services</a>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<a class="btn btn-default" href="#">Nodes</a>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<a class="btn btn-default" href="#">Key/Value</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-md-offset-1 col-sm-3 col-sm-offset-0">
|
||||
<a class="btn btn-warning" href="#">5 checks failing</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3">
|
||||
<a class="btn btn-dropdown btn-default" href="#">
|
||||
us-east-1
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>Colors</h2>
|
||||
<hr>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<div style="width: 75px; height: 75px; display:inline-block;" class="bg-purple"></div>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-light-purple"></div>
|
||||
</li>
|
||||
<li>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-red"></div>
|
||||
</li>
|
||||
<li>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-orange"></div>
|
||||
</li>
|
||||
<li>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-dark-green"></div>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-green"></div>
|
||||
</li>
|
||||
<li>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-gray"></div>
|
||||
<div style="width: 75px; height: 75px; display:inline-block" class="bg-light-gray"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<h2>Headings</h2>
|
||||
<hr>
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h4>Heading 4</h4>
|
||||
<h5>Heading 5</h5>
|
||||
<p>Paragraph text. Consul makes it simple for services to
|
||||
register themselves and to discover other services via a
|
||||
DNS or HTTP interface. Register external services such as
|
||||
SaaS providers as well.</p>
|
||||
<small>Small note text, if you need to include anything extra.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Panels</h2>
|
||||
<hr>
|
||||
<p>Panels are for displaying data in the 2nd (right) column.
|
||||
They show extensive information and are flexible, but also
|
||||
use the highlight colors to allow for scanning.</p>
|
||||
<hr>
|
||||
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-bar"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
HTTP Server Accessible
|
||||
<small>httpAccess</small>
|
||||
<span class="panel-note">critical</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<p>Sends an HTTP request to the HTTP routers /health endpoint.
|
||||
This should return 200 OK. If it returns anything else,
|
||||
the headers are dumped.</p>
|
||||
<h5>OUTPUT</h5>
|
||||
<pre>
|
||||
HTTP/1.1 503 SERVICE UNAVAILABLE
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Date: Sun, 20 Apr 2014 15:40:03 GMT
|
||||
Server: gunicorn/0.17.4
|
||||
Content-Length: 0
|
||||
Connection: keep-alive
|
||||
</pre>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-bar"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Mux Accessible
|
||||
<small>muxAccess</small>
|
||||
<span class="panel-note">passing</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<p>Makes a TCP connection to the muxer, dumps a relevant error if the connection fails.</p>
|
||||
|
||||
<h5>OUTPUT</h5>
|
||||
<pre>
|
||||
Socket connect Successful
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-bar"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Router Accessible
|
||||
<small>routerAccess</small>
|
||||
<span class="panel-note">warning</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<p>Makes a TCP connection to the router, dumps a relevant error if the connection fails.</p>
|
||||
|
||||
<h5>OUTPUT</h5>
|
||||
<pre>
|
||||
Socket connect timed out
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>Loaders</h2>
|
||||
<hr>
|
||||
<p>Pending...</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h2>Icons</h2>
|
||||
<hr>
|
||||
<p>Pending...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Buttons</h2>
|
||||
<hr>
|
||||
<a href="#" class="btn btn-default">Default button</a>
|
||||
<a href="#" class="btn btn-primary">Primary button</a>
|
||||
<a href="#" class="btn btn-success">Success button</a>
|
||||
<a href="#" class="btn btn-warning">Warning button</a>
|
||||
<a href="#" class="btn btn-danger">Danger button</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<h2>Lists</h2>
|
||||
<hr>
|
||||
<p>Lists are used primarily for the first (left) column
|
||||
view. They are designed as a quick summary, with links
|
||||
embedded for the top-level item as well as sub-items (
|
||||
such as a list of nodes, as below).</p>
|
||||
<hr>
|
||||
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<div class="list-bar bg-green"></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
<a href="#" class="subtle">vagrant-cloud-http</a>
|
||||
<small>vagrant-cloud-http</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">5 passing</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item">
|
||||
<div class="list-bar bg-green"></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
<a href="#" class="subtle">vagrant-cloud-http</a>
|
||||
<small>vagrant-cloud-http</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">5 passing</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item">
|
||||
<div class="list-bar bg-orange"></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
<a href="#" class="subtle">vagrant-cloud-http</a>
|
||||
<small>vagrant-cloud-http</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">1 failing</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item">
|
||||
<div class="list-bar bg-red"></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
<a href="#" class="subtle">vagrant-cloud-http</a>
|
||||
<small>vagrant-cloud-http</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">2 failing</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
<li><a class="subtle" href="#">node-10-0-109</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,80 @@
|
|||
.btn {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
border-width: 2px;
|
||||
color: $gray;
|
||||
@include transition(background-color .2s ease-in-out);
|
||||
@include transition(border-color .2s ease-in-out);
|
||||
@include transition(color .2s ease-in-out);
|
||||
|
||||
outline: none;
|
||||
outline-color: white;
|
||||
|
||||
&:hover {
|
||||
color: darken($gray, 10%);
|
||||
background-color: lighten($gray-background, 5%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
outline-color: white;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
outline-color: white;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.btn-primary, &.active {
|
||||
color: $purple-dark;
|
||||
background-color: transparent;
|
||||
border: 2px solid $purple;
|
||||
|
||||
&:hover {
|
||||
background-color: $light-purple;
|
||||
color: darken($purple, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-warning {
|
||||
color: $orange-faded;
|
||||
background-color: transparent;
|
||||
border: 2px solid $orange-faded;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($orange-faded, 29%);
|
||||
color: darken($orange-faded, 10%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.btn-success {
|
||||
color: $green-dark;
|
||||
background-color: transparent;
|
||||
border: 2px solid $green-dark;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($green-faded, 24%);
|
||||
color: darken($green-dark, 10%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
color: $red;
|
||||
background-color: transparent;
|
||||
border: 2px solid $red;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($red, 38%);
|
||||
color: darken($red, 10%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
.form-group {
|
||||
.form-control {
|
||||
@include transition(border-color .2s ease-in-out);
|
||||
@include transition(box-shadow .2s ease-in-out);
|
||||
@include transition(border-color .2s ease-in-out);
|
||||
}
|
||||
|
||||
&.valid {
|
||||
.form-control {
|
||||
border-color: $green-faded;
|
||||
box-shadow: 0 0 5px $green-faded;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
background-color: $gray-background;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
.list-group-item {
|
||||
padding: 0;
|
||||
border-width: 2px;
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 0px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
@include transition(background-color .3s ease-in-out);
|
||||
|
||||
.list-group-item-heading, .list-inline {
|
||||
margin: 10px 15px 10px 15px;
|
||||
padding: 0px 5px 10px 5px;
|
||||
}
|
||||
|
||||
.list-inline {
|
||||
padding-left: 0px;
|
||||
color: $gray;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list-group-item-heading {
|
||||
border-bottom: 2px solid #eee;
|
||||
color: $gray-darker;
|
||||
|
||||
.heading-helper {
|
||||
float: right;
|
||||
font-weight: 600;
|
||||
color: $gray-light;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-top-right-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
&.list-link:hover {
|
||||
cursor: pointer;
|
||||
background-color: lighten($gray-background, 8%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@include transition(border-color .1s linear);
|
||||
border-color: $purple;
|
||||
|
||||
.list-bar {
|
||||
@include transition(background-color .1s linear);
|
||||
background-color: $purple;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ul.list-broken {
|
||||
li {
|
||||
// border-top: 2px lighten($gray-background, 5%) solid;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
// border-bottom: 2px lighten($gray-background, 5%) solid;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@mixin transition($transition) {
|
||||
-webkit-transition: $transition;
|
||||
-moz-transition: $transition;
|
||||
-ms-transition: $transition;
|
||||
-o-transition: $transition;
|
||||
transition: $transition;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
.top-brand {
|
||||
margin-top: 20px;
|
||||
background: transparent url('consul-logo.png') 0 no-repeat;
|
||||
background-size: 30px 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
|
||||
.topbar {
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 100px;
|
||||
border-bottom: 1px #eee solid;
|
||||
|
||||
.btn {
|
||||
margin-top: 20px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.btn-dropdown {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
ul.dropdown-menu {
|
||||
li {
|
||||
|
||||
a {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: $gray;
|
||||
@include transition(background-color .1s ease-in-out);
|
||||
|
||||
&:hover {
|
||||
color: darken($gray, 10%);
|
||||
background-color: lighten($gray-background, 5%);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
.panel {
|
||||
border-width: 2px;
|
||||
border-color: $gray-background;
|
||||
@include transition(background-color .3s ease-in-out);
|
||||
|
||||
.panel-heading {
|
||||
background-color: transparent;
|
||||
border-width: 2px;
|
||||
border-color: $gray-background;
|
||||
}
|
||||
|
||||
h3.panel-title {
|
||||
padding: 4px 0px 4px 0px;
|
||||
font-size: 20px;
|
||||
color: $gray-light;
|
||||
color: $gray-darker;
|
||||
border-radius: 3px;
|
||||
|
||||
small {
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.panel-note {
|
||||
margin-top: 5px;
|
||||
float: right;
|
||||
font-weight: 600;
|
||||
color: $gray-light;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: $text-color;
|
||||
}
|
||||
h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
h4.check {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
@include transition(background-color .1s linear);
|
||||
}
|
||||
|
||||
&.panel-link {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
&.panel-short {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
&.panel-link:hover {
|
||||
cursor: pointer;
|
||||
background-color: lighten($gray-background, 8%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
>.panel-heading {
|
||||
border-color: $purple;
|
||||
}
|
||||
|
||||
@include transition(border-color .1s linear);
|
||||
border-color: $purple;
|
||||
|
||||
.panel-bar {
|
||||
@include transition(background-color .1s linear);
|
||||
background-color: $purple;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
body {
|
||||
-webkit-font-smoothing:antialiased;
|
||||
font-size: 16px;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $purple;
|
||||
font-weight: 600;
|
||||
@include transition(color .2s ease-in-out);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: darken($purple, 10%);
|
||||
}
|
||||
|
||||
&.subtle {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: $purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: $purple-dark;
|
||||
background-color: $gray-background;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
font-size: 14px;
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
small {
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: $gray-darker;
|
||||
}
|
||||
|
||||
h5 {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
h4.breadcrumbs {
|
||||
padding-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
a {
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $gray;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Colors
|
||||
|
||||
$gray-light: lighten(gray, 50%);
|
||||
$black: #242424;
|
||||
$gray-darker: #555;
|
||||
$gray: #777;
|
||||
$gray-light: #939393;
|
||||
$gray-background: #E6E6E6;
|
||||
$red: #dd4e58;
|
||||
$red-dark: #c5454e;
|
||||
$red-darker: #b03c44;
|
||||
$tan: #f0f0e5;
|
||||
$consul-gray: #909090;
|
||||
$consul-footer-gray: #d7d4d7;
|
||||
$purple-dark: #69499a;
|
||||
$purple: lighten($purple-dark, 20%);
|
||||
$light-purple: #f7f3f9;
|
||||
|
||||
$green-faded: #BBF085;
|
||||
$green-dark: #86B457;
|
||||
$red-faded: $red;
|
||||
$white-faded: darken(white, 2%);
|
||||
$orange-faded: #FFAC5E;
|
||||
|
||||
// Type
|
||||
$text-color: #555;
|
|
@ -0,0 +1,97 @@
|
|||
@import "mixins";
|
||||
@import "variables";
|
||||
@import "type";
|
||||
@import "panels";
|
||||
@import "nav";
|
||||
@import "buttons";
|
||||
@import "lists";
|
||||
@import "forms";
|
||||
|
||||
@media (min-width: 768px) { // + 18
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) { // + 22
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) { // + 30
|
||||
.container {
|
||||
width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
button:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.buffer-small {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.border-left {
|
||||
display: block;
|
||||
height: 700px;
|
||||
|
||||
.line {
|
||||
margin: 0 auto;
|
||||
background-color: $gray-background;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.row {
|
||||
&.colored {
|
||||
background-color: $light-purple;
|
||||
}
|
||||
}
|
||||
|
||||
.bordered {
|
||||
border-left: 2px solid $gray-background;
|
||||
}
|
||||
|
||||
|
||||
.bg-purple {
|
||||
background-color: $purple;
|
||||
}
|
||||
|
||||
.bg-light-purple {
|
||||
background-color: $light-purple;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: $orange-faded;
|
||||
}
|
||||
|
||||
.bg-green {
|
||||
background-color: $green-faded;
|
||||
}
|
||||
|
||||
.bg-dark-green {
|
||||
background-color: $green-dark;
|
||||
}
|
||||
|
||||
.bg-red {
|
||||
background-color: $red-faded;
|
||||
}
|
||||
|
||||
.bg-gray {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.bg-light-gray {
|
||||
background-color: $gray-background;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
#ember-testing-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 640px;
|
||||
height: 384px;
|
||||
overflow: auto;
|
||||
z-index: 9999;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
}
|
||||
#ember-testing {
|
||||
zoom: 50%;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
if (window.location.search.indexOf("?test") !== -1) {
|
||||
document.write(
|
||||
'<div id="qunit"></div>' +
|
||||
'<div id="qunit-fixture"></div>' +
|
||||
'<div id="ember-testing-container">' +
|
||||
' <div id="ember-testing"></div>' +
|
||||
'</div>' +
|
||||
'<link rel="stylesheet" href="tests/runner.css">' +
|
||||
'<link rel="stylesheet" href="tests/vendor/qunit-1.12.0.css">' +
|
||||
'<script src="tests/vendor/qunit-1.12.0.js"></script>' +
|
||||
'<script src="tests/tests.js"></script>'
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// in order to see the app running inside the QUnit runner
|
||||
App.rootElement = '#ember-testing';
|
||||
|
||||
// Common test setup
|
||||
App.setupForTesting();
|
||||
App.injectTestHelpers();
|
||||
|
||||
// common QUnit module declaration
|
||||
module("Integration tests", {
|
||||
setup: function() {
|
||||
// before each test, ensure the application is ready to run.
|
||||
Ember.run(App, App.advanceReadiness);
|
||||
},
|
||||
|
||||
teardown: function() {
|
||||
// reset the application state between each test
|
||||
App.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// QUnit test case
|
||||
test("/", function() {
|
||||
// async helper telling the application to go to the '/' route
|
||||
visit("/");
|
||||
|
||||
// helper waiting the application is idle before running the callback
|
||||
andThen(function() {
|
||||
equal(find("h1").text(), "Base", "Application header is rendered");
|
||||
equal(find("li").length, 3, "There are three items in the list");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* QUnit v1.12.0 - A JavaScript Unit Testing Framework
|
||||
*
|
||||
* http://qunitjs.com
|
||||
*
|
||||
* Copyright 2012 jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*/
|
||||
|
||||
/** Font Family and Sizes */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
|
||||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
||||
#qunit-tests { font-size: smaller; }
|
||||
|
||||
|
||||
/** Resets */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/** Header */
|
||||
|
||||
#qunit-header {
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
|
||||
color: #8699a4;
|
||||
background-color: #0d3349;
|
||||
|
||||
font-size: 1.5em;
|
||||
line-height: 1em;
|
||||
font-weight: normal;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
-moz-border-radius: 5px 5px 0 0;
|
||||
-webkit-border-top-right-radius: 5px;
|
||||
-webkit-border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
#qunit-header a {
|
||||
text-decoration: none;
|
||||
color: #c2ccd1;
|
||||
}
|
||||
|
||||
#qunit-header a:hover,
|
||||
#qunit-header a:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar label {
|
||||
display: inline-block;
|
||||
padding: 0 .5em 0 .1em;
|
||||
}
|
||||
|
||||
#qunit-banner {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar {
|
||||
padding: 0.5em 0 0.5em 2em;
|
||||
color: #5E740B;
|
||||
background-color: #eee;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#qunit-userAgent {
|
||||
padding: 0.5em 0 0.5em 2.5em;
|
||||
background-color: #2b81af;
|
||||
color: #fff;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-container {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/** Tests: Pass/Fail */
|
||||
|
||||
#qunit-tests {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests li {
|
||||
padding: 0.4em 0.5em 0.4em 2.5em;
|
||||
border-bottom: 1px solid #fff;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests li strong {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#qunit-tests li a {
|
||||
padding: 0.5em;
|
||||
color: #c2ccd1;
|
||||
text-decoration: none;
|
||||
}
|
||||
#qunit-tests li a:hover,
|
||||
#qunit-tests li a:focus {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#qunit-tests li .runtime {
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.qunit-assert-list {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
}
|
||||
|
||||
.qunit-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests table {
|
||||
border-collapse: collapse;
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
#qunit-tests th {
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
padding: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
#qunit-tests td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#qunit-tests pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#qunit-tests del {
|
||||
background-color: #e0f2be;
|
||||
color: #374e0c;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests ins {
|
||||
background-color: #ffcaca;
|
||||
color: #500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*** Test Counts */
|
||||
|
||||
#qunit-tests b.counts { color: black; }
|
||||
#qunit-tests b.passed { color: #5E740B; }
|
||||
#qunit-tests b.failed { color: #710909; }
|
||||
|
||||
#qunit-tests li li {
|
||||
padding: 5px;
|
||||
background-color: #fff;
|
||||
border-bottom: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/*** Passing Styles */
|
||||
|
||||
#qunit-tests li li.pass {
|
||||
color: #3c510c;
|
||||
background-color: #fff;
|
||||
border-left: 10px solid #C6E746;
|
||||
}
|
||||
|
||||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
||||
#qunit-tests .pass .test-name { color: #366097; }
|
||||
|
||||
#qunit-tests .pass .test-actual,
|
||||
#qunit-tests .pass .test-expected { color: #999999; }
|
||||
|
||||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
||||
|
||||
/*** Failing Styles */
|
||||
|
||||
#qunit-tests li li.fail {
|
||||
color: #710909;
|
||||
background-color: #fff;
|
||||
border-left: 10px solid #EE5757;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#qunit-tests > li:last-child {
|
||||
border-radius: 0 0 5px 5px;
|
||||
-moz-border-radius: 0 0 5px 5px;
|
||||
-webkit-border-bottom-right-radius: 5px;
|
||||
-webkit-border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
|
||||
#qunit-tests .fail .test-name,
|
||||
#qunit-tests .fail .module-name { color: #000000; }
|
||||
|
||||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
||||
#qunit-tests .fail .test-expected { color: green; }
|
||||
|
||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||||
|
||||
|
||||
/** Result */
|
||||
|
||||
#qunit-testresult {
|
||||
padding: 0.5em 0.5em 0.5em 2.5em;
|
||||
|
||||
color: #2b81af;
|
||||
background-color: #D2E0E6;
|
||||
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
#qunit-testresult .module-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/** Fixture */
|
||||
|
||||
#qunit-fixture {
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
width: 1000px;
|
||||
height: 1000px;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -10,13 +10,14 @@ The main interface to Consul is a RESTful HTTP API. The API can be
|
|||
used for CRUD for nodes, services, checks, and configuration. The endpoints are
|
||||
versioned to enable changes without breaking backwards compatibility.
|
||||
|
||||
All endpoints fall into one of 5 categories:
|
||||
All endpoints fall into one of 6 categories:
|
||||
|
||||
* kv - Key/Value store
|
||||
* agent - Agent control
|
||||
* catalog - Manages nodes and services
|
||||
* health - Manages health checks
|
||||
* status - Consul system status
|
||||
* internal - Internal APIs. Purposely undocumented, subject to change.
|
||||
|
||||
Each of the categories and their respective endpoints are documented below.
|
||||
|
||||
|
@ -146,6 +147,21 @@ keys sharing a prefix. If the "?recurse" query parameter is provided,
|
|||
then all keys with the prefix are deleted, otherwise only the specified
|
||||
key.
|
||||
|
||||
It is possible to also only list keys without any values by using the
|
||||
"?keys" query parameter along with a `GET` request. This will return
|
||||
a list of the keys under the given prefix. The optional "?seperator="
|
||||
can be used to list only up to a given seperator.
|
||||
|
||||
For example, listing "/web/" with a "/" seperator may return:
|
||||
|
||||
[
|
||||
"/web/bar",
|
||||
"/web/foo",
|
||||
"/web/subdir/"
|
||||
]
|
||||
|
||||
Using the key listing method may be suitable when you do not need
|
||||
the values or flags, or want to implement a key-space explorer.
|
||||
|
||||
## Agent
|
||||
|
||||
|
|
|
@ -93,6 +93,9 @@ The options below are all specified on the command-line.
|
|||
participate in a WAN gossip pool with server nodes in other datacenters. Servers act as gateways
|
||||
to other datacenters and forward traffic as appropriate.
|
||||
|
||||
* `-ui-dir` - This flag provides a the directory containing the Web UI resources
|
||||
for Consul. This must be provided to enable the Web UI. Directory must be readable.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
In addition to the command-line options, configuration can be put into
|
||||
|
@ -142,6 +145,8 @@ definitions support being updated during a reload.
|
|||
|
||||
* `server` - Equivalent to the `-server` command-line flag.
|
||||
|
||||
* `ui_dir` - Equivalent to the `-ui-dir` command-line flag.
|
||||
|
||||
* `advertise_addr` - The advertise address is used to change the address that we
|
||||
advertise to other nodes in the cluster. By default, the `-bind` address is
|
||||
advertised. However, in some cases, there may be a routable address that cannot
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
<ul class="main-links nav navbar-nav rls-sb">
|
||||
<li class="li-under"><a href="/intro/index.html">Intro</a></li>
|
||||
<li class="active li-under"><a href="/docs/index.html">Docs</a></li>
|
||||
<li class="li-under"><a href="/community.html">Community</a></li>
|
||||
<li class="li-under"><a href="/community.html">Community</a></li>
|
||||
<li class="li-under"><a href="http://demo.consul.io/">Demo</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="buttons nav navbar-nav rls-sb">
|
||||
|
|
|
@ -63,7 +63,8 @@
|
|||
<ul class="main-links nav navbar-nav navbar-right rls-sb">
|
||||
<li class="first li-under"><a href="/intro/index.html">Intro</a></li>
|
||||
<li class="li-under"><a href="/docs/index.html">Docs</a></li>
|
||||
<li class="li-under"><a href="/community.html">Community</a></li>
|
||||
<li class="li-under"><a href="/community.html">Community</a></li>
|
||||
<li class="li-under"><a href="http://demo.consul.io/">Demo</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
|
Loading…
Reference in New Issue