diff --git a/.gitignore b/.gitignore index efdec3784..0eaddf68a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,25 @@ rkt-* ./idea *.iml + +# UI rules + +# compiled output +/ui/dist +/ui/tmp + +# dependencies +/ui/node_modules +/ui/bower_components + +# misc +/ui/.sass-cache +/ui/connect.lock +/ui/coverage/* +/ui/libpeerconnection.log +/ui/npm-debug.log* +/ui/testem.log +.ignore + +# generated routes file +command/agent/bindata_assetfs.go diff --git a/.travis.yml b/.travis.yml index 251ace904..1411e1df5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,22 @@ language: go go: - 1.9.x +git: + depth: 300 + branches: only: - master +matrix: + include: + - env: + - env: RUN_UI_TESTS=1 SKIP_NOMAD_TESTS=1 + +cache: + directories: + - ui/node_modules + before_install: - sudo apt-get update - sudo apt-get install -y liblxc1 lxc-dev lxc shellcheck diff --git a/GNUmakefile b/GNUmakefile index fe567d6db..e5f27d1f5 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -47,7 +47,7 @@ ALL_TARGETS += freebsd_amd64 endif pkg/darwin_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for darwin/amd64 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -63,7 +63,7 @@ pkg/freebsd_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for freebsd/amd64 -o "$@" pkg/linux_386/nomad: $(SOURCE_FILES) ## Build Nomad for linux/386 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=linux GOARCH=386 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -71,7 +71,7 @@ pkg/linux_386/nomad: $(SOURCE_FILES) ## Build Nomad for linux/386 -o "$@" pkg/linux_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for linux/amd64 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -79,7 +79,7 @@ pkg/linux_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for linux/amd64 -o "$@" pkg/linux_arm/nomad: $(SOURCE_FILES) ## Build Nomad for linux/arm - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=linux GOARCH=arm CC=arm-linux-gnueabihf-gcc-5 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -87,7 +87,7 @@ pkg/linux_arm/nomad: $(SOURCE_FILES) ## Build Nomad for linux/arm -o "$@" pkg/linux_arm64/nomad: $(SOURCE_FILES) ## Build Nomad for linux/arm64 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc-5 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -100,7 +100,7 @@ pkg/linux_arm64/nomad: $(SOURCE_FILES) ## Build Nomad for linux/arm64 # CC=i686-w64-mingw32-gcc # CXX=i686-w64-mingw32-g++ pkg/windows_386/nomad: $(SOURCE_FILES) ## Build Nomad for windows/386 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=windows GOARCH=386 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -108,7 +108,7 @@ pkg/windows_386/nomad: $(SOURCE_FILES) ## Build Nomad for windows/386 -o "$@.exe" pkg/windows_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for windows/amd64 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=windows GOARCH=amd64 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -116,7 +116,7 @@ pkg/windows_amd64/nomad: $(SOURCE_FILES) ## Build Nomad for windows/amd64 -o "$@.exe" pkg/linux_amd64-lxc/nomad: $(SOURCE_FILES) ## Build Nomad+LXC for linux/amd64 - @echo "==> Building $@..." + @echo "==> Building $@ with tags $(GO_TAGS)..." @CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ go build \ -ldflags $(GO_LDFLAGS) \ @@ -149,6 +149,8 @@ deps: ## Install build and development dependencies go get -u github.com/axw/gocov/gocov go get -u gopkg.in/matm/v1/gocov-html go get -u github.com/ugorji/go/codec/codecgen + go get -u github.com/jteeuwen/go-bindata/... + go get -u github.com/elazarl/go-bindata-assetfs/... go get -u github.com/hashicorp/vault go get -u github.com/a8m/tree/cmd/tree @@ -158,7 +160,7 @@ check: ## Lint the source code @gometalinter \ --deadline 10m \ --vendor \ - --exclude '.*\.generated\.go:\d+:' \ + --exclude '(.*\.generated\.go:\d+:|bindata_assetfs)' \ --disable-all \ --sort severity \ $(CHECKS) \ @@ -185,20 +187,30 @@ dev: check ## Build for the current development platform @rm -f $(GOPATH)/bin/nomad @$(MAKE) --no-print-directory \ $(DEV_TARGET) \ - GO_TAGS=nomad_test + GO_TAGS="nomad_test $(NOMAD_UI_TAG)" @mkdir -p $(PROJECT_ROOT)/bin @mkdir -p $(GOPATH)/bin @cp $(PROJECT_ROOT)/$(DEV_TARGET) $(PROJECT_ROOT)/bin/ @cp $(PROJECT_ROOT)/$(DEV_TARGET) $(GOPATH)/bin .PHONY: release -release: clean check $(foreach t,$(ALL_TARGETS),pkg/$(t).zip) ## Build all release packages which can be built on this platform. +release: GO_TAGS="ui" +release: clean ember-dist static-assets check $(foreach t,$(ALL_TARGETS),pkg/$(t).zip) ## Build all release packages which can be built on this platform. @echo "==> Results:" @tree --dirsfirst $(PROJECT_ROOT)/pkg .PHONY: test -test: LOCAL_PACKAGES = $(shell go list ./... | grep -v '/vendor/') -test: dev ## Run Nomad test suites +test: ## Run the Nomad test suite and/or the Nomad UI test suite + @if [ ! $(SKIP_NOMAD_TESTS) ]; then \ + make test-nomad; \ + fi + @if [ $(RUN_UI_TESTS) ]; then \ + make test-ui; \ + fi + +.PHONY: test-nomad +test-nomad: LOCAL_PACKAGES = $(shell go list ./... | grep -v '/vendor/') +test-nomad: dev ## Run Nomad test suites @echo "==> Running Nomad test suites:" @NOMAD_TEST_RKT=1 \ go test \ @@ -229,6 +241,33 @@ testcluster: ## Bring up a Linux test cluster using Vagrant. Set PROVIDER if nec nomad-client03 \ $(if $(PROVIDER),--provider $(PROVIDER)) +.PHONY: static-assets +static-assets: ## Compile the static routes to serve alongside the API + @echo "--> Generating static assets" + @go-bindata-assetfs -pkg agent -prefix ui -modtime 1480000000 -tags ui ./ui/dist/... + @mv bindata_assetfs.go command/agent + +.PHONY: test-ui +test-ui: ## Run Noma UI test suite + @echo "--> Installing JavaScript assets" + @cd ui && yarn install + @cd ui && npm install phantomjs-prebuilt + @echo "--> Running ember tests" + @cd ui && phantomjs --version + @cd ui && npm test + +.PHONY: ember-dist +ember-dist: ## Build the static UI assets from source + @echo "--> Installing JavaScript assets" + @cd ui && yarn install + @cd ui && npm rebuild node-sass + @echo "--> Building Ember application" + @cd ui && npm run build + +.PHONY: dev-ui +dev-ui: ember-dist static-assets + @$(MAKE) NOMAD_UI_TAG="ui" dev ## Build a dev binary with the UI baked in + HELP_FORMAT=" \033[36m%-25s\033[0m %s\n" .PHONY: help help: ## Display this usage information diff --git a/README.md b/README.md index 8b67e2d74..e2cf0a085 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,17 @@ $ bin/nomad ... ``` +If the Nomad UI is desired in the development version, run `make dev-ui`. This will build the UI from source and compile it into the dev binary. + +```sh +$ make dev-ui +... +$ bin/nomad +... +``` + +**Note:** Building the Nomad UI from source requires Node, Yarn, and Ember CLI. These tools are already in the Vagrant VM. Read the [UI README](https://github.com/hashicorp/nomad/blob/master/ui/README.md) for more info. + To cross-compile Nomad, run `make release`. This will compile Nomad for multiple platforms and place the resulting binaries into the `./pkg` directory: diff --git a/Vagrantfile b/Vagrantfile index c31d94d59..9660a2a99 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -21,6 +21,13 @@ Vagrant.configure(2) do |config| vmCfg.vm.provision "shell", privileged: false, path: './scripts/vagrant-linux-unpriv-bootstrap.sh' + + # Expose the nomad api and ui to the host + vmCfg.vm.network "forwarded_port", guest: 4646, host: 4646, auto_correct: true + + # Expose Ember ports to the host (one for the site, one for livereload) + vmCfg.vm.network :forwarded_port, guest: 4201, host: 4201, auto_correct: true + vmCfg.vm.network :forwarded_port, guest: 49153, host: 49153, auto_correct: true end config.vm.define "freebsd", autostart: false, primary: false do |vmCfg| @@ -41,7 +48,7 @@ Vagrant.configure(2) do |config| vmCfg.vm.provision "shell", privileged: false, path: './scripts/vagrant-freebsd-unpriv-bootstrap.sh' - end + end # Test Cluster (Linux) 1.upto(3) do |n| @@ -59,25 +66,25 @@ Vagrant.configure(2) do |config| vmCfg.vm.provider "virtualbox" do |_| vmCfg.vm.network :private_network, ip: serverIP end - + vmCfg.vm.synced_folder '.', '/opt/gopath/src/github.com/hashicorp/nomad' - + vmCfg.vm.provision "shell", privileged: true, path: './scripts/vagrant-linux-priv-zeroconf.sh' end - + config.vm.define clientName, autostart: false, primary: false do |vmCfg| vmCfg.vm.box = LINUX_BASE_BOX vmCfg.vm.hostname = clientName vmCfg = configureProviders(vmCfg) vmCfg = configureLinuxProvisioners(vmCfg) - + vmCfg.vm.provider "virtualbox" do |_| vmCfg.vm.network :private_network, ip: clientIP end - + vmCfg.vm.synced_folder '.', '/opt/gopath/src/github.com/hashicorp/nomad' @@ -113,6 +120,10 @@ def configureLinuxProvisioners(vmCfg) privileged: true, path: './scripts/vagrant-linux-priv-rkt.sh' + vmCfg.vm.provision "shell", + privileged: false, + path: './scripts/vagrant-linux-priv-ui.sh' + return vmCfg end diff --git a/acl/policy.go b/acl/policy.go index cf2c4b245..757fe2fde 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -21,12 +21,13 @@ const ( // The Policy stanza is a short hand for granting several of these. When capabilities are // combined we take the union of all capabilities. If the deny capability is present, it // takes precedence and overwrites all other capabilities. - NamespaceCapabilityDeny = "deny" - NamespaceCapabilityListJobs = "list-jobs" - NamespaceCapabilityReadJob = "read-job" - NamespaceCapabilitySubmitJob = "submit-job" - NamespaceCapabilityReadLogs = "read-logs" - NamespaceCapabilityReadFS = "read-fs" + NamespaceCapabilityDeny = "deny" + NamespaceCapabilityListJobs = "list-jobs" + NamespaceCapabilityReadJob = "read-job" + NamespaceCapabilitySubmitJob = "submit-job" + NamespaceCapabilityReadLogs = "read-logs" + NamespaceCapabilityReadFS = "read-fs" + NamespaceCapabilitySentinelOverride = "sentinel-override" ) var ( @@ -77,6 +78,9 @@ func isNamespaceCapabilityValid(cap string) bool { case NamespaceCapabilityDeny, NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, NamespaceCapabilitySubmitJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS: return true + // Seperate the enterprise-only capabilities + case NamespaceCapabilitySentinelOverride: + return true default: return false } diff --git a/acl/policy_test.go b/acl/policy_test.go index 1c3f394c2..9c553cee7 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -151,6 +151,25 @@ func TestParse(t *testing.T) { "Invalid namespace name", nil, }, + { + ` + namespace "default" { + capabilities = ["sentinel-override"] + } + `, + "", + &Policy{ + Namespaces: []*NamespacePolicy{ + &NamespacePolicy{ + Name: "default", + Policy: "", + Capabilities: []string{ + NamespaceCapabilitySentinelOverride, + }, + }, + }, + }, + }, } for idx, tc := range tcases { diff --git a/api/jobs.go b/api/jobs.go index 4684571e7..4ec71af4a 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -52,30 +52,43 @@ func (j *Jobs) Validate(job *Job, q *WriteOptions) (*JobValidateResponse, *Write return &resp, wm, err } +// RegisterOptions is used to pass through job registration parameters +type RegisterOptions struct { + EnforceIndex bool + ModifyIndex uint64 + PolicyOverride bool +} + // Register is used to register a new job. It returns the ID // of the evaluation, along with any errors encountered. func (j *Jobs) Register(job *Job, q *WriteOptions) (*JobRegisterResponse, *WriteMeta, error) { - - var resp JobRegisterResponse - - req := &RegisterJobRequest{Job: job} - wm, err := j.client.write("/v1/jobs", req, &resp, q) - if err != nil { - return nil, nil, err - } - return &resp, wm, nil + return j.RegisterOpts(job, nil, q) } // EnforceRegister is used to register a job enforcing its job modify index. func (j *Jobs) EnforceRegister(job *Job, modifyIndex uint64, q *WriteOptions) (*JobRegisterResponse, *WriteMeta, error) { + opts := RegisterOptions{EnforceIndex: true, ModifyIndex: modifyIndex} + return j.RegisterOpts(job, &opts, q) +} + +// Register is used to register a new job. It returns the ID +// of the evaluation, along with any errors encountered. +func (j *Jobs) RegisterOpts(job *Job, opts *RegisterOptions, q *WriteOptions) (*JobRegisterResponse, *WriteMeta, error) { + // Format the request + req := &RegisterJobRequest{ + Job: job, + } + if opts != nil { + if opts.EnforceIndex { + req.EnforceIndex = true + req.JobModifyIndex = opts.ModifyIndex + } + if opts.PolicyOverride { + req.PolicyOverride = true + } + } var resp JobRegisterResponse - - req := &RegisterJobRequest{ - Job: job, - EnforceIndex: true, - JobModifyIndex: modifyIndex, - } wm, err := j.client.write("/v1/jobs", req, &resp, q) if err != nil { return nil, nil, err @@ -208,21 +221,36 @@ func (j *Jobs) PeriodicForce(jobID string, q *WriteOptions) (string, *WriteMeta, return resp.EvalID, wm, nil } +// PlanOptions is used to pass through job planning parameters +type PlanOptions struct { + Diff bool + PolicyOverride bool +} + func (j *Jobs) Plan(job *Job, diff bool, q *WriteOptions) (*JobPlanResponse, *WriteMeta, error) { + opts := PlanOptions{Diff: diff} + return j.PlanOpts(job, &opts, q) +} + +func (j *Jobs) PlanOpts(job *Job, opts *PlanOptions, q *WriteOptions) (*JobPlanResponse, *WriteMeta, error) { if job == nil { return nil, nil, fmt.Errorf("must pass non-nil job") } - var resp JobPlanResponse + // Setup the request req := &JobPlanRequest{ - Job: job, - Diff: diff, + Job: job, } + if opts != nil { + req.Diff = opts.Diff + req.PolicyOverride = opts.PolicyOverride + } + + var resp JobPlanResponse wm, err := j.client.write("/v1/job/"+*job.ID+"/plan", req, &resp, q) if err != nil { return nil, nil, err } - return &resp, wm, nil } @@ -794,6 +822,7 @@ type JobRegisterRequest struct { // register only occurs if the job is new. EnforceIndex bool JobModifyIndex uint64 + PolicyOverride bool WriteRequest } @@ -803,6 +832,7 @@ type RegisterJobRequest struct { Job *Job EnforceIndex bool `json:",omitempty"` JobModifyIndex uint64 `json:",omitempty"` + PolicyOverride bool `json:",omitempty"` } // JobRegisterResponse is used to respond to a job registration @@ -827,8 +857,9 @@ type JobDeregisterResponse struct { } type JobPlanRequest struct { - Job *Job - Diff bool + Job *Job + Diff bool + PolicyOverride bool WriteRequest } diff --git a/api/namespace_test.go b/api/namespace_test.go index 9f168e0de..d58d1adb8 100644 --- a/api/namespace_test.go +++ b/api/namespace_test.go @@ -53,8 +53,8 @@ func TestNamespace_Info(t *testing.T) { // Trying to retrieve a namespace before it exists returns an error _, _, err := namespaces.Info("foo", nil) - assert.Nil(err) - assert.Contains("not found", err.Error()) + assert.NotNil(err) + assert.Contains(err.Error(), "not found") // Register the namespace ns := testNamespace() diff --git a/api/sentinel.go b/api/sentinel.go new file mode 100644 index 000000000..c1e52c7cb --- /dev/null +++ b/api/sentinel.go @@ -0,0 +1,79 @@ +package api + +import "fmt" + +// SentinelPolicies is used to query the Sentinel Policy endpoints. +type SentinelPolicies struct { + client *Client +} + +// SentinelPolicies returns a new handle on the Sentinel policies. +func (c *Client) SentinelPolicies() *SentinelPolicies { + return &SentinelPolicies{client: c} +} + +// List is used to dump all of the policies. +func (a *SentinelPolicies) List(q *QueryOptions) ([]*SentinelPolicyListStub, *QueryMeta, error) { + var resp []*SentinelPolicyListStub + qm, err := a.client.query("/v1/sentinel/policies", &resp, q) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + +// Upsert is used to create or update a policy +func (a *SentinelPolicies) Upsert(policy *SentinelPolicy, q *WriteOptions) (*WriteMeta, error) { + if policy == nil || policy.Name == "" { + return nil, fmt.Errorf("missing policy name") + } + wm, err := a.client.write("/v1/sentinel/policy/"+policy.Name, policy, nil, q) + if err != nil { + return nil, err + } + return wm, nil +} + +// Delete is used to delete a policy +func (a *SentinelPolicies) Delete(policyName string, q *WriteOptions) (*WriteMeta, error) { + if policyName == "" { + return nil, fmt.Errorf("missing policy name") + } + wm, err := a.client.delete("/v1/sentinel/policy/"+policyName, nil, q) + if err != nil { + return nil, err + } + return wm, nil +} + +// Info is used to query a specific policy +func (a *SentinelPolicies) Info(policyName string, q *QueryOptions) (*SentinelPolicy, *QueryMeta, error) { + if policyName == "" { + return nil, nil, fmt.Errorf("missing policy name") + } + var resp SentinelPolicy + wm, err := a.client.query("/v1/sentinel/policy/"+policyName, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + +type SentinelPolicy struct { + Name string + Description string + Scope string + EnforcementLevel string + Policy string + CreateIndex uint64 + ModifyIndex uint64 +} + +type SentinelPolicyListStub struct { + Name string + Description string + Scope string + EnforcementLevel string + CreateIndex uint64 + ModifyIndex uint64 +} diff --git a/api/sentinel_test.go b/api/sentinel_test.go new file mode 100644 index 000000000..32fc2a6f9 --- /dev/null +++ b/api/sentinel_test.go @@ -0,0 +1,109 @@ +// +build pro ent + +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSentinelPolicies_ListUpsert(t *testing.T) { + t.Parallel() + c, s, _ := makeACLClient(t, nil, nil) + defer s.Stop() + ap := c.SentinelPolicies() + + // Listing when nothing exists returns empty + result, qm, err := ap.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 1 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(result); n != 0 { + t.Fatalf("expected 0 policies, got: %d", n) + } + + // Register a policy + policy := &SentinelPolicy{ + Name: "test", + Description: "test", + EnforcementLevel: "advisory", + Scope: "submit-job", + Policy: "main = rule { true }", + } + wm, err := ap.Upsert(policy, nil) + assert.Nil(t, err) + assertWriteMeta(t, wm) + + // Check the list again + result, qm, err = ap.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + if len(result) != 1 { + t.Fatalf("expected policy, got: %#v", result) + } +} + +func TestSentinelPolicies_Delete(t *testing.T) { + t.Parallel() + c, s, _ := makeACLClient(t, nil, nil) + defer s.Stop() + ap := c.SentinelPolicies() + + // Register a policy + policy := &SentinelPolicy{ + Name: "test", + Description: "test", + EnforcementLevel: "advisory", + Scope: "submit-job", + Policy: "main = rule { true } ", + } + wm, err := ap.Upsert(policy, nil) + assert.Nil(t, err) + assertWriteMeta(t, wm) + + // Delete the policy + wm, err = ap.Delete(policy.Name, nil) + assert.Nil(t, err) + assertWriteMeta(t, wm) + + // Check the list again + result, qm, err := ap.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + if len(result) != 0 { + t.Fatalf("unexpected policy, got: %#v", result) + } +} + +func TestSentinelPolicies_Info(t *testing.T) { + t.Parallel() + c, s, _ := makeACLClient(t, nil, nil) + defer s.Stop() + ap := c.SentinelPolicies() + + // Register a policy + policy := &SentinelPolicy{ + Name: "test", + Description: "test", + EnforcementLevel: "advisory", + Scope: "submit-job", + Policy: "main = rule { true }", + } + wm, err := ap.Upsert(policy, nil) + assert.Nil(t, err) + assertWriteMeta(t, wm) + + // Query the policy + out, qm, err := ap.Info(policy.Name, nil) + assert.Nil(t, err) + assertQueryMeta(t, qm) + assert.Equal(t, policy.Name, out.Name) +} diff --git a/appveyor.yml b/appveyor.yml index 3fab7ab67..d37d7c0ac 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,20 +1,27 @@ version: "build-{branch}-{build}" image: Visual Studio 2017 clone_folder: c:\gopath\src\github.com\hashicorp\nomad + environment: GOPATH: c:\gopath GOBIN: c:\gopath\bin + + matrix: + - RUN_UI_TESTS: 1 + SKIP_NOMAD_TESTS: 1 + - {} + install: -- cmd: set PATH=%GOBIN%;c:\go\bin;%PATH% -- cmd: echo %Path% -- cmd: go version -- cmd: go env -- ps: mkdir C:\gopath\bin -- ps: appveyor DownloadFile "https://releases.hashicorp.com/vault/0.7.0/vault_0.7.0_windows_amd64.zip" -FileName "C:\\gopath\\bin\\vault.zip" -- ps: Expand-Archive C:\gopath\bin\vault.zip -DestinationPath C:\gopath\bin -- ps: appveyor DownloadFile "https://releases.hashicorp.com/consul/0.7.0/consul_0.7.0_windows_amd64.zip" -FileName "C:\\gopath\\bin\\consul.zip" -- ps: Expand-Archive C:\gopath\bin\consul.zip -DestinationPath C:\gopath\bin -#- cmd: go install -tags nomad_test + - cmd: set PATH=%GOBIN%;c:\go\bin;%PATH% + - cmd: echo %Path% + - cmd: go version + - cmd: go env + - ps: mkdir C:\gopath\bin + - ps: appveyor DownloadFile "https://releases.hashicorp.com/vault/0.7.0/vault_0.7.0_windows_amd64.zip" -FileName "C:\\gopath\\bin\\vault.zip" + - ps: Expand-Archive C:\gopath\bin\vault.zip -DestinationPath C:\gopath\bin + - ps: appveyor DownloadFile "https://releases.hashicorp.com/consul/0.7.0/consul_0.7.0_windows_amd64.zip" -FileName "C:\\gopath\\bin\\consul.zip" + - ps: Expand-Archive C:\gopath\bin\consul.zip -DestinationPath C:\gopath\bin + #- cmd: go install -tags nomad_test build_script: -#- cmd: go test -tags nomad_test ./... -- cmd: go install -tags nomad_test + #- cmd: go test -tags nomad_test ./... + - cmd: go install -tags nomad_test diff --git a/command/agent/agent.go b/command/agent/agent.go index 50409c09b..c33d587f8 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -153,6 +153,9 @@ func convertServerConfig(agentConfig *Config, logOutput io.Writer) (*nomad.Confi if agentConfig.ACL.ReplicationToken != "" { conf.ReplicationToken = agentConfig.ACL.ReplicationToken } + if agentConfig.Sentinel != nil { + conf.SentinelConfig = agentConfig.Sentinel + } // Set up the bind addresses rpcAddr, err := net.ResolveTCPAddr("tcp", agentConfig.normalizedAddrs.RPC) diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl index 03a63fa3a..188f085be 100644 --- a/command/agent/config-test-fixtures/basic.hcl +++ b/command/agent/config-test-fixtures/basic.hcl @@ -154,3 +154,13 @@ tls { key_file = "pipe" verify_https_client = true } +sentinel { + import "foo" { + path = "foo" + args = ["a", "b", "c"] + } + import "bar" { + path = "bar" + args = ["x", "y", "z"] + } +} diff --git a/command/agent/config.go b/command/agent/config.go index 1ab766d27..ad514b589 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -130,6 +130,9 @@ type Config struct { // HTTPAPIResponseHeaders allows users to configure the Nomad http agent to // set arbritrary headers on API responses HTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"` + + // Sentinel holds sentinel related settings + Sentinel *config.SentinelConfig `mapstructure:"sentinel"` } // AtlasConfig is used to enable an parameterize the Atlas integration @@ -615,6 +618,7 @@ func DefaultConfig() *Config { collectionInterval: 1 * time.Second, }, TLSConfig: &config.TLSConfig{}, + Sentinel: &config.SentinelConfig{}, Version: version.GetVersion(), } } @@ -775,6 +779,14 @@ func (c *Config) Merge(b *Config) *Config { result.Vault = result.Vault.Merge(b.Vault) } + // Apply the sentinel config + if result.Sentinel == nil && b.Sentinel != nil { + server := *b.Sentinel + result.Sentinel = &server + } else if b.Sentinel != nil { + result.Sentinel = result.Sentinel.Merge(b.Sentinel) + } + // Merge config files lists result.Files = append(result.Files, b.Files...) diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 0722679e9..37dc5ab20 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -97,6 +97,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error { "tls", "http_api_response_headers", "acl", + "sentinel", } if err := checkHCLKeys(list, valid); err != nil { return multierror.Prefix(err, "config:") @@ -120,6 +121,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error { delete(m, "tls") delete(m, "http_api_response_headers") delete(m, "acl") + delete(m, "sentinel") // Decode the rest if err := mapstructure.WeakDecode(m, result); err != nil { @@ -203,6 +205,13 @@ func parseConfig(result *Config, list *ast.ObjectList) error { } } + // Parse Sentinel config + if o := list.Filter("sentinel"); len(o.Items) > 0 { + if err := parseSentinel(&result.Sentinel, o); err != nil { + return multierror.Prefix(err, "sentinel->") + } + } + // Parse out http_api_response_headers fields. These are in HCL as a list so // we need to iterate over them and merge them. if headersO := list.Filter("http_api_response_headers"); len(headersO.Items) > 0 { @@ -835,6 +844,40 @@ func parseVaultConfig(result **config.VaultConfig, list *ast.ObjectList) error { return nil } +func parseSentinel(result **config.SentinelConfig, list *ast.ObjectList) error { + list = list.Elem() + if len(list.Items) > 1 { + return fmt.Errorf("only one 'sentinel' block allowed") + } + + // Get our sentinel object + obj := list.Items[0] + + // Value should be an object + var listVal *ast.ObjectList + if ot, ok := obj.Val.(*ast.ObjectType); ok { + listVal = ot.List + } else { + return fmt.Errorf("sentinel value: should be an object") + } + + // Check for invalid keys + valid := []string{ + "import", + } + if err := checkHCLKeys(listVal, valid); err != nil { + return err + } + + var config config.SentinelConfig + if err := hcl.DecodeObject(&config, listVal); err != nil { + return err + } + + *result = &config + return nil +} + func checkHCLKeys(node ast.Node, valid []string) error { var list *ast.ObjectList switch n := node.(type) { diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go index ce54bb908..2d5902742 100644 --- a/command/agent/config_parse_test.go +++ b/command/agent/config_parse_test.go @@ -177,6 +177,20 @@ func TestConfig_Parse(t *testing.T) { HTTPAPIResponseHeaders: map[string]string{ "Access-Control-Allow-Origin": "*", }, + Sentinel: &config.SentinelConfig{ + Imports: []*config.SentinelImport{ + &config.SentinelImport{ + Name: "foo", + Path: "foo", + Args: []string{"a", "b", "c"}, + }, + &config.SentinelImport{ + Name: "bar", + Path: "bar", + Args: []string{"x", "y", "z"}, + }, + }, + }, }, false, }, diff --git a/command/agent/config_test.go b/command/agent/config_test.go index a2fe8efda..d33c239df 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -34,6 +34,7 @@ func TestConfig_Merge(t *testing.T) { Atlas: &AtlasConfig{}, Vault: &config.VaultConfig{}, Consul: &config.ConsulConfig{}, + Sentinel: &config.SentinelConfig{}, } c2 := &Config{ @@ -312,6 +313,15 @@ func TestConfig_Merge(t *testing.T) { ClientAutoJoin: &trueValue, ChecksUseAdvertise: &trueValue, }, + Sentinel: &config.SentinelConfig{ + Imports: []*config.SentinelImport{ + &config.SentinelImport{ + Name: "foo", + Path: "foo", + Args: []string{"a", "b", "c"}, + }, + }, + }, } result := c0.Merge(c1) diff --git a/command/agent/http.go b/command/agent/http.go index b1bf1f177..8244f2328 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -9,10 +9,12 @@ import ( "net" "net/http" "net/http/pprof" + "os" "strconv" "time" "github.com/NYTimes/gziphandler" + "github.com/elazarl/go-bindata-assetfs" "github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/nomad/structs" "github.com/ugorji/go/codec" @@ -27,6 +29,18 @@ const ( // this is checked to switch between the ACLToken and // AtlasACLToken scadaHTTPAddr = "SCADA" + + // ErrEntOnly is the error returned if accessing an enterprise only + // endpoint + ErrEntOnly = "Nomad Enterprise only endpoint" +) + +var ( + // Set to false by stub_asset if the ui build tag isn't enabled + uiEnabled = true + + // Overridden if the ui build tag isn't enabled + stubHTML = "" ) // HTTPServer is used to wrap an Agent and expose it over an HTTP interface @@ -186,6 +200,16 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) + if uiEnabled { + s.mux.Handle("/ui/", http.StripPrefix("/ui/", handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))) + } else { + // Write the stubHTML + s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(stubHTML)) + }) + } + s.mux.Handle("/", handleRootRedirect()) + if enableDebug { s.mux.HandleFunc("/debug/pprof/", pprof.Index) s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) @@ -204,6 +228,22 @@ type HTTPCodedError interface { Code() int } +type UIAssetWrapper struct { + FileSystem *assetfs.AssetFS +} + +func (fs *UIAssetWrapper) Open(name string) (http.File, error) { + if file, err := fs.FileSystem.Open(name); err == nil { + return file, nil + } else { + // serve index.html instead of 404ing + if err == os.ErrNotExist { + return fs.FileSystem.Open("index.html") + } + return nil, err + } +} + func CodedError(c int, s string) HTTPCodedError { return &codedError{s, c} } @@ -221,6 +261,22 @@ func (e *codedError) Code() int { return e.code } +func handleUI(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + header := w.Header() + header.Add("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'none'; frame-ancestors 'none'") + h.ServeHTTP(w, req) + return + }) +} + +func handleRootRedirect() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/ui/", 307) + return + }) +} + // wrap is used to wrap functions to make them more convenient func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { f := func(resp http.ResponseWriter, req *http.Request) { diff --git a/command/agent/http_oss.go b/command/agent/http_oss.go index 2e2675503..e442c5c41 100644 --- a/command/agent/http_oss.go +++ b/command/agent/http_oss.go @@ -2,5 +2,17 @@ package agent +import "net/http" + // registerEnterpriseHandlers is a no-op for the oss release -func (s *HTTPServer) registerEnterpriseHandlers() {} +func (s *HTTPServer) registerEnterpriseHandlers() { + s.mux.HandleFunc("/v1/namespaces", s.wrap(s.entOnly)) + s.mux.HandleFunc("/v1/namespace", s.wrap(s.entOnly)) + s.mux.HandleFunc("/v1/namespace/", s.wrap(s.entOnly)) + s.mux.HandleFunc("/v1/sentinel/policies", s.wrap(s.entOnly)) + s.mux.HandleFunc("/v1/sentinel/policy/", s.wrap(s.entOnly)) +} + +func (s *HTTPServer) entOnly(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return nil, CodedError(501, ErrEntOnly) +} diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 5fcf65161..2dc8a5811 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -124,8 +124,9 @@ func (s *HTTPServer) jobPlan(resp http.ResponseWriter, req *http.Request, sJob := ApiJobToStructJob(args.Job) planReq := structs.JobPlanRequest{ - Job: sJob, - Diff: args.Diff, + Job: sJob, + Diff: args.Diff, + PolicyOverride: args.PolicyOverride, WriteRequest: structs.WriteRequest{ Region: args.WriteRequest.Region, }, @@ -355,6 +356,7 @@ func (s *HTTPServer) jobUpdate(resp http.ResponseWriter, req *http.Request, Job: sJob, EnforceIndex: args.EnforceIndex, JobModifyIndex: args.JobModifyIndex, + PolicyOverride: args.PolicyOverride, WriteRequest: structs.WriteRequest{ Region: args.WriteRequest.Region, SecretID: args.WriteRequest.SecretID, diff --git a/command/agent/stub_asset.go b/command/agent/stub_asset.go new file mode 100644 index 000000000..00ef7222d --- /dev/null +++ b/command/agent/stub_asset.go @@ -0,0 +1,26 @@ +// +build !ui + +package agent + +import ( + assetfs "github.com/elazarl/go-bindata-assetfs" +) + +func init() { + uiEnabled = false + stubHTML = ` + +

Nomad UI is not available in this binary. To get Nomad UI do one of the following:

+ + +` +} + +// assetFS is a stub for building Nomad without a UI. +func assetFS() *assetfs.AssetFS { + return nil +} diff --git a/command/namespace_apply.go b/command/namespace_apply.go index ff4690693..8042512f3 100644 --- a/command/namespace_apply.go +++ b/command/namespace_apply.go @@ -14,9 +14,10 @@ type NamespaceApplyCommand struct { func (c *NamespaceApplyCommand) Help() string { helpText := ` -Usage: nomad namespace apply [options] +Usage: nomad namespace apply [options] -Apply is used to create or update a namespace. +Apply is used to create or update a namespace. It takes the namespace name to +create or update as its only argument. General Options: @@ -24,9 +25,6 @@ General Options: Apply Options: - -name - The name of the namespace. - -description An optional description for the namespace. ` @@ -36,7 +34,6 @@ Apply Options: func (c *NamespaceApplyCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ - "-name": complete.PredictAnything, "-description": complete.PredictAnything, }) } @@ -50,24 +47,25 @@ func (c *NamespaceApplyCommand) Synopsis() string { } func (c *NamespaceApplyCommand) Run(args []string) int { - var name, description string + var description string flags := c.Meta.FlagSet("namespace apply", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } - flags.StringVar(&name, "name", "", "") flags.StringVar(&description, "description", "", "") if err := flags.Parse(args); err != nil { return 1 } - // Check that we got no arguments + // Check that we get exactly one argument args = flags.Args() - if l := len(args); l != 0 { + if l := len(args); l != 1 { c.Ui.Error(c.Help()) return 1 } + name := args[0] + // Validate we have at-least a name if name == "" { c.Ui.Error("Namespace name required") @@ -93,5 +91,6 @@ func (c *NamespaceApplyCommand) Run(args []string) int { return 1 } + c.Ui.Output(fmt.Sprintf("Successfully applied namespace %q!", name)) return 0 } diff --git a/command/namespace_apply_test.go b/command/namespace_apply_test.go index c8f25c1cf..360efcd27 100644 --- a/command/namespace_apply_test.go +++ b/command/namespace_apply_test.go @@ -50,7 +50,7 @@ func TestNamespaceApplyCommand_Good(t *testing.T) { // Create a namespace name, desc := "foo", "bar" - if code := cmd.Run([]string{"-address=" + url, "-name=" + name, "-description=" + desc}); code != 0 { + if code := cmd.Run([]string{"-address=" + url, "-description=" + desc, name}); code != 0 { t.Fatalf("expected exit 0, got: %d; %v", code, ui.ErrorWriter.String()) } diff --git a/command/namespace_delete.go b/command/namespace_delete.go index 0be8608b8..cc5c02e47 100644 --- a/command/namespace_delete.go +++ b/command/namespace_delete.go @@ -67,5 +67,6 @@ func (c *NamespaceDeleteCommand) Run(args []string) int { return 1 } + c.Ui.Output(fmt.Sprintf("Successfully deleted namespace %q!", namespace)) return 0 } diff --git a/command/namespace_list.go b/command/namespace_list.go index 15c157d8e..1d583cb94 100644 --- a/command/namespace_list.go +++ b/command/namespace_list.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "sort" "strings" "github.com/hashicorp/nomad/api" @@ -102,6 +103,9 @@ func formatNamespaces(namespaces []*api.Namespace) string { return "No namespaces found" } + // Sort the output by namespace name + sort.Slice(namespaces, func(i, j int) bool { return namespaces[i].Name < namespaces[j].Name }) + rows := make([]string, len(namespaces)+1) rows[0] = "Name|Description" for i, ns := range namespaces { diff --git a/command/plan.go b/command/plan.go index b01444ff1..b712448fd 100644 --- a/command/plan.go +++ b/command/plan.go @@ -68,6 +68,9 @@ Plan Options: Determines whether the diff between the remote job and planned job is shown. Defaults to true. + -policy-override + Sets the flag to force override any soft mandatory Sentinel policies. + -verbose Increase diff verbosity. ` @@ -81,8 +84,9 @@ func (c *PlanCommand) Synopsis() string { func (c *PlanCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ - "-diff": complete.PredictNothing, - "-verbose": complete.PredictNothing, + "-diff": complete.PredictNothing, + "-policy-override": complete.PredictNothing, + "-verbose": complete.PredictNothing, }) } @@ -91,11 +95,12 @@ func (c *PlanCommand) AutocompleteArgs() complete.Predictor { } func (c *PlanCommand) Run(args []string) int { - var diff, verbose bool + var diff, policyOverride, verbose bool flags := c.Meta.FlagSet("plan", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } flags.BoolVar(&diff, "diff", true, "") + flags.BoolVar(&policyOverride, "policy-override", false, "") flags.BoolVar(&verbose, "verbose", false, "") if err := flags.Parse(args); err != nil { @@ -134,8 +139,17 @@ func (c *PlanCommand) Run(args []string) int { client.SetNamespace(*n) } + // Setup the options + opts := &api.PlanOptions{} + if diff { + opts.Diff = true + } + if policyOverride { + opts.PolicyOverride = true + } + // Submit the job - resp, _, err := client.Jobs().Plan(job, diff, nil) + resp, _, err := client.Jobs().PlanOpts(job, opts, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error during plan: %s", err)) return 255 diff --git a/command/run.go b/command/run.go index 74deeedd4..4c0195fca 100644 --- a/command/run.go +++ b/command/run.go @@ -77,8 +77,12 @@ Run Options: the evaluation ID will be printed to the screen, which can be used to examine the evaluation using the eval-status command. - -verbose - Display full information. + -output + Output the JSON that would be submitted to the HTTP API without submitting + the job. + + -policy-override + Sets the flag to force override any soft mandatory Sentinel policies. -vault-token If set, the passed Vault token is stored in the job before sending to the @@ -86,9 +90,8 @@ Run Options: the job file. This overrides the token found in $VAULT_TOKEN environment variable and that found in the job. - -output - Output the JSON that would be submitted to the HTTP API without submitting - the job. + -verbose + Display full information. ` return strings.TrimSpace(helpText) } @@ -100,11 +103,12 @@ func (c *RunCommand) Synopsis() string { func (c *RunCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ - "-check-index": complete.PredictNothing, - "-detach": complete.PredictNothing, - "-verbose": complete.PredictNothing, - "-vault-token": complete.PredictAnything, - "-output": complete.PredictNothing, + "-check-index": complete.PredictNothing, + "-detach": complete.PredictNothing, + "-verbose": complete.PredictNothing, + "-vault-token": complete.PredictAnything, + "-output": complete.PredictNothing, + "-policy-override": complete.PredictNothing, }) } @@ -113,7 +117,7 @@ func (c *RunCommand) AutocompleteArgs() complete.Predictor { } func (c *RunCommand) Run(args []string) int { - var detach, verbose, output bool + var detach, verbose, output, override bool var checkIndexStr, vaultToken string flags := c.Meta.FlagSet("run", FlagSetClient) @@ -121,6 +125,7 @@ func (c *RunCommand) Run(args []string) int { flags.BoolVar(&detach, "detach", false, "") flags.BoolVar(&verbose, "verbose", false, "") flags.BoolVar(&output, "output", false, "") + flags.BoolVar(&override, "policy-override", false, "") flags.StringVar(&checkIndexStr, "check-index", "", "") flags.StringVar(&vaultToken, "vault-token", "", "") @@ -205,13 +210,18 @@ func (c *RunCommand) Run(args []string) int { return 1 } - // Submit the job - var resp *api.JobRegisterResponse + // Set the register options + opts := &api.RegisterOptions{} if enforce { - resp, _, err = client.Jobs().EnforceRegister(job, checkIndex, nil) - } else { - resp, _, err = client.Jobs().Register(job, nil) + opts.EnforceIndex = true + opts.ModifyIndex = checkIndex } + if override { + opts.PolicyOverride = true + } + + // Submit the job + resp, _, err := client.Jobs().RegisterOpts(job, opts, nil) if err != nil { if strings.Contains(err.Error(), api.RegisterEnforceIndexErrPrefix) { // Format the error specially if the error is due to index diff --git a/command/sentinel.go b/command/sentinel.go new file mode 100644 index 000000000..fe8ffba60 --- /dev/null +++ b/command/sentinel.go @@ -0,0 +1,19 @@ +package command + +import "github.com/mitchellh/cli" + +type SentinelCommand struct { + Meta +} + +func (f *SentinelCommand) Help() string { + return "This command is accessed by using one of the subcommands below." +} + +func (f *SentinelCommand) Synopsis() string { + return "Interact with Sentinel policies" +} + +func (f *SentinelCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/sentinel_apply.go b/command/sentinel_apply.go new file mode 100644 index 000000000..00f6488be --- /dev/null +++ b/command/sentinel_apply.go @@ -0,0 +1,127 @@ +package command + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/posener/complete" +) + +type SentinelApplyCommand struct { + Meta +} + +func (c *SentinelApplyCommand) Help() string { + helpText := ` +Usage: nomad sentinel apply [options] + +Apply is used to write a new Sentinel policy or update an existing one. +The name of the policy and file must be specified. The file will be read +from stdin by specifying "-". + +General Options: + + ` + generalOptionsUsage() + ` + +Apply Options: + + -description + Sets a human readable description for the policy. + + -scope (default: submit-job) + Sets the scope of the policy and when it should be enforced. + + -level (default: advisory) + Sets the enforcment level of the policy. Must be one of advisory, + soft-mandatory, hard-mandatory. + +` + return strings.TrimSpace(helpText) +} + +func (c *SentinelApplyCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-description": complete.PredictAnything, + "-scope": complete.PredictAnything, + "-level": complete.PredictAnything, + }) +} + +func (c *SentinelApplyCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *SentinelApplyCommand) Synopsis() string { + return "Create a new or update existing Sentinel policies" +} + +func (c *SentinelApplyCommand) Run(args []string) int { + var description, scope, enfLevel string + var err error + flags := c.Meta.FlagSet("sentinel apply", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.StringVar(&description, "description", "", "") + flags.StringVar(&scope, "scope", "submit-job", "") + flags.StringVar(&enfLevel, "level", "advisory", "") + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly two arguments + args = flags.Args() + if l := len(args); l != 2 { + c.Ui.Error(c.Help()) + return 1 + } + + // Get the name and file + policyName := args[0] + + // Read the file contents + file := args[1] + var rawPolicy []byte + if file == "-" { + rawPolicy, err = ioutil.ReadAll(os.Stdin) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err)) + return 1 + } + } else { + rawPolicy, err = ioutil.ReadFile(file) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err)) + return 1 + } + } + + // Construct the policy + sp := &api.SentinelPolicy{ + Name: policyName, + Description: description, + Scope: scope, + EnforcementLevel: enfLevel, + Policy: string(rawPolicy), + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Get the list of policies + _, err = client.SentinelPolicies().Upsert(sp, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error writing Sentinel policy: %s", err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf("Successfully wrote %q Sentinel policy!", + policyName)) + return 0 +} diff --git a/command/sentinel_apply_test.go b/command/sentinel_apply_test.go new file mode 100644 index 000000000..65f979cc1 --- /dev/null +++ b/command/sentinel_apply_test.go @@ -0,0 +1,12 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestSentinelApplyCommand_Implements(t *testing.T) { + t.Parallel() + var _ cli.Command = &SentinelApplyCommand{} +} diff --git a/command/sentinel_delete.go b/command/sentinel_delete.go new file mode 100644 index 000000000..1e7a7958b --- /dev/null +++ b/command/sentinel_delete.go @@ -0,0 +1,75 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/posener/complete" +) + +type SentinelDeleteCommand struct { + Meta +} + +func (c *SentinelDeleteCommand) Help() string { + helpText := ` +Usage: nomad sentinel delete [options] + +Delete is used to delete an existing Sentinel policy. + +General Options: + + ` + generalOptionsUsage() + ` + +` + return strings.TrimSpace(helpText) +} + +func (c *SentinelDeleteCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{}) +} + +func (c *SentinelDeleteCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *SentinelDeleteCommand) Synopsis() string { + return "Delete an existing Sentinel policies" +} + +func (c *SentinelDeleteCommand) Run(args []string) int { + flags := c.Meta.FlagSet("sentinel delete", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one arguments + args = flags.Args() + if l := len(args); l != 1 { + c.Ui.Error(c.Help()) + return 1 + } + + // Get the name and file + policyName := args[0] + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Get the list of policies + _, err = client.SentinelPolicies().Delete(policyName, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error deleting Sentinel policy: %s", err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf("Successfully deleted %q Sentinel policy!", + policyName)) + return 0 +} diff --git a/command/sentinel_delete_test.go b/command/sentinel_delete_test.go new file mode 100644 index 000000000..313e438aa --- /dev/null +++ b/command/sentinel_delete_test.go @@ -0,0 +1,12 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestSentinelDeleteCommand_Implements(t *testing.T) { + t.Parallel() + var _ cli.Command = &SentinelDeleteCommand{} +} diff --git a/command/sentinel_list.go b/command/sentinel_list.go new file mode 100644 index 000000000..a41b8bd2e --- /dev/null +++ b/command/sentinel_list.go @@ -0,0 +1,75 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/posener/complete" +) + +type SentinelListCommand struct { + Meta +} + +func (c *SentinelListCommand) Help() string { + helpText := ` +Usage: nomad sentinel list [options] + +List is used to display all the installed Sentinel policies. + +General Options: + + ` + generalOptionsUsage() + ` + +` + return strings.TrimSpace(helpText) +} + +func (c *SentinelListCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{}) +} + +func (c *SentinelListCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *SentinelListCommand) Synopsis() string { + return "Display all Sentinel policies" +} + +func (c *SentinelListCommand) Run(args []string) int { + flags := c.Meta.FlagSet("sentinel list", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Get the list of policies + policies, _, err := client.SentinelPolicies().List(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error listing Sentinel policies: %s", err)) + return 1 + } + + if len(policies) == 0 { + c.Ui.Output("No policies found") + return 0 + } + + out := []string{} + out = append(out, "Name|Scope|Enforcement Level|Description") + for _, p := range policies { + line := fmt.Sprintf("%s|%s|%s|%s", p.Name, p.Scope, p.EnforcementLevel, p.Description) + out = append(out, line) + } + c.Ui.Output(formatList(out)) + return 0 +} diff --git a/command/sentinel_list_test.go b/command/sentinel_list_test.go new file mode 100644 index 000000000..98d1a307b --- /dev/null +++ b/command/sentinel_list_test.go @@ -0,0 +1,12 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestSentinelListCommand_Implements(t *testing.T) { + t.Parallel() + var _ cli.Command = &SentinelListCommand{} +} diff --git a/command/sentinel_read.go b/command/sentinel_read.go new file mode 100644 index 000000000..b1570ca45 --- /dev/null +++ b/command/sentinel_read.go @@ -0,0 +1,98 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/posener/complete" +) + +type SentinelReadCommand struct { + Meta +} + +func (c *SentinelReadCommand) Help() string { + helpText := ` +Usage: nomad sentinel read [options] + +Read is used to inspect a Sentinel policy. + +General Options: + + ` + generalOptionsUsage() + ` + +Read Options: + + -raw + Prints only the raw policy + +` + return strings.TrimSpace(helpText) +} + +func (c *SentinelReadCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-raw": complete.PredictNothing, + }) +} + +func (c *SentinelReadCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *SentinelReadCommand) Synopsis() string { + return "Inspects an existing Sentinel policies" +} + +func (c *SentinelReadCommand) Run(args []string) int { + var raw bool + flags := c.Meta.FlagSet("sentinel read", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&raw, "raw", false, "") + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one arguments + args = flags.Args() + if l := len(args); l != 1 { + c.Ui.Error(c.Help()) + return 1 + } + + // Get the name and file + policyName := args[0] + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Query the policy + policy, _, err := client.SentinelPolicies().Info(policyName, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Sentinel policy: %s", err)) + return 1 + } + + // Check for only the raw policy + if raw { + c.Ui.Output(policy.Policy) + return 0 + } + + // Output the base information + info := []string{ + fmt.Sprintf("Name|%s", policy.Name), + fmt.Sprintf("Scope|%s", policy.Scope), + fmt.Sprintf("Enforcement Level|%s", policy.EnforcementLevel), + fmt.Sprintf("Description|%s", policy.Description), + } + c.Ui.Output(formatKV(info)) + c.Ui.Output("Policy:") + c.Ui.Output(policy.Policy) + return 0 +} diff --git a/command/sentinel_read_test.go b/command/sentinel_read_test.go new file mode 100644 index 000000000..8abb9d0c8 --- /dev/null +++ b/command/sentinel_read_test.go @@ -0,0 +1,12 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestSentinelReadCommand_Implements(t *testing.T) { + t.Parallel() + var _ cli.Command = &SentinelReadCommand{} +} diff --git a/commands.go b/commands.go index 5007ea82b..1620d4456 100644 --- a/commands.go +++ b/commands.go @@ -279,6 +279,31 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "sentinel": func() (cli.Command, error) { + return &command.SentinelCommand{ + Meta: meta, + }, nil + }, + "sentinel list": func() (cli.Command, error) { + return &command.SentinelListCommand{ + Meta: meta, + }, nil + }, + "sentinel apply": func() (cli.Command, error) { + return &command.SentinelApplyCommand{ + Meta: meta, + }, nil + }, + "sentinel delete": func() (cli.Command, error) { + return &command.SentinelDeleteCommand{ + Meta: meta, + }, nil + }, + "sentinel read": func() (cli.Command, error) { + return &command.SentinelReadCommand{ + Meta: meta, + }, nil + }, "server-force-leave": func() (cli.Command, error) { return &command.ServerForceLeaveCommand{ Meta: meta, diff --git a/nomad/config.go b/nomad/config.go index 418d3f79a..db4939106 100644 --- a/nomad/config.go +++ b/nomad/config.go @@ -239,6 +239,12 @@ type Config struct { // ReplicationToken is the ACL Token Secret ID used to fetch from // the Authoritative Region. ReplicationToken string + + // SentinelGCInterval is the interval that we GC unused policies. + SentinelGCInterval time.Duration + + // SentinelConfig is this Agent's Sentinel configuration + SentinelConfig *config.SentinelConfig } // CheckVersion is used to check if the ProtocolVersion is valid @@ -296,6 +302,7 @@ func DefaultConfig() *Config { RPCHoldTimeout: 5 * time.Second, TLSConfig: &config.TLSConfig{}, ReplicationBackoff: 30 * time.Second, + SentinelGCInterval: 30 * time.Second, } // Enable all known schedulers by default diff --git a/nomad/fsm_test.go b/nomad/fsm_test.go index 1e1cd65c8..bf32749ef 100644 --- a/nomad/fsm_test.go +++ b/nomad/fsm_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + "strings" "testing" "time" @@ -330,6 +331,46 @@ func TestFSM_RegisterJob(t *testing.T) { } } +func TestFSM_RegisterJob_BadNamespace(t *testing.T) { + t.Parallel() + fsm := testFSM(t) + + job := mock.Job() + job.Namespace = "foo" + req := structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{ + Namespace: job.Namespace, + }, + } + buf, err := structs.Encode(structs.JobRegisterRequestType, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := fsm.Apply(makeLog(buf)) + if resp == nil { + t.Fatalf("no resp: %v", resp) + } + err, ok := resp.(error) + if !ok { + t.Fatalf("resp not of error type: %T %v", resp, resp) + } + if !strings.Contains(err.Error(), "non-existant namespace") { + t.Fatalf("bad error: %v", err) + } + + // Verify we are not registered + ws := memdb.NewWatchSet() + jobOut, err := fsm.State().JobByID(ws, req.Namespace, req.Job.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if jobOut != nil { + t.Fatalf("job found!") + } +} + func TestFSM_DeregisterJob_Purge(t *testing.T) { t.Parallel() fsm := testFSM(t) diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index bd6a63810..2e91337c6 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -75,12 +75,22 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis // Check job submission permissions if aclObj, err := j.srv.resolveToken(args.SecretID); err != nil { return err - } else if aclObj != nil && !aclObj.AllowNamespaceOperation(structs.DefaultNamespace, acl.NamespaceCapabilitySubmitJob) { - return structs.ErrPermissionDenied + } else if aclObj != nil { + if !aclObj.AllowNsOp(structs.DefaultNamespace, acl.NamespaceCapabilitySubmitJob) { + return structs.ErrPermissionDenied + } + // Check if override is set and we do not have permissions + if args.PolicyOverride { + if !aclObj.AllowNsOp(structs.DefaultNamespace, acl.NamespaceCapabilitySentinelOverride) { + j.srv.logger.Printf("[WARN] nomad.job: policy override attempted without permissions for Job %q", args.Job.ID) + return structs.ErrPermissionDenied + } + j.srv.logger.Printf("[WARN] nomad.job: policy override set for Job %q", args.Job.ID) + } } // Lookup the job - snap, err := j.srv.fsm.State().Snapshot() + snap, err := j.srv.State().Snapshot() if err != nil { return err } @@ -149,6 +159,16 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis } } + // Enforce Sentinel policies + policyWarnings, err := j.enforceSubmitJob(args.PolicyOverride, args.Job) + if err != nil { + return err + } + if policyWarnings != nil { + reply.Warnings = structs.MergeMultierrorWarnings(warnings, + canonicalizeWarnings, policyWarnings) + } + // Clear the Vault token args.Job.VaultToken = "" @@ -158,7 +178,11 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis args.Job.SetSubmitTime() // Commit this update via Raft - _, index, err := j.srv.raftApply(structs.JobRegisterRequestType, args) + fsmErr, index, err := j.srv.raftApply(structs.JobRegisterRequestType, args) + if err, ok := fsmErr.(error); ok && err != nil { + j.srv.logger.Printf("[ERR] nomad.job: Register failed: %v", err) + return err + } if err != nil { j.srv.logger.Printf("[ERR] nomad.job: Register failed: %v", err) return err @@ -917,6 +941,31 @@ func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) // Set the warning message reply.Warnings = structs.MergeMultierrorWarnings(warnings, canonicalizeWarnings) + // Check job submission permissions, which we assume is the same for plan + if aclObj, err := j.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil { + if !aclObj.AllowNsOp(structs.DefaultNamespace, acl.NamespaceCapabilitySubmitJob) { + return structs.ErrPermissionDenied + } + // Check if override is set and we do not have permissions + if args.PolicyOverride { + if !aclObj.AllowNsOp(structs.DefaultNamespace, acl.NamespaceCapabilitySentinelOverride) { + return structs.ErrPermissionDenied + } + } + } + + // Enforce Sentinel policies + policyWarnings, err := j.enforceSubmitJob(args.PolicyOverride, args.Job) + if err != nil { + return err + } + if policyWarnings != nil { + reply.Warnings = structs.MergeMultierrorWarnings(warnings, + canonicalizeWarnings, policyWarnings) + } + // Acquire a snapshot of the state snap, err := j.srv.fsm.State().Snapshot() if err != nil { @@ -1166,7 +1215,11 @@ func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispa } // Commit this update via Raft - _, jobCreateIndex, err := j.srv.raftApply(structs.JobRegisterRequestType, regReq) + fsmErr, jobCreateIndex, err := j.srv.raftApply(structs.JobRegisterRequestType, regReq) + if err, ok := fsmErr.(error); ok && err != nil { + j.srv.logger.Printf("[ERR] nomad.job: Dispatched job register failed: %v", err) + return err + } if err != nil { j.srv.logger.Printf("[ERR] nomad.job: Dispatched job register failed: %v", err) return err diff --git a/nomad/job_endpoint_oss.go b/nomad/job_endpoint_oss.go new file mode 100644 index 000000000..fd4aaa8e7 --- /dev/null +++ b/nomad/job_endpoint_oss.go @@ -0,0 +1,10 @@ +// +build !ent + +package nomad + +import "github.com/hashicorp/nomad/nomad/structs" + +// enforceSubmitJob is used to check any Sentinel policies for the submit-job scope +func (j *Job) enforceSubmitJob(override bool, job *structs.Job) (error, error) { + return nil, nil +} diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 07b08b6a7..da70a5216 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -140,6 +140,42 @@ func TestJobEndpoint_Register_ACL(t *testing.T) { } } +func TestJobEndpoint_Register_InvalidNamespace(t *testing.T) { + t.Parallel() + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + // Create the register request + job := mock.Job() + job.Namespace = "foo" + req := &structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + + // Try without a token, expect failure + var resp structs.JobRegisterResponse + err := msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp) + if err == nil || !strings.Contains(err.Error(), "non-existant namespace") { + t.Fatalf("expected namespace error: %v", err) + } + + // Check for the job in the FSM + state := s1.fsm.State() + ws := memdb.NewWatchSet() + out, err := state.JobByID(ws, job.Namespace, job.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if out != nil { + t.Fatalf("expected no job") + } +} + func TestJobEndpoint_Register_InvalidDriverConfig(t *testing.T) { t.Parallel() s1 := testServer(t, func(c *Config) { @@ -2728,6 +2764,39 @@ func TestJobEndpoint_LatestDeployment_Blocking(t *testing.T) { } } +func TestJobEndpoint_Plan_ACL(t *testing.T) { + t.Parallel() + s1, root := testACLServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + // Create a plan request + job := mock.Job() + planReq := &structs.JobPlanRequest{ + Job: job, + Diff: true, + WriteRequest: structs.WriteRequest{ + Region: "global", + Namespace: job.Namespace, + }, + } + + // Try without a token, expect failure + var planResp structs.JobPlanResponse + if err := msgpackrpc.CallWithCodec(codec, "Job.Plan", planReq, &planResp); err == nil { + t.Fatalf("expected error") + } + + // Try with a token + planReq.SecretID = root.SecretID + if err := msgpackrpc.CallWithCodec(codec, "Job.Plan", planReq, &planResp); err != nil { + t.Fatalf("err: %v", err) + } +} + func TestJobEndpoint_Plan_WithDiff(t *testing.T) { t.Parallel() s1 := testServer(t, func(c *Config) { diff --git a/nomad/periodic.go b/nomad/periodic.go index 69c6f7050..a1b358b12 100644 --- a/nomad/periodic.go +++ b/nomad/periodic.go @@ -52,7 +52,10 @@ func (s *Server) DispatchJob(job *structs.Job) (*structs.Evaluation, error) { Namespace: job.Namespace, }, } - _, index, err := s.raftApply(structs.JobRegisterRequestType, req) + fsmErr, index, err := s.raftApply(structs.JobRegisterRequestType, req) + if err, ok := fsmErr.(error); ok && err != nil { + return nil, err + } if err != nil { return nil, err } diff --git a/nomad/periodic_test.go b/nomad/periodic_test.go index 1d038430d..1db9c07d6 100644 --- a/nomad/periodic_test.go +++ b/nomad/periodic_test.go @@ -208,7 +208,6 @@ func TestPeriodicDispatch_Add_Remove_Namespaced(t *testing.T) { job := mock.PeriodicJob() job2 := mock.PeriodicJob() job2.Namespace = "test" - added, err := p.Add(job) assert.Nil(err) assert.True(added) @@ -515,6 +514,10 @@ func TestPeriodicDispatch_Run_SameID_Different_Namespace(t *testing.T) { t.Fatalf("got %d tracked; want 2", l) } + if l := len(p.Tracked()); l != 2 { + t.Fatalf("got %d tracked; want 2", l) + } + time.Sleep(2 * time.Second) // Check that the jobs were launched correctly. diff --git a/nomad/server.go b/nomad/server.go index c97dc98ea..d958c08b6 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -166,6 +166,9 @@ type Server struct { // aclCache is used to maintain the parsed ACL objects aclCache *lru.TwoQueueCache + // EnterpriseState is used to fill in state for Pro/Ent builds + EnterpriseState + left bool shutdown bool shutdownCh chan struct{} @@ -309,6 +312,11 @@ func NewServer(config *Config, consulCatalog consul.CatalogAPI, logger *log.Logg return nil, fmt.Errorf("failed to create deployment watcher: %v", err) } + // Setup the enterprise state + if err := s.setupEnterprise(config); err != nil { + return nil, err + } + // Monitor leadership changes go s.monitorLeadership() @@ -333,6 +341,9 @@ func NewServer(config *Config, consulCatalog consul.CatalogAPI, logger *log.Logg // Emit metrics go s.heartbeatStats() + // Start enterprise background workers + s.startEnterpriseBackground() + // Done return s, nil } diff --git a/nomad/server_setup_oss.go b/nomad/server_setup_oss.go new file mode 100644 index 000000000..b97260fc7 --- /dev/null +++ b/nomad/server_setup_oss.go @@ -0,0 +1,11 @@ +// +build !pro,!ent + +package nomad + +type EnterpriseState struct{} + +func (s *Server) setupEnterprise(config *Config) error { + return nil +} + +func (s *Server) startEnterpriseBackground() {} diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index af6c284d4..e7b9fe852 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -659,6 +659,14 @@ func (s *StateStore) upsertJobImpl(index uint64, job *structs.Job, keepVersion b if job.Namespace == "" { panic("empty namespace") } + + // Assert the namespace exists + if exists, err := s.namespaceExists(txn, job.Namespace); err != nil { + return err + } else if !exists { + return fmt.Errorf("job %q is in non-existant namespace %q", job.ID, job.Namespace) + } + // Check if the job already exists existing, err := txn.First("jobs", "id", job.Namespace, job.ID) if err != nil { diff --git a/nomad/state/state_store_oss.go b/nomad/state/state_store_oss.go new file mode 100644 index 000000000..0679d0398 --- /dev/null +++ b/nomad/state/state_store_oss.go @@ -0,0 +1,13 @@ +// +build !pro,!ent + +package state + +import ( + memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/nomad/structs" +) + +// namespaceExists returns whether a namespace exists +func (s *StateStore) namespaceExists(txn *memdb.Txn, namespace string) (bool, error) { + return namespace == structs.DefaultNamespace, nil +} diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index bb11fb71c..f2e06ece5 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -57,10 +57,10 @@ func TestStateStore_Blocking_Timeout(t *testing.T) { } func TestStateStore_Blocking_MinQuery(t *testing.T) { - job := mock.Job() + node := mock.Node() count := 0 queryFn := func(ws memdb.WatchSet, s *StateStore) (interface{}, uint64, error) { - _, err := s.JobByID(ws, job.Namespace, job.ID) + _, err := s.NodeByID(ws, node.ID) if err != nil { return nil, 0, err } @@ -81,7 +81,7 @@ func TestStateStore_Blocking_MinQuery(t *testing.T) { defer cancel() time.AfterFunc(5*time.Millisecond, func() { - state.UpsertJob(11, job) + state.UpsertNode(11, node) }) resp, idx, err := state.BlockingQuery(queryFn, 10, deadlineCtx) @@ -461,75 +461,6 @@ func TestStateStore_Deployments(t *testing.T) { } } -func TestStateStore_Deployments_Namespace(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - ns1 := "namespaced" - deploy1 := mock.Deployment() - deploy2 := mock.Deployment() - deploy1.Namespace = ns1 - deploy2.Namespace = ns1 - - ns2 := "new-namespace" - deploy3 := mock.Deployment() - deploy4 := mock.Deployment() - deploy3.Namespace = ns2 - deploy4.Namespace = ns2 - - // Create watchsets so we can test that update fires the watch - watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()} - _, err := state.DeploymentsByNamespace(watches[0], ns1) - assert.Nil(err) - _, err = state.DeploymentsByNamespace(watches[1], ns2) - assert.Nil(err) - - assert.Nil(state.UpsertDeployment(1001, deploy1)) - assert.Nil(state.UpsertDeployment(1002, deploy2)) - assert.Nil(state.UpsertDeployment(1003, deploy3)) - assert.Nil(state.UpsertDeployment(1004, deploy4)) - assert.True(watchFired(watches[0])) - assert.True(watchFired(watches[1])) - - ws := memdb.NewWatchSet() - iter1, err := state.DeploymentsByNamespace(ws, ns1) - assert.Nil(err) - iter2, err := state.DeploymentsByNamespace(ws, ns2) - assert.Nil(err) - - var out1 []*structs.Deployment - for { - raw := iter1.Next() - if raw == nil { - break - } - out1 = append(out1, raw.(*structs.Deployment)) - } - - var out2 []*structs.Deployment - for { - raw := iter2.Next() - if raw == nil { - break - } - out2 = append(out2, raw.(*structs.Deployment)) - } - - assert.Len(out1, 2) - assert.Len(out2, 2) - - for _, deploy := range out1 { - assert.Equal(ns1, deploy.Namespace) - } - for _, deploy := range out2 { - assert.Equal(ns2, deploy.Namespace) - } - - index, err := state.Index("deployment") - assert.Nil(err) - assert.EqualValues(1004, index) - assert.False(watchFired(ws)) -} - func TestStateStore_DeploymentsByIDPrefix(t *testing.T) { state := testStateStore(t) deploy := mock.Deployment() @@ -616,54 +547,6 @@ func TestStateStore_DeploymentsByIDPrefix(t *testing.T) { } } -func TestStateStore_DeploymentsByIDPrefix_Namespaces(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - deploy1 := mock.Deployment() - deploy1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2" - deploy2 := mock.Deployment() - deploy2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2" - sharedPrefix := "aabb" - - ns1, ns2 := "namespace1", "namespace2" - deploy1.Namespace = ns1 - deploy2.Namespace = ns2 - - assert.Nil(state.UpsertDeployment(1000, deploy1)) - assert.Nil(state.UpsertDeployment(1001, deploy2)) - - gatherDeploys := func(iter memdb.ResultIterator) []*structs.Deployment { - var deploys []*structs.Deployment - for { - raw := iter.Next() - if raw == nil { - break - } - deploy := raw.(*structs.Deployment) - deploys = append(deploys, deploy) - } - return deploys - } - - ws := memdb.NewWatchSet() - iter1, err := state.DeploymentsByIDPrefix(ws, ns1, sharedPrefix) - assert.Nil(err) - iter2, err := state.DeploymentsByIDPrefix(ws, ns2, sharedPrefix) - assert.Nil(err) - - deploysNs1 := gatherDeploys(iter1) - deploysNs2 := gatherDeploys(iter2) - assert.Len(deploysNs1, 1) - assert.Len(deploysNs2, 1) - - iter1, err = state.DeploymentsByIDPrefix(ws, ns1, deploy1.ID[:8]) - assert.Nil(err) - - deploysNs1 = gatherDeploys(iter1) - assert.Len(deploysNs1, 1) - assert.False(watchFired(ws)) -} - func TestStateStore_UpsertNode_Node(t *testing.T) { state := testStateStore(t) node := mock.Node() @@ -1277,6 +1160,21 @@ func TestStateStore_UpsertJob_NoEphemeralDisk(t *testing.T) { } } +func TestStateStore_UpsertJob_BadNamespace(t *testing.T) { + assert := assert.New(t) + state := testStateStore(t) + job := mock.Job() + job.Namespace = "foo" + + err := state.UpsertJob(1000, job) + assert.Contains(err.Error(), "non-existant namespace") + + ws := memdb.NewWatchSet() + out, err := state.JobByID(ws, job.Namespace, job.ID) + assert.Nil(err) + assert.Nil(out) +} + // Upsert a job that is the child of a parent job and ensures its summary gets // updated. func TestStateStore_UpsertJob_ChildJob(t *testing.T) { @@ -1635,75 +1533,6 @@ func TestStateStore_Jobs(t *testing.T) { } } -func TestStateStore_JobsByNamespace(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - ns1 := "new" - job1 := mock.Job() - job2 := mock.Job() - job1.Namespace = ns1 - job2.Namespace = ns1 - - ns2 := "new-namespace" - job3 := mock.Job() - job4 := mock.Job() - job3.Namespace = ns2 - job4.Namespace = ns2 - - // Create watchsets so we can test that update fires the watch - watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()} - _, err := state.JobsByNamespace(watches[0], ns1) - assert.Nil(err) - _, err = state.JobsByNamespace(watches[1], ns2) - assert.Nil(err) - - assert.Nil(state.UpsertJob(1001, job1)) - assert.Nil(state.UpsertJob(1002, job2)) - assert.Nil(state.UpsertJob(1003, job3)) - assert.Nil(state.UpsertJob(1004, job4)) - assert.True(watchFired(watches[0])) - assert.True(watchFired(watches[1])) - - ws := memdb.NewWatchSet() - iter1, err := state.JobsByNamespace(ws, ns1) - assert.Nil(err) - iter2, err := state.JobsByNamespace(ws, ns2) - assert.Nil(err) - - var out1 []*structs.Job - for { - raw := iter1.Next() - if raw == nil { - break - } - out1 = append(out1, raw.(*structs.Job)) - } - - var out2 []*structs.Job - for { - raw := iter2.Next() - if raw == nil { - break - } - out2 = append(out2, raw.(*structs.Job)) - } - - assert.Len(out1, 2) - assert.Len(out2, 2) - - for _, job := range out1 { - assert.Equal(ns1, job.Namespace) - } - for _, job := range out2 { - assert.Equal(ns2, job.Namespace) - } - - index, err := state.Index("jobs") - assert.Nil(err) - assert.EqualValues(1004, index) - assert.False(watchFired(ws)) -} - func TestStateStore_JobVersions(t *testing.T) { state := testStateStore(t) var jobs []*structs.Job @@ -1826,83 +1655,6 @@ func TestStateStore_JobsByIDPrefix(t *testing.T) { } } -func TestStateStore_JobsByIDPrefix_Namespaces(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - job1 := mock.Job() - job2 := mock.Job() - - jobID := "redis" - ns1, ns2 := "namespace1", "namespace2" - job1.ID = jobID - job2.ID = jobID - job1.Namespace = ns1 - job2.Namespace = ns2 - - assert.Nil(state.UpsertJob(1000, job1)) - assert.Nil(state.UpsertJob(1001, job2)) - - gatherJobs := func(iter memdb.ResultIterator) []*structs.Job { - var jobs []*structs.Job - for { - raw := iter.Next() - if raw == nil { - break - } - jobs = append(jobs, raw.(*structs.Job)) - } - return jobs - } - - // Try full match - ws := memdb.NewWatchSet() - iter1, err := state.JobsByIDPrefix(ws, ns1, jobID) - assert.Nil(err) - iter2, err := state.JobsByIDPrefix(ws, ns2, jobID) - assert.Nil(err) - - jobsNs1 := gatherJobs(iter1) - assert.Len(jobsNs1, 1) - - jobsNs2 := gatherJobs(iter2) - assert.Len(jobsNs2, 1) - - // Try prefix - iter1, err = state.JobsByIDPrefix(ws, ns1, "re") - assert.Nil(err) - iter2, err = state.JobsByIDPrefix(ws, ns2, "re") - assert.Nil(err) - - jobsNs1 = gatherJobs(iter1) - jobsNs2 = gatherJobs(iter2) - assert.Len(jobsNs1, 1) - assert.Len(jobsNs2, 1) - - job3 := mock.Job() - job3.ID = "riak" - job3.Namespace = ns1 - assert.Nil(state.UpsertJob(1003, job3)) - assert.True(watchFired(ws)) - - ws = memdb.NewWatchSet() - iter1, err = state.JobsByIDPrefix(ws, ns1, "r") - assert.Nil(err) - iter2, err = state.JobsByIDPrefix(ws, ns2, "r") - assert.Nil(err) - - jobsNs1 = gatherJobs(iter1) - jobsNs2 = gatherJobs(iter2) - assert.Len(jobsNs1, 2) - assert.Len(jobsNs2, 1) - - iter1, err = state.JobsByIDPrefix(ws, ns1, "ri") - assert.Nil(err) - - jobsNs1 = gatherJobs(iter1) - assert.Len(jobsNs1, 1) - assert.False(watchFired(ws)) -} - func TestStateStore_JobsByPeriodic(t *testing.T) { state := testStateStore(t) var periodic, nonPeriodic []*structs.Job @@ -2662,72 +2414,6 @@ func TestStateStore_UpsertEvals_Eval(t *testing.T) { } } -func TestStateStore_UpsertEvals_Namespace(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - ns1 := "namespaced" - eval1 := mock.Eval() - eval2 := mock.Eval() - eval1.Namespace = ns1 - eval2.Namespace = ns1 - - ns2 := "new-namespace" - eval3 := mock.Eval() - eval4 := mock.Eval() - eval3.Namespace = ns2 - eval4.Namespace = ns2 - - // Create watchsets so we can test that update fires the watch - watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()} - _, err := state.EvalsByNamespace(watches[0], ns1) - assert.Nil(err) - _, err = state.EvalsByNamespace(watches[1], ns2) - assert.Nil(err) - - assert.Nil(state.UpsertEvals(1001, []*structs.Evaluation{eval1, eval2, eval3, eval4})) - assert.True(watchFired(watches[0])) - assert.True(watchFired(watches[1])) - - ws := memdb.NewWatchSet() - iter1, err := state.EvalsByNamespace(ws, ns1) - assert.Nil(err) - iter2, err := state.EvalsByNamespace(ws, ns2) - assert.Nil(err) - - var out1 []*structs.Evaluation - for { - raw := iter1.Next() - if raw == nil { - break - } - out1 = append(out1, raw.(*structs.Evaluation)) - } - - var out2 []*structs.Evaluation - for { - raw := iter2.Next() - if raw == nil { - break - } - out2 = append(out2, raw.(*structs.Evaluation)) - } - - assert.Len(out1, 2) - assert.Len(out2, 2) - - for _, eval := range out1 { - assert.Equal(ns1, eval.Namespace) - } - for _, eval := range out2 { - assert.Equal(ns2, eval.Namespace) - } - - index, err := state.Index("evals") - assert.Nil(err) - assert.EqualValues(1001, index) - assert.False(watchFired(ws)) -} - func TestStateStore_UpsertEvals_CancelBlocked(t *testing.T) { state := testStateStore(t) @@ -3307,52 +2993,6 @@ func TestStateStore_EvalsByIDPrefix(t *testing.T) { } } -func TestStateStore_EvalsByIDPrefix_Namespaces(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - eval1 := mock.Eval() - eval1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2" - eval2 := mock.Eval() - eval2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2" - sharedPrefix := "aabb" - - ns1, ns2 := "namespace1", "namespace2" - eval1.Namespace = ns1 - eval2.Namespace = ns2 - - assert.Nil(state.UpsertEvals(1000, []*structs.Evaluation{eval1, eval2})) - - gatherEvals := func(iter memdb.ResultIterator) []*structs.Evaluation { - var evals []*structs.Evaluation - for { - raw := iter.Next() - if raw == nil { - break - } - evals = append(evals, raw.(*structs.Evaluation)) - } - return evals - } - - ws := memdb.NewWatchSet() - iter1, err := state.EvalsByIDPrefix(ws, ns1, sharedPrefix) - assert.Nil(err) - iter2, err := state.EvalsByIDPrefix(ws, ns2, sharedPrefix) - assert.Nil(err) - - evalsNs1 := gatherEvals(iter1) - evalsNs2 := gatherEvals(iter2) - assert.Len(evalsNs1, 1) - assert.Len(evalsNs2, 1) - - iter1, err = state.EvalsByIDPrefix(ws, ns1, eval1.ID[:8]) - assert.Nil(err) - - evalsNs1 = gatherEvals(iter1) - assert.Len(evalsNs1, 1) - assert.False(watchFired(ws)) -} - func TestStateStore_RestoreEval(t *testing.T) { state := testStateStore(t) eval := mock.Eval() @@ -3742,79 +3382,6 @@ func TestStateStore_UpsertAlloc_Alloc(t *testing.T) { } } -func TestStateStore_UpsertAlloc_AllocsByNamespace(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - ns1 := "namespaced" - alloc1 := mock.Alloc() - alloc2 := mock.Alloc() - alloc1.Namespace = ns1 - alloc1.Job.Namespace = ns1 - alloc2.Namespace = ns1 - alloc2.Job.Namespace = ns1 - - ns2 := "new-namespace" - alloc3 := mock.Alloc() - alloc4 := mock.Alloc() - alloc3.Namespace = ns2 - alloc3.Job.Namespace = ns2 - alloc4.Namespace = ns2 - alloc4.Job.Namespace = ns2 - - assert.Nil(state.UpsertJob(999, alloc1.Job)) - assert.Nil(state.UpsertJob(1000, alloc3.Job)) - - // Create watchsets so we can test that update fires the watch - watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()} - _, err := state.AllocsByNamespace(watches[0], ns1) - assert.Nil(err) - _, err = state.AllocsByNamespace(watches[1], ns2) - assert.Nil(err) - - assert.Nil(state.UpsertAllocs(1001, []*structs.Allocation{alloc1, alloc2, alloc3, alloc4})) - assert.True(watchFired(watches[0])) - assert.True(watchFired(watches[1])) - - ws := memdb.NewWatchSet() - iter1, err := state.AllocsByNamespace(ws, ns1) - assert.Nil(err) - iter2, err := state.AllocsByNamespace(ws, ns2) - assert.Nil(err) - - var out1 []*structs.Allocation - for { - raw := iter1.Next() - if raw == nil { - break - } - out1 = append(out1, raw.(*structs.Allocation)) - } - - var out2 []*structs.Allocation - for { - raw := iter2.Next() - if raw == nil { - break - } - out2 = append(out2, raw.(*structs.Allocation)) - } - - assert.Len(out1, 2) - assert.Len(out2, 2) - - for _, alloc := range out1 { - assert.Equal(ns1, alloc.Namespace) - } - for _, alloc := range out2 { - assert.Equal(ns2, alloc.Namespace) - } - - index, err := state.Index("allocs") - assert.Nil(err) - assert.EqualValues(1001, index) - assert.False(watchFired(ws)) -} - func TestStateStore_UpsertAlloc_Deployment(t *testing.T) { state := testStateStore(t) deployment := mock.Deployment() @@ -4707,53 +4274,6 @@ func TestStateStore_AllocsByIDPrefix(t *testing.T) { } } -func TestStateStore_AllocsByIDPrefix_Namespaces(t *testing.T) { - assert := assert.New(t) - state := testStateStore(t) - alloc1 := mock.Alloc() - alloc1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2" - alloc2 := mock.Alloc() - alloc2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2" - sharedPrefix := "aabb" - - ns1, ns2 := "namespace1", "namespace2" - alloc1.Namespace = ns1 - alloc2.Namespace = ns2 - - assert.Nil(state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2})) - - gatherAllocs := func(iter memdb.ResultIterator) []*structs.Allocation { - var allocs []*structs.Allocation - for { - raw := iter.Next() - if raw == nil { - break - } - alloc := raw.(*structs.Allocation) - allocs = append(allocs, alloc) - } - return allocs - } - - ws := memdb.NewWatchSet() - iter1, err := state.AllocsByIDPrefix(ws, ns1, sharedPrefix) - assert.Nil(err) - iter2, err := state.AllocsByIDPrefix(ws, ns2, sharedPrefix) - assert.Nil(err) - - allocsNs1 := gatherAllocs(iter1) - allocsNs2 := gatherAllocs(iter2) - assert.Len(allocsNs1, 1) - assert.Len(allocsNs2, 1) - - iter1, err = state.AllocsByIDPrefix(ws, ns1, alloc1.ID[:8]) - assert.Nil(err) - - allocsNs1 = gatherAllocs(iter1) - assert.Len(allocsNs1, 1) - assert.False(watchFired(ws)) -} - func TestStateStore_Allocs(t *testing.T) { state := testStateStore(t) var allocs []*structs.Allocation diff --git a/nomad/structs/config/sentinel.go b/nomad/structs/config/sentinel.go new file mode 100644 index 000000000..c5ea34afb --- /dev/null +++ b/nomad/structs/config/sentinel.go @@ -0,0 +1,23 @@ +package config + +// SentinelConfig is configuration specific to Sentinel +type SentinelConfig struct { + // Imports are the configured imports + Imports []*SentinelImport `hcl:"import,expand"` +} + +// SentinelImport is used per configured import +type SentinelImport struct { + Name string `hcl:",key"` + Path string `hcl:"path"` + Args []string `hcl:"args"` +} + +// Merge is used to merge two Sentinel configs together. The settings from the input always take precedence. +func (a *SentinelConfig) Merge(b *SentinelConfig) *SentinelConfig { + result := *a + if len(b.Imports) > 0 { + result.Imports = append(result.Imports, b.Imports...) + } + return &result +} diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index c4a66dd9b..1ca19e35d 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -344,6 +344,9 @@ type JobRegisterRequest struct { EnforceIndex bool JobModifyIndex uint64 + // PolicyOverride is set when the user is attempting to override any policies + PolicyOverride bool + WriteRequest } @@ -383,6 +386,8 @@ type JobListRequest struct { type JobPlanRequest struct { Job *Job Diff bool // Toggles an annotated diff + // PolicyOverride is set when the user is attempting to override any policies + PolicyOverride bool WriteRequest } diff --git a/scripts/vagrant-linux-priv-ui.sh b/scripts/vagrant-linux-priv-ui.sh new file mode 100755 index 000000000..3bcabc4fb --- /dev/null +++ b/scripts/vagrant-linux-priv-ui.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Install NVM for simple node.js version management +wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash + +# This enables NVM without a logout/login +export NVM_DIR="/home/vagrant/.nvm" +# shellcheck source=/dev/null +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm + +# Install Node, Ember CLI, and Phantom for UI development +nvm install 6.11.0 +nvm alias default 6.11.0 +npm install -g ember-cli phantomjs-prebuilt + +# Install Yarn for front-end dependency management +curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.24.6 +export PATH="$HOME/.yarn/bin:\$PATH" diff --git a/ui/.editorconfig b/ui/.editorconfig new file mode 100644 index 000000000..219985c22 --- /dev/null +++ b/ui/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/ui/.ember-cli b/ui/.ember-cli new file mode 100644 index 000000000..dec208fd9 --- /dev/null +++ b/ui/.ember-cli @@ -0,0 +1,10 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": false, + "proxy": "http://127.0.0.1:4646" +} diff --git a/ui/.eslintignore b/ui/.eslintignore new file mode 100644 index 000000000..2d1320508 --- /dev/null +++ b/ui/.eslintignore @@ -0,0 +1 @@ +mirage/ diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js new file mode 100644 index 000000000..7c2ebd1c4 --- /dev/null +++ b/ui/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + globals: { + server: true, + }, + env: { + browser: true, + es6: true, + }, + extends: 'eslint:recommended', + parserOptions: { + ecmaVersion: 2017, + sourceType: 'module', + }, + rules: { + indent: ['error', 2, { SwitchCase: 1 }], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single', 'avoid-escape'], + semi: ['error', 'always'], + }, +}; diff --git a/ui/.travis.yml b/ui/.travis.yml new file mode 100644 index 000000000..385f003d4 --- /dev/null +++ b/ui/.travis.yml @@ -0,0 +1,21 @@ +--- +language: node_js +node_js: + - "6" + +sudo: false + +cache: + directories: + - $HOME/.npm + +before_install: + - npm config set spin false + - npm install -g phantomjs-prebuilt + - phantomjs --version + +install: + - npm install + +script: + - npm test diff --git a/ui/.watchmanconfig b/ui/.watchmanconfig new file mode 100644 index 000000000..e7834e3e4 --- /dev/null +++ b/ui/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..813f63d6f --- /dev/null +++ b/ui/README.md @@ -0,0 +1,73 @@ +# Nomad UI + +The official Nomad UI. + +## Prerequisites + +This is an [ember.js](https://emberjs.com/) project, and you will need the following tools installed on your computer. + +* [Node.js](https://nodejs.org/) +* [Yarn](https://yarnpkg.com) +* [Ember CLI](https://ember-cli.com/) +* [PhantomJS](http://phantomjs.org/) (for running tests) + +## Installation + +The Nomad UI gets cloned along with the rest of Nomad. To install dependencies, do the following from the root of the Nomad project: + +``` +$ cd ui +$ yarn +``` + +## Running / Development + +First, make sure nomad is running. The UI, in development mode, runs independently from Nomad, so this could be an official release or a dev branch. Likewise, Nomad can be running in server mode or dev mode. As long as the API is accessible, the UI will work as expected. + +* `ember serve` +* Visit your app at [http://localhost:4200](http://localhost:4200). + +## Running / Development with Vagrant + +All necessary tools for UI development are installed as part of the Vagrantfile. This is primarily to make it easy to build the UI from source while working on Nomad. Due to the filesystem requirements of [Broccoli](http://broccolijs.com/) (which powers Ember CLI), it is strongly discouraged to use Vagrant for developing changes to the UI. + +That said, development with Vagrant is still possible, but the `ember serve` command requires two modifications: + +* `--watch polling`: This allows the vm to notice file changes made in the host environment. +* `--port 4201`: The default port 4200 is not forwarded, since local development is recommended. + +This makes the full command for running the UI in development mode in Vagrant: + +``` +$ ember serve --watch polling --port 4201 +``` + +### Running Tests + +Nomad UI tests can be run independently of Nomad golang tests. + +* `ember test` (single run, headless browser) +* `ember test --server` (watches for changes, runs in a full browser) + +### Building + +Typically `make release` or `make dev-ui` will be the desired build workflow, but in the event that build artifacts need to be inspected, `ember build` will output compiled files in `ui/dist`. + +* `ember build` (development) +* `ember build --environment production` (production) + +### Releasing + +Nomad UI releases are in lockstep with Nomad releases and are integrated into the `make release` toolchain. + +### Troubleshooting + +#### The UI is running, but none of the API requests are working + +By default (according to the `.embercli` file) a proxy address of `http://localhost:4646` is used. If you are running Nomad at a different address, you will need to override this setting when running ember serve: `ember serve --proxy http://newlocation:1111`. + +#### Nomad is running in Vagrant, but I can't access the API from my host machine + +Nomad binds to `127.0.0.1:4646` by default, which is the loopback address. Try running nomad bound to `0.0.0.0`: `bin/nomad -bind 0.0.0.0`. + +Ports also need to be forwarded in the Vagrantfile. 4646 is already forwarded, but if a port other than the default is being used, that port needs to be added to the Vagrantfile and `vagrant reload` needs to be run. diff --git a/ui/app/adapters/agent.js b/ui/app/adapters/agent.js new file mode 100644 index 000000000..3e68a1d35 --- /dev/null +++ b/ui/app/adapters/agent.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + pathForType: () => 'agent/members', + urlForFindRecord() { + const [, ...args] = arguments; + return this.urlForFindAll(...args); + }, +}); diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js new file mode 100644 index 000000000..dac69f286 --- /dev/null +++ b/ui/app/adapters/application.js @@ -0,0 +1,58 @@ +import Ember from 'ember'; +import RESTAdapter from 'ember-data/adapters/rest'; + +const { get, computed, inject } = Ember; + +export const namespace = 'v1'; + +export default RESTAdapter.extend({ + namespace, + + token: inject.service(), + + headers: computed('token.secret', function() { + const token = this.get('token.secret'); + return ( + token && { + 'X-Nomad-Token': token, + } + ); + }), + + // Single record requests deviate from REST practice by using + // the singular form of the resource name. + // + // REST: /some-resources/:id + // Nomad: /some-resource/:id + // + // This is the original implementation of _buildURL + // without the pluralization of modelName + urlForFindRecord(id, modelName) { + let path; + let url = []; + let host = get(this, 'host'); + let prefix = this.urlPrefix(); + + if (modelName) { + path = modelName.camelize(); + if (path) { + url.push(path); + } + } + + if (id) { + url.push(encodeURIComponent(id)); + } + + if (prefix) { + url.unshift(prefix); + } + + url = url.join('/'); + if (!host && url && url.charAt(0) !== '/') { + url = '/' + url; + } + + return url; + }, +}); diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js new file mode 100644 index 000000000..c51690a05 --- /dev/null +++ b/ui/app/adapters/job.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import ApplicationAdapter from './application'; + +const { RSVP } = Ember; + +export default ApplicationAdapter.extend({ + findRecord(store, { modelName }, id, snapshot) { + // To make a findRecord response reflect the findMany response, the JobSummary + // from /summary needs to be stitched into the response. + return RSVP.hash({ + job: this._super(...arguments), + summary: this.ajax(`${this.buildURL(modelName, id, snapshot, 'findRecord')}/summary`), + }).then(({ job, summary }) => { + job.JobSummary = summary; + return job; + }); + }, + + findAllocations(job) { + const url = `${this.buildURL('job', job.get('id'), job, 'findRecord')}/allocations`; + return this.ajax(url, 'GET').then(allocs => { + return this.store.pushPayload('allocation', { + allocations: allocs, + }); + }); + }, + + fetchRawDefinition(job) { + const url = this.buildURL('job', job.get('id'), job, 'findRecord'); + return this.ajax(url, 'GET'); + }, +}); diff --git a/ui/app/adapters/node.js b/ui/app/adapters/node.js new file mode 100644 index 000000000..349e44b82 --- /dev/null +++ b/ui/app/adapters/node.js @@ -0,0 +1,12 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + findAllocations(node) { + const url = `${this.buildURL('node', node.get('id'), node, 'findRecord')}/allocations`; + return this.ajax(url, 'GET').then(allocs => { + return this.store.pushPayload('allocation', { + allocations: allocs, + }); + }); + }, +}); diff --git a/ui/app/app.js b/ui/app/app.js new file mode 100644 index 000000000..831ad6106 --- /dev/null +++ b/ui/app/app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +let App; + +Ember.MODEL_FACTORY_INJECTIONS = true; + +App = Ember.Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/ui/app/components/.gitkeep b/ui/app/components/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js new file mode 100644 index 000000000..c6bf62788 --- /dev/null +++ b/ui/app/components/allocation-row.js @@ -0,0 +1,20 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'tr', + + classNames: ['allocation-row', 'is-interactive'], + + allocation: null, + + // Used to determine whether the row should mention the node or the job + context: null, + + onClick() {}, + + click(event) { + this.get('onClick')(event); + }, +}); diff --git a/ui/app/components/allocation-status-bar.js b/ui/app/components/allocation-status-bar.js new file mode 100644 index 000000000..0a3734626 --- /dev/null +++ b/ui/app/components/allocation-status-bar.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import DistributionBar from './distribution-bar'; + +const { computed } = Ember; + +export default DistributionBar.extend({ + layoutName: 'components/distribution-bar', + + allocationContainer: null, + + data: computed( + 'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}', + function() { + if (!this.get('allocationContainer')) { + return []; + } + + const allocs = this.get('allocationContainer').getProperties( + 'queuedAllocs', + 'completeAllocs', + 'failedAllocs', + 'runningAllocs', + 'startingAllocs', + 'lostAllocs' + ); + return [ + { label: 'Queued', value: allocs.queuedAllocs, className: 'queued' }, + { + label: 'Starting', + value: allocs.startingAllocs, + className: 'starting', + layers: 2, + }, + { label: 'Running', value: allocs.runningAllocs, className: 'running' }, + { + label: 'Complete', + value: allocs.completeAllocs, + className: 'complete', + }, + { label: 'Failed', value: allocs.failedAllocs, className: 'failed' }, + { label: 'Lost', value: allocs.lostAllocs, className: 'lost' }, + ]; + } + ), +}); diff --git a/ui/app/components/attributes-section.js b/ui/app/components/attributes-section.js new file mode 100644 index 000000000..f2503c9fc --- /dev/null +++ b/ui/app/components/attributes-section.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/client-node-row.js b/ui/app/components/client-node-row.js new file mode 100644 index 000000000..ed4c47dfc --- /dev/null +++ b/ui/app/components/client-node-row.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'tr', + classNames: ['client-node-row', 'is-interactive'], + + node: null, + + onClick() {}, + + click(event) { + this.get('onClick')(event); + }, + + didReceiveAttrs() { + // Reload the node in order to get detail information + const node = this.get('node'); + if (node) { + node.reload(); + } + }, +}); diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js new file mode 100644 index 000000000..590bea2ae --- /dev/null +++ b/ui/app/components/distribution-bar.js @@ -0,0 +1,141 @@ +import Ember from 'ember'; +import d3 from 'npm:d3-selection'; +import 'npm:d3-transition'; +import styleStringProperty from '../utils/properties/style-string'; + +const { Component, computed, run, assign, guidFor } = Ember; +const sumAggregate = (total, val) => total + val; + +export default Component.extend({ + classNames: ['chart', 'distribution-bar'], + classNameBindings: ['isNarrow:is-narrow'], + + chart: null, + data: null, + activeDatum: null, + isNarrow: false, + + tooltipStyle: styleStringProperty('tooltipPosition'), + maskId: null, + + _data: computed('data', function() { + const data = this.get('data'); + const sum = data.mapBy('value').reduce(sumAggregate, 0); + + return data.map(({ label, value, className, layers }, index) => ({ + label, + value, + className, + layers, + index, + percent: value / sum, + offset: + data + .slice(0, index) + .mapBy('value') + .reduce(sumAggregate, 0) / sum, + })); + }), + + didInsertElement() { + const chart = d3.select(this.$('svg')[0]); + const maskId = `dist-mask-${guidFor(this)}`; + this.setProperties({ chart, maskId }); + + this.$('svg clipPath').attr('id', maskId); + + chart.on('mouseleave', () => { + run(() => { + this.set('isActive', false); + this.set('activeDatum', null); + chart + .selectAll('g') + .classed('active', false) + .classed('inactive', false); + }); + }); + + this.renderChart(); + }, + + didUpdateAttrs() { + this.renderChart(); + }, + + // prettier-ignore + /* eslint-disable */ + renderChart() { + const { chart, _data, isNarrow } = this.getProperties('chart', '_data', 'isNarrow'); + const width = this.$('svg').width(); + const filteredData = _data.filter(d => d.value > 0); + + let slices = chart.select('.bars').selectAll('g').data(filteredData); + let sliceCount = filteredData.length; + + slices.exit().remove(); + + let slicesEnter = slices.enter() + .append('g') + .on('mouseenter', d => { + run(() => { + const slice = slices.filter(datum => datum === d); + slices.classed('active', false).classed('inactive', true); + slice.classed('active', true).classed('inactive', false); + this.set('activeDatum', d); + + const box = slice.node().getBBox(); + const pos = box.x + box.width / 2; + + // Ensure that the position is set before the tooltip is visible + run.schedule('afterRender', this, () => this.set('isActive', true)); + this.set('tooltipPosition', { + left: pos, + }); + }); + }); + + slices = slices.merge(slicesEnter); + slices.attr('class', d => d.className || `slice-${filteredData.indexOf(d)}`); + + const setWidth = d => `${width * d.percent - (d.index === sliceCount - 1 || d.index === 0 ? 1 : 2)}px` + const setOffset = d => `${width * d.offset + (d.index === 0 ? 0 : 1)}px` + + let hoverTargets = slices.selectAll('.target').data(d => [d]); + hoverTargets.enter() + .append('rect') + .attr('class', 'target') + .attr('width', setWidth) + .attr('height', '100%') + .attr('x', setOffset) + .merge(hoverTargets) + .transition() + .duration(200) + .attr('width', setWidth) + .attr('x', setOffset) + + + let layers = slices.selectAll('.bar').data((d, i) => { + return new Array(d.layers || 1).fill(assign({ index: i }, d)); + }); + layers.enter() + .append('rect') + .attr('width', setWidth) + .attr('x', setOffset) + .attr('y', () => isNarrow ? '50%' : 0) + .attr('clip-path', `url(#${this.get('maskId')})`) + .attr('height', () => isNarrow ? '6px' : '100%') + .merge(layers) + .attr('class', (d, i) => `bar layer-${i}`) + .transition() + .duration(200) + .attr('width', setWidth) + .attr('x', setOffset) + + if (isNarrow) { + d3.select(this.get('element')).select('.mask') + .attr('height', '6px') + .attr('y', '50%'); + } + }, + /* eslint-enable */ +}); diff --git a/ui/app/components/job-deployment.js b/ui/app/components/job-deployment.js new file mode 100644 index 000000000..1a2476be8 --- /dev/null +++ b/ui/app/components/job-deployment.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + classNames: ['job-deployment', 'boxed-section'], + + deployment: null, + isOpen: false, +}); diff --git a/ui/app/components/job-deployment/deployment-metrics.js b/ui/app/components/job-deployment/deployment-metrics.js new file mode 100644 index 000000000..f2503c9fc --- /dev/null +++ b/ui/app/components/job-deployment/deployment-metrics.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/job-deployments-stream.js b/ui/app/components/job-deployments-stream.js new file mode 100644 index 000000000..95bd4f198 --- /dev/null +++ b/ui/app/components/job-deployments-stream.js @@ -0,0 +1,43 @@ +import Ember from 'ember'; +import moment from 'moment'; + +const { Component, computed } = Ember; + +export default Component.extend({ + tagName: 'ol', + classNames: ['timeline'], + + deployments: computed(() => []), + + sortedDeployments: computed('deployments.@each.version.submitTime', function() { + return this.get('deployments') + .sortBy('version.submitTime') + .reverse(); + }), + + annotatedDeployments: computed('sortedDeployments.@each.version', function() { + const deployments = this.get('sortedDeployments'); + return deployments.map((deployment, index) => { + const meta = {}; + + if (index === 0) { + meta.showDate = true; + } else { + const previousDeployment = deployments.objectAt(index - 1); + const previousSubmitTime = previousDeployment.get('version.submitTime'); + const submitTime = deployment.get('submitTime'); + if ( + submitTime && + previousSubmitTime && + moment(previousSubmitTime) + .startOf('day') + .diff(moment(submitTime).startOf('day'), 'days') > 0 + ) { + meta.showDate = true; + } + } + + return { deployment, meta }; + }); + }), +}); diff --git a/ui/app/components/job-diff-fields-and-objects.js b/ui/app/components/job-diff-fields-and-objects.js new file mode 100644 index 000000000..f2503c9fc --- /dev/null +++ b/ui/app/components/job-diff-fields-and-objects.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/job-diff.js b/ui/app/components/job-diff.js new file mode 100644 index 000000000..2bba7310e --- /dev/null +++ b/ui/app/components/job-diff.js @@ -0,0 +1,16 @@ +import Ember from 'ember'; + +const { Component, computed } = Ember; + +export default Component.extend({ + classNames: ['job-diff'], + classNameBindings: ['isEdited:is-edited', 'isAdded:is-added', 'isDeleted:is-deleted'], + + diff: null, + + verbose: true, + + isEdited: computed.equal('diff.Type', 'Edited'), + isAdded: computed.equal('diff.Type', 'Added'), + isDeleted: computed.equal('diff.Type', 'Deleted'), +}); diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js new file mode 100644 index 000000000..1ac98b43d --- /dev/null +++ b/ui/app/components/job-row.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'tr', + classNames: ['job-row', 'is-interactive'], + + job: null, + + onClick() {}, + + click(event) { + this.get('onClick')(event); + }, + + didReceiveAttrs() { + // Reload the job in order to get detail information + const job = this.get('job'); + if (job) { + job.reload(); + } + }, +}); diff --git a/ui/app/components/job-version.js b/ui/app/components/job-version.js new file mode 100644 index 000000000..0ffe77803 --- /dev/null +++ b/ui/app/components/job-version.js @@ -0,0 +1,54 @@ +import Ember from 'ember'; + +const { Component, computed } = Ember; + +const changeTypes = ['Added', 'Deleted', 'Edited']; + +export default Component.extend({ + classNames: ['job-version', 'boxed-section'], + + version: null, + isOpen: false, + + // Passes through to the job-diff component + verbose: true, + + changeCount: computed('version.diff', function() { + const diff = this.get('version.diff'); + const taskGroups = diff.TaskGroups || []; + + if (!diff) { + return 0; + } + + return ( + fieldChanges(diff) + + taskGroups.reduce(arrayOfFieldChanges, 0) + + (taskGroups.mapBy('Tasks') || []).reduce(flatten, []).reduce(arrayOfFieldChanges, 0) + ); + }), + + actions: { + toggleDiff() { + this.toggleProperty('isOpen'); + }, + }, +}); + +const flatten = (accumulator, array) => accumulator.concat(array); +const countChanges = (total, field) => (changeTypes.includes(field.Type) ? total + 1 : total); + +function fieldChanges(diff) { + return ( + (diff.Fields || []).reduce(countChanges, 0) + + (diff.Objects || []).reduce(arrayOfFieldChanges, 0) + ); +} + +function arrayOfFieldChanges(count, diff) { + if (!diff) { + return count; + } + + return count + fieldChanges(diff); +} diff --git a/ui/app/components/job-versions-stream.js b/ui/app/components/job-versions-stream.js new file mode 100644 index 000000000..4a285de2b --- /dev/null +++ b/ui/app/components/job-versions-stream.js @@ -0,0 +1,36 @@ +import Ember from 'ember'; +import moment from 'moment'; + +const { Component, computed } = Ember; + +export default Component.extend({ + tagName: 'ol', + classNames: ['timeline'], + + versions: computed(() => []), + + // Passes through to the job-diff component + verbose: true, + + annotatedVersions: computed('versions.[]', function() { + const versions = this.get('versions'); + return versions.map((version, index) => { + const meta = {}; + + if (index === 0) { + meta.showDate = true; + } else { + const previousVersion = versions.objectAt(index - 1); + if ( + moment(previousVersion.get('submitTime')) + .startOf('day') + .diff(moment(version.get('submitTime')).startOf('day'), 'days') > 0 + ) { + meta.showDate = true; + } + } + + return { version, meta }; + }); + }), +}); diff --git a/ui/app/components/json-viewer.js b/ui/app/components/json-viewer.js new file mode 100644 index 000000000..e871515c8 --- /dev/null +++ b/ui/app/components/json-viewer.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; +import JSONFormatterPkg from 'npm:json-formatter-js'; + +const { Component, computed, run } = Ember; + +// json-formatter-js is packaged in a funny way that ember-cli-browserify +// doesn't unwrap properly. +const { default: JSONFormatter } = JSONFormatterPkg; + +export default Component.extend({ + classNames: ['json-viewer'], + + json: null, + expandDepth: 2, + + formatter: computed('json', 'expandDepth', function() { + return new JSONFormatter(this.get('json'), this.get('expandDepth'), { + theme: 'nomad', + }); + }), + + didReceiveAttrs() { + const json = this.get('json'); + if (!json) { + return; + } + + run.scheduleOnce('afterRender', this, embedViewer); + }, +}); + +function embedViewer() { + this.$().empty().append(this.get('formatter').render()); +} diff --git a/ui/app/components/list-pagination.js b/ui/app/components/list-pagination.js new file mode 100644 index 000000000..c18301d4a --- /dev/null +++ b/ui/app/components/list-pagination.js @@ -0,0 +1,44 @@ +import Ember from 'ember'; + +const { Component, computed } = Ember; + +export default Component.extend({ + source: computed(() => []), + size: 25, + page: 1, + spread: 2, + + startsAt: computed('size', 'page', function() { + return (this.get('page') - 1) * this.get('size') + 1; + }), + + endsAt: computed('source.[]', 'size', 'page', function() { + return Math.min(this.get('page') * this.get('size'), this.get('source.length')); + }), + + lastPage: computed('source.[]', 'size', function() { + return Math.ceil(this.get('source.length') / this.get('size')); + }), + + pageLinks: computed('source.[]', 'page', 'spread', function() { + const { spread, page, lastPage } = this.getProperties('spread', 'page', 'lastPage'); + + // When there is only one page, don't bother with page links + if (lastPage === 1) { + return []; + } + + const lowerBound = Math.max(1, page - spread); + const upperBound = Math.min(lastPage, page + spread) + 1; + + return Array(upperBound - lowerBound).fill(null).map((_, index) => ({ + pageNumber: lowerBound + index, + })); + }), + + list: computed('source.[]', 'page', 'size', function() { + const size = this.get('size'); + const start = (this.get('page') - 1) * size; + return this.get('source').slice(start, start + size); + }), +}); diff --git a/ui/app/components/list-pagination/list-pager.js b/ui/app/components/list-pagination/list-pager.js new file mode 100644 index 000000000..f2503c9fc --- /dev/null +++ b/ui/app/components/list-pagination/list-pager.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui/app/components/list-table.js b/ui/app/components/list-table.js new file mode 100644 index 000000000..39cf15fe6 --- /dev/null +++ b/ui/app/components/list-table.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +const { Component, computed } = Ember; + +export default Component.extend({ + tagName: 'table', + classNames: ['table'], + + source: computed(() => []), + + // Plan for a future with metadata (e.g., isSelected) + decoratedSource: computed('source.[]', function() { + return this.get('source').map(row => ({ + model: row, + })); + }), +}); diff --git a/ui/app/components/list-table/sort-by.js b/ui/app/components/list-table/sort-by.js new file mode 100644 index 000000000..eac7deae0 --- /dev/null +++ b/ui/app/components/list-table/sort-by.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +const { Component, computed } = Ember; + +export default Component.extend({ + tagName: 'th', + + // The prop that the table is currently sorted by + currentProp: '', + + // The prop this sorter controls + prop: '', + + classNames: ['is-selectable'], + classNameBindings: ['isActive:is-active', 'sortDescending:desc:asc'], + + isActive: computed('currentProp', 'prop', function() { + return this.get('currentProp') === this.get('prop'); + }), + + shouldSortDescending: computed('sortDescending', 'isActive', function() { + return !this.get('isActive') || !this.get('sortDescending'); + }), +}); diff --git a/ui/app/components/list-table/table-body.js b/ui/app/components/list-table/table-body.js new file mode 100644 index 000000000..4c756a699 --- /dev/null +++ b/ui/app/components/list-table/table-body.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'tbody', +}); diff --git a/ui/app/components/list-table/table-head.js b/ui/app/components/list-table/table-head.js new file mode 100644 index 000000000..c1f0c7825 --- /dev/null +++ b/ui/app/components/list-table/table-head.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'thead', +}); diff --git a/ui/app/components/search-box.js b/ui/app/components/search-box.js new file mode 100644 index 000000000..8efb5dd5e --- /dev/null +++ b/ui/app/components/search-box.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; + +const { Component, computed, run } = Ember; + +export default Component.extend({ + // Passed to the component (mutable) + searchTerm: null, + + // Used as a debounce buffer + _searchTerm: computed.reads('searchTerm'), + + // Used to throttle sets to searchTerm + debounce: 150, + + classNames: ['field', 'has-addons'], + + actions: { + setSearchTerm(e) { + this.set('_searchTerm', e.target.value); + run.debounce(this, updateSearch, this.get('debounce')); + }, + }, +}); + +function updateSearch() { + this.set('searchTerm', this.get('_searchTerm')); +} diff --git a/ui/app/components/server-agent-row.js b/ui/app/components/server-agent-row.js new file mode 100644 index 000000000..5fbb6832f --- /dev/null +++ b/ui/app/components/server-agent-row.js @@ -0,0 +1,33 @@ +import Ember from 'ember'; + +const { Component, inject, computed } = Ember; + +export default Component.extend({ + // TODO Switch back to the router service style when it is no longer feature-flagged + // router: inject.service('router'), + _router: inject.service('-routing'), + router: computed.alias('_router.router'), + + tagName: 'tr', + classNames: ['server-agent-row', 'is-interactive'], + classNameBindings: ['isActive:is-active'], + + agent: null, + isActive: computed('agent', 'router.currentURL', function() { + // TODO Switch back to the router service style when it is no longer feature-flagged + // const targetURL = this.get('router').urlFor('servers.server', this.get('agent')); + // const currentURL = `${this.get('router.rootURL').slice(0, -1)}${this.get('router.currentURL')}`; + + const router = this.get('router'); + const targetURL = router.generate('servers.server', this.get('agent')); + const currentURL = `${router.get('rootURL').slice(0, -1)}${router + .get('currentURL') + .split('?')[0]}`; + + return currentURL === targetURL; + }), + + click() { + this.get('router').transitionTo('servers.server', this.get('agent')); + }, +}); diff --git a/ui/app/components/task-group-row.js b/ui/app/components/task-group-row.js new file mode 100644 index 000000000..e23541e9c --- /dev/null +++ b/ui/app/components/task-group-row.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + tagName: 'tr', + + classNames: ['task-group-row', 'is-interactive'], + + taskGroup: null, + + onClick() {}, + + click(event) { + this.get('onClick')(event); + }, +}); diff --git a/ui/app/controllers/.gitkeep b/ui/app/controllers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js new file mode 100644 index 000000000..5a764f2a6 --- /dev/null +++ b/ui/app/controllers/allocations/allocation.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; + +const { Controller, computed } = Ember; + +export default Controller.extend(Sortable, { + queryParams: { + sortProperty: 'sort', + sortDescending: 'desc', + }, + + sortProperty: 'name', + sortDescending: false, + + listToSort: computed.alias('model.states'), + sortedStates: computed.alias('listSorted'), +}); diff --git a/ui/app/controllers/freestyle.js b/ui/app/controllers/freestyle.js new file mode 100644 index 000000000..71e5bde38 --- /dev/null +++ b/ui/app/controllers/freestyle.js @@ -0,0 +1,47 @@ +import Ember from 'ember'; +import FreestyleController from 'ember-freestyle/controllers/freestyle'; + +const { inject, computed } = Ember; + +export default FreestyleController.extend({ + emberFreestyle: inject.service(), + + timerTicks: 0, + + startTimer: function() { + this.set( + 'timer', + setInterval(() => { + this.incrementProperty('timerTicks'); + }, 500) + ); + }.on('init'), + + stopTimer: function() { + clearInterval(this.get('timer')); + }.on('willDestroy'), + + distributionBarData: computed(() => { + return [ + { label: 'one', value: 10 }, + { label: 'two', value: 20 }, + { label: 'three', value: 30 }, + ]; + }), + + distributionBarDataWithClasses: computed(() => { + return [ + { label: 'Queued', value: 10, className: 'queued' }, + { label: 'Complete', value: 20, className: 'complete' }, + { label: 'Failed', value: 30, className: 'failed' }, + ]; + }), + + distributionBarDataRotating: computed('timerTicks', () => { + return [ + { label: 'one', value: Math.round(Math.random() * 50) }, + { label: 'two', value: Math.round(Math.random() * 50) }, + { label: 'three', value: Math.round(Math.random() * 50) }, + ]; + }), +}); diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js new file mode 100644 index 000000000..a63491531 --- /dev/null +++ b/ui/app/controllers/jobs/index.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; +import Searchable from 'nomad-ui/mixins/searchable'; + +const { Controller, computed } = Ember; + +export default Controller.extend(Sortable, Searchable, { + pendingJobs: computed.filterBy('model', 'status', 'pending'), + runningJobs: computed.filterBy('model', 'status', 'running'), + deadJobs: computed.filterBy('model', 'status', 'dead'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 10, + + sortProperty: 'modifyIndex', + sortDescending: true, + + searchProps: computed(() => ['id', 'name']), + + listToSort: computed.alias('model'), + listToSearch: computed.alias('listSorted'), + sortedJobs: computed.alias('listSearched'), + + isShowingDeploymentDetails: false, + + actions: { + gotoJob(job) { + this.transitionToRoute('jobs.job', job); + }, + }, +}); diff --git a/ui/app/controllers/jobs/job.js b/ui/app/controllers/jobs/job.js new file mode 100644 index 000000000..f1dcf8c6d --- /dev/null +++ b/ui/app/controllers/jobs/job.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +const { Controller, computed } = Ember; + +export default Controller.extend({ + breadcrumbs: computed('model.{name,id}', function() { + return [ + { label: 'Jobs', args: ['jobs'] }, + { label: this.get('model.name'), args: ['jobs.job', this.get('model.id')] }, + ]; + }), +}); diff --git a/ui/app/controllers/jobs/job/definition.js b/ui/app/controllers/jobs/job/definition.js new file mode 100644 index 000000000..14dd7214a --- /dev/null +++ b/ui/app/controllers/jobs/job/definition.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +const { Controller, computed, inject } = Ember; + +export default Controller.extend({ + jobController: inject.controller('jobs.job'), + + job: computed.alias('model.job'), + + breadcrumbs: computed.alias('jobController.breadcrumbs'), +}); diff --git a/ui/app/controllers/jobs/job/deployments.js b/ui/app/controllers/jobs/job/deployments.js new file mode 100644 index 000000000..97e3c9981 --- /dev/null +++ b/ui/app/controllers/jobs/job/deployments.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +const { Controller, computed, inject } = Ember; + +export default Controller.extend({ + jobController: inject.controller('jobs.job'), + + job: computed.alias('model'), + deployments: computed.alias('model.deployments'), + + breadcrumbs: computed.alias('jobController.breadcrumbs'), +}); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js new file mode 100644 index 000000000..f90d4db20 --- /dev/null +++ b/ui/app/controllers/jobs/job/index.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; + +const { Controller, computed, inject } = Ember; + +export default Controller.extend(Sortable, { + jobController: inject.controller('jobs.job'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 10, + + sortProperty: 'name', + sortDescending: false, + + breadcrumbs: computed.alias('jobController.breadcrumbs'), + job: computed.alias('model'), + + taskGroups: computed('model.taskGroups.[]', function() { + return this.get('model.taskGroups') || []; + }), + + listToSort: computed.alias('taskGroups'), + sortedTaskGroups: computed.alias('listSorted'), + + actions: { + gotoTaskGroup(taskGroup) { + this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup); + }, + }, +}); diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js new file mode 100644 index 000000000..89974810b --- /dev/null +++ b/ui/app/controllers/jobs/job/task-group.js @@ -0,0 +1,44 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; +import Searchable from 'nomad-ui/mixins/searchable'; + +const { Controller, computed, inject } = Ember; + +export default Controller.extend(Sortable, Searchable, { + jobController: inject.controller('jobs.job'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 10, + + sortProperty: 'name', + sortDescending: false, + + searchProps: computed(() => ['id', 'name']), + + allocations: computed('model.allocations.[]', function() { + return this.get('model.allocations') || []; + }), + + listToSort: computed.alias('allocations'), + listToSearch: computed.alias('listSorted'), + sortedAllocations: computed.alias('listSearched'), + + breadcrumbs: computed('jobController.breadcrumbs.[]', 'model.{name}', function() { + return this.get('jobController.breadcrumbs').concat([ + { label: this.get('model.name'), args: ['jobs.job.task-group', this.get('model.name')] }, + ]); + }), + + actions: { + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + }, + }, +}); diff --git a/ui/app/controllers/jobs/job/versions.js b/ui/app/controllers/jobs/job/versions.js new file mode 100644 index 000000000..420495630 --- /dev/null +++ b/ui/app/controllers/jobs/job/versions.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +const { Controller, computed, inject } = Ember; + +export default Controller.extend({ + jobController: inject.controller('jobs.job'), + + job: computed.alias('model'), + versions: computed.alias('model.versions'), + + breadcrumbs: computed.alias('jobController.breadcrumbs'), +}); diff --git a/ui/app/controllers/nodes/index.js b/ui/app/controllers/nodes/index.js new file mode 100644 index 000000000..3fb12bdb5 --- /dev/null +++ b/ui/app/controllers/nodes/index.js @@ -0,0 +1,35 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; +import Searchable from 'nomad-ui/mixins/searchable'; + +const { Controller, computed } = Ember; + +export default Controller.extend(Sortable, Searchable, { + nodes: computed.alias('model.nodes'), + agents: computed.alias('model.agents'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 8, + + sortProperty: 'modifyIndex', + sortDescending: true, + + searchProps: computed(() => ['id', 'name', 'datacenter']), + + listToSort: computed.alias('nodes'), + listToSearch: computed.alias('listSorted'), + sortedNodes: computed.alias('listSearched'), + + actions: { + gotoNode(node) { + this.transitionToRoute('nodes.node', node); + }, + }, +}); diff --git a/ui/app/controllers/nodes/node.js b/ui/app/controllers/nodes/node.js new file mode 100644 index 000000000..04d3c792e --- /dev/null +++ b/ui/app/controllers/nodes/node.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; +import Searchable from 'nomad-ui/mixins/searchable'; + +const { Controller, computed } = Ember; + +export default Controller.extend(Sortable, Searchable, { + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 8, + + sortProperty: 'modifyIndex', + sortDescending: true, + + searchProps: computed(() => ['id', 'name']), + + listToSort: computed.alias('model.allocations'), + listToSearch: computed.alias('listSorted'), + sortedAllocations: computed.alias('listSearched'), + + actions: { + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + }, + }, +}); diff --git a/ui/app/controllers/servers.js b/ui/app/controllers/servers.js new file mode 100644 index 000000000..bf9dac326 --- /dev/null +++ b/ui/app/controllers/servers.js @@ -0,0 +1,25 @@ +import Ember from 'ember'; +import Sortable from 'nomad-ui/mixins/sortable'; + +const { Controller, computed } = Ember; + +export default Controller.extend(Sortable, { + nodes: computed.alias('model.nodes'), + agents: computed.alias('model.agents'), + + queryParams: { + currentPage: 'page', + searchTerm: 'search', + sortProperty: 'sort', + sortDescending: 'desc', + }, + + currentPage: 1, + pageSize: 8, + + sortProperty: 'isLeader', + sortDescending: true, + + listToSort: computed.alias('agents'), + sortedAgents: computed.alias('listSorted'), +}); diff --git a/ui/app/controllers/servers/server.js b/ui/app/controllers/servers/server.js new file mode 100644 index 000000000..c9ef734e8 --- /dev/null +++ b/ui/app/controllers/servers/server.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +const { Controller } = Ember; + +export default Controller.extend({ + activeTab: 'tags', + + actions: { + setTab(tab) { + this.set('activeTab', tab); + }, + }, +}); diff --git a/ui/app/controllers/settings/tokens.js b/ui/app/controllers/settings/tokens.js new file mode 100644 index 000000000..4173de7f8 --- /dev/null +++ b/ui/app/controllers/settings/tokens.js @@ -0,0 +1,20 @@ +import Ember from 'ember'; + +const { Controller, inject } = Ember; + +export default Controller.extend({ + token: inject.service(), + + actions: { + setTokenProperty(property, event) { + this.get('token').set(property, event.currentTarget.value); + }, + + clearTokenProperties() { + this.get('token').setProperties({ + secret: undefined, + accessor: undefined, + }); + }, + }, +}); diff --git a/ui/app/helpers/.gitkeep b/ui/app/helpers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/helpers/css-class.js b/ui/app/helpers/css-class.js new file mode 100644 index 000000000..a23d3b90d --- /dev/null +++ b/ui/app/helpers/css-class.js @@ -0,0 +1,15 @@ +import Ember from 'ember'; + +/** + * CSS Class + * + * Usage: {{css-class updateType}} + * + * Outputs a css friendly class string from any human string. + * Differs from dasherize by handling slashes. + */ +export function cssClass([updateType]) { + return updateType.replace(/\//g, '-').dasherize(); +} + +export default Ember.Helper.helper(cssClass); diff --git a/ui/app/helpers/format-percentage.js b/ui/app/helpers/format-percentage.js new file mode 100644 index 000000000..ff4358bc7 --- /dev/null +++ b/ui/app/helpers/format-percentage.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; + +/** + * Percentage Calculator + * + * Usage: {{format-percentage count total="number" complement="number"}} + * + * Outputs the ratio as a percentage, where ratio is either + * `count / total` or `count / (count + complement)` + */ +export function formatPercentage(params, options = {}) { + const value = safeNumber(params[0]); + const complement = options.complement; + + let ratio; + let total = options.total; + + if (total !== undefined) { + total = safeNumber(total); + } else if (complement !== undefined) { + total = value + safeNumber(complement); + } else { + // Ensures that ratio is between 0 and 1 when neither total or complement are defined + total = value; + } + + // Use zero instead of infinity when the divisor is zero + ratio = total ? value / total : 0; + + return 0 < ratio && ratio < 0.01 ? '< 1%' : Math.round(ratio * 100) + '%'; +} + +// If a value is not a number, treat it as zero +function safeNumber(value) { + return isNaN(value) ? 0 : +value; +} + +export default Ember.Helper.helper(formatPercentage); diff --git a/ui/app/helpers/href-to-spread.js b/ui/app/helpers/href-to-spread.js new file mode 100644 index 000000000..3c7d76d1b --- /dev/null +++ b/ui/app/helpers/href-to-spread.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import hrefTo from 'ember-href-to/helpers/href-to'; + +const { Helper } = Ember; + +/** + * Href-to Spread + * + * Usage: {{href-to-spread hrefToPositionalParamsAsArray query=whatever}} + * + * Does the same thing as href-to but takes an array of arguments instead of a static list. + * This way arguments can be managed in js and provided to the template. + */ +export default Helper.extend({ + compute([params], options = {}) { + return hrefTo.create().compute.call(this, params, options); + }, +}); diff --git a/ui/app/helpers/is-object.js b/ui/app/helpers/is-object.js new file mode 100644 index 000000000..0b7dae86d --- /dev/null +++ b/ui/app/helpers/is-object.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +const { Helper } = Ember; + +export function isObject([value]) { + const isObject = !Array.isArray(value) && value !== null && typeof value === 'object'; + return isObject; +} + +export default Helper.helper(isObject); diff --git a/ui/app/helpers/pluralize.js b/ui/app/helpers/pluralize.js new file mode 100644 index 000000000..161b366a4 --- /dev/null +++ b/ui/app/helpers/pluralize.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +const { Helper } = Ember; + +export function pluralize([term, count]) { + return count === 1 ? term : term.pluralize(); +} + +export default Helper.helper(pluralize); diff --git a/ui/app/index.html b/ui/app/index.html new file mode 100644 index 000000000..855963b70 --- /dev/null +++ b/ui/app/index.html @@ -0,0 +1,26 @@ + + + + + + Nomad + + + + {{content-for "head"}} + + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/ui/app/initializers/app-env.js b/ui/app/initializers/app-env.js new file mode 100644 index 000000000..0d5284f2d --- /dev/null +++ b/ui/app/initializers/app-env.js @@ -0,0 +1,12 @@ +export function initialize() { + const application = arguments[1] || arguments[0]; + + // Provides the app config to all templates + application.inject('controller', 'config', 'service:config'); + application.inject('component', 'config', 'service:config'); +} + +export default { + name: 'app-config', + initialize, +}; diff --git a/ui/app/initializers/fragment-serializer.js b/ui/app/initializers/fragment-serializer.js new file mode 100644 index 000000000..0f5824d74 --- /dev/null +++ b/ui/app/initializers/fragment-serializer.js @@ -0,0 +1,10 @@ +import FragmentSerializer from '../serializers/fragment'; + +export function initialize(application) { + application.register('serializer:-fragment', FragmentSerializer); +} + +export default { + name: 'fragment-serializer', + initialize: initialize, +}; diff --git a/ui/app/mixins/searchable.js b/ui/app/mixins/searchable.js new file mode 100644 index 000000000..24723b59e --- /dev/null +++ b/ui/app/mixins/searchable.js @@ -0,0 +1,64 @@ +import Ember from 'ember'; +import Fuse from 'npm:fuse.js'; + +const { Mixin, computed, get } = Ember; + +/** + Searchable mixin + + Simple search filtering behavior for a list of objects. + + Properties to override: + - searchTerm: the string to use as a query + - searchProps: the props on each object to search + - listToSearch: the list of objects to search + + Properties provided: + - listSearched: a subset of listToSearch of items that meet the search criteria, + ordered by relevance. +*/ +export default Mixin.create({ + searchTerm: '', + listToSearch: computed(() => []), + searchProps: null, + + fuse: computed('listToSearch.[]', 'searchProps.[]', function() { + return new Fuse(this.get('listToSearch'), { + shouldSort: true, + threshold: 0.6, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: this.get('searchProps') || [], + getFn(item, key) { + return get(item, key); + }, + }); + }), + + listSearched: computed('fuse', 'searchTerm', function() { + const { fuse, searchTerm } = this.getProperties('fuse', 'searchTerm'); + if (searchTerm && searchTerm.length) { + if (searchTerm.startsWith('/')) { + return regexSearch(searchTerm, fuse); + } + return fuse.search(searchTerm); + } + return this.get('listToSearch'); + }), +}); + +function regexSearch(term, { list, options: { keys } }) { + const regexStr = term.slice(1); + if (regexStr.length) { + try { + const regex = new RegExp(regexStr, 'i'); + // Test the value of each key for each object against the regex + // All that match are returned. + return list.filter(item => keys.some(key => regex.test(get(item, key)))); + } catch (e) { + // Swallow the error; most likely due to an eager search of an incomplete regex + } + } +} diff --git a/ui/app/mixins/sortable.js b/ui/app/mixins/sortable.js new file mode 100644 index 000000000..9c01096d9 --- /dev/null +++ b/ui/app/mixins/sortable.js @@ -0,0 +1,31 @@ +import Ember from 'ember'; + +const { Mixin, computed } = Ember; + +/** + Sortable mixin + + Simple sorting behavior for a list of objects. + + Properties to override: + - sortProperty: the property to sort by + - sortDescending: when true, the list is reversed + - listToSort: the list of objects to sort + + Properties provided: + - listSorted: a copy of listToSort that has been sorted +*/ +export default Mixin.create({ + // Override in mixin consumer + sortProperty: null, + sortDescending: true, + listToSort: computed(() => []), + + listSorted: computed('listToSort.[]', 'sortProperty', 'sortDescending', function() { + const sorted = this.get('listToSort').sortBy(this.get('sortProperty')); + if (this.get('sortDescending')) { + return sorted.reverse(); + } + return sorted; + }), +}); diff --git a/ui/app/models/.gitkeep b/ui/app/models/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/models/agent.js b/ui/app/models/agent.js new file mode 100644 index 000000000..aea6c7ee5 --- /dev/null +++ b/ui/app/models/agent.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; + +const { computed, inject } = Ember; + +export default Model.extend({ + system: inject.service(), + + name: attr('string'), + address: attr('string'), + serfPort: attr('string'), + rpcPort: attr('string'), + tags: attr({ defaultValue: () => ({}) }), + status: attr('string'), + datacenter: attr('string'), + region: attr('string'), + + rpcAddr: computed('address', 'port', function() { + const { address, rpcPort } = this.getProperties('address', 'rpcPort'); + return address && rpcPort && `${address}:${rpcPort}`; + }), + + isLeader: computed('system.leader.rpcAddr', function() { + return this.get('system.leader.rpcAddr') === this.get('rpcAddr'); + }), +}); diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js new file mode 100644 index 000000000..ea24a54db --- /dev/null +++ b/ui/app/models/allocation.js @@ -0,0 +1,66 @@ +import Ember from 'ember'; +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo } from 'ember-data/relationships'; +import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; +import fetch from 'fetch'; +import PromiseObject from '../utils/classes/promise-object'; +import timeout from '../utils/timeout'; +import shortUUIDProperty from '../utils/properties/short-uuid'; + +const { computed, RSVP } = Ember; + +export default Model.extend({ + shortId: shortUUIDProperty('id'), + job: belongsTo('job'), + node: belongsTo('node'), + name: attr('string'), + taskGroupName: attr('string'), + resources: fragment('resources'), + modifyIndex: attr('number'), + + clientStatus: attr('string'), + desiredStatus: attr('string'), + + taskGroup: computed('taskGroupName', 'job.taskGroups.[]', function() { + const taskGroups = this.get('job.taskGroups'); + return taskGroups && taskGroups.findBy('name', this.get('taskGroupName')); + }), + + percentMemory: computed( + 'taskGroup.reservedMemory', + 'stats.ResourceUsage.MemoryStats.Cache', + function() { + const used = this.get('stats.ResourceUsage.MemoryStats.Cache'); + const total = this.get('taskGroup.reservedMemory'); + if (!total || !used) { + return 0; + } + return used / total; + } + ), + + percentCPU: computed('stats.ResourceUsage.CpuStats.Percent', function() { + return this.get('stats.ResourceUsage.CpuStats.Percent') || 0; + }), + + stats: computed('node.{isPartial,httpAddr}', function() { + const nodeIsPartial = this.get('node.isPartial'); + + // If the node doesn't have an httpAddr, it's a partial record. + // Once it reloads, this property will be dirtied and stats will load. + if (nodeIsPartial) { + return PromiseObject.create({ + // Never resolve, so the promise object is always in a state of pending + promise: new RSVP.Promise(() => {}), + }); + } + + const url = `//${this.get('node.httpAddr')}/v1/client/allocation/${this.get('id')}/stats`; + return PromiseObject.create({ + promise: RSVP.Promise.race([fetch(url).then(res => res.json()), timeout(2000)]), + }); + }), + + states: fragmentArray('task-state'), +}); diff --git a/ui/app/models/deployment.js b/ui/app/models/deployment.js new file mode 100644 index 000000000..2a952afb7 --- /dev/null +++ b/ui/app/models/deployment.js @@ -0,0 +1,55 @@ +import Ember from 'ember'; +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo, hasMany } from 'ember-data/relationships'; +import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import shortUUIDProperty from '../utils/properties/short-uuid'; +import sumAggregation from '../utils/properties/sum-aggregation'; + +const { computed } = Ember; + +export default Model.extend({ + shortId: shortUUIDProperty('id'), + + job: belongsTo('job'), + versionNumber: attr('number'), + + // If any task group is not promoted yet requires promotion and the deployment + // is still running, the deployment needs promotion. + requiresPromotion: computed('taskGroupSummaries.@each.promoted', function() { + return ( + this.get('status') === 'running' && + this.get('taskGroupSummaries') + .toArray() + .some(summary => summary.get('requiresPromotion') && !summary.get('promoted')) + ); + }), + + status: attr('string'), + statusDescription: attr('string'), + taskGroupSummaries: fragmentArray('task-group-deployment-summary'), + allocations: hasMany('allocations'), + + version: computed('versionNumber', 'job.versions.content.@each.number', function() { + return (this.get('job.versions') || []).findBy('number', this.get('versionNumber')); + }), + + placedCanaries: sumAggregation('taskGroupSummaries', 'placedCanaries'), + desiredCanaries: sumAggregation('taskGroupSummaries', 'desiredCanaries'), + desiredTotal: sumAggregation('taskGroupSummaries', 'desiredTotal'), + placedAllocs: sumAggregation('taskGroupSummaries', 'placedAllocs'), + healthyAllocs: sumAggregation('taskGroupSummaries', 'healthyAllocs'), + unhealthyAllocs: sumAggregation('taskGroupSummaries', 'unhealthyAllocs'), + + statusClass: computed('status', function() { + const classMap = { + running: 'is-running', + successful: 'is-primary', + paused: 'is-light', + failed: 'is-error', + cancelled: 'is-cancelled', + }; + + return classMap[this.get('status')] || 'is-dark'; + }), +}); diff --git a/ui/app/models/job-version.js b/ui/app/models/job-version.js new file mode 100644 index 000000000..6204c1198 --- /dev/null +++ b/ui/app/models/job-version.js @@ -0,0 +1,11 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo } from 'ember-data/relationships'; + +export default Model.extend({ + job: belongsTo('job'), + stable: attr('boolean'), + submitTime: attr('date'), + number: attr('number'), + diff: attr(), +}); diff --git a/ui/app/models/job.js b/ui/app/models/job.js new file mode 100644 index 000000000..51d570847 --- /dev/null +++ b/ui/app/models/job.js @@ -0,0 +1,73 @@ +import Ember from 'ember'; +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { hasMany } from 'ember-data/relationships'; +import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import sumAggregation from '../utils/properties/sum-aggregation'; + +const { computed } = Ember; + +export default Model.extend({ + region: attr('string'), + name: attr('string'), + type: attr('string'), + priority: attr('number'), + allAtOnce: attr('boolean'), + + status: attr('string'), + statusDescription: attr('string'), + createIndex: attr('number'), + modifyIndex: attr('number'), + + periodic: attr('boolean'), + parameterized: attr('boolean'), + + datacenters: attr(), + taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), + taskGroupSummaries: fragmentArray('task-group-summary'), + + // Aggregate allocation counts across all summaries + queuedAllocs: sumAggregation('taskGroupSummaries', 'queuedAllocs'), + startingAllocs: sumAggregation('taskGroupSummaries', 'startingAllocs'), + runningAllocs: sumAggregation('taskGroupSummaries', 'runningAllocs'), + completeAllocs: sumAggregation('taskGroupSummaries', 'completeAllocs'), + failedAllocs: sumAggregation('taskGroupSummaries', 'failedAllocs'), + lostAllocs: sumAggregation('taskGroupSummaries', 'lostAllocs'), + + allocsList: computed.collect( + 'queuedAllocs', + 'startingAllocs', + 'runningAllocs', + 'completeAllocs', + 'failedAllocs', + 'lostAllocs' + ), + + totalAllocs: computed.sum('allocsList'), + + pendingChildren: attr('number'), + runningChildren: attr('number'), + deadChildren: attr('number'), + + versions: hasMany('job-versions'), + allocations: hasMany('allocations'), + deployments: hasMany('deployments'), + + runningDeployment: computed('deployments.@each.status', function() { + return this.get('deployments').findBy('status', 'running'); + }), + + fetchRawDefinition() { + return this.store.adapterFor('job').fetchRawDefinition(this); + }, + + statusClass: computed('status', function() { + const classMap = { + pending: 'is-pending', + running: 'is-primary', + dead: 'is-light', + }; + + return classMap[this.get('status')] || 'is-dark'; + }), +}); diff --git a/ui/app/models/network.js b/ui/app/models/network.js new file mode 100644 index 000000000..01896c5fb --- /dev/null +++ b/ui/app/models/network.js @@ -0,0 +1,12 @@ +import attr from 'ember-data/attr'; +import Fragment from 'ember-data-model-fragments/fragment'; +import { array } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + device: attr('string'), + cidr: attr('string'), + ip: attr('string'), + mbits: attr('number'), + reservedPorts: array(), + dynamicPorts: array(), +}); diff --git a/ui/app/models/node-attributes.js b/ui/app/models/node-attributes.js new file mode 100644 index 000000000..6bfaba7ca --- /dev/null +++ b/ui/app/models/node-attributes.js @@ -0,0 +1,29 @@ +import Ember from 'ember'; +import attr from 'ember-data/attr'; +import Fragment from 'ember-data-model-fragments/fragment'; +import flat from 'npm:flat'; + +const { computed, get } = Ember; +const { unflatten } = flat; + +export default Fragment.extend({ + attributes: attr(), + + attributesStructured: computed('attributes', function() { + // `unflatten` doesn't sort keys before unflattening, so manual preprocessing is necessary. + const original = this.get('attributes'); + const attrs = Object.keys(original).sort().reduce((obj, key) => { + obj[key] = original[key]; + return obj; + }, {}); + return unflatten(attrs, { overwrite: true }); + }), + + unknownProperty(key) { + // Returns the exact value in index 0 and the subtree in index 1 + // + // ex: nodeAttrs.get('driver.docker') + // [ "1", { version: "17.05.0-ce", volumes: { enabled: "1" } } ] + return get(this.get('attributesStructured'), key); + }, +}); diff --git a/ui/app/models/node.js b/ui/app/models/node.js new file mode 100644 index 000000000..6966acfb9 --- /dev/null +++ b/ui/app/models/node.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { hasMany } from 'ember-data/relationships'; +import { fragment } from 'ember-data-model-fragments/attributes'; +import shortUUIDProperty from '../utils/properties/short-uuid'; +import ipParts from '../utils/ip-parts'; + +const { computed } = Ember; + +export default Model.extend({ + // Available from list response + name: attr('string'), + datacenter: attr('string'), + isDraining: attr('boolean'), + status: attr('string'), + statusDescription: attr('string'), + shortId: shortUUIDProperty('id'), + modifyIndex: attr('number'), + + // Available from single response + httpAddr: attr('string'), + tlsEnabled: attr('boolean'), + attributes: fragment('node-attributes'), + resources: fragment('resources'), + reserved: fragment('resources'), + + address: computed('httpAddr', function() { + return ipParts(this.get('httpAddr')).address; + }), + + port: computed('httpAddr', function() { + return ipParts(this.get('httpAddr')).port; + }), + + isPartial: computed('httpAddr', function() { + return this.get('httpAddr') == null; + }), + + allocations: hasMany('allocations'), +}); diff --git a/ui/app/models/resources.js b/ui/app/models/resources.js new file mode 100644 index 000000000..5bb900ee4 --- /dev/null +++ b/ui/app/models/resources.js @@ -0,0 +1,11 @@ +import attr from 'ember-data/attr'; +import Fragment from 'ember-data-model-fragments/fragment'; +import { fragmentArray } from 'ember-data-model-fragments/attributes'; + +export default Fragment.extend({ + cpu: attr('number'), + memory: attr('number'), + disk: attr('number'), + iops: attr('number'), + networks: fragmentArray('network', { defaultValue: () => [] }), +}); diff --git a/ui/app/models/task-event.js b/ui/app/models/task-event.js new file mode 100644 index 000000000..1a26daadc --- /dev/null +++ b/ui/app/models/task-event.js @@ -0,0 +1,148 @@ +import Ember from 'ember'; +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; +import moment from 'moment'; + +const { computed } = Ember; +const displayProps = [ + 'message', + 'validationError', + 'setupError', + 'driverError', + 'downloadError', + 'killReason', + 'killTimeout', + 'killError', + 'exitCode', + 'signal', + 'startDelay', + 'restartReason', + 'failedSibling', + 'taskSignal', + 'taskSignalReason', + 'driverMessage', +]; + +export default Fragment.extend({ + state: fragmentOwner(), + + type: attr('string'), + signal: attr('number'), + exitCode: attr('number'), + + time: attr('date'), + timeNanos: attr('number'), + + downloadError: attr('string'), + driverError: attr('string'), + driverMessage: attr('string'), + killError: attr('string'), + killReason: attr('string'), + killTimeout: attr('number'), + restartReason: attr('string'), + setupError: attr('string'), + startDelay: attr('number'), + taskSignal: attr('string'), + taskSignalReason: attr('string'), + validationError: attr('string'), + vaultError: attr('string'), + message: attr('string'), + failedSibling: attr('string'), + + displayMessage: computed(...displayProps, function() { + let desc = ''; + switch (this.get('type')) { + case 'Task Setup': + desc = this.get('message'); + break; + case 'Started': + desc = 'Task started by client'; + break; + case 'Received': + desc = 'Task received by client'; + break; + case 'Failed Validation': + desc = this.get('validationError') || 'Validation of task failed'; + break; + case 'Setup Failure': + desc = this.get('setupError') || 'Task setup failed'; + break; + case 'Driver Failure': + desc = this.get('driverError') || 'Failed to start task'; + break; + case 'Downloading Artifacts': + desc = 'Client is downloading artifacts'; + break; + case 'Failed Artifact Download': + desc = this.get('downloadError') || 'Failed to download artifacts'; + break; + case 'Killing': + desc = + this.get('killReason') || + (this.get('killTimeout') && + `Sent interrupt. Waiting ${this.get('killTimeout')} before force killing`); + break; + case 'Killed': + desc = this.get('killError') || 'Task successfully killed'; + break; + case 'Terminated': + var parts = [`Exit Code: ${this.get('exitCode')}`]; + if (this.get('signal')) { + parts.push(`Signal: ${this.get('signal')}`); + } + if (this.get('message')) { + parts.push(`Exit Message: ${this.get('message')}`); + } + desc = parts.join(', '); + break; + case 'Restarting': + var timerMessage = `Task restarting in ${moment + .duration(this.get('startDelay') / 1000000, 'ms') + .humanize()}`; + if (this.get('restartReason') && this.get('restartReason') !== 'Restart within policy') { + desc = `${this.get('restartReason')} - ${timerMessage}`; + } else { + desc = timerMessage; + } + break; + case 'Not Restarting': + desc = this.get('restartReason') || 'Task exceeded restart policy'; + break; + case 'Sibling Task Failed': + desc = this.get('failedSibling') + ? `Task's sibling ${this.get('failedSibling')} failed` + : "Task's sibling failed"; + break; + case 'Signaling': + var signal = this.get('taskSignal'); + var reason = this.get('taskSignalReason'); + + if (!signal && !reason) { + desc = 'Task being sent a signal'; + } else if (!signal) { + desc = reason; + } else if (!reason) { + desc = `Task being sent signal ${signal}`; + } else { + desc = `Task being sent signal ${signal}: ${reason}`; + } + + break; + case 'Restart Signaled': + desc = this.get('restartReason') || 'Task signaled to restart'; + break; + case 'Driver': + desc = this.get('driverMessage'); + break; + case 'Leader Task Dead': + desc = 'Leader Task in Group dead'; + break; + case 'Generic': + desc = this.get('message'); + break; + } + + return desc; + }), +}); diff --git a/ui/app/models/task-group-deployment-summary.js b/ui/app/models/task-group-deployment-summary.js new file mode 100644 index 000000000..9a6b0eab3 --- /dev/null +++ b/ui/app/models/task-group-deployment-summary.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +const { computed } = Ember; + +export default Fragment.extend({ + deployment: fragmentOwner(), + + name: attr('string'), + + autoRevert: attr('boolean'), + promoted: attr('boolean'), + requiresPromotion: computed.gt('desiredCanaries', 0), + + placedCanaries: attr('number'), + desiredCanaries: attr('number'), + desiredTotal: attr('number'), + placedAllocs: attr('number'), + healthyAllocs: attr('number'), + unhealthyAllocs: attr('number'), +}); diff --git a/ui/app/models/task-group-summary.js b/ui/app/models/task-group-summary.js new file mode 100644 index 000000000..5d937f73b --- /dev/null +++ b/ui/app/models/task-group-summary.js @@ -0,0 +1,29 @@ +import Ember from 'ember'; +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner } from 'ember-data-model-fragments/attributes'; + +const { computed } = Ember; + +export default Fragment.extend({ + job: fragmentOwner(), + name: attr('string'), + + queuedAllocs: attr('number'), + startingAllocs: attr('number'), + runningAllocs: attr('number'), + completeAllocs: attr('number'), + failedAllocs: attr('number'), + lostAllocs: attr('number'), + + allocsList: computed.collect( + 'queuedAllocs', + 'startingAllocs', + 'runningAllocs', + 'completeAllocs', + 'failedAllocs', + 'lostAllocs' + ), + + totalAllocs: computed.sum('allocsList'), +}); diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js new file mode 100644 index 000000000..56e83d59a --- /dev/null +++ b/ui/app/models/task-group.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; +import sumAggregation from '../utils/properties/sum-aggregation'; + +const { computed } = Ember; + +export default Fragment.extend({ + job: fragmentOwner(), + + name: attr('string'), + count: attr('number'), + + tasks: fragmentArray('task'), + + allocations: computed('job.allocations.@each.taskGroup', function() { + return this.get('job.allocations').filterBy('taskGroupName', this.get('name')); + }), + + reservedCPU: sumAggregation('tasks', 'reservedCPU'), + reservedMemory: sumAggregation('tasks', 'reservedMemory'), + reservedDisk: sumAggregation('tasks', 'reservedDisk'), + + reservedEphemeralDisk: attr('number'), + + queuedOrStartingAllocs: computed('summary.{queuedAllocs,startingAllocs}', function() { + return this.get('summary.queuedAllocs') + this.get('summary.startingAllocs'); + }), + + summary: computed('job.taskGroupSummaries.[]', function() { + return this.get('job.taskGroupSummaries').findBy('name', this.get('name')); + }), +}); diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js new file mode 100644 index 000000000..e67b0110b --- /dev/null +++ b/ui/app/models/task-state.js @@ -0,0 +1,25 @@ +import Ember from 'ember'; +import Fragment from 'ember-data-model-fragments/fragment'; +import attr from 'ember-data/attr'; +import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; + +const { computed } = Ember; + +export default Fragment.extend({ + name: attr('string'), + state: attr('string'), + startedAt: attr('date'), + finishedAt: attr('date'), + failed: attr('boolean'), + + isActive: computed.none('finishedAt'), + + allocation: fragmentOwner(), + task: computed('allocation.taskGroup.tasks.[]', function() { + const tasks = this.get('allocation.taskGroup.tasks'); + return tasks && tasks.findBy('name', this.get('name')); + }), + + resources: fragment('resources'), + events: fragmentArray('task-event'), +}); diff --git a/ui/app/models/task.js b/ui/app/models/task.js new file mode 100644 index 000000000..90025400d --- /dev/null +++ b/ui/app/models/task.js @@ -0,0 +1,12 @@ +import attr from 'ember-data/attr'; +import Fragment from 'ember-data-model-fragments/fragment'; + +export default Fragment.extend({ + name: attr('string'), + driver: attr('string'), + + reservedMemory: attr('number'), + reservedCPU: attr('number'), + reservedDisk: attr('number'), + reservedEphemeralDisk: attr('number'), +}); diff --git a/ui/app/resolver.js b/ui/app/resolver.js new file mode 100644 index 000000000..2fb563d6c --- /dev/null +++ b/ui/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/ui/app/router.js b/ui/app/router.js new file mode 100644 index 000000000..ee03341de --- /dev/null +++ b/ui/app/router.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; +import config from './config/environment'; + +const Router = Ember.Router.extend({ + location: config.locationType, + rootURL: config.rootURL, +}); + +Router.map(function() { + this.route('jobs', function() { + this.route('job', { path: '/:job_id' }, function() { + this.route('task-group', { path: '/:name' }); + this.route('definition'); + this.route('versions'); + this.route('deployments'); + }); + }); + + this.route('nodes', function() { + this.route('node', { path: '/:node_id' }); + }); + + this.route('servers', function() { + this.route('server', { path: '/:agent_id' }); + }); + + this.route('allocations', function() { + this.route('allocation', { path: '/:allocation_id' }); + }); + + this.route('settings', function() { + this.route('tokens'); + }); + + if (config.environment === 'development') { + this.route('freestyle'); + } +}); + +export default Router; diff --git a/ui/app/routes/.gitkeep b/ui/app/routes/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js new file mode 100644 index 000000000..5f46de610 --- /dev/null +++ b/ui/app/routes/application.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({}); diff --git a/ui/app/routes/index.js b/ui/app/routes/index.js new file mode 100644 index 000000000..f07c338c8 --- /dev/null +++ b/ui/app/routes/index.js @@ -0,0 +1,9 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + redirect() { + this.transitionTo('jobs'); + }, +}); diff --git a/ui/app/routes/jobs.js b/ui/app/routes/jobs.js new file mode 100644 index 000000000..4d0fe1625 --- /dev/null +++ b/ui/app/routes/jobs.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +const { Route, inject } = Ember; + +export default Route.extend({ + store: inject.service(), + + model() { + return this.get('store').findAll('job'); + }, +}); diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js new file mode 100644 index 000000000..abe106775 --- /dev/null +++ b/ui/app/routes/jobs/job.js @@ -0,0 +1,15 @@ +import Ember from 'ember'; + +const { Route, inject } = Ember; + +export default Route.extend({ + store: inject.service(), + + model({ job_id }) { + return this.get('store') + .find('job', job_id) + .then(job => { + return job.get('allocations').then(() => job); + }); + }, +}); diff --git a/ui/app/routes/jobs/job/definition.js b/ui/app/routes/jobs/job/definition.js new file mode 100644 index 000000000..4f24dbc60 --- /dev/null +++ b/ui/app/routes/jobs/job/definition.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + model() { + const job = this.modelFor('jobs.job'); + return job.fetchRawDefinition().then(definition => ({ + job, + definition, + })); + }, +}); diff --git a/ui/app/routes/jobs/job/deployments.js b/ui/app/routes/jobs/job/deployments.js new file mode 100644 index 000000000..a048c898a --- /dev/null +++ b/ui/app/routes/jobs/job/deployments.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +const { Route, RSVP } = Ember; + +export default Route.extend({ + model() { + const job = this.modelFor('jobs.job'); + return RSVP.all([job.get('deployments'), job.get('versions')]).then(() => job); + }, +}); diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js new file mode 100644 index 000000000..2be6107ab --- /dev/null +++ b/ui/app/routes/jobs/job/task-group.js @@ -0,0 +1,20 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + model({ name }) { + // If the job is a partial (from the list request) it won't have task + // groups. Reload the job to ensure task groups are present. + return this.modelFor('jobs.job') + .reload() + .then(job => { + return job + .hasMany('allocations') + .reload() + .then(() => { + return job.get('taskGroups').findBy('name', name); + }); + }); + }, +}); diff --git a/ui/app/routes/jobs/job/versions.js b/ui/app/routes/jobs/job/versions.js new file mode 100644 index 000000000..637d663c3 --- /dev/null +++ b/ui/app/routes/jobs/job/versions.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + model() { + const job = this.modelFor('jobs.job'); + return job.get('versions').then(() => job); + }, +}); diff --git a/ui/app/routes/nodes.js b/ui/app/routes/nodes.js new file mode 100644 index 000000000..793a28a87 --- /dev/null +++ b/ui/app/routes/nodes.js @@ -0,0 +1,19 @@ +import Ember from 'ember'; + +const { Route, inject, RSVP } = Ember; + +export default Route.extend({ + store: inject.service(), + system: inject.service(), + + beforeModel() { + return this.get('system.leader'); + }, + + model() { + return RSVP.hash({ + nodes: this.get('store').findAll('node'), + agents: this.get('store').findAll('agent'), + }); + }, +}); diff --git a/ui/app/routes/nodes/node.js b/ui/app/routes/nodes/node.js new file mode 100644 index 000000000..4e5511637 --- /dev/null +++ b/ui/app/routes/nodes/node.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { Route, inject } = Ember; + +export default Route.extend({ + store: inject.service(), + + afterModel(model) { + if (model.get('isPartial')) { + return model.reload().then(node => node.get('allocations')); + } + return model.get('allocations'); + }, +}); diff --git a/ui/app/routes/servers.js b/ui/app/routes/servers.js new file mode 100644 index 000000000..793a28a87 --- /dev/null +++ b/ui/app/routes/servers.js @@ -0,0 +1,19 @@ +import Ember from 'ember'; + +const { Route, inject, RSVP } = Ember; + +export default Route.extend({ + store: inject.service(), + system: inject.service(), + + beforeModel() { + return this.get('system.leader'); + }, + + model() { + return RSVP.hash({ + nodes: this.get('store').findAll('node'), + agents: this.get('store').findAll('agent'), + }); + }, +}); diff --git a/ui/app/serializers/agent.js b/ui/app/serializers/agent.js new file mode 100644 index 000000000..e3cb04997 --- /dev/null +++ b/ui/app/serializers/agent.js @@ -0,0 +1,26 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + datacenter: 'dc', + address: 'Addr', + serfPort: 'Port', + }, + + normalize(typeHash, hash) { + hash.ID = hash.Name; + hash.Datacenter = hash.Tags && hash.Tags.dc; + hash.Region = hash.Tags && hash.Tags.region; + hash.RpcPort = hash.Tags && hash.Tags.port; + + return this._super(typeHash, hash); + }, + + normalizeResponse(store, typeClass, hash, ...args) { + return this._super(store, typeClass, hash.Members, ...args); + }, + + normalizeSingleResponse(store, typeClass, hash, id, ...args) { + return this._super(store, typeClass, hash.findBy('Name', id), id, ...args); + }, +}); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js new file mode 100644 index 000000000..1eec7ec7b --- /dev/null +++ b/ui/app/serializers/allocation.js @@ -0,0 +1,25 @@ +import Ember from 'ember'; +import ApplicationSerializer from './application'; + +const { get } = Ember; + +export default ApplicationSerializer.extend({ + attrs: { + taskGroupName: 'TaskGroup', + states: 'TaskStates', + }, + + normalize(typeHash, hash) { + // Transform the map-based TaskStates object into an array-based + // TaskState fragment list + hash.TaskStates = Object.keys(get(hash, 'TaskStates') || {}).map(key => { + const state = get(hash, `TaskStates.${key}`); + const summary = { Name: key }; + Object.keys(state).forEach(stateKey => (summary[stateKey] = state[stateKey])); + summary.Resources = hash.TaskResources && hash.TaskResources[key]; + return summary; + }); + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/serializers/application.js b/ui/app/serializers/application.js new file mode 100644 index 000000000..339995e2a --- /dev/null +++ b/ui/app/serializers/application.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import JSONSerializer from 'ember-data/serializers/json'; + +const { makeArray } = Ember; + +export default JSONSerializer.extend({ + primaryKey: 'ID', + + keyForAttribute(attr) { + return attr.camelize().capitalize(); + }, + + keyForRelationship(attr, relationshipType) { + const key = `${attr.camelize().capitalize()}ID`; + return relationshipType === 'hasMany' ? key.pluralize() : key; + }, + + // Modeled after the pushPayload for ember-data/serializers/rest + pushPayload(store, payload) { + const documentHash = { + data: [], + included: [], + }; + + Object.keys(payload).forEach(key => { + const modelName = this.modelNameFromPayloadKey(key); + const serializer = store.serializerFor(modelName); + const type = store.modelFor(modelName); + + makeArray(payload[key]).forEach(hash => { + const { data, included } = serializer.normalize(type, hash, key); + documentHash.data.push(data); + if (included) { + documentHash.included.push(...included); + } + }); + + store.push(documentHash); + }); + }, + + modelNameFromPayloadKey(key) { + return key.dasherize().singularize(); + }, +}); diff --git a/ui/app/serializers/deployment.js b/ui/app/serializers/deployment.js new file mode 100644 index 000000000..2cae8c7a9 --- /dev/null +++ b/ui/app/serializers/deployment.js @@ -0,0 +1,35 @@ +import Ember from 'ember'; +import ApplicationSerializer from './application'; + +const { get, assign } = Ember; + +export default ApplicationSerializer.extend({ + attrs: { + versionNumber: 'JobVersion', + }, + + normalize(typeHash, hash) { + hash.TaskGroupSummaries = Object.keys(get(hash, 'TaskGroups') || {}).map(key => { + const deploymentStats = get(hash, `TaskGroups.${key}`); + return assign({ Name: key }, deploymentStats); + }); + + return this._super(typeHash, hash); + }, + + extractRelationships(modelClass, hash) { + const namespace = this.store.adapterFor(modelClass.modelName).get('namespace'); + const id = this.extractId(modelClass, hash); + + return assign( + { + allocations: { + links: { + related: `/${namespace}/deployment/allocations/${id}`, + }, + }, + }, + this._super(modelClass, hash) + ); + }, +}); diff --git a/ui/app/serializers/fragment.js b/ui/app/serializers/fragment.js new file mode 100644 index 000000000..838b442f3 --- /dev/null +++ b/ui/app/serializers/fragment.js @@ -0,0 +1,3 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({}); diff --git a/ui/app/serializers/job-version.js b/ui/app/serializers/job-version.js new file mode 100644 index 000000000..490b6d740 --- /dev/null +++ b/ui/app/serializers/job-version.js @@ -0,0 +1,22 @@ +import Ember from 'ember'; +import ApplicationSerializer from './application'; + +const { assign } = Ember; + +export default ApplicationSerializer.extend({ + attrs: { + number: 'Version', + }, + + normalizeFindHasManyResponse(store, modelClass, hash, id, requestType) { + const zippedVersions = hash.Versions.map((version, index) => + assign({}, version, { + Diff: hash.Diffs && hash.Diffs[index], + ID: `${version.ID}-${version.Version}`, + SubmitTime: Math.floor(version.SubmitTime / 1000000), + SubmitTimeNanos: version.SubmitTime % 1000000, + }) + ); + return this._super(store, modelClass, zippedVersions, hash, id, requestType); + }, +}); diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js new file mode 100644 index 000000000..ce8866e79 --- /dev/null +++ b/ui/app/serializers/job.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; +import ApplicationSerializer from './application'; + +const { get } = Ember; + +export default ApplicationSerializer.extend({ + attrs: { + parameterized: 'ParameterizedJob', + }, + + normalize(typeHash, hash) { + // Transform the map-based JobSummary object into an array-based + // JobSummary fragment list + hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary')).map(key => { + const allocStats = get(hash, `JobSummary.Summary.${key}`); + const summary = { Name: key }; + + Object.keys(allocStats).forEach( + allocKey => (summary[`${allocKey}Allocs`] = allocStats[allocKey]) + ); + + return summary; + }); + + // Lift the children stats out of the JobSummary object + const childrenStats = get(hash, 'JobSummary.Children'); + if (childrenStats) { + Object.keys(childrenStats).forEach( + childrenKey => (hash[`${childrenKey}Children`] = childrenStats[childrenKey]) + ); + } + + return this._super(typeHash, hash); + }, + + extractRelationships(modelClass, hash) { + const { modelName } = modelClass; + const jobURL = this.store + .adapterFor(modelName) + .buildURL(modelName, this.extractId(modelClass, hash), hash, 'findRecord'); + + return { + allocations: { + links: { + related: `${jobURL}/allocations`, + }, + }, + versions: { + links: { + related: `${jobURL}/versions?diffs=true`, + }, + }, + deployments: { + links: { + related: `${jobURL}/deployments`, + }, + }, + }; + }, +}); diff --git a/ui/app/serializers/network.js b/ui/app/serializers/network.js new file mode 100644 index 000000000..00fea3be8 --- /dev/null +++ b/ui/app/serializers/network.js @@ -0,0 +1,9 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + cidr: 'CIDR', + ip: 'IP', + mbits: 'MBits', + }, +}); diff --git a/ui/app/serializers/node-attributes.js b/ui/app/serializers/node-attributes.js new file mode 100644 index 000000000..6e4be279f --- /dev/null +++ b/ui/app/serializers/node-attributes.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + return this._super(typeHash, { Attributes: hash }); + }, +}); diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js new file mode 100644 index 000000000..c1203a85d --- /dev/null +++ b/ui/app/serializers/node.js @@ -0,0 +1,22 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + httpAddr: 'HTTPAddr', + }, + + extractRelationships(modelClass, hash) { + const { modelName } = modelClass; + const nodeURL = this.store + .adapterFor(modelName) + .buildURL(modelName, this.extractId(modelClass, hash), hash, 'findRecord'); + + return { + allocations: { + links: { + related: `${nodeURL}/allocations`, + }, + }, + }; + }, +}); diff --git a/ui/app/serializers/resources.js b/ui/app/serializers/resources.js new file mode 100644 index 000000000..a657ed43b --- /dev/null +++ b/ui/app/serializers/resources.js @@ -0,0 +1,10 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + attrs: { + cpu: 'CPU', + memory: 'MemoryMB', + disk: 'DiskMB', + iops: 'IOPS', + }, +}); diff --git a/ui/app/serializers/task-event.js b/ui/app/serializers/task-event.js new file mode 100644 index 000000000..d4d30ded6 --- /dev/null +++ b/ui/app/serializers/task-event.js @@ -0,0 +1,14 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + // Time is in the form of nanoseconds since epoch, but JS dates + // only understand time to the millisecond precision. So store + // the time (precise to ms) as a date, and store the remaining ns + // as a number to deal with when it comes up. + hash.Time = Math.floor(hash.Time / 1000000); + hash.TimeNanos = hash.Time % 1000000; + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/serializers/task-group.js b/ui/app/serializers/task-group.js new file mode 100644 index 000000000..107ddf11e --- /dev/null +++ b/ui/app/serializers/task-group.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import ApplicationSerializer from './application'; + +const { copy } = Ember; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + // Provide EphemeralDisk to each task + hash.Tasks.forEach(task => { + task.EphemeralDisk = copy(hash.EphemeralDisk); + }); + + hash.ReservedEphemeralDisk = hash.EphemeralDisk.SizeMB; + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/serializers/task-state.js b/ui/app/serializers/task-state.js new file mode 100644 index 000000000..0ec3369a1 --- /dev/null +++ b/ui/app/serializers/task-state.js @@ -0,0 +1,13 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + // TODO API: finishedAt is always marshaled as a date even when unset. + // To simplify things, unset it here when it's the empty date value. + if (hash.FinishedAt === '0001-01-01T00:00:00Z') { + hash.FinishedAt = null; + } + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/serializers/task.js b/ui/app/serializers/task.js new file mode 100644 index 000000000..425b76811 --- /dev/null +++ b/ui/app/serializers/task.js @@ -0,0 +1,16 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + // Lift the reserved resource numbers out of the Resources object + const resources = hash.Resources; + if (resources) { + hash.ReservedMemory = resources.MemoryMB; + hash.ReservedCPU = resources.CPU; + hash.ReservedDisk = resources.DiskMB; + hash.ReservedEphemeralDisk = hash.EphemeralDisk.SizeMB; + } + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/services/config.js b/ui/app/services/config.js new file mode 100644 index 000000000..a6f392a17 --- /dev/null +++ b/ui/app/services/config.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; +import config from '../config/environment'; + +const { Service, get, computed } = Ember; + +export default Service.extend({ + unknownProperty(path) { + return get(config, path); + }, + + isDev: computed.equal('environment', 'development'), + isProd: computed.equal('environment', 'production'), +}); diff --git a/ui/app/services/ember-freestyle.js b/ui/app/services/ember-freestyle.js new file mode 100644 index 000000000..fa276c8d9 --- /dev/null +++ b/ui/app/services/ember-freestyle.js @@ -0,0 +1,5 @@ +import EmberFreestyle from 'ember-freestyle/services/ember-freestyle'; + +export default EmberFreestyle.extend({ + defaultTheme: 'monokai-sublime', +}); diff --git a/ui/app/services/system.js b/ui/app/services/system.js new file mode 100644 index 000000000..4ff2f6b5b --- /dev/null +++ b/ui/app/services/system.js @@ -0,0 +1,21 @@ +import Ember from 'ember'; +import fetch from 'fetch'; +import PromiseObject from '../utils/classes/promise-object'; +import { namespace } from '../adapters/application'; + +const { Service, computed } = Ember; + +export default Service.extend({ + leader: computed(function() { + return PromiseObject.create({ + promise: fetch(`/${namespace}/status/leader`) + .then(res => res.json()) + .then(rpcAddr => ({ rpcAddr })) + .then(leader => { + // Dirty self so leader can be used as a dependent key + this.notifyPropertyChange('leader.rpcAddr'); + return leader; + }), + }); + }), +}); diff --git a/ui/app/services/token.js b/ui/app/services/token.js new file mode 100644 index 000000000..0a28c78b3 --- /dev/null +++ b/ui/app/services/token.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; + +const { Service, computed } = Ember; + +export default Service.extend({ + accessor: computed({ + get() { + return window.sessionStorage.nomadTokenAccessor; + }, + set(key, value) { + if (value == null) { + window.sessionStorage.removeItem('nomadTokenAccessor'); + } else { + window.sessionStorage.nomadTokenAccessor = value; + } + return value; + }, + }), + + secret: computed({ + get() { + return window.sessionStorage.nomadTokenSecret; + }, + set(key, value) { + if (value == null) { + window.sessionStorage.removeItem('nomadTokenSecret'); + } else { + window.sessionStorage.nomadTokenSecret = value; + } + + return value; + }, + }), +}); diff --git a/ui/app/styles/app.scss b/ui/app/styles/app.scss new file mode 100644 index 000000000..3c94df8b0 --- /dev/null +++ b/ui/app/styles/app.scss @@ -0,0 +1,3 @@ +@import "./core"; +@import "./components"; +@import "./charts"; diff --git a/ui/app/styles/charts.scss b/ui/app/styles/charts.scss new file mode 100644 index 000000000..e70738650 --- /dev/null +++ b/ui/app/styles/charts.scss @@ -0,0 +1,17 @@ +@import "./charts/distribution-bar"; +@import "./charts/tooltip"; +@import "./charts/colors"; + +.inline-chart { + height: 1.5rem; + display: flex; + align-items: center; +} + +// Patterns are templates referenced by other SVG fill properties. +// Move the pattern svgs out of sight. +.svg-pattern { + overflow: hidden; + position: absolute; + left: -100%; +} diff --git a/ui/app/styles/charts/colors.scss b/ui/app/styles/charts/colors.scss new file mode 100644 index 000000000..4f3c6335f --- /dev/null +++ b/ui/app/styles/charts/colors.scss @@ -0,0 +1,86 @@ +$queued: $grey-lighter; +$starting: $grey-lighter; +$running: $primary; +$complete: $nomad-green-dark; +$failed: $danger; +$lost: $dark; + +.chart { + .queued { + fill: $queued; + } + + .starting, .pending { + .layer-0 { + fill: $starting; + } + + .layer-1 { + fill: url(#diagonal-stripe-3); + fill-opacity: 0.2; + } + } + + .running { + fill: $running; + } + + .complete { + fill: $complete; + } + + .failed { + fill: $failed; + } + + .lost { + fill: $lost; + } +} + +.color-swatch { + display: inline-block; + height: 1rem; + width: 1rem; + margin-right: 0.25rem; + vertical-align: middle; + border-radius: $radius; + + $color-sequence: $orange, $yellow, $green, $turquoise, $blue, $purple, $red; + @for $i from 1 through length($color-sequence) { + &.swatch-#{$i - 1} { + background: nth($color-sequence, $i); + } + } + + &.queued { + box-shadow: inset 0 0 0 1px rgba($black, 0.3); + background: $queued; + } + + &.starting, &.pending { + background: repeating-linear-gradient( + -45deg, + $starting, + $starting 3px, + darken($starting, 25%) 3px, + darken($starting, 25%) 6px + ); + } + + &.running { + background: $running; + } + + &.complete { + background: $complete; + } + + &.failed { + background: $failed; + } + + &.lost { + background: $lost; + } +} diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss new file mode 100644 index 000000000..7ea4530ad --- /dev/null +++ b/ui/app/styles/charts/distribution-bar.scss @@ -0,0 +1,96 @@ +.chart.distribution-bar { + display: block; + height: 100%; + + svg { + display: inline-block; + height: 100%; + width: 100%; + + .bars { + rect { + transition: opacity 0.3s ease-in-out; + opacity: 1; + } + + .inactive { + opacity: 0.2; + } + + .target { + opacity: 0; + } + + $color-sequence: $orange, + $yellow, + $green, + $turquoise, + $blue, + $purple, + $red; + + @for $i from 1 through length($color-sequence) { + .slice-#{$i - 1} { + fill: nth($color-sequence, $i); + } + } + } + } + + &.split-view { + display: flex; + flex-direction: row; + align-items: center; + + svg { + width: 50%; + height: 30px; + } + + .legend { + list-style: none; + width: 50%; + padding: 1.5em; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + + li { + display: block; + background-color: transparent; + transition: background-color 0.1s ease-in-out; + border: 1px solid $grey-blue; + padding: 0.25em 0.75em; + margin: 0.25em; + border-radius: $radius; + + // Ensure two columns, but don't use the full width + width: 35%; + + .label, .value { + display: inline; + font-weight: $weight-normal; + } + + &.is-active { + background-color: rgba($info, 0.1); + } + + &.is-empty { + color: darken($grey-blue, 20%); + border: none; + + .label { + color: darken($grey-blue, 20%); + } + } + } + } + } + + &.is-narrow .bar { + transform: translateY(-50%); + } +} diff --git a/ui/app/styles/charts/tooltip.scss b/ui/app/styles/charts/tooltip.scss new file mode 100644 index 000000000..08bdb533b --- /dev/null +++ b/ui/app/styles/charts/tooltip.scss @@ -0,0 +1,93 @@ +.chart { + position: relative; + + .tooltip { + position: absolute; + top: 0; + display: none; + background: $white; + color: rgba($black, 0.6); + border: 1px solid $grey; + min-width: 150px; + margin-top: -10px; + transform: translate(-50%, -100%); + transition: 0.2s top ease-out, 0.2s left ease-out; + pointer-events: none; + z-index: $z-tooltip; + + &::before { + pointer-events: none; + display: inline-block; + content: ""; + width: 0; + height: 0; + border-top: 7px solid $grey; + border-right: 7px solid transparent; + border-left: 7px solid transparent; + position: absolute; + transform: translateX(-7px); + left: 50%; + bottom: -8px; + z-index: 2; + } + + &::after { + pointer-events: none; + display: inline-block; + content: ""; + width: 0; + height: 0; + border-top: 6px solid $white; + border-right: 6px solid transparent; + border-left: 6px solid transparent; + position: absolute; + transform: translateX(-6px); + left: 50%; + bottom: -6px; + z-index: 2; + } + + &.active { + display: block; + } + + ol { + list-style: none; + + li { + display: flex; + flex-flow: row; + flex-wrap: nowrap; + justify-content: space-between; + padding: 0.25rem 0.5rem; + + span { + display: inline-block; + } + + .label { + font-weight: $weight-bold; + color: rgba($black, 0.6); + margin: 0; + + &.is-empty { + color: rgba($grey, 0.6); + } + } + + &.active { + color: $black; + background: $white-ter; + + .label { + color: $black; + } + } + + + li { + border-top: 1px solid $grey-light; + } + } + } + } +} diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss new file mode 100644 index 000000000..7832c934d --- /dev/null +++ b/ui/app/styles/components.scss @@ -0,0 +1,11 @@ +@import "./components/badge"; +@import "./components/boxed-section"; +@import "./components/breadcrumbs"; +@import "./components/gutter"; +@import "./components/inline-definitions"; +@import "./components/job-diff"; +@import "./components/json-viewer"; +@import "./components/metrics"; +@import "./components/node-status-light"; +@import "./components/status-text"; +@import "./components/timeline"; diff --git a/ui/app/styles/components/badge.scss b/ui/app/styles/components/badge.scss new file mode 100644 index 000000000..e5fc8b173 --- /dev/null +++ b/ui/app/styles/components/badge.scss @@ -0,0 +1,36 @@ +.badge { + font-size: $size-7; + height: 1.5em; + line-height: 1; + border-radius: $radius; + padding: 0.25em 0.75em; + + @each $name, $pair in $colors { + $color: nth($pair, 1); + $color-invert: nth($pair, 2); + + &.is-#{$name} { + background-color: $color; + color: $color-invert; + + &.is-faded { + color: rgba($color-invert, 0.8); + } + + &.is-hollow { + box-shadow: 0 0 0 1px $color; + background: $white; + color: darken($color, 10%); + } + + &.is-subtle { + background: rgba($color, 0.3); + color: $white; + } + } + } + + &.is-faded { + color: rgba($text, 0.8); + } +} diff --git a/ui/app/styles/components/boxed-section.scss b/ui/app/styles/components/boxed-section.scss new file mode 100644 index 000000000..51de36369 --- /dev/null +++ b/ui/app/styles/components/boxed-section.scss @@ -0,0 +1,108 @@ +.boxed-section { + margin-bottom: 1.5em; + + .boxed-section-head, + .boxed-section-foot { + padding: 0.75em 1.5em; + border: 1px solid $grey-blue; + background: $white-ter; + display: flex; + flex-direction: row; + align-items: baseline; + flex-wrap: wrap; + + .pull-right { + margin-left: auto; + } + } + + .boxed-section-head { + border-top-left-radius: $radius; + border-top-right-radius: $radius; + + &.is-light { + background: $white; + } + + & + .boxed-section-body { + padding: 1.5em; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + .boxed-section-body { + padding: 0.75em 1.5em; + background: $white; + border: 1px solid $grey-blue; + margin-top: -1px; + border-radius: $radius; + + &.is-full-bleed { + padding: 0; + border: none; + + // Often components will have a DOM presence but no layout. + // In that case, pass through. + > *:first-child, + > .ember-view:first-child > *:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + &.is-dark { + background: darken($dark, 5%); + border-color: lighten($dark, 30%); + color: $white; + } + + &.with-foot { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + + .boxed-section-foot { + margin-top: -1px; + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; + } + + .boxed-section-row { + width: 100%; + display: flex; + align-items: baseline; + + + .boxed-section-row { + margin-top: 0.5em; + } + } + + &.is-small { + font-size: $size-7; + } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + $color-invert: nth($pair, 2); + + &.is-#{$name} { + > .boxed-section-head { + background: $color; + border-color: $color; + color: $color-invert; + } + + > .boxed-section-body { + border-color: $color; + } + + > .boxed-section-foot { + border-color: $color; + background: lighten($color, 40%); + color: $color; + } + } + } +} diff --git a/ui/app/styles/components/breadcrumbs.scss b/ui/app/styles/components/breadcrumbs.scss new file mode 100644 index 000000000..e7d5942e6 --- /dev/null +++ b/ui/app/styles/components/breadcrumbs.scss @@ -0,0 +1,26 @@ +.breadcrumbs { + .breadcrumb { + color: $white; + opacity: 0.7; + text-decoration: none; + + &:hover { + color: $primary-invert; + opacity: 1; + } + + + .breadcrumb { + margin-left: 15px; + &::before { + content: "/"; + color: $primary; + position: relative; + left: -7px; + } + } + + &:last-child { + opacity: 1; + } + } +} diff --git a/ui/app/styles/components/gutter.scss b/ui/app/styles/components/gutter.scss new file mode 100644 index 000000000..0e47bf881 --- /dev/null +++ b/ui/app/styles/components/gutter.scss @@ -0,0 +1,4 @@ +.gutter { + height: 100%; + border-right: 1px solid $grey-blue; +} diff --git a/ui/app/styles/components/inline-definitions.scss b/ui/app/styles/components/inline-definitions.scss new file mode 100644 index 000000000..cc064c48e --- /dev/null +++ b/ui/app/styles/components/inline-definitions.scss @@ -0,0 +1,28 @@ +.inline-definitions { + .label { + text-transform: uppercase; + display: inline; + color: darken($grey-blue, 20%); + margin-right: 2rem; + font-size: inherit; + font-weight: $weight-semibold; + } + + .pair { + margin-right: 2em; + + .term { + font-weight: $weight-semibold; + margin-right: 0.5em; + } + + &.is-faded { + color: darken($grey-blue, 20%); + } + + .has-emphasis { + color: $text; + font-weight: $weight-semibold; + } + } +} diff --git a/ui/app/styles/components/job-diff.scss b/ui/app/styles/components/job-diff.scss new file mode 100644 index 000000000..3fe6b2810 --- /dev/null +++ b/ui/app/styles/components/job-diff.scss @@ -0,0 +1,74 @@ +.job-diff { + color: $grey-blue; + font-family: $family-monospace; + + &.is-added, + &.is-deleted { + padding-left: 1ch; + } + + &.is-edited { + padding-left: 2ch; + } + + .diff-section-label { + text-indent: 0; + padding-left: 2ch; + + &.is-added { + text-indent: -2ch; + } + + &.is-edited { + text-indent: -4ch; + } + + &.is-deleted { + text-indent: -2ch; + } + + .marker { + &.is-added { + color: $nomad-green; + } + + &.is-edited { + color: $orange; + } + + &.is-deleted { + color: $red; + } + } + } + + .diff-section-block { + padding-left: 0; + } + + .diff-section-bold { + color: $white; + font-weight: $weight-bold; + } + + .diff-section-change { + color: $white; + font-weight: $weight-bold; + } + + .diff-section-table { + display: table; + } + + .diff-section-table-row { + display: table-row; + } + + .diff-section-table-cell { + display: table-cell; + + & + .diff-section-table-cell { + text-indent: 1ch; + } + } +} diff --git a/ui/app/styles/components/json-viewer.scss b/ui/app/styles/components/json-viewer.scss new file mode 100644 index 000000000..dd215bc97 --- /dev/null +++ b/ui/app/styles/components/json-viewer.scss @@ -0,0 +1,144 @@ +@mixin theme( + $default-color: black, + $string-color: green, + $number-color: blue, + $boolean-color: red, + $null-color: #855A00, + $undefined-color: rgb(202, 11, 105), + $function-color: #FF20ED, + $toggler-opacity: 0.6, + $toggler-color: #45376F, + $bracket-color: blue, + $key-color: #00008B, + $url-color: blue +) { + font-family: monospace; + &, a, a:hover { + color: $default-color; + text-decoration: none; + } + + .json-formatter-row { + margin-left: 1rem; + } + + .json-formatter-children { + &.json-formatter-empty { + opacity: 0.5; + margin-left: 1rem; + + &:after { + display: none; + } + &.json-formatter-object:after { + content: "No properties"; + } + &.json-formatter-array:after { + content: "[]"; + } + } + } + + .json-formatter-string { + color: $string-color; + white-space: pre; + word-wrap: break-word; + } + + .json-formatter-number { + color: $number-color; + } + .json-formatter-boolean { + color: $boolean-color; + } + .json-formatter-null { + color: $null-color; + } + .json-formatter-undefined { + color: $undefined-color; + } + .json-formatter-function { + color: $function-color; + } + .json-formatter-date { + background-color: fade($default-color, 5%); + } + .json-formatter-url { + text-decoration: underline; + color: $url-color; + cursor: pointer; + } + + .json-formatter-bracket { + color: $bracket-color; + } + .json-formatter-key { + color: $key-color; + cursor: pointer; + padding-right: 0.2rem; + } + + .json-formatter-constructor-name { + cursor: pointer; + } + + .json-formatter-toggler { + line-height: 1rem; + font-size: 1rem; + vertical-align: baseline; + opacity: $toggler-opacity; + cursor: pointer; + padding-right: 0.3rem; + + &:after { + display: inline-block; + transition: none; + content: '+'; + } + } + + // Inline preview on hover (optional) + > a > .json-formatter-preview-text { + opacity: 0; + transition: opacity .15s ease-in; + font-style: italic; + } + + &:hover > a > .json-formatter-preview-text { + opacity: 0.6; + } + + // Open state + &.json-formatter-open { + > .json-formatter-toggler-link .json-formatter-toggler:after { + transform: none; + content: '-'; + } + > .json-formatter-children:after { + display: inline-block; + } + > a > .json-formatter-preview-text { + display: none; + } + &.json-formatter-empty:after { + display: block; + } + } +} + +.json-formatter-nomad.json-formatter-row { + @include theme( + $grey, + $nomad-green, + $packer-blue, + $consul-pink, + $terraform-purple, + $terraform-purple-dark, + $white, + 0.6, + $black, + $grey-dark, + $white, + $blue + ); +} diff --git a/ui/app/styles/components/metrics.scss b/ui/app/styles/components/metrics.scss new file mode 100644 index 000000000..0f94e3fa1 --- /dev/null +++ b/ui/app/styles/components/metrics.scss @@ -0,0 +1,64 @@ +.metric-group { + display: inline-flex; + width: auto; + align-items: flex-start; + + .metric { + padding: 0.75em 1em; + border: 1px solid $grey-blue; + text-align: center; + display: flex; + flex-direction: column; + min-width: 120px; + + + .metric { + margin-left: -1px; + } + + &:first-child { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; + } + + &:last-child { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; + } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + $color-invert: nth($pair, 2); + + &.is-#{$name} { + border-color: $color; + color: $color; + background: lighten($color, 40%); + + .label { + color: $color; + } + } + } + + &.is-faded { + color: $grey-blue; + border-color: $grey-blue; + background: $white; + + .label { + color: $grey-blue; + } + } + + .label { + font-size: 1.1em; + font-weight: $weight-semibold; + margin-bottom: 0; + } + + .value { + font-size: 2em; + margin-bottom: 0; + } + } +} diff --git a/ui/app/styles/components/node-status-light.scss b/ui/app/styles/components/node-status-light.scss new file mode 100644 index 000000000..8acb0cfce --- /dev/null +++ b/ui/app/styles/components/node-status-light.scss @@ -0,0 +1,29 @@ +$size: 0.75em; + +.node-status-light { + display: inline-block; + height: $size; + width: $size; + border-radius: $size / 2; + vertical-align: middle; + + background: $black; + + &.ready { + background: $primary; + } + + &.down { + background: $danger; + } + + &.initializing { + background: repeating-linear-gradient( + -45deg, + $grey-lighter, + $grey-lighter 3px, + darken($grey-lighter, 25%) 3px, + darken($grey-lighter, 25%) 6px + ); + } +} diff --git a/ui/app/styles/components/status-text.scss b/ui/app/styles/components/status-text.scss new file mode 100644 index 000000000..7e49ab4e1 --- /dev/null +++ b/ui/app/styles/components/status-text.scss @@ -0,0 +1,13 @@ +.status-text { + &.node-ready { + color: $nomad-green-dark; + } + + &.node-down { + color: $danger; + } + + &.node-initializing { + color: $grey; + } +} diff --git a/ui/app/styles/components/timeline.scss b/ui/app/styles/components/timeline.scss new file mode 100644 index 000000000..bc3ab077a --- /dev/null +++ b/ui/app/styles/components/timeline.scss @@ -0,0 +1,56 @@ +.timeline { + list-style: none; + position: relative; + z-index: $z-base; + + &::before { + content: " "; + position: absolute; + display: block; + top: 0; + left: 1em; + bottom: 0; + width: 1px; + background: $grey-blue; + z-index: $z-base - 1; + } + + > li { + position: relative; + z-index: $z-base; + } + + .timeline-note { + padding-left: 2em; + margin-top: 3em; + transform: translateY(-50%); + color: darken($grey-blue, 20%); + font-size: $size-7; + + &:first-child { + margin-top: 0; + } + + &::before { + content: " "; + position: absolute; + display: block; + width: 10px; + height: 10px; + border-radius: 10px; + left: 1em; + top: 50%; + transform: translate(-25%, -50%); + border: 1px solid $grey-blue; + background: $white; + } + } + + .timeline-object { + margin-bottom: 1em; + + > .boxed-section { + margin-bottom: 0; + } + } +} diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss new file mode 100644 index 000000000..402e6b185 --- /dev/null +++ b/ui/app/styles/core.scss @@ -0,0 +1,64 @@ +// Utils +@import "./utils/z-indices"; + +// Start with Bulma variables as a foundation +@import "bulma/sass/utilities/initial-variables"; + +// Override variables where appropriate +@import "./utils/product-colors"; + +$orange: #fa8e23; +$green: #2eb039; +$blue: $vagrant-blue; +$purple: $terraform-purple; +$red: #c84034; +$grey-blue: #bbc4d1; + +$primary: $nomad-green; +$warning: $orange; +$warning-invert: $white; +$danger: $red; +$dark: #234; + +$radius: 2px; + +$body-size: 14px; +$title-size: 1.75rem; +$size-5: 1.15rem; +$size-4: 1.3rem; +$size-7: 0.85rem; + +$title-weight: $weight-semibold; + +$family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + +$text: $black; + +$header-height: 112px; +$gutter-width: 250px; + +// Bring in the rest of Bulma +@import "bulma/bulma"; + +// Override Bulma details where appropriate +@import "./core/buttons"; +@import "./core/columns"; +@import "./core/forms"; +@import "./core/level"; +@import "./core/menu"; +@import "./core/message"; +@import "./core/nav"; +@import "./core/notification"; +@import "./core/pagination"; +@import "./core/progress"; +@import "./core/section"; +@import "./core/table"; +@import "./core/tabs"; +@import "./core/tag"; +@import "./core/title"; +@import "./core/typography"; + +// Add unique core extensions +@import "./core/page-layout"; +@import "./core/bumper"; diff --git a/ui/app/styles/core/bumper.scss b/ui/app/styles/core/bumper.scss new file mode 100644 index 000000000..97c3f0134 --- /dev/null +++ b/ui/app/styles/core/bumper.scss @@ -0,0 +1,7 @@ +.bumper-left { + margin-left: 1.5rem; +} + +.bumper-right { + margin-right: 1.5rem; +} diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss new file mode 100644 index 000000000..0a0c69c62 --- /dev/null +++ b/ui/app/styles/core/buttons.scss @@ -0,0 +1,101 @@ +$button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2); + +.button { + font-weight: $weight-bold; + box-shadow: $button-box-shadow-standard; + border: 1px solid transparent; + + &:active, + &.is-active, + &:focus, + &.is-focused { + box-shadow: $button-box-shadow-standard; + } + + &.is-inverted.is-outlined { + box-shadow: none; + } + + &.is-compact { + padding: 0.25em 0.75em; + margin: -0.25em -0.25em -0.25em 0; + + &.pull-right { + margin-right: -1em; + } + } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + $color-invert: nth($pair, 2); + + &.is-#{$name} { + border-color: darken($color, 10%); + + &:hover, + &.is-hovered { + background-color: lighten($color, 5%); + border-color: darken($color, 10%); + } + + &:active, + &.is-active { + background-color: darken($color, 5%); + border-color: darken($color, 10%); + box-shadow: $button-box-shadow-standard; + } + + &:focus, + &.is-focused { + border-color: darken($color, 10%); + box-shadow: $button-box-shadow-standard; + } + + &.is-outlined { + border-color: $grey-lighter; + + &.is-important { + border-color: $color; + } + + &:hover, + &.is-hovered, + &:focus, + &.is-focused { + background-color: transparent; + border-color: darken($color, 10%); + color: $color; + } + + &:active, + &.is-active { + background-color: transparent; + border-color: darken($color, 10%); + color: darken($color, 10%); + } + } + + &.is-inverted.is-outlined { + border-color: rgba($color-invert, 0.5); + color: rgba($color-invert, 0.9); + + &:hover, + &.is-hovered, + &:focus, + &.is-focused { + background-color: transparent; + border-color: $color-invert; + color: $color-invert; + } + + &:active, + &.is-active { + background-color: rgba($color-invert, 0.2); + border-color: $color-invert; + color: $color-invert; + box-shadow: none; + } + } + } + } +} diff --git a/ui/app/styles/core/columns.scss b/ui/app/styles/core/columns.scss new file mode 100644 index 000000000..910595047 --- /dev/null +++ b/ui/app/styles/core/columns.scss @@ -0,0 +1,13 @@ +.columns { + .column { + &.is-centered { + align-self: center; + justify-self: center; + text-align: center; + } + + &.is-minimum { + flex-grow: 0; + } + } +} diff --git a/ui/app/styles/core/forms.scss b/ui/app/styles/core/forms.scss new file mode 100644 index 000000000..741dd892e --- /dev/null +++ b/ui/app/styles/core/forms.scss @@ -0,0 +1,37 @@ +@mixin input { + @include control; + background-color: #fff; + border-color: $grey-blue; + color: $text; + + &:hover, &.is-hovered, &:active, &.is-active, &:focus, &.is-focused { + border-color: darken($grey-blue, 5%); + } + + &[disabled] { + background-color: $grey-blue; + border-color: darken($grey-blue, 5%); + } +} + +.input, .textarea { + @include input; + box-shadow: none; + padding: 0.75em 1.5em; + height: auto; + + &::placeholder { + color: $grey-blue; + } + + &.is-compact { + padding: 0.25em 0.75em; + margin: -0.25em -0.25em -0.25em 0; + } +} + +.field { + &.is-inline { + display: inline-block; + } +} diff --git a/ui/app/styles/core/level.scss b/ui/app/styles/core/level.scss new file mode 100644 index 000000000..01d65ffcc --- /dev/null +++ b/ui/app/styles/core/level.scss @@ -0,0 +1,11 @@ +.level { + .level-item { + &.is-pulled-right { + justify-content: flex-end; + } + + &.is-pulled-left { + justify-content: flex-start; + } + } +} diff --git a/ui/app/styles/core/menu.scss b/ui/app/styles/core/menu.scss new file mode 100644 index 000000000..e137fc1e9 --- /dev/null +++ b/ui/app/styles/core/menu.scss @@ -0,0 +1,35 @@ +.menu { + .menu-list { + a { + font-weight: $weight-semibold; + padding: 0.5rem 1.5rem; + border-radius: 0; + box-shadow: inset 0 0 0 $grey-blue; + transition: box-shadow 0.1s ease-in-out; + text-decoration: none; + + &:hover, + &.is-active { + background: transparent; + box-shadow: inset -3px 0 0 $blue; + color: $blue; + } + } + } + + .menu-label { + margin: 1rem 0.75rem 0.5rem; + padding-top: 0.5rem; + color: darken($grey-blue, 20%); + letter-spacing: 0; + + &:first-child { + margin-top: 0; + padding-top: 1rem; + } + + &:not(:first-child) { + border-top: 1px solid $grey-blue; + } + } +} diff --git a/ui/app/styles/core/message.scss b/ui/app/styles/core/message.scss new file mode 100644 index 000000000..b5e71e6b7 --- /dev/null +++ b/ui/app/styles/core/message.scss @@ -0,0 +1,25 @@ +.message { + background: $body-background; + + .message-header { + background: $body-background; + color: $text; + font-size: $size-5; + font-weight: $weight-semibold; + padding-left: 0; + padding-right: 0; + } + + .message-body { + border: 1px solid $grey-lighter; + border-radius: $radius; + + &.is-full-bleed { + padding: 0; + } + + .table { + margin-bottom: 0; + } + } +} diff --git a/ui/app/styles/core/nav.scss b/ui/app/styles/core/nav.scss new file mode 100644 index 000000000..fc0df0c7c --- /dev/null +++ b/ui/app/styles/core/nav.scss @@ -0,0 +1,65 @@ +.nav { + &.is-primary { + background: linear-gradient( + to right, + $nomad-green-darker, + $nomad-green-dark + ); + height: 3.5rem; + color: $primary-invert; + padding-left: 20px; + padding-right: 20px; + + .nav-item { + color: rgba($primary-invert, 0.8); + text-decoration: none; + + &:hover { + color: $primary-invert; + } + + &.is-active, + &.active { + color: $primary-invert; + border-bottom-color: $primary-invert; + } + + + .nav-item { + position: relative; + + &::before { + width: 1px; + height: 1em; + background: rgba($primary-invert, 0.5); + content: " "; + display: block; + position: absolute; + left: 0px; + } + } + + &.is-logo img { + height: 26px; + max-height: 26px; + } + } + } + + &.is-secondary { + background-color: $nomad-green-dark; + padding: 1.25rem 20px 1.25rem 0; + height: 4.5rem; + font-weight: $weight-semibold; + color: $primary-invert; + + .nav-item { + font-size: $size-4; + } + } + + .nav-item { + &.is-gutter { + width: $gutter-width; + } + } +} diff --git a/ui/app/styles/core/notification.scss b/ui/app/styles/core/notification.scss new file mode 100644 index 000000000..b41a63c8b --- /dev/null +++ b/ui/app/styles/core/notification.scss @@ -0,0 +1,44 @@ +.notification { + padding: 0.5rem 1rem; + border: 1px solid $grey-blue; + + &.is-pending { + background: $grey-blue; + color: findColorInvert(darken($grey-blue, 10%)); + border-color: $grey-blue; + } + + &.is-running { + background: lighten($blue, 40%); + color: $blue; + border-color: $blue; + } + + &.is-error { + background: lighten($danger, 40%); + color: $danger; + border-color: $danger; + } + + &.is-cancelled { + background: lighten($orange, 40%); + color: $orange; + border-color: $orange; + } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + + &.is-#{$name} { + background: lighten($color, 40%); + color: $color; + border-color: $color; + } + } + + &.is-light { + background: lighten($white-ter, 5%); + color: darken($grey-blue, 20%); + border-color: $grey-blue; + } +} diff --git a/ui/app/styles/core/page-layout.scss b/ui/app/styles/core/page-layout.scss new file mode 100644 index 000000000..26c8de78e --- /dev/null +++ b/ui/app/styles/core/page-layout.scss @@ -0,0 +1,46 @@ +html, body, body > .ember-view { + height: 100%; + width: 100%; +} + +.page-layout { + height: 100%; + display: flex; + flex-direction: column; + + .page-header { + position: fixed; + width: 100%; + z-index: $z-header; + + // Defensive styles in case header height goes over 100px, causing + // the left gutter menu to be on top of the header. + height: $header-height; + overflow: hidden; + } + + .page-body { + display: flex; + flex: 1; + flex-direction: row; + justify-content: space-between; + margin-top: $header-height; + + .page-column { + flex: 1; + + &.is-left { + min-width: $gutter-width; + max-width: $gutter-width; + position: fixed; + bottom: 0; + top: $header-height; + z-index: $z-gutter; + } + + &.is-right { + margin-left: $gutter-width; + } + } + } +} diff --git a/ui/app/styles/core/pagination.scss b/ui/app/styles/core/pagination.scss new file mode 100644 index 000000000..c33495a73 --- /dev/null +++ b/ui/app/styles/core/pagination.scss @@ -0,0 +1,30 @@ +.pagination { + color: $grey; + + .pagination-numbers { + padding: 0.75rem 0.5rem; + white-space: nowrap; + order: 2; + } + + .pagination-previous, + .pagination-next, + .pagination-link { + color: $text; + border: none; + border-radius: 0; + margin: 0; + padding: 0.75rem 0.5rem; + height: auto; + text-decoration: none; + + &:hover { + box-shadow: none; + background-color: darken($white-ter, 5%); + } + + &:active { + background-color: darken($white-ter, 10%); + } + } +} diff --git a/ui/app/styles/core/progress.scss b/ui/app/styles/core/progress.scss new file mode 100644 index 000000000..bae86d9fd --- /dev/null +++ b/ui/app/styles/core/progress.scss @@ -0,0 +1,5 @@ +.progress { + &.is-small { + height: 6px; + } +} diff --git a/ui/app/styles/core/section.scss b/ui/app/styles/core/section.scss new file mode 100644 index 000000000..1543ec9a8 --- /dev/null +++ b/ui/app/styles/core/section.scss @@ -0,0 +1,4 @@ +.section { + padding: 1.5rem; + max-width: 1200px; +} diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss new file mode 100644 index 000000000..171379635 --- /dev/null +++ b/ui/app/styles/core/table.scss @@ -0,0 +1,231 @@ +.table { + color: $text; + border-radius: $radius; + border: 1px solid $grey-blue; + border-collapse: separate; + + &.is-fixed { + table-layout: fixed; + + td { + text-overflow: ellipsis; + overflow: hidden; + } + } + + &.with-foot { + margin-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + + .table-foot { + margin-bottom: 1.5rem; + } + } + + &.is-compact { + th, + td { + padding: 0.75em; + } + } + + &.is-darkened { + tbody tr:not(.is-selected) { + background-color: $white-bis; + + &:hover { + background-color: $white-ter; + } + } + + &.is-striped { + tbody tr:not(.is-selected) { + &:nth-child(even) { + background-color: $white-ter; + + &:hover { + background-color: darken($white-ter, 5%); + } + } + } + } + } + + th, + td { + padding: 0.75em 1.5em; + border: none; + + &.is-three-quarters { + width: 75%; + } + + &.is-two-thirds { + width: 66.66%; + } + + &.is-half { + width: 50%; + } + + &.is-one-third { + width: 33.33%; + } + + &.is-one-quarter { + width: 25%; + } + + // Only use px modifiers when text needs to be truncated. + // In this and only this scenario are percentages not effective. + &.is-200px { + width: 200px; + max-width: 200px; + } + + &.is-truncatable { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + @for $i from 1 through 11 { + &.is-#{$i} { + width: 100% / 12 * $i; + max-width: 100% / 12 * $i; + } + } + + a { + color: $blue; + text-decoration: underline; + font-weight: $weight-normal; + + &.is-primary { + color: $text; + text-decoration: none; + font-weight: $weight-semibold; + } + } + } + + thead { + background: $white-ter; + border: 1px solid $grey-blue; + + tr { + &:hover { + background-color: inherit; + } + } + + td, + th { + color: $grey-light; + font-weight: $weight-normal; + vertical-align: bottom; + border-bottom: 1px solid $grey-blue; + + &.is-selectable { + padding: 0; + + a { + display: block; + padding: 0.75em 1.5em; + width: 100%; + text-decoration: none; + + &:hover { + background-color: darken($white-ter, 5%); + } + } + } + + &.is-active { + position: relative; + + &::after { + content: ""; + width: 10px; + right: 1.5em; + top: 0.75em; + bottom: 0.75em; + position: absolute; + display: block; + } + + &.asc::after { + content: "⬇"; + } + + &.desc::after { + content: "⬆"; + } + } + + a { + color: $grey; + } + } + } + + tbody { + tr { + &.is-interactive { + cursor: pointer; + box-sizing: border-box; + } + + &.is-active { + background: rgba($blue, 0.1); + + td:first-child { + position: relative; + + &::after { + position: absolute; + content: ""; + width: 3px; + top: 0; + bottom: 0; + left: -1px; + display: block; + background: $blue; + } + } + } + } + + td.is-subheading { + font-weight: $weight-bold; + background: $white; + color: $blue; + } + + td { + border: 1px solid $grey-blue; + border-width: 0 0 1px; + padding: 1.25em 1.5em; + } + } + + .is-faded { + color: $grey-light; + } +} + +.table tfoot, +.table-foot { + margin-top: -1px; + background: $white-ter; + border: 1px solid $grey-blue; + border-bottom-right-radius: $radius; + border-bottom-left-radius: $radius; + overflow: hidden; + + .pagination { + padding: 0; + margin: 0; + } +} diff --git a/ui/app/styles/core/tabs.scss b/ui/app/styles/core/tabs.scss new file mode 100644 index 000000000..b3c033c02 --- /dev/null +++ b/ui/app/styles/core/tabs.scss @@ -0,0 +1,46 @@ +.tabs { + background: $white-ter; + font-weight: $weight-semibold; + + ul { + padding-left: 1em; + padding-right: 1em; + } + + a { + padding: 1em 1.5em; + color: darken($grey-blue, 20%); + border-bottom: none; + box-shadow: inset 0 0 0 $grey-blue; + transition: box-shadow 0.1s ease-in-out; + text-decoration: none; + + &:hover, + &.is-active { + text-decoration: none; + border-bottom: none; + box-shadow: inset 0 -3px 0 $blue; + color: $blue; + } + } + + li:first-child { + padding-left: 0; + } + + li:last-child { + padding-right: 0; + } + + &.is-subnav { + position: fixed; + top: $header-height; + left: $gutter-width; + right: 0; + z-index: $z-subnav; + + + * { + margin-top: 5em; + } + } +} diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss new file mode 100644 index 000000000..dd7f31e0f --- /dev/null +++ b/ui/app/styles/core/tag.scss @@ -0,0 +1,42 @@ +.tag { + text-transform: uppercase; + border-radius: 200px; + font-weight: $weight-normal; + font-size: $size-7; + line-height: 1; + height: 1.5em; + + &.is-pending { + background: $grey-blue; + color: findColorInvert(darken($grey-blue, 10%)); + } + + &.is-running { + background: $blue; + color: $blue-invert; + } + + &.is-error { + background: $danger; + color: $danger-invert; + } + + &.is-cancelled { + background: $orange; + color: $orange-invert; + } + + &.is-hollow { + font-weight: $weight-semibold; + color: darken($grey-blue, 20%); + background: transparent; + } + + &.no-text-transform { + text-transform: none; + } + + &.is-outlined { + box-shadow: 0 0 0 1px $white; + } +} diff --git a/ui/app/styles/core/title.scss b/ui/app/styles/core/title.scss new file mode 100644 index 000000000..f6464e8e8 --- /dev/null +++ b/ui/app/styles/core/title.scss @@ -0,0 +1,7 @@ +.title { + &.is-4, + &.is-5, + &.is-6 { + margin-bottom: 0.5rem; + } +} diff --git a/ui/app/styles/core/typography.scss b/ui/app/styles/core/typography.scss new file mode 100644 index 000000000..9e0730cf9 --- /dev/null +++ b/ui/app/styles/core/typography.scss @@ -0,0 +1,21 @@ +a { + color: $blue; + text-decoration: underline; + + &:hover { + color: $blue; + text-decoration: underline; + } +} + +code { + color: currentColor; + background: transparent; + text-transform: none; + padding: 0; + font-size: 1em; +} + +.nowrap { + white-space: nowrap; +} diff --git a/ui/app/styles/utils/product-colors.scss b/ui/app/styles/utils/product-colors.scss new file mode 100644 index 000000000..689b9e84e --- /dev/null +++ b/ui/app/styles/utils/product-colors.scss @@ -0,0 +1,15 @@ +$consul-pink: #FF0087; +$consul-pink-dark: #C62A71; + +$packer-blue: #1DAEFF; +$packer-blue-dark: #1D94DD; + +$terraform-purple: #5C4EE5; +$terraform-purple-dark: #4040B2; + +$vagrant-blue: #1563FF; +$vagrant-blue-dark: #104EB2; + +$nomad-green: #25BA81; +$nomad-green-dark: #1d9467; +$nomad-green-darker: #16704D; diff --git a/ui/app/styles/utils/z-indices.scss b/ui/app/styles/utils/z-indices.scss new file mode 100644 index 000000000..bc6a2860c --- /dev/null +++ b/ui/app/styles/utils/z-indices.scss @@ -0,0 +1,5 @@ +$z-tooltip: 2500; +$z-header: 2000; +$z-gutter: 2000; +$z-subnav: 2000; +$z-base: 100; diff --git a/ui/app/templates/allocations.hbs b/ui/app/templates/allocations.hbs new file mode 100644 index 000000000..ce0bdbbe5 --- /dev/null +++ b/ui/app/templates/allocations.hbs @@ -0,0 +1,8 @@ +
+ {{#global-header class="page-header"}} + Allocations + {{/global-header}} + {{#gutter-menu class="page-body"}} + {{outlet}} + {{/gutter-menu}} +
diff --git a/ui/app/templates/allocations/allocation.hbs b/ui/app/templates/allocations/allocation.hbs new file mode 100644 index 000000000..2e5cbbfb5 --- /dev/null +++ b/ui/app/templates/allocations/allocation.hbs @@ -0,0 +1,91 @@ +
+

Allocation {{model.name}}

+

+ For job {{model.job.name}} + on node {{model.node.shortId}} +

+ +
+
+ Tasks +
+ {{#list-table + source=sortedStates + sortProperty=sortProperty + sortDescending=sortDescending + class="is-striped tasks" as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="state"}}State{{/t.sort-by}} + Last Event + {{#t.sort-by prop="events.lastObject.time"}}Time{{/t.sort-by}} + Addresses + {{/t.head}} + {{#t.body as |row|}} + + {{row.model.task.name}} + {{row.model.state}} + + {{#if row.model.events.lastObject.displayMessage}} + {{row.model.events.lastObject.displayMessage}} + {{else}} + No message + {{/if}} + + {{moment-format row.model.events.lastObject.time "MM/DD/YY HH:mm:ss [UTC]"}} + + + + + {{/t.body}} + {{/list-table}} +
+ + {{#each model.states as |state|}} +
+
+ {{state.task.name}} ({{state.state}}) Started: {{moment-format state.startedAt "MM/DD/YY HH:mm:ss [UTC]"}} + {{#unless state.isActive}} + Ended: {{moment-format state.finishedAt "MM/DD/YY HH:mm:ss [UTC]"}} + {{/unless}} +
+ + + + + + + + + + {{#each (reverse state.events) as |event|}} + + + + + + {{/each}} + +
TimeTypeDescription
{{moment-format event.time "MM/DD/YY HH:mm:ss [UTC]"}}{{event.type}} + {{#if event.displayMessage}} + {{event.displayMessage}} + {{else}} + No message + {{/if}} +
+
+ {{/each}} +
diff --git a/ui/app/templates/application.hbs b/ui/app/templates/application.hbs new file mode 100644 index 000000000..dceb76dfa --- /dev/null +++ b/ui/app/templates/application.hbs @@ -0,0 +1,2 @@ +{{partial "svg-patterns"}} +{{outlet}} diff --git a/ui/app/templates/components/.gitkeep b/ui/app/templates/components/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs new file mode 100644 index 000000000..9da0e3722 --- /dev/null +++ b/ui/app/templates/components/allocation-row.hbs @@ -0,0 +1,49 @@ + + + {{allocation.shortId}} + + +{{allocation.name}} + + {{allocation.clientStatus}} + +{{#if (eq context "job")}} + {{allocation.node.shortId}} +{{else if (eq context "node")}} + + {{allocation.job.name}} + / {{allocation.taskGroup.name}} + +{{/if}} + + {{#if allocation.stats.isPending}} + ... + {{else if allocation.stats.isRejected}} + !!! + {{else}} +
+ + {{allocation.percentCPU}} + +
+ {{/if}} + + + {{#if allocation.stats.isPending}} + ... + {{else if allocation.stats.isRejected}} + !!! + {{else}} +
+ + {{allocation.percentMemory}} + +
+ {{/if}} + diff --git a/ui/app/templates/components/attributes-section.hbs b/ui/app/templates/components/attributes-section.hbs new file mode 100644 index 000000000..dd55bb591 --- /dev/null +++ b/ui/app/templates/components/attributes-section.hbs @@ -0,0 +1,18 @@ +{{#each-in attributes as |key value|}} + {{#if (is-object value)}} + + + {{#if prefix}}{{prefix}}.{{/if}}{{key}} + + + {{attributes-section prefix=(if prefix (concat prefix '.' key) key) attributes=value}} + {{else}} + + + {{#if prefix}}{{prefix}}.{{/if}} + {{~key}} + + {{value}} + + {{/if}} +{{/each-in}} diff --git a/ui/app/templates/components/attributes-table.hbs b/ui/app/templates/components/attributes-table.hbs new file mode 100644 index 000000000..f81006c22 --- /dev/null +++ b/ui/app/templates/components/attributes-table.hbs @@ -0,0 +1,11 @@ + + + + + + + + + {{attributes-section attributes=attributes}} + +
NameValue
diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs new file mode 100644 index 000000000..a49a7e5e8 --- /dev/null +++ b/ui/app/templates/components/client-node-row.hbs @@ -0,0 +1,13 @@ +{{node.shortId}} +{{node.name}} +{{node.status}} +{{node.address}} +{{node.port}} +{{node.datacenter}} + + {{#if node.allocations.isPending}} + ... + {{else}} + {{node.allocations.length}} + {{/if}} + diff --git a/ui/app/templates/components/distribution-bar.hbs b/ui/app/templates/components/distribution-bar.hbs new file mode 100644 index 000000000..30dcd3827 --- /dev/null +++ b/ui/app/templates/components/distribution-bar.hbs @@ -0,0 +1,28 @@ + + + + + + + + +{{#if hasBlock}} + {{yield (hash + data=_data + activeDatum=activeDatum + )}} +{{else}} +
+
    + {{#each _data as |datum index|}} +
  1. + + + {{datum.label}} + + {{datum.value}} +
  2. + {{/each}} +
+
+{{/if}} diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs new file mode 100644 index 000000000..41eeba082 --- /dev/null +++ b/ui/app/templates/components/global-header.hbs @@ -0,0 +1,22 @@ + + diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs new file mode 100644 index 000000000..556cfa37c --- /dev/null +++ b/ui/app/templates/components/gutter-menu.hbs @@ -0,0 +1,22 @@ +
+
+ +
+
+
+ {{yield}} +
diff --git a/ui/app/templates/components/job-deployment-details.hbs b/ui/app/templates/components/job-deployment-details.hbs new file mode 100644 index 000000000..8ec04e76b --- /dev/null +++ b/ui/app/templates/components/job-deployment-details.hbs @@ -0,0 +1,5 @@ +{{yield (hash + metrics=(component "job-deployment/deployment-metrics" deployment=deployment) + taskGroups=(component "job-deployment/task-groups" deployment=deployment) + allocations=(component "job-deployment/deployment-allocations" deployment=deployment) +)}} diff --git a/ui/app/templates/components/job-deployment.hbs b/ui/app/templates/components/job-deployment.hbs new file mode 100644 index 000000000..2d7a4cfd2 --- /dev/null +++ b/ui/app/templates/components/job-deployment.hbs @@ -0,0 +1,24 @@ +
+ {{deployment.shortId}} + {{deployment.status}} + {{#if deployment.requiresPromotion}} + Requires Promotion + {{/if}} + + + Version + #{{deployment.version.number}} + | {{moment-from-now deployment.version.submitTime}} + + + +
+{{#if isOpen}} +
+ {{#job-deployment-details deployment=deployment as |d|}} + {{d.metrics}} + {{d.taskGroups}} + {{d.allocations}} + {{/job-deployment-details}} +
+{{/if}} diff --git a/ui/app/templates/components/job-deployment/deployment-allocations.hbs b/ui/app/templates/components/job-deployment/deployment-allocations.hbs new file mode 100644 index 000000000..d00cdec8d --- /dev/null +++ b/ui/app/templates/components/job-deployment/deployment-allocations.hbs @@ -0,0 +1,22 @@ +
+
+ Allocations +
+
+ {{#list-table + source=deployment.allocations + class="allocations" as |t|}} + {{#t.head}} + ID + Name + Status + Node + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job"}} + {{/t.body}} + {{/list-table}} +
+
diff --git a/ui/app/templates/components/job-deployment/deployment-metrics.hbs b/ui/app/templates/components/job-deployment/deployment-metrics.hbs new file mode 100644 index 000000000..dae3390ab --- /dev/null +++ b/ui/app/templates/components/job-deployment/deployment-metrics.hbs @@ -0,0 +1,40 @@ +
+
+
+
+

Canaries

+

{{deployment.placedCanaries}} / {{deployment.desiredCanaries}}

+
+
+ +
+
+

Placed

+

{{deployment.placedAllocs}}

+
+
+

Desired

+

{{deployment.desiredTotal}}

+
+
+ +
+
+

Healthy

+

{{deployment.healthyAllocs}}

+
+
+ +
+
+

Unhealthy

+

{{deployment.unhealthyAllocs}}

+
+
+
+
+
+ {{deployment.statusDescription}} +
+
+
diff --git a/ui/app/templates/components/job-deployment/task-groups.hbs b/ui/app/templates/components/job-deployment/task-groups.hbs new file mode 100644 index 000000000..1b0ebcc76 --- /dev/null +++ b/ui/app/templates/components/job-deployment/task-groups.hbs @@ -0,0 +1,38 @@ +
+
+ Task Groups +
+
+ {{#list-table + source=deployment.taskGroupSummaries + class="task-groups" as |t|}} + {{#t.head}} + Name + Needs Promotion? + Auto Revert? + Canaries + Allocs + Healthy Allocs + Unhealthy Allocs + {{/t.head}} + {{#t.body as |row|}} + + {{row.model.name}} + + {{#if row.model.requiresPromotion}} + {{if row.model.promoted "No" "Yes"}} + {{else}} + N/A + {{/if}} + + {{if row.model.autoRevert "Yes" "No"}} + {{or row.model.placedCanaries 0}} / {{row.model.desiredCanaries}} + {{row.model.placedAllocs}} / {{row.model.desiredTotal}} + {{row.model.healthyAllocs}} + {{row.model.unhealthyAllocs}} + + {{/t.body}} + {{/list-table}} +
+
+ diff --git a/ui/app/templates/components/job-deployments-stream.hbs b/ui/app/templates/components/job-deployments-stream.hbs new file mode 100644 index 000000000..b92050b13 --- /dev/null +++ b/ui/app/templates/components/job-deployments-stream.hbs @@ -0,0 +1,14 @@ +{{#each annotatedDeployments as |record|}} + {{#if record.meta.showDate}} +
  • + {{#if record.deployment.version.submitTime}} + {{moment-format record.deployment.version.submitTime "MMMM D, YYYY"}} + {{else}} + Unknown time + {{/if}} +
  • + {{/if}} +
  • + {{job-deployment deployment=record.deployment}} +
  • +{{/each}} diff --git a/ui/app/templates/components/job-diff-fields-and-objects.hbs b/ui/app/templates/components/job-diff-fields-and-objects.hbs new file mode 100644 index 000000000..7da3703c8 --- /dev/null +++ b/ui/app/templates/components/job-diff-fields-and-objects.hbs @@ -0,0 +1,52 @@ +
    + {{#each fields as |field|}} +
    + + + {{#if (eq (lowercase field.Type) "added")}} + + + {{else if (eq (lowercase field.Type) "deleted")}} + - + {{else if (eq (lowercase field.Type) "edited")}} + +/- + {{/if}} + + {{field.Name}}: + + {{#if (eq (lowercase field.Type) "added")}} + "{{field.New}}" + {{else if (eq (lowercase field.Type) "deleted")}} + "{{field.Old}}" + {{else if (eq (lowercase field.Type) "edited")}} + "{{field.Old}}" => "{{field.New}}" + {{else}} + "{{field.New}}" + {{/if}} +
    + {{/each}} +
    + +{{#each objects as |object|}} +
    + + {{#if (eq (lowercase object.Type) "added")}} + + + {{else if (eq (lowercase object.Type) "deleted")}} + - + {{else if (eq (lowercase object.Type) "edited")}} + +/- + {{/if}} + + {{object.Name}} { + + } +
    +{{/each}} diff --git a/ui/app/templates/components/job-diff.hbs b/ui/app/templates/components/job-diff.hbs new file mode 100644 index 000000000..7a3311a50 --- /dev/null +++ b/ui/app/templates/components/job-diff.hbs @@ -0,0 +1,88 @@ +{{!-- Job heading --}} +
    + + {{#if (eq (lowercase diff.Type) "added")}} + + + {{else if (eq (lowercase diff.Type) "deleted")}} + - + {{else if (eq (lowercase diff.Type) "edited")}} + +/- + {{/if}} + + Job: "{{diff.ID}}" +
    + +{{!-- Show job field and object diffs if the job is edited --}} +{{#if (or verbose (eq (lowercase diff.Type) "edited"))}} + +{{/if}} + +{{!-- Each task group --}} +{{#each diff.TaskGroups as |group|}} +
    + + {{#if (eq (lowercase group.Type) "added")}} + + + {{else if (eq (lowercase group.Type) "deleted")}} + - + {{else if (eq (lowercase group.Type) "edited")}} + +/- + {{/if}} + + Task Group: "{{group.Name}}" + {{#if group.Updates}} + ({{#each-in group.Updates as |updateType count|}} + {{count}} {{updateType}} + {{/each-in}}) + {{/if}} + + {{!-- Show task group field and object diffs if the task group is edited --}} + {{#if (or (eq (lowercase group.Type) "edited") verbose)}} + + {{/if}} + + {{!-- Each task --}} + {{#each group.Tasks as |task|}} +
    + + {{#if (eq (lowercase task.Type) "added")}} + + + {{else if (eq (lowercase task.Type) "deleted")}} + - + {{else if (eq (lowercase task.Type) "edited")}} + +/- + {{/if}} + + Task: "{{task.Name}}" + {{#if task.Annotations}} + ({{#each task.Annotations as |annotation index|}} + {{annotation}} + {{#unless (eq index (dec annotations.length))}},{{/unless}} + {{/each}}) + {{/if}} + {{#if (or verbose (eq (lowercase task.Type "edited")))}} + {{job-diff-fields-and-objects fields=task.Fields objects=task.Objects}} + {{/if}} +
    + {{/each}} +
    +{{/each}} diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs new file mode 100644 index 000000000..36a3d8d50 --- /dev/null +++ b/ui/app/templates/components/job-row.hbs @@ -0,0 +1,10 @@ +{{job.name}} + + {{job.status}} + +{{job.type}} +{{job.priority}} +{{job.taskGroups.length}} + +
    {{allocation-status-bar allocationContainer=job isNarrow=true}}
    + diff --git a/ui/app/templates/components/job-version.hbs b/ui/app/templates/components/job-version.hbs new file mode 100644 index 000000000..d82bea583 --- /dev/null +++ b/ui/app/templates/components/job-version.hbs @@ -0,0 +1,21 @@ +
    + Version #{{version.number}} + + Stable + {{version.stable}} + + + Submitted + {{moment-format version.submitTime "MM/DD/YY HH:mm:ss [UTC]"}} + + {{#if version.diff}} + + {{else}} + No Changes + {{/if}} +
    +{{#if isOpen}} +
    + {{job-diff diff=version.diff verbose=verbose}} +
    +{{/if}} diff --git a/ui/app/templates/components/job-versions-stream.hbs b/ui/app/templates/components/job-versions-stream.hbs new file mode 100644 index 000000000..a9fc61720 --- /dev/null +++ b/ui/app/templates/components/job-versions-stream.hbs @@ -0,0 +1,10 @@ +{{#each annotatedVersions as |record|}} + {{#if record.meta.showDate}} +
  • + {{moment-format record.version.submitTime "MMMM D, YYYY"}} +
  • + {{/if}} +
  • + {{job-version version=record.version verbose=verbose}} +
  • +{{/each}} diff --git a/ui/app/templates/components/list-pagination.hbs b/ui/app/templates/components/list-pagination.hbs new file mode 100644 index 000000000..bb86e606a --- /dev/null +++ b/ui/app/templates/components/list-pagination.hbs @@ -0,0 +1,16 @@ +{{#if source.length}} + {{yield (hash + first=(component "list-pagination/list-pager" page=1 visible=(not (eq page 1))) + prev=(component "list-pagination/list-pager" page=(dec page) visible=(not (eq page 1))) + next=(component "list-pagination/list-pager" page=(inc page) visible=(not (eq page lastPage))) + last=(component "list-pagination/list-pager" page=lastPage visible=(not (eq page lastPage))) + pageLinks=pageLinks + currentPage=page + totalPages=lastPage + startsAt=startsAt + endsAt=endsAt + list=list +)}} +{{else}} + {{yield to="inverse"}} +{{/if}} diff --git a/ui/app/templates/components/list-pagination/list-pager.hbs b/ui/app/templates/components/list-pagination/list-pager.hbs new file mode 100644 index 000000000..cc3437927 --- /dev/null +++ b/ui/app/templates/components/list-pagination/list-pager.hbs @@ -0,0 +1,5 @@ +{{#if visible}} + {{#link-to (query-params currentPage=page) class=attrs.class}} + {{yield}} + {{/link-to}} +{{/if}} diff --git a/ui/app/templates/components/list-table.hbs b/ui/app/templates/components/list-table.hbs new file mode 100644 index 000000000..4480eca21 --- /dev/null +++ b/ui/app/templates/components/list-table.hbs @@ -0,0 +1,8 @@ +{{yield (hash + head=(component "list-table/table-head") + body=(component "list-table/table-body" rows=decoratedSource) + sort-by=(component "list-table/sort-by" + currentProp=sortProperty + sortDescending=sortDescending + ) +)}} diff --git a/ui/app/templates/components/list-table/sort-by.hbs b/ui/app/templates/components/list-table/sort-by.hbs new file mode 100644 index 000000000..64521875e --- /dev/null +++ b/ui/app/templates/components/list-table/sort-by.hbs @@ -0,0 +1,3 @@ +{{#link-to (query-params sortProperty=prop sortDescending=shouldSortDescending)}} + {{yield}} +{{/link-to}} diff --git a/ui/app/templates/components/list-table/table-body.hbs b/ui/app/templates/components/list-table/table-body.hbs new file mode 100644 index 000000000..b501b61ab --- /dev/null +++ b/ui/app/templates/components/list-table/table-body.hbs @@ -0,0 +1,3 @@ +{{#each rows key=key as |row|}} + {{yield row}} +{{/each}} diff --git a/ui/app/templates/components/list-table/table-head.hbs b/ui/app/templates/components/list-table/table-head.hbs new file mode 100644 index 000000000..ee1921105 --- /dev/null +++ b/ui/app/templates/components/list-table/table-head.hbs @@ -0,0 +1,3 @@ + + {{yield}} + diff --git a/ui/app/templates/components/search-box.hbs b/ui/app/templates/components/search-box.hbs new file mode 100644 index 000000000..a7217004d --- /dev/null +++ b/ui/app/templates/components/search-box.hbs @@ -0,0 +1,3 @@ +
    + +
    diff --git a/ui/app/templates/components/server-agent-row.hbs b/ui/app/templates/components/server-agent-row.hbs new file mode 100644 index 000000000..e74c0d4b4 --- /dev/null +++ b/ui/app/templates/components/server-agent-row.hbs @@ -0,0 +1,6 @@ +{{agent.name}} +{{agent.status}} +{{if agent.isLeader "True" "False"}} +{{agent.address}} +{{agent.serfPort}} +{{agent.datacenter}} diff --git a/ui/app/templates/components/task-group-row.hbs b/ui/app/templates/components/task-group-row.hbs new file mode 100644 index 000000000..36e95c60c --- /dev/null +++ b/ui/app/templates/components/task-group-row.hbs @@ -0,0 +1,8 @@ +{{taskGroup.name}} +{{taskGroup.count}} + +
    {{allocation-status-bar allocationContainer=taskGroup.summary isNarrow=true}}
    + +{{taskGroup.reservedCPU}} MHz +{{taskGroup.reservedMemory}} MiB +{{taskGroup.reservedEphemeralDisk}} MiB diff --git a/ui/app/templates/freestyle.hbs b/ui/app/templates/freestyle.hbs new file mode 100644 index 000000000..18f8a0ae1 --- /dev/null +++ b/ui/app/templates/freestyle.hbs @@ -0,0 +1,120 @@ +
    + {{#freestyle-guide title='Nomad UI'subtitle='Styles and Patterns'}} + {{#freestyle-section name='Buttons' as |section|}} + {{#freestyle-subsection name='Standard' section=section}} + {{#freestyle-usage 'buttons-standard' title='Standard Buttons'}} +
    + Button + White + Light + Dark + Black + Link +
    + + {{/freestyle-usage}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='Outlines' section=section}} + {{#freestyle-usage 'buttons-outlines' title='Outline Buttons'}} + + {{/freestyle-usage}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='Hollow' section=section}} + {{#freestyle-usage 'buttons-hollow' title='Hollow Buttons'}} + + {{/freestyle-usage}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='Sizing' section=section}} + {{#freestyle-usage 'buttons-sizing' title='Button Sizes'}} +
    + Small + Normal + Medium + Large +
    + {{/freestyle-usage}} + {{/freestyle-subsection}} + {{/freestyle-section}} + + {{#freestyle-section name='Distribution Bar' as |section|}} + {{#freestyle-subsection name='Standard' section=section}} + {{#freestyle-usage 'distribution-bar-standard'}} +
    + {{distribution-bar data=distributionBarData}} +
    + {{/freestyle-usage}} + {{#freestyle-annotation}} +
    + The distribution bar chart proportionally show data in a single bar. It includes a tooltip out of the box, assumes the size of the container element, and is designed to be styled with CSS. +
    + {{/freestyle-annotation}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='With Classes' section=section}} + {{#freestyle-usage 'distribution-bar-with-classes'}} +
    + {{distribution-bar data=distributionBarDataWithClasses}} +
    + {{/freestyle-usage}} + {{#freestyle-annotation}} +
    + If a datum provides a className property, it will be assigned to the corresponding rect element, allowing for custom colorization. +
    + {{/freestyle-annotation}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='Flexible Sizing' section=section}} + {{#freestyle-usage 'distribution-bar-sizing-1'}} +
    + {{distribution-bar data=distributionBarData}} +
    + {{/freestyle-usage}} + {{#freestyle-usage 'distribution-bar-sizing-2'}} +
    + {{distribution-bar data=distributionBarData}} +
    + {{/freestyle-usage}} + {{#freestyle-annotation}} +
    + Distribution bar assumes the dimensions of the container. +
    + {{/freestyle-annotation}} + {{/freestyle-subsection}} + + {{#freestyle-subsection name='Updating Data' section=section}} + {{#freestyle-usage 'distribution-bar-updating'}} +
    + {{distribution-bar data=distributionBarDataRotating}} +
    + {{/freestyle-usage}} + {{#freestyle-annotation}} +
    + Distribution bar animates with data changes. +
    + {{/freestyle-annotation}} + {{/freestyle-subsection}} + {{/freestyle-section}} +{{/freestyle-guide}} +
    diff --git a/ui/app/templates/index.hbs b/ui/app/templates/index.hbs new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/templates/jobs.hbs b/ui/app/templates/jobs.hbs new file mode 100644 index 000000000..8fab3547f --- /dev/null +++ b/ui/app/templates/jobs.hbs @@ -0,0 +1,3 @@ +
    + {{outlet}} +
    diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs new file mode 100644 index 000000000..4ab6544fe --- /dev/null +++ b/ui/app/templates/jobs/index.hbs @@ -0,0 +1,42 @@ +{{#global-header class="page-header"}} + Jobs +{{/global-header}} +{{#gutter-menu class="page-body"}} +
    +
    +
    {{search-box searchTerm=(mut searchTerm) placeholder="Search jobs..."}}
    +
    + {{#list-pagination + source=sortedJobs + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + {{#t.sort-by prop="type"}}Type{{/t.sort-by}} + {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}} + Groups + Allocation Status + {{/t.head}} + {{#t.body key="model.id" as |row|}} + {{job-row job=row.model onClick=(action "gotoJob" row.model)}} + {{/t.body}} + {{/list-table}} +
    + +
    + {{/list-pagination}} +
    +{{/gutter-menu}} diff --git a/ui/app/templates/jobs/job.hbs b/ui/app/templates/jobs/job.hbs new file mode 100644 index 000000000..c24cd6895 --- /dev/null +++ b/ui/app/templates/jobs/job.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/ui/app/templates/jobs/job/definition.hbs b/ui/app/templates/jobs/job/definition.hbs new file mode 100644 index 000000000..1d528f8f5 --- /dev/null +++ b/ui/app/templates/jobs/job/definition.hbs @@ -0,0 +1,15 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb|}} + {{breadcrumb.label}} + {{/each}} +{{/global-header}} +{{#gutter-menu class="page-body"}} + {{partial "jobs/job/subnav"}} +
    +
    +
    + {{json-viewer json=model.definition}} +
    +
    +
    +{{/gutter-menu}} diff --git a/ui/app/templates/jobs/job/deployments.hbs b/ui/app/templates/jobs/job/deployments.hbs new file mode 100644 index 000000000..b83b05ff4 --- /dev/null +++ b/ui/app/templates/jobs/job/deployments.hbs @@ -0,0 +1,11 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb|}} + {{breadcrumb.label}} + {{/each}} +{{/global-header}} +{{#gutter-menu class="page-body"}} + {{partial "jobs/job/subnav"}} +
    + {{job-deployments-stream deployments=deployments}} +
    +{{/gutter-menu}} diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs new file mode 100644 index 000000000..987af217f --- /dev/null +++ b/ui/app/templates/jobs/job/index.hbs @@ -0,0 +1,110 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb|}} + {{breadcrumb.label}} + {{/each}} +{{/global-header}} +{{#gutter-menu class="page-body"}} + {{partial "jobs/job/subnav"}} +
    +

    + {{model.name}} + {{model.status}} + {{#if model.periodic}} + periodic + {{else if model.parameterized}} + parameterized + {{/if}} +

    + +
    +
    + Type: {{model.type}} | + Priority: {{model.priority}} +
    +
    + +
    +
    +
    Allocation Status {{taskGroups.length}}
    +
    +
    + {{#allocation-status-bar allocationContainer=model class="split-view" as |chart|}} +
      + {{#each chart.data as |datum index|}} +
    1. + + {{datum.value}} + + {{datum.label}} + +
    2. + {{/each}} +
    + {{/allocation-status-bar}} +
    +
    + + {{#if model.runningDeployment}} +
    +
    +
    + Active Deployment + {{model.runningDeployment.shortId}} + {{#if model.runningDeployment.version.submitTime}} + {{moment-from-now model.runningDeployment.version.submitTime}} + {{/if}} +
    +
    + Running + {{#if model.runningDeployment.requiresPromotion}} + Deployment is running but requires promotion + {{/if}} +
    +
    +
    + {{#job-deployment-details deployment=model.runningDeployment as |d|}} + {{d.metrics}} + {{#if isShowingDeploymentDetails}} + {{d.taskGroups}} + {{d.allocations}} + {{/if}} + {{/job-deployment-details}} +
    + +
    + {{/if}} + +
    +
    + Task Groups +
    +
    + {{#list-pagination + source=sortedTaskGroups + sortProperty=sortProperty + sortDescending=sortDescending as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="count"}}Count{{/t.sort-by}} + {{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}} + {{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}} + {{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}} + {{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}} + {{/t.head}} + {{#t.body as |row|}} + {{task-group-row taskGroup=row.model onClick=(action "gotoTaskGroup" row.model)}} + {{/t.body}} + {{/list-table}} + {{/list-pagination}} +
    +
    +
    +{{/gutter-menu}} diff --git a/ui/app/templates/jobs/job/subnav.hbs b/ui/app/templates/jobs/job/subnav.hbs new file mode 100644 index 000000000..77e44a2f1 --- /dev/null +++ b/ui/app/templates/jobs/job/subnav.hbs @@ -0,0 +1,8 @@ +
    +
      +
    • {{#link-to "jobs.job.index" job activeClass="is-active"}}Overview{{/link-to}}
    • +
    • {{#link-to "jobs.job.definition" job activeClass="is-active"}}Definition{{/link-to}}
    • +
    • {{#link-to "jobs.job.versions" job activeClass="is-active"}}Versions{{/link-to}}
    • +
    • {{#link-to "jobs.job.deployments" job activeClass="is-active"}}Deployments{{/link-to}}
    • +
    +
    diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs new file mode 100644 index 000000000..fc39e5589 --- /dev/null +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -0,0 +1,97 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb|}} + {{breadcrumb.label}} + {{/each}} +{{/global-header}} +{{#gutter-menu class="page-body"}} +
    +
      +
    • {{#link-to "jobs.job.task-group" model.job model activeClass="is-active"}}Overview{{/link-to}}
    • +
    +
    +
    +

    + {{model.name}} +

    + +
    +
    + Task Group Details + + # Tasks {{model.tasks.length}} + Reserved CPU {{model.reservedCPU}} MHz + Reserved Memory {{model.reservedMemory}} MiB + Reserved Disk {{model.reservedEphemeralDisk}} MiB +
    +
    + +
    +
    +
    Allocation Status {{allocations.length}}
    +
    +
    + {{#allocation-status-bar allocationContainer=model.summary class="split-view" as |chart|}} +
      + {{#each chart.data as |datum index|}} +
    1. + + {{datum.value}} + + {{datum.label}} + +
    2. + {{/each}} +
    + {{/allocation-status-bar}} +
    +
    + +
    +
    + Allocations + {{search-box + searchTerm=(mut searchTerm) + placeholder="Search allocations..." + class="is-inline pull-right" + inputClass="is-compact"}} +
    +
    + {{#list-pagination + source=sortedAllocations + size=pageSize + page=currentPage + class="allocations" as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending as |t|}} + {{#t.head}} + {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="clientStatus"}}Status{{/t.sort-by}} + {{#t.sort-by prop="node.shortId"}}Node{{/t.sort-by}} + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}} + {{/t.body}} + {{/list-table}} + + {{/list-pagination}} +
    +
    +
    +{{/gutter-menu}} diff --git a/ui/app/templates/jobs/job/versions.hbs b/ui/app/templates/jobs/job/versions.hbs new file mode 100644 index 000000000..279e2a9ea --- /dev/null +++ b/ui/app/templates/jobs/job/versions.hbs @@ -0,0 +1,11 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb|}} + {{breadcrumb.label}} + {{/each}} +{{/global-header}} +{{#gutter-menu class="page-body"}} + {{partial "jobs/job/subnav"}} +
    + {{job-versions-stream versions=versions verbose=true}} +
    +{{/gutter-menu}} diff --git a/ui/app/templates/nodes.hbs b/ui/app/templates/nodes.hbs new file mode 100644 index 000000000..8fab3547f --- /dev/null +++ b/ui/app/templates/nodes.hbs @@ -0,0 +1,3 @@ +
    + {{outlet}} +
    diff --git a/ui/app/templates/nodes/index.hbs b/ui/app/templates/nodes/index.hbs new file mode 100644 index 000000000..580adbbae --- /dev/null +++ b/ui/app/templates/nodes/index.hbs @@ -0,0 +1,43 @@ +{{#global-header class="page-header"}} + Nodes +{{/global-header}} +{{#gutter-menu class="page-body"}} +
    +
    +
    {{search-box searchTerm=(mut searchTerm) placeholder="Search nodes..."}}
    +
    + {{#list-pagination + source=sortedNodes + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="id"}}ID{{/t.sort-by}} + {{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + Address + Port + {{#t.sort-by prop="datacenter"}}Datacenter{{/t.sort-by}} + # Allocs + {{/t.head}} + {{#t.body as |row|}} + {{client-node-row node=row.model onClick=(action "gotoNode" row.model)}} + {{/t.body}} + {{/list-table}} +
    + +
    + {{/list-pagination}} +
    +{{/gutter-menu}} diff --git a/ui/app/templates/nodes/node.hbs b/ui/app/templates/nodes/node.hbs new file mode 100644 index 000000000..d44a40eab --- /dev/null +++ b/ui/app/templates/nodes/node.hbs @@ -0,0 +1,76 @@ +{{#global-header class="page-header"}} + Nodes + {{model.shortId}} +{{/global-header}} +{{#gutter-menu class="page-body"}} +
    +

    + + {{or model.name model.shortId}} + {{model.id}} +

    + +
    +
    + Client Details + Status {{model.status}} + Address {{model.httpAddr}} + Datacenter {{model.datacenter}} +
    +
    + +
    +
    +
    Allocations {{model.allocations.length}}
    + {{search-box + searchTerm=(mut searchTerm) + placeholder="Search allocations..." + class="is-inline pull-right" + inputClass="is-compact"}} +
    +
    + {{#list-pagination + source=sortedAllocations + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="allocations with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="shortId"}}ID{{/t.sort-by}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="clientStatus"}}Status{{/t.sort-by}} + {{#t.sort-by prop="job.name"}}Job{{/t.sort-by}} + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="node" onClick=(action "gotoAllocation" row.model)}} + {{/t.body}} + {{/list-table}} +
    + +
    + {{/list-pagination}} +
    +
    + +
    +
    + Attributes +
    +
    + {{attributes-table attributes=model.attributes.attributesStructured}} +
    +
    +
    +{{/gutter-menu}} diff --git a/ui/app/templates/servers.hbs b/ui/app/templates/servers.hbs new file mode 100644 index 000000000..e4d12d28b --- /dev/null +++ b/ui/app/templates/servers.hbs @@ -0,0 +1,42 @@ +
    + {{#global-header class="page-header"}} + Nodes + {{/global-header}} + {{#gutter-menu class="page-body"}} +
    + {{#list-pagination + source=sortedAgents + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + {{#t.sort-by prop="isLeader"}}Leader{{/t.sort-by}} + {{#t.sort-by prop="address"}}Address{{/t.sort-by}} + {{#t.sort-by prop="serfPort"}}port{{/t.sort-by}} + {{#t.sort-by prop="datacenter"}}Datacenter{{/t.sort-by}} + {{/t.head}} + {{#t.body as |row|}} + {{server-agent-row agent=row.model}} + {{/t.body}} + {{/list-table}} +
    + +
    + {{/list-pagination}} + {{outlet}} +
    + {{/gutter-menu}} +
    diff --git a/ui/app/templates/servers/server.hbs b/ui/app/templates/servers/server.hbs new file mode 100644 index 000000000..8c32c9184 --- /dev/null +++ b/ui/app/templates/servers/server.hbs @@ -0,0 +1,25 @@ +
    +
    +
    +
    + Tags +
    + + + + + + + + + {{#each-in model.tags as |name value|}} + + + + + {{/each-in}} + +
    NameValue
    {{name}}{{value}}
    +
    +
    +
    diff --git a/ui/app/templates/settings.hbs b/ui/app/templates/settings.hbs new file mode 100644 index 000000000..e781df8a2 --- /dev/null +++ b/ui/app/templates/settings.hbs @@ -0,0 +1,7 @@ +
    + {{#global-header class="page-header"}} + {{/global-header}} + {{#gutter-menu class="page-body"}} + {{outlet}} + {{/gutter-menu}} +
    diff --git a/ui/app/templates/settings/tokens.hbs b/ui/app/templates/settings/tokens.hbs new file mode 100644 index 000000000..f6a41ef76 --- /dev/null +++ b/ui/app/templates/settings/tokens.hbs @@ -0,0 +1,37 @@ +
    +

    Access Control Tokens

    +
    +
    +

    Clusters that use Access Control Lists require tokens to perform certain tasks. By providing a token Secret ID, each future request will be authenticated, potentially authorizing read access to additional information. By providing a token Accessor ID, the policies and rules for the token will be listed.

    + +
    +
    +
    +

    Token Storage

    +

    To protect Secret IDs, tokens are stored client-side in session storage. Your ACL token is automatically cleared from storage upon closing your browser window. You can also manually clear your token instead.

    +
    +
    + +
    +
    +
    + +
    + +
    + +
    +

    Sent with every request to determine authorization

    +
    + +
    + +
    + +
    +

    Used to look up authorized policies

    +
    +
    +
    +
    + diff --git a/ui/app/templates/svg-patterns.hbs b/ui/app/templates/svg-patterns.hbs new file mode 100644 index 000000000..b4a02f624 --- /dev/null +++ b/ui/app/templates/svg-patterns.hbs @@ -0,0 +1,10 @@ + + + + {{! Evenly sized diagonal stripes}} + + + + + + diff --git a/ui/app/utils/classes/promise-object.js b/ui/app/utils/classes/promise-object.js new file mode 100644 index 000000000..8732062b3 --- /dev/null +++ b/ui/app/utils/classes/promise-object.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +const { ObjectProxy, PromiseProxyMixin } = Ember; + +export default ObjectProxy.extend(PromiseProxyMixin); diff --git a/ui/app/utils/ip-parts.js b/ui/app/utils/ip-parts.js new file mode 100644 index 000000000..d85be088a --- /dev/null +++ b/ui/app/utils/ip-parts.js @@ -0,0 +1,20 @@ +// Splits any IP address into an address and a port +export default function ipParts(ip) { + const parts = ip ? ip.split(':') : []; + if (parts.length === 0) { + // ipv4, no port + return { address: ip, port: undefined }; + } else if (parts.length === 2) { + // ipv4, with port + return { address: parts[0], port: parts[1] }; + } else if (ip.startsWith('[')) { + // ipv6, with port + return { + address: parts.slice(0, parts.length - 1).join(':'), + port: parts[parts.length - 1], + }; + } else { + // ipv6, no port + return { address: ip, port: undefined }; + } +} diff --git a/ui/app/utils/properties/short-uuid.js b/ui/app/utils/properties/short-uuid.js new file mode 100644 index 000000000..67ac94ea6 --- /dev/null +++ b/ui/app/utils/properties/short-uuid.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +// An Ember.Computed property for taking the first segment +// of a uuid. +// +// ex. id: 123456-7890-abcd-efghijk +// short: shortUUIDProperty('id') // 123456 +export default function shortUUIDProperty(uuidKey) { + return computed(uuidKey, function() { + return this.get(uuidKey).split('-')[0]; + }); +} diff --git a/ui/app/utils/properties/style-string.js b/ui/app/utils/properties/style-string.js new file mode 100644 index 000000000..0f91706f8 --- /dev/null +++ b/ui/app/utils/properties/style-string.js @@ -0,0 +1,27 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +// An Ember.Computed property for transforming an object into an +// html compatible style attribute +// +// ex. styleProps: { color: '#FF0', border-width: '1px' } +// styleStr: styleStringProperty('styleProps') // color:#FF0;border-width:1px +export default function styleStringProperty(prop) { + return computed(prop, function() { + const styles = this.get(prop); + let str = ''; + + if (styles) { + str = Object.keys(styles) + .reduce(function(arr, key) { + const val = styles[key]; + arr.push(key + ':' + (typeof val === 'number' ? val.toFixed(2) + 'px' : val)); + return arr; + }, []) + .join(';'); + } + + return str.htmlSafe(); + }); +} diff --git a/ui/app/utils/properties/sum-aggregation.js b/ui/app/utils/properties/sum-aggregation.js new file mode 100644 index 000000000..e6561ced3 --- /dev/null +++ b/ui/app/utils/properties/sum-aggregation.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +// An Ember.Computed property for summating all properties from a +// set of objects. +// +// ex. list: [ { foo: 1 }, { foo: 3 } ] +// sum: sumAggregationProperty('list', 'foo') // 4 +export default function sumAggregationProperty(listKey, propKey) { + return computed(`${listKey}.@each.${propKey}`, function() { + return this.get(listKey).mapBy(propKey).reduce((sum, count) => sum + count, 0); + }); +} diff --git a/ui/app/utils/timeout.js b/ui/app/utils/timeout.js new file mode 100644 index 000000000..f9db48850 --- /dev/null +++ b/ui/app/utils/timeout.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +const { RSVP } = Ember; + +// An always failing promise used to race against other promises +export default function timeout(duration) { + return new RSVP.Promise((resolve, reject) => { + setTimeout(() => { + reject(`Timeout of ${duration}ms exceeded`); + }, duration); + }); +} diff --git a/ui/config/environment.js b/ui/config/environment.js new file mode 100644 index 000000000..a769944b9 --- /dev/null +++ b/ui/config/environment.js @@ -0,0 +1,58 @@ +/* eslint-env node */ + +module.exports = function(environment) { + var ENV = { + modulePrefix: 'nomad-ui', + environment: environment, + rootURL: '/ui/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. 'with-controller': true + 'ember-routing-router-service': true, + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + + ENV['ember-cli-mirage'] = { + // enabled: false, + }; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + + ENV.browserify = { + tests: true, + }; + } + + if (environment === 'production') { + } + + return ENV; +}; diff --git a/ui/config/targets.js b/ui/config/targets.js new file mode 100644 index 000000000..2bebfacad --- /dev/null +++ b/ui/config/targets.js @@ -0,0 +1,10 @@ +/* eslint-env node */ + +module.exports = { + browsers: [ + 'ie 9', + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions' + ] +}; diff --git a/ui/ember-cli-build.js b/ui/ember-cli-build.js new file mode 100644 index 000000000..0956228ee --- /dev/null +++ b/ui/ember-cli-build.js @@ -0,0 +1,28 @@ +/* eslint-env node */ +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +const environment = EmberApp.env(); +const isProd = environment === 'production'; + +module.exports = function(defaults) { + var app = new EmberApp(defaults, { + addons: { + blacklist: isProd ? ['ember-freestyle'] : [], + }, + }); + + // Use `app.import` to add additional libraries to the generated + // output files. + // + // If you need to use different assets in different + // environments, specify an object as the first parameter. That + // object's keys should be the environment name and the values + // should be the asset to use in that environment. + // + // If the library that you are including contains AMD or ES6 + // modules that you would like to import into your application + // please specify an object with the list of modules as keys + // along with the exports of each module as its value. + + return app.toTree(); +}; diff --git a/ui/lib/bulma/index.js b/ui/lib/bulma/index.js new file mode 100644 index 000000000..0e773d0a7 --- /dev/null +++ b/ui/lib/bulma/index.js @@ -0,0 +1,33 @@ +/* eslint-env node */ +'use strict'; + +var path = require('path'); +var Funnel = require('broccoli-funnel'); + +module.exports = { + name: 'bulma', + + isDevelopingAddon: function() { + return true; + }, + + included: function(app) { + this._super.included.apply(this, arguments); + + // see: https://github.com/ember-cli/ember-cli/issues/3718 + while (typeof app.import !== 'function' && app.app) { + app = app.app; + } + + this.bulmaPath = path.dirname(require.resolve('bulma')); + return app; + }, + + treeForStyles: function() { + return new Funnel(this.bulmaPath, { + srcDir: '/', + destDir: 'app/styles/bulma', + annotation: 'Funnel (bulma)', + }); + }, +}; diff --git a/ui/lib/bulma/package.json b/ui/lib/bulma/package.json new file mode 100644 index 000000000..5dc02af6c --- /dev/null +++ b/ui/lib/bulma/package.json @@ -0,0 +1,6 @@ +{ + "name": "bulma", + "keywords": [ + "ember-addon" + ] +} diff --git a/ui/mirage/common.js b/ui/mirage/common.js new file mode 100644 index 000000000..5bc38ac24 --- /dev/null +++ b/ui/mirage/common.js @@ -0,0 +1,68 @@ +import { faker } from 'ember-cli-mirage'; +import { provide } from './utils'; + +// Realistically, resource reservations have a low cardinality +const CPU_RESERVATIONS = [250, 500, 1000, 2000, 2500, 4000]; +const MEMORY_RESERVATIONS = [256, 512, 1024, 2048, 4096, 8192]; +const DISK_RESERVATIONS = [200, 500, 1000, 2000, 5000, 10000, 100000]; +const IOPS_RESERVATIONS = [100000, 250000, 500000, 1000000, 10000000, 20000000]; + +// There is also a good chance that certain resource restrictions are unbounded +IOPS_RESERVATIONS.push(...Array(1000).fill(0)); +DISK_RESERVATIONS.push(...Array(500).fill(0)); + +export const DATACENTERS = provide( + 15, + (n, i) => `${faker.address.countryCode().toLowerCase()}${i}` +); + +export const HOSTS = provide(100, () => { + const ip = Math.random() > 0.5 ? faker.internet.ip() : `[${ipv6()}]`; + return `${ip}:${faker.random.number({ min: 4000, max: 4999 })}`; +}); + +export function generateResources(options = {}) { + return { + CPU: faker.random.arrayElement(CPU_RESERVATIONS), + MemoryMB: faker.random.arrayElement(MEMORY_RESERVATIONS), + DiskMB: faker.random.arrayElement(DISK_RESERVATIONS), + IOPS: faker.random.arrayElement(IOPS_RESERVATIONS), + Networks: generateNetworks(options.networks), + }; +} + +export function generateNetworks(options = {}) { + return Array(faker.random.number({ min: 1, max: 3 })) + .fill(null) + .map(() => ({ + Device: `eth${faker.random.number({ max: 5 })}`, + CIDR: '', + IP: faker.internet.ip(), + MBits: 10, + ReservedPorts: Array( + faker.random.number({ + min: options.minPorts || 0, + max: options.maxPorts || 3, + }) + ) + .fill(null) + .map(() => ({ + Label: faker.hacker.noun(), + Value: faker.random.number({ min: 5000, max: 60000 }), + })), + })); +} + +// Faker v4.0 has a built-in ipv6 function. Once Mirage upgrades, +// this code can be removed. +function ipv6() { + const subnets = []; + for (var i = 0; i < 8; i++) { + var subnet = []; + for (var char = 0; char < 4; char++) { + subnet.push(faker.random.number(16).toString(16)); + } + subnets.push(subnet.join('')); + } + return subnets.join(':'); +} diff --git a/ui/mirage/config.js b/ui/mirage/config.js new file mode 100644 index 000000000..07566d72c --- /dev/null +++ b/ui/mirage/config.js @@ -0,0 +1,95 @@ +import Ember from 'ember'; +import { HOSTS } from './common'; + +const { copy } = Ember; + +export function findLeader(schema) { + const agent = schema.agents.first(); + return `${agent.address}:${agent.tags.port}`; +} + +export default function() { + this.timing = 200; // delay for each request, automatically set to 0 during testing + + this.namespace = 'v1'; + + this.get('/jobs', function({ jobs }) { + const json = this.serialize(jobs.all()); + return json.map(job => filterKeys(job, 'TaskGroups')); + }); + + this.get('/job/:id'); + + this.get('/job/:id/summary', function({ jobSummaries }, { params }) { + return this.serialize(jobSummaries.findBy({ jobId: params.id })); + }); + + this.get('/job/:id/allocations', function({ allocations }, { params }) { + return this.serialize(allocations.where({ jobId: params.id })); + }); + + this.get('/job/:id/versions', function({ jobVersions }, { params }) { + return this.serialize(jobVersions.where({ jobId: params.id })); + }); + + this.get('/job/:id/deployments', function({ deployments }, { params }) { + return this.serialize(deployments.where({ jobId: params.id })); + }); + + this.get('/deployment/:id'); + + this.get('/deployment/allocations/:id', function(schema, { params }) { + const job = schema.jobs.find(schema.deployments.find(params.id).jobId); + const allocations = schema.allocations.where({ jobId: job.id }); + + return this.serialize(allocations.slice(0, 3)); + }); + + this.get('/nodes', function({ nodes }) { + const json = this.serialize(nodes.all()); + return json; + }); + + this.get('/node/:id'); + + this.get('/node/:id/allocations', function({ allocations }, { params }) { + return this.serialize(allocations.where({ nodeId: params.id })); + }); + + this.get('/allocation/:id'); + + this.get('/agent/members', function({ agents }) { + return { + Members: this.serialize(agents.all()), + }; + }); + + this.get('/status/leader', function(schema) { + return JSON.stringify(findLeader(schema)); + }); + + // TODO: in the future, this hack may be replaceable with dynamic host name + // support in pretender: https://github.com/pretenderjs/pretender/issues/210 + HOSTS.forEach(host => { + this.get(`http://${host}/v1/client/allocation/:id/stats`, function( + { clientAllocationStats }, + { params } + ) { + return this.serialize(clientAllocationStats.find(params.id)); + }); + + this.get(`http://${host}/v1/client/stats`, function({ clientStats }) { + return this.serialize(clientStats.find(host)); + }); + }); +} + +function filterKeys(object, ...keys) { + const clone = copy(object, true); + + keys.forEach(key => { + delete clone[key]; + }); + + return clone; +} diff --git a/ui/mirage/factories/agent.js b/ui/mirage/factories/agent.js new file mode 100644 index 000000000..f523d5834 --- /dev/null +++ b/ui/mirage/factories/agent.js @@ -0,0 +1,26 @@ +import { Factory, faker } from 'ember-cli-mirage'; +import { provide } from '../utils'; +import { DATACENTERS } from '../common'; + +const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); +const AGENT_STATUSES = ['alive', 'leaving', 'left', 'failed']; + +export default Factory.extend({ + id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), + name: () => `nomad@${faker.internet.ip()}`, + + status: faker.list.random(...AGENT_STATUSES), + serf_port: faker.random.number({ min: 4000, max: 4999 }), + + address() { + return this.name.split('@')[1]; + }, + + tags() { + const rpcPortCandidate = faker.random.number({ min: 4000, max: 4999 }); + return { + port: rpcPortCandidate === this.serf_port ? rpcPortCandidate + 1 : rpcPortCandidate, + dc: faker.list.random(...DATACENTERS)(), + }; + }, +}); diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js new file mode 100644 index 000000000..8ae323ef7 --- /dev/null +++ b/ui/mirage/factories/allocation.js @@ -0,0 +1,86 @@ +import Ember from 'ember'; +import { Factory, faker, trait } from 'ember-cli-mirage'; +import { provide, pickOne } from '../utils'; + +const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); +const CLIENT_STATUSES = ['pending', 'running', 'complete', 'failed', 'lost']; +const DESIRED_STATUSES = ['run', 'stop', 'evict']; + +export default Factory.extend({ + id: i => (i >= 100 ? `${UUIDS[i % 100]}-${i}` : UUIDS[i]), + + clientStatus: faker.list.random(...CLIENT_STATUSES), + desiredStatus: faker.list.random(...DESIRED_STATUSES), + + // Meta property for hinting at task events + useMessagePassthru: false, + + withTaskWithPorts: trait({ + afterCreate(allocation, server) { + const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup }); + const resources = taskGroup.taskIds.map(id => + server.create( + 'task-resources', + { + allocation, + name: server.db.tasks.find(id).name, + }, + 'withReservedPorts' + ) + ); + + allocation.update({ taskResourcesIds: resources.mapBy('id') }); + }, + }), + + afterCreate(allocation, server) { + Ember.assert( + '[Mirage] No jobs! make sure jobs are created before allocations', + server.db.jobs.length + ); + Ember.assert( + '[Mirage] No nodes! make sure nodes are created before allocations', + server.db.nodes.length + ); + + const job = allocation.jobId ? server.db.jobs.find(allocation.jobId) : pickOne(server.db.jobs); + const node = allocation.nodeId + ? server.db.nodes.find(allocation.nodeId) + : pickOne(server.db.nodes); + const taskGroup = allocation.taskGroup + ? server.db.taskGroups.findBy({ name: allocation.taskGroup }) + : pickOne(server.db.taskGroups.where({ jobId: job.id })); + + const states = taskGroup.taskIds.map(id => + server.create('task-state', { + allocation, + name: server.db.tasks.find(id).name, + useMessagePassthru: allocation.useMessagePassthru, + }) + ); + + const resources = taskGroup.taskIds.map(id => + server.create('task-resources', { + allocation, + name: server.db.tasks.find(id).name, + }) + ); + + allocation.update({ + jobId: job.id, + nodeId: node.id, + taskStateIds: states.mapBy('id'), + task_state_ids: states.mapBy('id'), + taskResourcesIds: resources.mapBy('id'), + taskGroup: taskGroup.name, + name: allocation.name || `${taskGroup.name}.[${faker.random.number(10)}]`, + }); + + // Each allocation has a corresponding allocation stats running on some client. + // Create that record, even though it's not a relationship. + server.create('client-allocation-stats', { + id: allocation.id, + _tasks: states.mapBy('name'), + }); + }, +}); diff --git a/ui/mirage/factories/client-allocation-stats.js b/ui/mirage/factories/client-allocation-stats.js new file mode 100644 index 000000000..379dc0a18 --- /dev/null +++ b/ui/mirage/factories/client-allocation-stats.js @@ -0,0 +1,42 @@ +import { Factory } from 'ember-cli-mirage'; + +export default Factory.extend({ + resourceUsage: generateResources, + + _taskNames: () => [], // Set by allocation + + tasks() { + var hash = {}; + this._taskNames.forEach(task => { + hash[task] = { + Pids: null, + ResourceUsage: generateResources(), + Timestamp: Date.now(), + }; + }); + return hash; + }, +}); + +function generateResources() { + return { + CpuStats: { + Measured: ['Throttled Periods', 'Throttled Time', 'Percent'], + Percent: 0.14159538847117795, + SystemMode: 0, + ThrottledPeriods: 0, + ThrottledTime: 0, + TotalTicks: 3.256693934837093, + UserMode: 0, + }, + MemoryStats: { + Cache: 1744896, + KernelMaxUsage: 0, + KernelUsage: 0, + MaxUsage: 4710400, + Measured: ['RSS', 'Cache', 'Swap', 'Max Usage'], + RSS: 1486848, + Swap: 0, + }, + }; +} diff --git a/ui/mirage/factories/client-stats.js b/ui/mirage/factories/client-stats.js new file mode 100644 index 000000000..8b8b4e13f --- /dev/null +++ b/ui/mirage/factories/client-stats.js @@ -0,0 +1,47 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +export default Factory.extend({ + allDirStats: () => ({ + Available: 0, + Device: '', + InodesUsedPercent: 0.1, + Mountpoint: '', + Size: 200000000000, + Used: 100000000000, + UsedPercent: 50.0, + }), + + CPU: () => [ + Array(faker.list.random(1, 2, 4, 6, 8, 12, 16, 24, 32)).fill(0).map((cpu, index) => ({ + CPU: `cpu${index}`, + Idle: 20, + System: 40, + Total: 80, + User: 40, + })), + ], + + CPUTicksConsumed: 1000000, + + diskStats: () => [ + Array(faker.random.number({ min: 1, max: 5 })).fill(0).map((disk, index) => ({ + Available: 100000000000, + Device: `/dev/disk${index}`, + InodesUsedPercent: 0.10000000001, + Mountpoint: '/', + Size: 2000000000000, + Used: 1000000000000, + UsedPercent: 50.0, + })), + ], + + memory: () => ({ + Available: 60000000000, + Free: 25000000000, + Total: 95000000000, + Used: 10000000000, + }), + + timestamp: 149000000000, + uptime: 193838, +}); diff --git a/ui/mirage/factories/deployment-task-group-summary.js b/ui/mirage/factories/deployment-task-group-summary.js new file mode 100644 index 000000000..1b6fa1316 --- /dev/null +++ b/ui/mirage/factories/deployment-task-group-summary.js @@ -0,0 +1,30 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +export default Factory.extend({ + name: '', + + autoRevert: () => Math.random() > 0.5, + promoted: () => Math.random() > 0.5, + + desiredTotal: faker.random.number({ min: 1, max: 10 }), + + desiredCanaries() { + return faker.random.number(Math.floor(this.desiredTotal / 2)); + }, + + placedCanaries() { + return faker.random.number(this.desiredCanaries); + }, + + placedAllocs() { + return faker.random.number(this.desiredTotal); + }, + + healthyAllocs() { + return faker.random.number(this.placedAllocs); + }, + + unhealthyAllocs() { + return this.placedAllocs - this.healthyAllocs; + }, +}); diff --git a/ui/mirage/factories/deployment.js b/ui/mirage/factories/deployment.js new file mode 100644 index 000000000..8f26f086f --- /dev/null +++ b/ui/mirage/factories/deployment.js @@ -0,0 +1,37 @@ +import { Factory, faker, trait } from 'ember-cli-mirage'; +import { provide } from '../utils'; + +const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); +const DEPLOYMENT_STATUSES = ['running', 'successful', 'paused', 'failed', 'cancelled']; + +export default Factory.extend({ + id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), + + jobId: null, + versionNumber: null, + + status: faker.list.random(...DEPLOYMENT_STATUSES), + statusDescription: () => faker.lorem.sentence(), + + notActive: trait({ + status: faker.list.random(...DEPLOYMENT_STATUSES.without('running')), + }), + + active: trait({ + status: 'running', + }), + + afterCreate(deployment, server) { + const job = server.db.jobs.find(deployment.jobId); + const groups = job.taskGroupIds.map(id => + server.create('deployment-task-group-summary', { + deployment, + name: server.db.taskGroups.find(id).name, + }) + ); + + deployment.update({ + deploymentTaskGroupSummaryIds: groups.mapBy('id'), + }); + }, +}); diff --git a/ui/mirage/factories/job-summary.js b/ui/mirage/factories/job-summary.js new file mode 100644 index 000000000..c766b8b22 --- /dev/null +++ b/ui/mirage/factories/job-summary.js @@ -0,0 +1,22 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +export default Factory.extend({ + // Hidden property used to compute the Summary hash + groupNames: [], + + JobID: '', + + Summary: function() { + return this.groupNames.reduce((summary, group) => { + summary[group] = { + Queued: faker.random.number(10), + Complete: faker.random.number(10), + Failed: faker.random.number(10), + Running: faker.random.number(10), + Starting: faker.random.number(10), + Lost: faker.random.number(10), + }; + return summary; + }, {}); + }, +}); diff --git a/ui/mirage/factories/job-version.js b/ui/mirage/factories/job-version.js new file mode 100644 index 000000000..a91b8abd7 --- /dev/null +++ b/ui/mirage/factories/job-version.js @@ -0,0 +1,274 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +const REF_TIME = new Date(); + +export default Factory.extend({ + stable: faker.random.boolean, + submitTime: () => faker.date.past(2 / 365, REF_TIME) * 1000000, + diff() { + return generateDiff(this); + }, + + jobId: null, + version: 0, + + // Directive to restrict any related deployments from having a 'running' status + noActiveDeployment: false, + + // Directive to restrict any related deployments from having a status other than 'running' + activeDeployment: false, + + afterCreate(version, server) { + const args = [ + 'deployment', + version.noActiveDeployment && 'notActive', + version.activeDeployment && 'active', + { + jobId: version.jobId, + versionNumber: version.version, + }, + ].compact(); + server.create(...args); + }, +}); + +function generateDiff(version) { + return { + Fields: null, + ID: version.jobId, + Objects: null, + TaskGroups: [ + { + Fields: [ + { + Annotations: null, + Name: 'Count', + New: '2', + Old: '4', + Type: 'Edited', + }, + ], + Name: 'cache', + Objects: [ + { + Fields: [ + { + Annotations: null, + Name: 'Attempts', + New: '15', + Old: '10', + Type: 'Edited', + }, + { + Annotations: null, + Name: 'Delay', + New: '25000000000', + Old: '25000000000', + Type: 'None', + }, + { + Annotations: null, + Name: 'Interval', + New: '900000000000', + Old: '900000000000', + Type: 'None', + }, + { + Annotations: null, + Name: 'Mode', + New: 'delay', + Old: 'delay', + Type: 'None', + }, + ], + Name: 'RestartPolicy', + Objects: null, + Type: 'Edited', + }, + ], + Tasks: [ + { + Annotations: null, + Fields: null, + Name: 'redis', + Objects: [ + { + Fields: [ + { + Annotations: null, + Name: 'CPU', + New: '500', + Old: '500', + Type: 'None', + }, + { + Annotations: null, + Name: 'DiskMB', + New: '0', + Old: '0', + Type: 'None', + }, + { + Annotations: null, + Name: 'IOPS', + New: '0', + Old: '0', + Type: 'None', + }, + { + Annotations: null, + Name: 'MemoryMB', + New: '512', + Old: '256', + Type: 'Edited', + }, + ], + Name: 'Resources', + Objects: null, + Type: 'Edited', + }, + { + Fields: [ + { + Annotations: null, + Name: 'MaxFileSizeMB', + New: '15', + Old: '10', + Type: 'Edited', + }, + { + Annotations: null, + Name: 'MaxFiles', + New: '10', + Old: '10', + Type: 'None', + }, + ], + Name: 'LogConfig', + Objects: null, + Type: 'Edited', + }, + { + Fields: [ + { + Annotations: null, + Name: 'AddressMode', + New: 'auto', + Old: 'auto', + Type: 'None', + }, + { + Annotations: null, + Name: 'Name', + New: 'global-redis-check', + Old: 'global-redis-check', + Type: 'None', + }, + { + Annotations: null, + Name: 'PortLabel', + New: 'db', + Old: 'db', + Type: 'None', + }, + ], + Name: 'Service', + Objects: [ + { + Fields: [ + { + Annotations: null, + Name: 'Command', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'InitialStatus', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'Interval', + New: '10000000000', + Old: '10000000000', + Type: 'None', + }, + { + Annotations: null, + Name: 'Method', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'Name', + New: 'alive', + Old: 'alive', + Type: 'None', + }, + { + Annotations: null, + Name: 'Path', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'PortLabel', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'Protocol', + New: '', + Old: '', + Type: 'None', + }, + { + Annotations: null, + Name: 'TLSSkipVerify', + New: 'false', + Old: 'false', + Type: 'None', + }, + { + Annotations: null, + Name: 'Timeout', + New: '3000000000', + Old: '2000000000', + Type: 'Edited', + }, + { + Annotations: null, + Name: 'Type', + New: 'tcp', + Old: 'tcp', + Type: 'None', + }, + ], + Name: 'Check', + Objects: null, + Type: 'Edited', + }, + ], + Type: 'Edited', + }, + ], + Type: 'Edited', + }, + ], + Type: 'Edited', + Updates: null, + }, + ], + Type: 'Edited', + }; +} diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js new file mode 100644 index 000000000..ec4d60acf --- /dev/null +++ b/ui/mirage/factories/job.js @@ -0,0 +1,76 @@ +import { Factory, faker } from 'ember-cli-mirage'; +import { provide, provider } from '../utils'; +import { DATACENTERS } from '../common'; + +const JOB_PREFIXES = provide(5, faker.hacker.abbreviation); +const JOB_TYPES = ['service', 'batch', 'system']; +const JOB_STATUSES = ['pending', 'running', 'dead']; + +export default Factory.extend({ + id: i => `job-${i}`, + name: i => `${faker.list.random(...JOB_PREFIXES)()}-${faker.hacker.noun()}-${i}`, + + groupsCount: () => faker.random.number({ min: 1, max: 5 }), + + region: () => 'global', + type: faker.list.random(...JOB_TYPES), + priority: () => faker.random.number(200), + all_at_once: faker.random.boolean, + status: faker.list.random(...JOB_STATUSES), + datacenters: provider( + () => faker.random.number({ min: 1, max: 4 }), + faker.list.random(...DATACENTERS) + ), + + periodic: () => Math.random() > 0.5, + parameterized() { + return !this.periodic; + }, + + createIndex: i => i, + modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), + + // Directive used to control sub-resources + + // When false, no allocations are made + createAllocations: true, + + // When true, deployments for the job will never have a 'running' status + noActiveDeployment: false, + + // When true, deployments for the job will always have a 'running' status + activeDeployment: false, + + afterCreate(job, server) { + const groups = server.createList('task-group', job.groupsCount, { + job, + createAllocations: job.createAllocations, + }); + + job.update({ + taskGroupIds: groups.mapBy('id'), + task_group_ids: groups.mapBy('id'), + }); + + const jobSummary = server.create('job-summary', { + groupNames: groups.mapBy('name'), + job, + }); + + job.update({ + jobSummaryId: jobSummary.id, + job_summary_id: jobSummary.id, + }); + + Array(faker.random.number({ min: 1, max: 10 })) + .fill(null) + .map((_, index) => { + return server.create('job-version', { + job, + version: index, + noActiveDeployment: job.noActiveDeployment, + activeDeployment: job.activeDeployment, + }); + }); + }, +}); diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js new file mode 100644 index 000000000..3705fa187 --- /dev/null +++ b/ui/mirage/factories/node.js @@ -0,0 +1,68 @@ +import { Factory, faker, trait } from 'ember-cli-mirage'; +import { provide } from '../utils'; +import { DATACENTERS, HOSTS } from '../common'; + +const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); +const NODE_STATUSES = ['initializing', 'ready', 'down']; + +export default Factory.extend({ + id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), + name: i => `nomad@${HOSTS[i % HOSTS.length]}`, + + datacenter: faker.list.random(...DATACENTERS), + isDraining: faker.random.boolean, + status: faker.list.random(...NODE_STATUSES), + tls_enabled: faker.random.boolean, + + createIndex: i => i, + modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), + + httpAddr() { + return this.name.split('@')[1]; + }, + + forceIPv4: trait({ + name: i => { + const ipv4Hosts = HOSTS.filter(h => !h.startsWith('[')); + return `nomad@${ipv4Hosts[i % ipv4Hosts.length]}`; + }, + }), + + attributes() { + // TODO add variability to these + return { + 'os.version': '10.12.5', + 'cpu.modelname': 'Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz', + 'nomad.revision': 'f551dcb83e3ac144c9dbb90583b6e82d234662e9', + 'driver.docker.volumes.enabled': '1', + 'driver.docker': '1', + 'cpu.frequency': '2300', + 'memory.totalbytes': '17179869184', + 'driver.mock_driver': '1', + 'kernel.version': '16.6.0', + 'unique.network.ip-address': '127.0.0.1', + 'nomad.version': '0.5.5dev', + 'unique.hostname': 'bacon-mac', + 'cpu.arch': 'amd64', + 'os.name': 'darwin', + 'kernel.name': 'darwin', + 'unique.storage.volume': '/dev/disk1', + 'driver.docker.version': '17.03.1-ce', + 'cpu.totalcompute': '18400', + 'unique.storage.bytestotal': '249783500800', + 'cpu.numcores': '8', + 'os.signals': + 'SIGCONT,SIGSTOP,SIGSYS,SIGINT,SIGIOT,SIGXCPU,SIGSEGV,SIGUSR1,SIGTTIN,SIGURG,SIGUSR2,SIGABRT,SIGALRM,SIGCHLD,SIGFPE,SIGTSTP,SIGIO,SIGKILL,SIGQUIT,SIGXFSZ,SIGBUS,SIGHUP,SIGPIPE,SIGPROF,SIGTRAP,SIGTTOU,SIGILL,SIGTERM', + 'driver.raw_exec': '1', + 'unique.storage.bytesfree': '142954643456', + }; + }, + + afterCreate(node, server) { + // Each node has a corresponding client stats resource that's queried via node IP. + // Create that record, even though it's not a relationship. + server.create('client-stats', { + id: node.http_addr, + }); + }, +}); diff --git a/ui/mirage/factories/task-event.js b/ui/mirage/factories/task-event.js new file mode 100644 index 000000000..e922894f4 --- /dev/null +++ b/ui/mirage/factories/task-event.js @@ -0,0 +1,31 @@ +import { Factory, faker, trait } from 'ember-cli-mirage'; +import { provide } from '../utils'; + +const REF_TIME = new Date(); +const STATES = provide(10, faker.system.fileExt.bind(faker.system)); + +export default Factory.extend({ + type: faker.list.random(...STATES), + + // Message is a function of type, and this type uses the vanilla + // message property. + messagePassthru: trait({ + type: 'Task Setup', + }), + + signal: () => '', + exitCode: () => null, + time: () => faker.date.past(2 / 365, REF_TIME) * 1000000, + + downloadError: () => '', + driverError: () => '', + driverMessage: () => '', + killError: () => '', + killReason: () => '', + restartReason: () => '', + setupError: () => '', + taskSignalReason: () => '', + validationError: () => '', + vaultError: () => '', + message: () => faker.lorem.sentence(), +}); diff --git a/ui/mirage/factories/task-group.js b/ui/mirage/factories/task-group.js new file mode 100644 index 000000000..c77e20cdf --- /dev/null +++ b/ui/mirage/factories/task-group.js @@ -0,0 +1,41 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +const DISK_RESERVATIONS = [200, 500, 1000, 2000, 5000, 10000, 100000]; + +export default Factory.extend({ + name: id => `${faker.hacker.noun()}-g-${id}`, + count: () => faker.random.number({ min: 1, max: 4 }), + + ephemeralDisk: () => ({ + Sticky: faker.random.boolean(), + SizeMB: faker.random.arrayElement(DISK_RESERVATIONS), + Migrate: faker.random.boolean(), + }), + + // Directive used to control whether or not allocations are automatically + // created. + createAllocations: true, + + afterCreate(group, server) { + const tasks = server.createList('task', group.count, { + taskGroup: group, + }); + + group.update({ + taskIds: tasks.mapBy('id'), + task_ids: tasks.mapBy('id'), + }); + + if (group.createAllocations) { + Array(group.count) + .fill(null) + .forEach((_, i) => { + server.create('allocation', { + jobId: group.job.id, + taskGroup: group.name, + name: `${group.name}.[${i}]`, + }); + }); + } + }, +}); diff --git a/ui/mirage/factories/task-resources.js b/ui/mirage/factories/task-resources.js new file mode 100644 index 000000000..e6fe87de8 --- /dev/null +++ b/ui/mirage/factories/task-resources.js @@ -0,0 +1,12 @@ +import { Factory, trait } from 'ember-cli-mirage'; +import { generateResources } from '../common'; + +export default Factory.extend({ + name: () => '!!!this should be set by the allocation that owns this task state!!!', + + resources: generateResources, + + withReservedPorts: trait({ + resources: () => generateResources({ networks: { minPorts: 1 } }), + }), +}); diff --git a/ui/mirage/factories/task-state.js b/ui/mirage/factories/task-state.js new file mode 100644 index 000000000..bbcf28a02 --- /dev/null +++ b/ui/mirage/factories/task-state.js @@ -0,0 +1,35 @@ +import { Factory, faker } from 'ember-cli-mirage'; + +const TASK_STATUSES = ['pending', 'running', 'finished', 'failed']; +const REF_TIME = new Date(); + +export default Factory.extend({ + name: () => '!!!this should be set by the allocation that owns this task state!!!', + state: faker.list.random(...TASK_STATUSES), + startedAt: faker.date.past(2 / 365, REF_TIME), + finishedAt() { + if (['pending', 'running'].includes(this.state)) { + return '0001-01-01T00:00:00Z'; + } + return new Date(this.startedAt + Math.random(1000 * 60 * 3) + 50); + }, + + useMessagePassthru: false, + + afterCreate(state, server) { + const props = [ + 'task-event', + faker.random.number({ min: 1, max: 10 }), + state.useMessagePassthru && 'messagePassthru', + { + taskStateId: state.id, + }, + ].compact(); + + const events = server.createList(...props); + + state.update({ + eventIds: events.mapBy('id'), + }); + }, +}); diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js new file mode 100644 index 000000000..47afe3bbd --- /dev/null +++ b/ui/mirage/factories/task.js @@ -0,0 +1,13 @@ +import { Factory, faker } from 'ember-cli-mirage'; +import { generateResources } from '../common'; + +export default Factory.extend({ + // Hidden property used to compute the Summary hash + groupNames: [], + + JobID: '', + + name: id => `task-${faker.hacker.noun()}-${id}`, + + Resources: generateResources, +}); diff --git a/ui/mirage/models/allocation.js b/ui/mirage/models/allocation.js new file mode 100644 index 000000000..22ef8847d --- /dev/null +++ b/ui/mirage/models/allocation.js @@ -0,0 +1,6 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + task_states: hasMany('task-state'), + task_resources: hasMany('task-resources'), +}); diff --git a/ui/mirage/models/client-allocation-stats.js b/ui/mirage/models/client-allocation-stats.js new file mode 100644 index 000000000..ddb04151d --- /dev/null +++ b/ui/mirage/models/client-allocation-stats.js @@ -0,0 +1,3 @@ +import { Model } from 'ember-cli-mirage'; + +export default Model.extend(); diff --git a/ui/mirage/models/client-stats.js b/ui/mirage/models/client-stats.js new file mode 100644 index 000000000..ddb04151d --- /dev/null +++ b/ui/mirage/models/client-stats.js @@ -0,0 +1,3 @@ +import { Model } from 'ember-cli-mirage'; + +export default Model.extend(); diff --git a/ui/mirage/models/deployment-task-group-summary.js b/ui/mirage/models/deployment-task-group-summary.js new file mode 100644 index 000000000..54e44c89e --- /dev/null +++ b/ui/mirage/models/deployment-task-group-summary.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + deployment: belongsTo(), +}); diff --git a/ui/mirage/models/deployment.js b/ui/mirage/models/deployment.js new file mode 100644 index 000000000..136e6f253 --- /dev/null +++ b/ui/mirage/models/deployment.js @@ -0,0 +1,5 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + deploymentTaskGroupSummaries: hasMany('deployment-task-group-summary'), +}); diff --git a/ui/mirage/models/job-summary.js b/ui/mirage/models/job-summary.js new file mode 100644 index 000000000..5e76efb41 --- /dev/null +++ b/ui/mirage/models/job-summary.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + job: belongsTo(), +}); diff --git a/ui/mirage/models/job.js b/ui/mirage/models/job.js new file mode 100644 index 000000000..3a69c0281 --- /dev/null +++ b/ui/mirage/models/job.js @@ -0,0 +1,6 @@ +import { Model, hasMany, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + task_groups: hasMany('task-group'), + job_summary: belongsTo('job-summary'), +}); diff --git a/ui/mirage/models/task-event.js b/ui/mirage/models/task-event.js new file mode 100644 index 000000000..373c9d65a --- /dev/null +++ b/ui/mirage/models/task-event.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + taskState: belongsTo(), +}); diff --git a/ui/mirage/models/task-group.js b/ui/mirage/models/task-group.js new file mode 100644 index 000000000..7300fe442 --- /dev/null +++ b/ui/mirage/models/task-group.js @@ -0,0 +1,6 @@ +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + job: belongsTo(), + tasks: hasMany(), +}); diff --git a/ui/mirage/models/task-resources.js b/ui/mirage/models/task-resources.js new file mode 100644 index 000000000..6f2bac1e6 --- /dev/null +++ b/ui/mirage/models/task-resources.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + allocation: belongsTo(), +}); diff --git a/ui/mirage/models/task-state.js b/ui/mirage/models/task-state.js new file mode 100644 index 000000000..3d0710624 --- /dev/null +++ b/ui/mirage/models/task-state.js @@ -0,0 +1,6 @@ +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + allocation: belongsTo(), + events: hasMany('task-event'), +}); diff --git a/ui/mirage/models/task.js b/ui/mirage/models/task.js new file mode 100644 index 000000000..359d354e0 --- /dev/null +++ b/ui/mirage/models/task.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + taskGroup: belongsTo(), +}); diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js new file mode 100644 index 000000000..2c4a0b07b --- /dev/null +++ b/ui/mirage/scenarios/default.js @@ -0,0 +1,5 @@ +export default function(server) { + server.createList('agent', 3); + server.createList('node', 50); + server.createList('job', 15); +} diff --git a/ui/mirage/serializers/allocation.js b/ui/mirage/serializers/allocation.js new file mode 100644 index 000000000..883e3cfe8 --- /dev/null +++ b/ui/mirage/serializers/allocation.js @@ -0,0 +1,22 @@ +import ApplicationSerializer from './application'; +import { arrToObj } from '../utils'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['task_states', 'task_resources'], + + serialize() { + var json = ApplicationSerializer.prototype.serialize.apply(this, arguments); + if (json instanceof Array) { + json.forEach(serializeAllocation); + } else { + serializeAllocation(json); + } + return json; + }, +}); + +function serializeAllocation(allocation) { + allocation.TaskStates = allocation.TaskStates.reduce(arrToObj('Name'), {}); + allocation.TaskResources = allocation.TaskResources.reduce(arrToObj('Name', 'Resources'), {}); +} diff --git a/ui/mirage/serializers/application.js b/ui/mirage/serializers/application.js new file mode 100644 index 000000000..5bee3867e --- /dev/null +++ b/ui/mirage/serializers/application.js @@ -0,0 +1,19 @@ +import { RestSerializer } from 'ember-cli-mirage'; + +const keyCase = str => (str === 'id' ? 'ID' : str.camelize().capitalize().replace(/Id/g, 'ID')); + +export default RestSerializer.extend({ + serialize() { + const json = RestSerializer.prototype.serialize.apply(this, arguments); + const keys = Object.keys(json); + if (keys.length === 1) { + return json[keys[0]]; + } else { + return json; + } + }, + + keyForAttribute: keyCase, + keyForRelationship: keyCase, + keyForEmbeddedRelationship: keyCase, +}); diff --git a/ui/mirage/serializers/deployment.js b/ui/mirage/serializers/deployment.js new file mode 100644 index 000000000..cdd0fb108 --- /dev/null +++ b/ui/mirage/serializers/deployment.js @@ -0,0 +1,21 @@ +import ApplicationSerializer from './application'; +import { arrToObj } from '../utils'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['deploymentTaskGroupSummaries'], + + serialize() { + var json = ApplicationSerializer.prototype.serialize.apply(this, arguments); + if (json instanceof Array) { + json.forEach(serializeDeployment); + } else { + serializeDeployment(json); + } + return json; + }, +}); + +function serializeDeployment(deployment) { + deployment.TaskGroups = deployment.DeploymentTaskGroupSummaries.reduce(arrToObj('Name'), {}); +} diff --git a/ui/mirage/serializers/job-version.js b/ui/mirage/serializers/job-version.js new file mode 100644 index 000000000..49cb47bb1 --- /dev/null +++ b/ui/mirage/serializers/job-version.js @@ -0,0 +1,25 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + serialize() { + var json = ApplicationSerializer.prototype.serialize.apply(this, arguments); + + if (!(json instanceof Array)) { + json = [json]; + } + + return json + .sortBy('SubmitTime') + .reverse() + .reduce( + (hash, version) => { + hash.Diffs.push(version.Diff); + delete version.Diff; + + hash.Versions.push(version); + return hash; + }, + { Versions: [], Diffs: [] } + ); + }, +}); diff --git a/ui/mirage/serializers/job.js b/ui/mirage/serializers/job.js new file mode 100644 index 000000000..bfa8caedb --- /dev/null +++ b/ui/mirage/serializers/job.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['task_groups', 'job_summary'], +}); diff --git a/ui/mirage/serializers/task-group.js b/ui/mirage/serializers/task-group.js new file mode 100644 index 000000000..04ac613a2 --- /dev/null +++ b/ui/mirage/serializers/task-group.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['tasks'], +}); diff --git a/ui/mirage/serializers/task-state.js b/ui/mirage/serializers/task-state.js new file mode 100644 index 000000000..506c19038 --- /dev/null +++ b/ui/mirage/serializers/task-state.js @@ -0,0 +1,6 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + embed: true, + include: ['events'], +}); diff --git a/ui/mirage/utils.js b/ui/mirage/utils.js new file mode 100644 index 000000000..5a48ad3d7 --- /dev/null +++ b/ui/mirage/utils.js @@ -0,0 +1,28 @@ +import { faker } from 'ember-cli-mirage'; + +export function provide(count, provider) { + if (typeof count === 'function') { + count = count(); + } + return Array(count) + .fill(null) + .map(provider); +} + +export function provider() { + return () => provide(...arguments); +} + +export function pickOne(list) { + return list[faker.random.number(list.length - 1)]; +} + +export function arrToObj(prop, alias = '') { + return (obj, element) => { + const name = element[prop]; + delete element[prop]; + + obj[name] = alias ? element[alias] : element; + return obj; + }; +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..c4f37ed75 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,86 @@ +{ + "name": "nomad-ui", + "version": "0.0.0", + "description": "The web ui for Nomad, by HashiCorp.", + "directories": { + "doc": "doc", + "test": "tests" + }, + "repository": "", + "scripts": { + "build": "ember build -prod", + "start": "ember server --proxy=http://127.0.0.1:4646", + "test": "ember test", + "precommit": "lint-staged" + }, + "lint-staged": { + "gitDir": "../", + "linters": { + "ui/{app,tests,config,lib,mirage}/**/*.js": [ + "prettier --single-quote --trailing-comma es5 --print-width 100 --write", + "git add" + ], + "ui/app/styles/**/*.*": [ + "prettier --write", + "git add" + ] + } + }, + "devDependencies": { + "broccoli-asset-rev": "^2.4.5", + "bulma": "0.4.2", + "core-js": "^2.4.1", + "d3-selection": "^1.1.0", + "d3-transition": "^1.1.0", + "ember-ajax": "^3.0.0", + "ember-browserify": "^1.1.13", + "ember-cli": "2.13.2", + "ember-cli-babel": "^6.0.0", + "ember-cli-bourbon": "2.0.0-beta.1", + "ember-cli-dependency-checker": "^1.3.0", + "ember-cli-eslint": "^3.0.0", + "ember-cli-htmlbars": "^1.1.1", + "ember-cli-htmlbars-inline-precompile": "^0.4.0", + "ember-cli-inject-live-reload": "^1.4.1", + "ember-cli-mirage": "^0.3.3", + "ember-cli-moment-shim": "^3.3.3", + "ember-cli-qunit": "^4.0.0", + "ember-cli-sass": "^6.2.0", + "ember-cli-shims": "^1.1.0", + "ember-cli-sri": "^2.1.0", + "ember-cli-string-helpers": "^1.4.0", + "ember-cli-uglify": "^1.2.0", + "ember-composable-helpers": "^2.0.3", + "ember-data": "^2.14.0", + "ember-data-model-fragments": "^2.14.0", + "ember-export-application-global": "^2.0.0", + "ember-fetch": "^3.2.7", + "ember-freestyle": "^0.4.1", + "ember-href-to": "^1.13.0", + "ember-load-initializers": "^1.0.0", + "ember-moment": "^7.3.1", + "ember-resolver": "^4.0.0", + "ember-sinon": "^0.7.0", + "ember-source": "~2.14.0", + "ember-truth-helpers": "^1.3.0", + "ember-welcome-page": "^3.0.0", + "eslint": "^4.0.0", + "flat": "^2.0.1", + "fuse.js": "^3.0.5", + "husky": "^0.13.4", + "json-formatter-js": "^2.2.0", + "lint-staged": "^3.6.1", + "loader.js": "^4.2.3", + "prettier": "^1.4.4" + }, + "engines": { + "node": ">= 4" + }, + "private": true, + "ember-addon": { + "paths": [ + "lib/bulma", + "lib/calendar" + ] + } +} diff --git a/ui/public/crossdomain.xml b/ui/public/crossdomain.xml new file mode 100644 index 000000000..0c16a7a07 --- /dev/null +++ b/ui/public/crossdomain.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/ui/public/favicon.png b/ui/public/favicon.png new file mode 100644 index 000000000..c4d912cec --- /dev/null +++ b/ui/public/favicon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a5449ff652b51b42adc6efda9f6e19c43801b1ab92e0b3e0ff6d026e326adbd +size 6714 diff --git a/ui/public/images/nomad-logo.svg b/ui/public/images/nomad-logo.svg new file mode 100644 index 000000000..c2a45cc1e --- /dev/null +++ b/ui/public/images/nomad-logo.svg @@ -0,0 +1 @@ +Asset 1 diff --git a/ui/public/robots.txt b/ui/public/robots.txt new file mode 100644 index 000000000..f5916452e --- /dev/null +++ b/ui/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/ui/testem.js b/ui/testem.js new file mode 100644 index 000000000..9a0e1deb1 --- /dev/null +++ b/ui/testem.js @@ -0,0 +1,7 @@ +/* eslint-env node */ +module.exports = { + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + launch_in_ci: ['PhantomJS'], + launch_in_dev: ['Chrome'], +}; diff --git a/ui/tests/.eslintrc.js b/ui/tests/.eslintrc.js new file mode 100644 index 000000000..f7be103a1 --- /dev/null +++ b/ui/tests/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + globals: { + server: true, + }, + env: { + embertest: true, + }, +}; diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js new file mode 100644 index 000000000..70d567262 --- /dev/null +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -0,0 +1,186 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; +import moment from 'moment'; + +let job; +let node; +let allocation; + +moduleForAcceptance('Acceptance | allocation detail', { + beforeEach() { + server.create('agent'); + + node = server.create('node'); + job = server.create('job', { groupCount: 0 }); + allocation = server.create('allocation', 'withTaskWithPorts', { + useMessagePassthru: true, + }); + + visit(`/allocations/${allocation.id}`); + }, +}); + +test('/allocation/:id should name the allocation and link to the corresponding job and node', function( + assert +) { + assert.ok( + find('h1') + .text() + .includes(allocation.name), + 'Allocation name is in the heading' + ); + assert.ok( + find('h3') + .text() + .includes(job.name), + 'Job name is in the subheading' + ); + assert.ok( + find('h3') + .text() + .includes(node.id.split('-')[0]), + 'Node short id is in the subheading' + ); + + click('h3 a:eq(0)'); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}`, 'Job link navigates to the job'); + }); + + visit(`/allocations/${allocation.id}`); + click('h3 a:eq(1)'); + andThen(() => { + assert.equal(currentURL(), `/nodes/${node.id}`, 'Node link navigates to the node'); + }); +}); + +test('/allocation/:id should list all tasks for the allocation', function(assert) { + assert.equal( + find('.tasks tbody tr').length, + server.db.taskStates.where({ allocationId: allocation.id }).length, + 'Table lists all tasks' + ); +}); + +test('each task row should list high-level information for the task', function(assert) { + const task = server.db.taskStates.where({ allocationId: allocation.id }).sortBy('name')[0]; + const taskResources = allocation.taskResourcesIds + .map(id => server.db.taskResources.find(id)) + .sortBy('name')[0]; + const reservedPorts = taskResources.resources.Networks[0].ReservedPorts; + const taskRow = find('.tasks tbody tr:eq(0)'); + const events = server.db.taskEvents.where({ taskStateId: task.id }); + const event = events[events.length - 1]; + + assert.equal( + taskRow + .find('td:eq(0)') + .text() + .trim(), + task.name, + 'Name' + ); + assert.equal( + taskRow + .find('td:eq(1)') + .text() + .trim(), + task.state, + 'State' + ); + assert.equal( + taskRow + .find('td:eq(2)') + .text() + .trim(), + event.message, + 'Event Message' + ); + assert.equal( + taskRow + .find('td:eq(3)') + .text() + .trim(), + moment(event.time / 1000000).format('MM/DD/YY HH:mm:ss [UTC]'), + 'Event Time' + ); + + assert.ok(reservedPorts.length, 'The task has reserved ports'); + + const addressesText = taskRow.find('td:eq(4)').text(); + reservedPorts.forEach(port => { + assert.ok(addressesText.includes(port.Label), `Found label ${port.Label}`); + assert.ok(addressesText.includes(port.Value), `Found value ${port.Value}`); + }); +}); + +test('/allocation/:id should list recent events for each task', function(assert) { + const tasks = server.db.taskStates.where({ allocationId: allocation.id }); + assert.equal( + find('.task-state-events').length, + tasks.length, + 'A task state event block per task' + ); +}); + +test('each recent events list should include the name, state, and time info for the task', function( + assert +) { + const task = server.db.taskStates.where({ allocationId: allocation.id })[0]; + const recentEventsSection = find('.task-state-events:eq(0)'); + const heading = recentEventsSection + .find('.message-header') + .text() + .trim(); + + assert.ok(heading.includes(task.name), 'Task name'); + assert.ok(heading.includes(task.state), 'Task state'); + assert.ok( + heading.includes(moment(task.startedAt).format('MM/DD/YY HH:mm:ss [UTC]')), + 'Task started at' + ); +}); + +test('each recent events list should list all recent events for the task', function(assert) { + const task = server.db.taskStates.where({ allocationId: allocation.id })[0]; + const events = server.db.taskEvents.where({ taskStateId: task.id }); + + assert.equal( + find('.task-state-events:eq(0) .task-events tbody tr').length, + events.length, + `Lists ${events.length} events` + ); +}); + +test('each recent event should list the time, type, and description of the event', function( + assert +) { + const task = server.db.taskStates.where({ allocationId: allocation.id })[0]; + const event = server.db.taskEvents.where({ taskStateId: task.id })[0]; + const recentEvent = find('.task-state-events:eq(0) .task-events tbody tr:last'); + + assert.equal( + recentEvent + .find('td:eq(0)') + .text() + .trim(), + moment(event.time / 1000000).format('MM/DD/YY HH:mm:ss [UTC]'), + 'Event timestamp' + ); + assert.equal( + recentEvent + .find('td:eq(1)') + .text() + .trim(), + event.type, + 'Event type' + ); + assert.equal( + recentEvent + .find('td:eq(2)') + .text() + .trim(), + event.message, + 'Event message' + ); +}); diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js new file mode 100644 index 000000000..521f6757c --- /dev/null +++ b/ui/tests/acceptance/client-detail-test.js @@ -0,0 +1,186 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +let node; + +moduleForAcceptance('Acceptance | client detail', { + beforeEach() { + server.create('node', 'forceIPv4'); + node = server.db.nodes[0]; + + // Related models + server.create('agent'); + server.create('job', { createAllocations: false }); + server.createList('allocation', 3, { nodeId: node.id }); + + visit(`/nodes/${node.id}`); + }, +}); + +test('/nodes/:id should have a breadrcumb trail linking back to nodes', function(assert) { + assert.equal(find('.breadcrumb:eq(0)').text(), 'Nodes', 'First breadcrumb says nodes'); + assert.equal( + find('.breadcrumb:eq(1)').text(), + node.id.split('-')[0], + 'Second breadcrumb says the node short id' + ); + + click('.breadcrumb:eq(0)'); + andThen(() => { + assert.equal(currentURL(), '/nodes', 'First breadcrumb links back to nodes'); + }); +}); + +test('/nodes/:id should list immediate details for the node in the title', function(assert) { + assert.ok( + find('.title') + .text() + .includes(node.name), + 'Title includes name' + ); + assert.ok( + find('.title') + .text() + .includes(node.id), + 'Title includes id' + ); + assert.ok(find(`.title .node-status-light.${node.status}`).length, 'Title includes status light'); +}); + +test('/nodes/:id should list additional detail for the node below the title', function(assert) { + assert.equal( + find('.inline-definitions .pair:eq(0)').text(), + `Status ${node.status}`, + 'Status is in additional details' + ); + assert.ok( + find('.inline-definitions .pair:eq(0) .status-text').hasClass(`node-${node.status}`), + 'Status is decorated with a status class' + ); + assert.equal( + find('.inline-definitions .pair:eq(1)').text(), + `Address ${node.httpAddr}`, + 'Address is in additional detals' + ); + assert.equal( + find('.inline-definitions .pair:eq(2)').text(), + `Datacenter ${node.datacenter}`, + 'Datacenter is in additional details' + ); +}); + +test('/nodes/:id should list all allocations on the node', function(assert) { + const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; + assert.equal( + find('.allocations tbody tr').length, + allocationsCount, + `Allocations table lists all ${allocationsCount} associated allocations` + ); +}); + +test('each allocation should have high-level details for the allocation', function(assert) { + const allocationRow = find('.allocations tbody tr:eq(0)'); + const allocation = server.db.allocations + .where({ nodeId: node.id }) + .sortBy('modifyIndex') + .reverse()[0]; + + const allocStats = server.db.clientAllocationStats.find(allocation.id); + const taskGroup = server.db.taskGroups.findBy({ + name: allocation.taskGroup, + jobId: allocation.jobId, + }); + + const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); + + assert.equal( + allocationRow + .find('td:eq(0)') + .text() + .trim(), + allocation.id.split('-')[0], + 'Allocation short ID' + ); + assert.equal( + allocationRow + .find('td:eq(1)') + .text() + .trim(), + allocation.name, + 'Allocation name' + ); + assert.equal( + allocationRow + .find('td:eq(2)') + .text() + .trim(), + allocation.clientStatus, + 'Client status' + ); + assert.ok( + allocationRow + .find('td:eq(3)') + .text() + .includes(server.db.jobs.find(allocation.jobId).name), + 'Job name' + ); + assert.ok( + allocationRow + .find('td:eq(3) .is-faded') + .text() + .includes(allocation.taskGroup), + 'Task group name' + ); + assert.equal( + allocationRow + .find('td:eq(4)') + .text() + .trim(), + allocStats.resourceUsage.CpuStats.Percent, + 'CPU %' + ); + assert.equal( + allocationRow + .find('td:eq(5)') + .text() + .trim(), + allocStats.resourceUsage.MemoryStats.Cache / + tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0), + 'Memory used' + ); +}); + +test('each allocation should link to the allocation detail page', function(assert) { + const allocation = server.db.allocations + .where({ nodeId: node.id }) + .sortBy('modifyIndex') + .reverse()[0]; + + click('.allocations tbody tr:eq(0) td:eq(0) a'); + + andThen(() => { + assert.equal( + currentURL(), + `/allocations/${allocation.id}`, + 'Allocation rows link to allocation detail pages' + ); + }); +}); + +test('each allocation should link to the job the allocation belongs to', function(assert) { + const allocation = server.db.allocations.where({ nodeId: node.id })[0]; + const job = server.db.jobs.find(allocation.jobId); + click('.allocations tbody tr:eq(0) td:eq(3) a'); + + andThen(() => { + assert.equal( + currentURL(), + `/jobs/${job.id}`, + 'Allocation rows link to the job detail page for the allocation' + ); + }); +}); + +test('/nodes/:id should list all attributes for the node', function(assert) { + assert.ok(find('.attributes-table'), 'Attributes table is on the page'); +}); diff --git a/ui/tests/acceptance/job-definition-test.js b/ui/tests/acceptance/job-definition-test.js new file mode 100644 index 000000000..77496f905 --- /dev/null +++ b/ui/tests/acceptance/job-definition-test.js @@ -0,0 +1,27 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +let job; + +moduleForAcceptance('Acceptance | job detail', { + beforeEach() { + server.create('node'); + server.create('job'); + job = server.db.jobs[0]; + visit(`/jobs/${job.id}/definition`); + }, +}); + +test('visiting /jobs/:job_id/definition', function(assert) { + assert.equal(currentURL(), `/jobs/${job.id}/definition`); +}); + +test('the job definition page contains a json viewer component', function(assert) { + assert.ok(find('.json-viewer').length, 'JSON viewer found'); +}); + +test('the job definition page requests the job to display in an unmutated form', function(assert) { + const jobURL = `/v1/job/${job.id}`; + const jobRequests = server.pretender.handledRequests.filter(req => req.url === jobURL); + assert.ok(jobRequests.length === 2, 'Two requests for the job were made'); +}); diff --git a/ui/tests/acceptance/job-deployments-test.js b/ui/tests/acceptance/job-deployments-test.js new file mode 100644 index 000000000..8e46e889b --- /dev/null +++ b/ui/tests/acceptance/job-deployments-test.js @@ -0,0 +1,343 @@ +import Ember from 'ember'; +import { test } from 'qunit'; +import moment from 'moment'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +const { get } = Ember; +const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); + +let job; +let deployments; +let sortedDeployments; + +moduleForAcceptance('Acceptance | job deployments', { + beforeEach() { + server.create('node'); + job = server.create('job'); + deployments = server.schema.deployments.where({ jobId: job.id }); + sortedDeployments = deployments.sort((a, b) => { + const aVersion = server.db.jobVersions.findBy({ jobId: a.jobId, version: a.versionNumber }); + const bVersion = server.db.jobVersions.findBy({ jobId: b.jobId, version: b.versionNumber }); + if (aVersion.submitTime < bVersion.submitTime) { + return 1; + } else if (aVersion.submitTime > bVersion.submitTime) { + return -1; + } + return 0; + }); + }, +}); + +test('/jobs/:id/deployments should list all job deployments', function(assert) { + visit(`/jobs/${job.id}/deployments`); + andThen(() => { + assert.ok( + find('.timeline-object').length, + deployments.length, + 'Each deployment gets a row in the timeline' + ); + }); +}); + +test('each deployment mentions the deployment shortId, status, version, and time since it was submitted', function( + assert +) { + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + const deployment = sortedDeployments.models[0]; + const version = server.db.jobVersions.findBy({ + jobId: deployment.jobId, + version: deployment.versionNumber, + }); + const deploymentRow = find('.timeline-object:eq(0)'); + + assert.ok(deploymentRow.text().includes(deployment.id.split('-')[0]), 'Short ID'); + assert.equal(deploymentRow.find('.tag').text(), deployment.status, 'Status'); + assert.ok( + deploymentRow.find('.tag').hasClass(classForStatus(deployment.status)), + 'Status Class' + ); + assert.ok(deploymentRow.text().includes(deployment.versionNumber), 'Version #'); + assert.ok( + deploymentRow.text().includes(moment(version.submitTime / 1000000).fromNow()), + 'Submit time ago' + ); + }); +}); + +test('when the deployment is running and needs promotion, the deployment item says so', function( + assert +) { + // Ensure the deployment needs deployment + const deployment = sortedDeployments.models[0]; + const taskGroupSummary = deployment.deploymentTaskGroupSummaryIds.map(id => + server.schema.deploymentTaskGroupSummaries.find(id) + )[0]; + + deployment.update('status', 'running'); + deployment.save(); + + taskGroupSummary.update({ + desiredCanaries: 1, + placedCanaries: 0, + promoted: false, + }); + + taskGroupSummary.save(); + + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + const deploymentRow = find('.timeline-object:eq(0)'); + assert.ok( + deploymentRow.find('.badge:contains("Requires Promotion")').length, + 'Requires Promotion badge found' + ); + }); +}); + +test('each deployment item can be opened to show details', function(assert) { + let deploymentRow; + + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + deploymentRow = find('.timeline-object:eq(0)'); + + assert.ok(deploymentRow.find('.boxed-section-body').length === 0, 'No deployment body'); + + click(deploymentRow.find('button')); + + andThen(() => { + assert.ok(deploymentRow.find('.boxed-section-body').length, 'Deployment body found'); + }); + }); +}); + +test('when open, a deployment shows the deployment metrics', function(assert) { + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + const deployment = sortedDeployments.models[0]; + const deploymentRow = find('.timeline-object:eq(0)'); + const taskGroupSummaries = deployment.deploymentTaskGroupSummaryIds.map(id => + server.db.deploymentTaskGroupSummaries.find(id) + ); + + click(deploymentRow.find('button')); + + andThen(() => { + assert.equal( + find('.deployment-metrics .label:contains("Canaries") + .value') + .text() + .trim(), + `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum( + taskGroupSummaries, + 'desiredCanaries' + )}`, + 'Canaries, both places and desired, are in the metrics' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Placed") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'placedAllocs'), + 'Placed allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Desired") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'desiredTotal'), + 'Desired allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Healthy") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'healthyAllocs'), + 'Healthy allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Unhealthy") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'unhealthyAllocs'), + 'Unhealthy allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .notification') + .text() + .trim(), + deployment.statusDescription, + 'Status description is in the metrics block' + ); + }); + }); +}); + +test('when open, a deployment shows a list of all task groups and their respective stats', function( + assert +) { + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + const deployment = sortedDeployments.models[0]; + const deploymentRow = find('.timeline-object:eq(0)'); + const taskGroupSummaries = deployment.deploymentTaskGroupSummaryIds.map(id => + server.db.deploymentTaskGroupSummaries.find(id) + ); + + click(deploymentRow.find('button')); + + andThen(() => { + assert.ok( + deploymentRow.find('.boxed-section-head:contains("Task Groups")').length, + 'Task groups found' + ); + + const taskGroupTable = deploymentRow.find( + '.boxed-section-head:contains("Task Groups") + .boxed-section-body tbody' + ); + + assert.equal( + taskGroupTable.find('tr').length, + taskGroupSummaries.length, + 'One row per task group' + ); + + const taskGroup = taskGroupSummaries[0]; + const taskGroupRow = taskGroupTable.find('tr:eq(0)'); + + assert.equal( + taskGroupRow + .find('td:eq(0)') + .text() + .trim(), + taskGroup.name, + 'Name' + ); + assert.equal( + taskGroupRow + .find('td:eq(1)') + .text() + .trim(), + promotionTestForTaskGroup(taskGroup), + 'Needs Promotion' + ); + assert.equal( + taskGroupRow + .find('td:eq(2)') + .text() + .trim(), + taskGroup.autoRevert ? 'Yes' : 'No', + 'Auto Revert' + ); + assert.equal( + taskGroupRow + .find('td:eq(3)') + .text() + .trim(), + `${taskGroup.placedCanaries} / ${taskGroup.desiredCanaries}`, + 'Canaries' + ); + assert.equal( + taskGroupRow + .find('td:eq(4)') + .text() + .trim(), + `${taskGroup.placedAllocs} / ${taskGroup.desiredTotal}`, + 'Allocs' + ); + assert.equal( + taskGroupRow + .find('td:eq(5)') + .text() + .trim(), + taskGroup.healthyAllocs, + 'Healthy Allocs' + ); + assert.equal( + taskGroupRow + .find('td:eq(6)') + .text() + .trim(), + taskGroup.unhealthyAllocs, + 'Unhealthy Allocs' + ); + }); + }); +}); + +test('when open, a deployment shows a list of all allocations for the deployment', function( + assert +) { + visit(`/jobs/${job.id}/deployments`); + + andThen(() => { + const deployment = sortedDeployments.models[0]; + const deploymentRow = find('.timeline-object:eq(0)'); + + // TODO: Make this less brittle. This logic is copied from the mirage config, + // since there is no reference to allocations on the deployment model. + const allocations = server.db.allocations.where({ jobId: deployment.jobId }).slice(0, 3); + + click(deploymentRow.find('button')); + + andThen(() => { + assert.ok( + deploymentRow.find('.boxed-section-head:contains("Allocations")').length, + 'Allocations found' + ); + + const allocationsTable = deploymentRow.find( + '.boxed-section-head:contains("Allocations") + .boxed-section-body tbody' + ); + + assert.equal( + allocationsTable.find('tr').length, + allocations.length, + 'One row per allocation' + ); + + const allocation = allocations[0]; + const allocationRow = allocationsTable.find('tr:eq(0)'); + + assert.equal( + allocationRow + .find('td:eq(0)') + .text() + .trim(), + allocation.id.split('-')[0], + 'Allocation is as expected' + ); + }); + }); +}); + +function classForStatus(status) { + const classMap = { + running: 'is-running', + successful: 'is-primary', + paused: 'is-light', + failed: 'is-error', + cancelled: 'is-cancelled', + }; + + return classMap[status] || 'is-dark'; +} + +function promotionTestForTaskGroup(taskGroup) { + if (taskGroup.desiredCanaries > 0 && taskGroup.promoted === false) { + return 'Yes'; + } else if (taskGroup.desiredCanaries > 0) { + return 'No'; + } + return 'N/A'; +} diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js new file mode 100644 index 000000000..e9abf8340 --- /dev/null +++ b/ui/tests/acceptance/job-detail-test.js @@ -0,0 +1,270 @@ +import Ember from 'ember'; +import moment from 'moment'; +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +const { get } = Ember; +const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); + +let job; + +moduleForAcceptance('Acceptance | job detail', { + beforeEach() { + server.create('node'); + server.create('job'); + job = server.db.jobs[0]; + visit(`/jobs/${job.id}`); + }, +}); + +test('visiting /jobs/:job_id', function(assert) { + assert.equal(currentURL(), `/jobs/${job.id}`); +}); + +test('breadcrumbs includes job name and link back to the jobs list', function(assert) { + assert.equal(find('.breadcrumb:eq(0)').text(), 'Jobs', 'First breadcrumb says jobs'); + assert.equal(find('.breadcrumb:eq(1)').text(), job.name, 'Second breadcrumb says the job name'); + + click('.breadcrumb:eq(0)'); + andThen(() => { + assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); + }); +}); + +test('the job detail page should contain basic information about the job', function(assert) { + assert.ok( + find('.title .tag:eq(0)') + .text() + .includes(job.status), + 'Status' + ); + assert.ok( + find('.job-stats span:eq(0)') + .text() + .includes(job.type), + 'Type' + ); + assert.ok( + find('.job-stats span:eq(1)') + .text() + .includes(job.priority), + 'Priority' + ); +}); + +test('the job detail page should list all task groups', function(assert) { + assert.equal( + find('.task-group-row').length, + server.db.taskGroups.where({ jobId: job.id }).length + ); +}); + +test('each row in the task group table should show basic information about the task group', function( + assert +) { + const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0]; + const taskGroupRow = find('.task-group-row:eq(0)'); + const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id }); + const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); + + assert.equal(taskGroupRow.find('td:eq(0)').text(), taskGroup.name, 'Name'); + assert.equal(taskGroupRow.find('td:eq(1)').text(), taskGroup.count, 'Count'); + assert.equal( + taskGroupRow.find('td:eq(3)').text(), + `${sum(tasks, 'Resources.CPU')} MHz`, + 'Reserved CPU' + ); + assert.equal( + taskGroupRow.find('td:eq(4)').text(), + `${sum(tasks, 'Resources.MemoryMB')} MiB`, + 'Reserved Memory' + ); + assert.equal( + taskGroupRow.find('td:eq(5)').text(), + `${taskGroup.ephemeralDisk.SizeMB} MiB`, + 'Reserved Disk' + ); +}); + +test('the allocations diagram lists all allocation status figures', function(assert) { + const legend = find('.distribution-bar .legend'); + const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id }); + const statusCounts = Object.keys(jobSummary.Summary).reduce( + (counts, key) => { + const group = jobSummary.Summary[key]; + counts.queued += group.Queued; + counts.starting += group.Starting; + counts.running += group.Running; + counts.complete += group.Complete; + counts.failed += group.Failed; + counts.lost += group.Lost; + return counts; + }, + { queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 } + ); + + assert.equal( + legend.find('li.queued .value').text(), + statusCounts.queued, + `${statusCounts.queued} are queued` + ); + + assert.equal( + legend.find('li.starting .value').text(), + statusCounts.starting, + `${statusCounts.starting} are starting` + ); + + assert.equal( + legend.find('li.running .value').text(), + statusCounts.running, + `${statusCounts.running} are running` + ); + + assert.equal( + legend.find('li.complete .value').text(), + statusCounts.complete, + `${statusCounts.complete} are complete` + ); + + assert.equal( + legend.find('li.failed .value').text(), + statusCounts.failed, + `${statusCounts.failed} are failed` + ); + + assert.equal( + legend.find('li.lost .value').text(), + statusCounts.lost, + `${statusCounts.lost} are lost` + ); +}); + +test('there is no active deployment section when the job has no active deployment', function( + assert +) { + // TODO: it would be better to not visit two different job pages in one test, but this + // way is much more convenient. + job = server.create('job', { noActiveDeployment: true }); + visit(`/jobs/${job.id}`); + + andThen(() => { + assert.ok(find('.active-deployment').length === 0, 'No active deployment'); + }); +}); + +test('the active deployment section shows up for the currently running deployment', function( + assert +) { + job = server.create('job', { activeDeployment: true }); + const deployment = server.db.deployments.where({ jobId: job.id })[0]; + const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({ + deploymentId: deployment.id, + }); + const version = server.db.jobVersions.findBy({ + jobId: job.id, + version: deployment.versionNumber, + }); + visit(`/jobs/${job.id}`); + + andThen(() => { + assert.ok(find('.active-deployment').length === 1, 'Active deployment'); + assert.equal( + find('.active-deployment > .boxed-section-head .badge') + .text() + .trim(), + deployment.id.split('-')[0], + 'The active deployment is the most recent running deployment' + ); + + assert.equal( + find('.active-deployment > .boxed-section-head .submit-time') + .text() + .trim(), + moment(version.submitTime / 1000000).fromNow(), + 'Time since the job was submitted is in the active deployment header' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Canaries") + .value') + .text() + .trim(), + `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum( + taskGroupSummaries, + 'desiredCanaries' + )}`, + 'Canaries, both places and desired, are in the metrics' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Placed") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'placedAllocs'), + 'Placed allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Desired") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'desiredTotal'), + 'Desired allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Healthy") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'healthyAllocs'), + 'Healthy allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .label:contains("Unhealthy") + .value') + .text() + .trim(), + sum(taskGroupSummaries, 'unhealthyAllocs'), + 'Unhealthy allocs aggregates across task groups' + ); + + assert.equal( + find('.deployment-metrics .notification') + .text() + .trim(), + deployment.statusDescription, + 'Status description is in the metrics block' + ); + }); +}); + +test('the active deployment section can be expanded to show task groups and allocations', function( + assert +) { + job = server.create('job', { activeDeployment: true }); + visit(`/jobs/${job.id}`); + + andThen(() => { + assert.ok( + find('.active-deployment .boxed-section-head:contains("Task Groups")').length === 0, + 'Task groups not found' + ); + assert.ok( + find('.active-deployment .boxed-section-head:contains("Allocations")').length === 0, + 'Allocations not found' + ); + }); + + click('.active-deployment-details-toggle'); + + andThen(() => { + assert.ok( + find('.active-deployment .boxed-section-head:contains("Task Groups")').length === 1, + 'Task groups found' + ); + assert.ok( + find('.active-deployment .boxed-section-head:contains("Allocations")').length === 1, + 'Allocations found' + ); + }); +}); diff --git a/ui/tests/acceptance/job-versions-test.js b/ui/tests/acceptance/job-versions-test.js new file mode 100644 index 000000000..0843243ea --- /dev/null +++ b/ui/tests/acceptance/job-versions-test.js @@ -0,0 +1,42 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; +import moment from 'moment'; + +let job; +let versions; + +moduleForAcceptance('Acceptance | job versions', { + beforeEach() { + job = server.create('job', { createAllocations: false }); + versions = server.db.jobVersions.where({ jobId: job.id }); + + visit(`/jobs/${job.id}/versions`); + }, +}); + +test('/jobs/:id/versions should list all job versions', function(assert) { + assert.ok( + find('.timeline-object').length, + versions.length, + 'Each version gets a row in the timeline' + ); +}); + +test('each version mentions the version number, the stability, and the submitted time', function( + assert +) { + const version = versions.sortBy('submitTime').reverse()[0]; + const versionRow = find('.timeline-object:eq(0)'); + + assert.ok(versionRow.text().includes(`Version #${version.version}`), 'Version #'); + assert.equal( + versionRow.find('.version-stability .badge').text(), + version.stable.toString(), + 'Stability' + ); + assert.equal( + versionRow.find('.version-submit-date .submit-date').text(), + moment(version.submitTime / 1000000).format('MM/DD/YY HH:mm:ss [UTC]'), + 'Submit time' + ); +}); diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js new file mode 100644 index 000000000..61f9a070a --- /dev/null +++ b/ui/tests/acceptance/jobs-list-test.js @@ -0,0 +1,68 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +moduleForAcceptance('Acceptance | jobs list', { + beforeEach() { + // Required for placing allocations (a result of creating jobs) + server.create('node'); + }, +}); + +test('visiting /jobs', function(assert) { + visit('/jobs'); + + andThen(() => { + assert.equal(currentURL(), '/jobs'); + }); +}); + +test('/jobs should list the first page of jobs sorted by modify index', function(assert) { + const jobsCount = 11; + const pageSize = 10; + server.createList('job', jobsCount, { createAllocations: false }); + + visit('/jobs'); + + andThen(() => { + const sortedJobs = server.db.jobs.sortBy('modifyIndex').reverse(); + assert.equal(find('.job-row').length, pageSize); + for (var jobNumber = 0; jobNumber < pageSize; jobNumber++) { + assert.equal( + find(`.job-row:eq(${jobNumber}) td:eq(0)`).text(), + sortedJobs[jobNumber].name, + 'Jobs are ordered' + ); + } + }); +}); + +test('each job row should contain information about the job', function(assert) { + server.createList('job', 2); + const job = server.db.jobs.sortBy('modifyIndex').reverse()[0]; + const taskGroups = server.db.taskGroups.where({ jobId: job.id }); + + visit('/jobs'); + + andThen(() => { + const jobRow = find('.job-row:eq(0)'); + + assert.equal(jobRow.find('td:eq(0)').text(), job.name, 'Name'); + assert.equal(jobRow.find('td:eq(0) a').attr('href'), `/ui/jobs/${job.id}`, 'Detail Link'); + assert.equal(jobRow.find('td:eq(1)').text().trim(), job.status, 'Status'); + assert.equal(jobRow.find('td:eq(2)').text(), job.type, 'Type'); + assert.equal(jobRow.find('td:eq(3)').text(), job.priority, 'Priority'); + assert.equal(jobRow.find('td:eq(4)').text(), taskGroups.length, '# Groups'); + }); +}); + +test('each job row should link to the corresponding job', function(assert) { + server.create('job'); + const job = server.db.jobs[0]; + + visit('/jobs'); + click('.job-row:eq(0) td:eq(0) a'); + + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}`); + }); +}); diff --git a/ui/tests/acceptance/nodes-list-test.js b/ui/tests/acceptance/nodes-list-test.js new file mode 100644 index 000000000..2b506aab6 --- /dev/null +++ b/ui/tests/acceptance/nodes-list-test.js @@ -0,0 +1,135 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; +import { findLeader } from '../../mirage/config'; +import ipParts from 'nomad-ui/utils/ip-parts'; + +function minimumSetup() { + server.createList('node', 1); + server.createList('agent', 1); +} + +moduleForAcceptance('Acceptance | nodes list'); + +test('/nodes should list one page of clients', function(assert) { + // Make sure to make more nodes than 1 page to assert that pagination is working + const nodesCount = 10; + const pageSize = 8; + + server.createList('node', nodesCount); + server.createList('agent', 1); + + visit('/nodes'); + + andThen(() => { + assert.equal(find('.client-node-row').length, pageSize); + assert.ok(find('.pagination').length, 'Pagination found on the page'); + + const sortedNodes = server.db.nodes.sortBy('modifyIndex').reverse(); + + for (var nodeNumber = 0; nodeNumber < pageSize; nodeNumber++) { + assert.equal( + find(`.client-node-row:eq(${nodeNumber}) td:eq(0)`).text(), + sortedNodes[nodeNumber].id.split('-')[0], + 'Nodes are ordered' + ); + } + }); +}); + +test('each client record should show high-level info of the client', function(assert) { + minimumSetup(); + const node = server.db.nodes[0]; + + visit('/nodes'); + + andThen(() => { + const nodeRow = find('.client-node-row:eq(0)'); + const allocations = server.db.allocations.where({ nodeId: node.id }); + const { address, port } = ipParts(node.httpAddr); + + assert.equal(nodeRow.find('td:eq(0)').text(), node.id.split('-')[0], 'ID'); + assert.equal(nodeRow.find('td:eq(1)').text(), node.name, 'Name'); + assert.equal(nodeRow.find('td:eq(2)').text(), node.status, 'Status'); + assert.equal(nodeRow.find('td:eq(3)').text(), address, 'Address'); + assert.equal(nodeRow.find('td:eq(4)').text(), port, 'Port'); + assert.equal(nodeRow.find('td:eq(5)').text(), node.datacenter, 'Datacenter'); + assert.equal(nodeRow.find('td:eq(6)').text(), allocations.length, '# Allocations'); + }); +}); + +test('each client should link to the client detail page', function(assert) { + minimumSetup(); + const node = server.db.nodes[0]; + + visit('/nodes'); + click('.client-node-row:eq(0)'); + + andThen(() => { + assert.equal(currentURL(), `/nodes/${node.id}`); + }); +}); + +test('/servers should list all servers', function(assert) { + const agentsCount = 10; + const pageSize = 8; + + server.createList('node', 1); + server.createList('agent', agentsCount); + + const leader = findLeader(server.schema); + + visit('/servers'); + + andThen(() => { + assert.equal(find('.server-agent-row').length, pageSize); + + const sortedAgents = server.db.agents + .sort((a, b) => { + if (`${a.address}:${a.tags.port}` === leader) { + return 1; + } else if (`${b.address}:${b.tags.port}` === leader) { + return -1; + } + return 0; + }) + .reverse(); + + for (var agentNumber = 0; agentNumber < 8; agentNumber++) { + assert.equal( + find(`.server-agent-row:eq(${agentNumber}) td:eq(0)`).text(), + sortedAgents[agentNumber].name, + 'Clients are ordered' + ); + } + }); +}); + +test('each server should show high-level info of the server', function(assert) { + minimumSetup(); + const agent = server.db.agents[0]; + + visit('/servers'); + + andThen(() => { + const agentRow = find('.server-agent-row:eq(0)'); + + assert.equal(agentRow.find('td:eq(0)').text(), agent.name, 'Name'); + assert.equal(agentRow.find('td:eq(1)').text(), agent.status, 'Status'); + assert.equal(agentRow.find('td:eq(2)').text(), 'True', 'Leader?'); + assert.equal(agentRow.find('td:eq(3)').text(), agent.address, 'Address'); + assert.equal(agentRow.find('td:eq(4)').text(), agent.serf_port, 'Serf Port'); + assert.equal(agentRow.find('td:eq(5)').text(), agent.tags.dc, 'Datacenter'); + }); +}); + +test('each server should link to the server detail page', function(assert) { + minimumSetup(); + const agent = server.db.agents[0]; + + visit('/servers'); + click('.server-agent-row:eq(0)'); + + andThen(() => { + assert.equal(currentURL(), `/servers/${agent.name}`); + }); +}); diff --git a/ui/tests/acceptance/server-detail-test.js b/ui/tests/acceptance/server-detail-test.js new file mode 100644 index 000000000..164a0449a --- /dev/null +++ b/ui/tests/acceptance/server-detail-test.js @@ -0,0 +1,40 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +let agent; + +moduleForAcceptance('Acceptance | server detail', { + beforeEach() { + server.createList('agent', 3); + agent = server.db.agents[0]; + visit(`/servers/${agent.name}`); + }, +}); + +test('visiting /servers/:server_name', function(assert) { + assert.equal(currentURL(), `/servers/${agent.name}`); +}); + +test('the server detail page should list all tags for the server', function(assert) { + const tags = agent.tags; + + assert.equal(find('.server-tags tbody tr').length, Object.keys(tags).length, '# of tags'); + Object.keys(tags).forEach((key, index) => { + const row = find(`.server-tags tbody tr:eq(${index})`); + assert.equal(row.find('td:eq(0)').text(), key, `Label: ${key}`); + assert.equal(row.find('td:eq(1)').text(), tags[key], `Value: ${tags[key]}`); + }); +}); + +test('the list of servers from /servers should still be present', function(assert) { + assert.equal(find('.server-agent-row').length, server.db.agents.length, '# of servers'); +}); + +test('the active server should be denoted in the table', function(assert) { + assert.equal(find('.server-agent-row.is-active').length, 1, 'Only one active server'); + assert.equal( + find('.server-agent-row.is-active td:eq(0)').text(), + agent.name, + 'Active server matches current route' + ); +}); diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js new file mode 100644 index 000000000..41b03aa1d --- /dev/null +++ b/ui/tests/acceptance/task-group-detail-test.js @@ -0,0 +1,199 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +let job; +let taskGroup; +let tasks; +let allocations; + +const sum = (total, n) => total + n; + +moduleForAcceptance('Acceptance | task group detail', { + beforeEach() { + server.create('node', 'forceIPv4'); + + job = server.create('job', { + groupsCount: 2, + createAllocations: false, + }); + + const taskGroups = server.db.taskGroups.where({ jobId: job.id }); + taskGroup = taskGroups[0]; + + tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); + + server.create('node', 'forceIPv4'); + + allocations = server.createList('allocation', 2, { + jobId: job.id, + taskGroup: taskGroup.name, + }); + + // Allocations associated to a different task group on the job to + // assert that they aren't showing up in on this page in error. + server.createList('allocation', 3, { + jobId: job.id, + taskGroup: taskGroups[1].name, + }); + + visit(`/jobs/${job.id}/${taskGroup.name}`); + }, +}); + +test('/jobs/:id/:task-group should list high-level metrics for the allocation', function(assert) { + const totalCPU = tasks.mapBy('Resources.CPU').reduce(sum, 0); + const totalMemory = tasks.mapBy('Resources.MemoryMB').reduce(sum, 0); + const totalDisk = taskGroup.ephemeralDisk.SizeMB; + + assert.equal( + find('.inline-definitions .pair:eq(0)').text(), + `# Tasks ${tasks.length}`, + '# Tasks' + ); + assert.equal( + find('.inline-definitions .pair:eq(1)').text(), + `Reserved CPU ${totalCPU} MHz`, + 'Aggregated CPU reservation for all tasks' + ); + assert.equal( + find('.inline-definitions .pair:eq(2)').text(), + `Reserved Memory ${totalMemory} MiB`, + 'Aggregated Memory reservation for all tasks' + ); + assert.equal( + find('.inline-definitions .pair:eq(3)').text(), + `Reserved Disk ${totalDisk} MiB`, + 'Aggregated Disk reservation for all tasks' + ); +}); + +test('/jobs/:id/:task-group should have breadcrumbs for job and jobs', function(assert) { + assert.equal(find('.breadcrumb:eq(0)').text(), 'Jobs', 'First breadcrumb says jobs'); + assert.equal(find('.breadcrumb:eq(1)').text(), job.name, 'Second breadcrumb says the job name'); + assert.equal( + find('.breadcrumb:eq(2)').text(), + taskGroup.name, + 'Third breadcrumb says the job name' + ); +}); + +test('/jobs/:id/:task-group first breadcrumb should link to jobs', function(assert) { + click('.breadcrumb:eq(0)'); + andThen(() => { + assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); + }); +}); + +test('/jobs/:id/:task-group second breadcrumb should link to the job for the task group', function( + assert +) { + click('.breadcrumb:eq(1)'); + andThen(() => { + assert.equal( + currentURL(), + `/jobs/${job.id}`, + 'Second breadcrumb links back to the job for the task group' + ); + }); +}); + +test('/jobs/:id/:task-group should list one page of allocations for the task group', function( + assert +) { + const pageSize = 10; + + server.createList('allocation', 10, { + jobId: job.id, + taskGroup: taskGroup.name, + }); + + visit('/jobs'); + visit(`/jobs/${job.id}/${taskGroup.name}`); + + andThen(() => { + assert.ok( + server.db.allocations.where({ jobId: job.id }).length > pageSize, + 'There are enough allocations to invoke pagination' + ); + + assert.equal( + find('.allocations tbody tr').length, + pageSize, + 'All allocations for the task group' + ); + }); +}); + +test('each allocation should show basic information about the allocation', function(assert) { + const allocation = allocations.sortBy('name')[0]; + const allocationRow = find('.allocations tbody tr:eq(0)'); + + assert.equal( + allocationRow + .find('td:eq(0)') + .text() + .trim(), + allocation.id.split('-')[0], + 'Allocation short id' + ); + assert.equal( + allocationRow + .find('td:eq(1)') + .text() + .trim(), + allocation.name, + 'Allocation name' + ); + assert.equal( + allocationRow + .find('td:eq(2)') + .text() + .trim(), + allocation.clientStatus, + 'Client status' + ); + assert.equal( + allocationRow + .find('td:eq(3)') + .text() + .trim(), + server.db.nodes.find(allocation.nodeId).id.split('-')[0], + 'Node name' + ); +}); + +test('each allocation should show stats about the allocation, retrieved directly from the node', function( + assert +) { + const allocation = allocations.sortBy('name')[0]; + const allocationRow = find('.allocations tbody tr:eq(0)'); + const allocStats = server.db.clientAllocationStats.find(allocation.id); + const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); + + assert.equal( + allocationRow + .find('td:eq(4)') + .text() + .trim(), + allocStats.resourceUsage.CpuStats.Percent, + 'CPU %' + ); + + assert.equal( + allocationRow + .find('td:eq(5)') + .text() + .trim(), + allocStats.resourceUsage.MemoryStats.Cache / + tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0), + 'Memory used' + ); + + const node = server.db.nodes.find(allocation.nodeId); + const nodeStatsUrl = `//${node.httpAddr}/v1/client/allocation/${allocation.id}/stats`; + + assert.ok( + server.pretender.handledRequests.some(req => req.url === nodeStatsUrl), + `Requests ${nodeStatsUrl}` + ); +}); diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js new file mode 100644 index 000000000..9c176a4e3 --- /dev/null +++ b/ui/tests/acceptance/token-test.js @@ -0,0 +1,71 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +let job; +let node; + +moduleForAcceptance('Acceptance | tokens', { + beforeEach() { + server.create('agent'); + node = server.create('node'); + job = server.create('job'); + }, +}); + +test('the token form sets the token in session storage', function(assert) { + const secret = 'this-is-the-secret'; + const accessor = 'this-is-the-accessor'; + + visit('/settings/tokens'); + + andThen(() => { + assert.ok(window.sessionStorage.nomadTokenSecret == null, 'No token secret set'); + assert.ok(window.sessionStorage.nomadTokenAccessor == null, 'No token accessor set'); + + fillIn('.token-secret', secret); + + andThen(() => { + assert.equal(window.sessionStorage.nomadTokenSecret, secret, 'Token secret was set'); + assert.ok(window.sessionStorage.nomadTokenAccessor == null, 'Token accessor was not set'); + }); + + fillIn('.token-accessor', accessor); + + andThen(() => { + assert.equal(window.sessionStorage.nomadTokenAccessor, accessor, 'Token accessor was set'); + }); + }); +}); + +test('the X-Nomad-Token header gets sent with requests once it is set', function(assert) { + const secret = 'this-is-the-secret'; + let requestPosition = 0; + + visit(`/jobs/${job.id}`); + visit(`/nodes/${node.id}`); + + andThen(() => { + assert.ok(server.pretender.handledRequests.length > 1, 'Requests have been made'); + + server.pretender.handledRequests.forEach(req => { + assert.notOk(req.requestHeaders['X-Nomad-Token'], `No token for ${req.url}`); + }); + + requestPosition = server.pretender.handledRequests.length; + }); + + visit('/settings/tokens'); + fillIn('.token-secret', secret); + + visit(`/jobs/${job.id}`); + visit(`/nodes/${node.id}`); + + andThen(() => { + const newRequests = server.pretender.handledRequests.slice(requestPosition); + assert.ok(newRequests.length > 1, 'New requests have been made'); + + newRequests.forEach(req => { + assert.equal(req.requestHeaders['X-Nomad-Token'], secret, `Token set for ${req.url}`); + }); + }); +}); diff --git a/ui/tests/helpers/destroy-app.js b/ui/tests/helpers/destroy-app.js new file mode 100644 index 000000000..1807e213c --- /dev/null +++ b/ui/tests/helpers/destroy-app.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; + +export default function destroyApp(application) { + Ember.run(application, 'destroy'); + if (window.server) { + window.server.shutdown(); + } +} diff --git a/ui/tests/helpers/module-for-acceptance.js b/ui/tests/helpers/module-for-acceptance.js new file mode 100644 index 000000000..3bfa0518a --- /dev/null +++ b/ui/tests/helpers/module-for-acceptance.js @@ -0,0 +1,26 @@ +import { module } from 'qunit'; +import Ember from 'ember'; +import startApp from '../helpers/start-app'; +import destroyApp from '../helpers/destroy-app'; + +const { RSVP: { Promise } } = Ember; + +export default function(name, options = {}) { + module(name, { + beforeEach() { + // Clear session storage (a side effect of token storage) + window.sessionStorage.clear(); + + this.application = startApp(); + + if (options.beforeEach) { + return options.beforeEach.apply(this, arguments); + } + }, + + afterEach() { + let afterEach = options.afterEach && options.afterEach.apply(this, arguments); + return Promise.resolve(afterEach).then(() => destroyApp(this.application)); + }, + }); +} diff --git a/ui/tests/helpers/module-for-serializer.js b/ui/tests/helpers/module-for-serializer.js new file mode 100644 index 000000000..e2a1fec8a --- /dev/null +++ b/ui/tests/helpers/module-for-serializer.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import { moduleForModel } from 'ember-qunit'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +const { getOwner } = Ember; + +export default function(modelName, description, options = { needs: [] }) { + // moduleForModel correctly wires up #Serializer.store, + // but module does not. + moduleForModel(modelName, description, { + unit: true, + needs: options.needs, + beforeEach() { + const model = this.subject(); + + // Initializers don't run automatically in unit tests + fragmentSerializerInitializer(getOwner(model)); + + // Reassign the subject to provide the serializer + this.subject = () => model.store.serializerFor(modelName); + + if (options.beforeEach) { + options.beforeEach.apply(this, arguments); + } + }, + afterEach() { + if (options.beforeEach) { + options.beforeEach.apply(this, arguments); + } + }, + }); +} diff --git a/ui/tests/helpers/resolver.js b/ui/tests/helpers/resolver.js new file mode 100644 index 000000000..319b45fc1 --- /dev/null +++ b/ui/tests/helpers/resolver.js @@ -0,0 +1,11 @@ +import Resolver from '../../resolver'; +import config from '../../config/environment'; + +const resolver = Resolver.create(); + +resolver.namespace = { + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, +}; + +export default resolver; diff --git a/ui/tests/helpers/start-app.js b/ui/tests/helpers/start-app.js new file mode 100644 index 000000000..9a605eb8d --- /dev/null +++ b/ui/tests/helpers/start-app.js @@ -0,0 +1,15 @@ +import Ember from 'ember'; +import Application from '../../app'; +import config from '../../config/environment'; + +export default function startApp(attrs) { + let attributes = Ember.merge({}, config.APP); + attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; + + return Ember.run(() => { + let application = Application.create(attributes); + application.setupForTesting(); + application.injectTestHelpers(); + return application; + }); +} diff --git a/ui/tests/index.html b/ui/tests/index.html new file mode 100644 index 000000000..acb2855aa --- /dev/null +++ b/ui/tests/index.html @@ -0,0 +1,33 @@ + + + + + + Ui Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/ui/tests/integration/.gitkeep b/ui/tests/integration/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/tests/integration/attributes-table-test.js b/ui/tests/integration/attributes-table-test.js new file mode 100644 index 000000000..384f7dc92 --- /dev/null +++ b/ui/tests/integration/attributes-table-test.js @@ -0,0 +1,87 @@ +import { test, moduleForComponent } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import flat from 'npm:flat'; + +const { flatten } = flat; + +moduleForComponent('attributes-table', 'Integration | Component | attributes table', { + integration: true, +}); + +const commonAttributes = { + key: 'value', + nested: { + props: 'are', + supported: 'just', + fine: null, + }, + so: { + are: { + deeply: { + nested: 'properties', + like: 'these ones', + }, + }, + }, +}; + +test('should render a row for each key/value pair in a deep object', function(assert) { + this.set('attributes', commonAttributes); + this.render(hbs`{{attributes-table attributes=attributes}}`); + + const rowsCount = Object.keys(flatten(commonAttributes)).length; + assert.equal( + this.$('tbody tr').has('td:eq(1)').length, + rowsCount, + `Table has ${rowsCount} rows with values` + ); +}); + +test('should render the full path of key/value pair from the root of the object', function(assert) { + this.set('attributes', commonAttributes); + this.render(hbs`{{attributes-table attributes=attributes}}`); + + assert.equal( + this.$('tbody tr:eq(0) td:eq(0)').text().trim(), + 'key', + 'Simple row renders only the key' + ); + assert.equal(this.$('tbody tr:eq(0) td:eq(1)').text().trim(), 'value'); + + assert.equal( + this.$('tbody tr:eq(8) td:eq(0)').text().trim(), + 'so.are.deeply.nested', + 'Complex row renders the full path to the key' + ); + assert.equal( + this.$('tbody tr:eq(8) td:eq(0) .is-faded').text().trim(), + 'so.are.deeply.', + 'The prefix is faded to put emphasis on the attribute' + ); + assert.equal(this.$('tbody tr:eq(8) td:eq(1)').text().trim(), 'properties'); +}); + +test('should render a row for key/value pairs even when the value is another object', function( + assert +) { + this.set('attributes', commonAttributes); + this.render(hbs`{{attributes-table attributes=attributes}}`); + + const countOfParentRows = countOfParentKeys(commonAttributes); + assert.equal( + this.$('tbody tr td[colspan="2"]').length, + countOfParentRows, + 'Each key for a nested object gets a row with no value' + ); +}); + +function countOfParentKeys(obj) { + return Object.keys(obj).reduce((count, key) => { + const value = obj[key]; + return isObject(value) ? count + 1 + countOfParentKeys(value) : count; + }, 0); +} + +function isObject(value) { + return !Array.isArray(value) && value != null && typeof value === 'object'; +} diff --git a/ui/tests/integration/job-diff-test.js b/ui/tests/integration/job-diff-test.js new file mode 100644 index 000000000..77d88e6d9 --- /dev/null +++ b/ui/tests/integration/job-diff-test.js @@ -0,0 +1,192 @@ +import { test, moduleForComponent } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('job-diff', 'Integration | Component | job diff', { + integration: true, +}); + +const commonTemplate = hbs` +
    +
    + {{job-diff diff=diff}} +
    +
    +`; + +test('job field diffs', function(assert) { + this.set('diff', { + ID: 'test-case-1', + Type: 'Edited', + Objects: null, + Fields: [ + field('Removed Field', 'deleted', 12), + field('Added Field', 'added', 'Foobar'), + field('Edited Field', 'edited', 512, 256), + ], + }); + + this.render(commonTemplate); + + assert.equal( + this.$('.diff-section-label').length, + 5, + 'A section label for each line, plus one for the group' + ); + assert.equal( + cleanWhitespace(this.$('.diff-section-label .diff-section-label.is-added').text()), + '+ Added Field: "Foobar"', + 'Added field is rendered correctly' + ); + assert.equal( + cleanWhitespace(this.$('.diff-section-label .diff-section-label.is-edited').text()), + '+/- Edited Field: "256" => "512"', + 'Edited field is rendered correctly' + ); + assert.equal( + cleanWhitespace(this.$('.diff-section-label .diff-section-label.is-deleted').text()), + '- Removed Field: "12"', + 'Removed field is rendered correctly' + ); +}); + +test('job object diffs', function(assert) { + this.set('diff', { + ID: 'test-case-2', + Type: 'Edited', + Objects: [ + { + Name: 'ComplexProperty', + Type: 'Edited', + Objects: null, + Fields: [ + field('Prop 1', 'added', 'prop-1-value'), + field('Prop 2', 'none', 'prop-2-is-the-same'), + field('Prop 3', 'edited', 'new value', 'some old value'), + field('Prop 4', 'deleted', 'delete me'), + ], + }, + { + Name: 'DeepConfiguration', + Type: 'Added', + Objects: [ + { + Name: 'VP Props', + Type: 'Added', + Objects: null, + Fields: [ + field('Engineering', 'added', 'Regina Phalange'), + field('Customer Support', 'added', 'Jerome Hendricks'), + field('HR', 'added', 'Jack Blue'), + field('Sales', 'added', 'Maria Lopez'), + ], + }, + ], + Fields: [field('Executive Prop', 'added', 'in charge')], + }, + { + Name: 'DatedStuff', + Type: 'Deleted', + Objects: null, + Fields: [field('Deprecated', 'deleted', 'useless')], + }, + ], + Fields: null, + }); + + this.render(commonTemplate); + + assert.ok( + cleanWhitespace(this.$('.diff-section-label > .diff-section-label.is-added').text()).startsWith( + '+ DeepConfiguration {' + ), + 'Added object starts with a JSON block' + ); + assert.ok( + cleanWhitespace( + this.$('.diff-section-label > .diff-section-label.is-edited').text() + ).startsWith('+/- ComplexProperty {'), + 'Edited object starts with a JSON block' + ); + assert.ok( + cleanWhitespace( + this.$('.diff-section-label > .diff-section-label.is-deleted').text() + ).startsWith('- DatedStuff {'), + 'Removed object starts with a JSON block' + ); + + assert.ok( + cleanWhitespace(this.$('.diff-section-label > .diff-section-label.is-added').text()).endsWith( + '}' + ), + 'Added object ends the JSON block' + ); + assert.ok( + cleanWhitespace(this.$('.diff-section-label > .diff-section-label.is-edited').text()).endsWith( + '}' + ), + 'Edited object starts with a JSON block' + ); + assert.ok( + cleanWhitespace(this.$('.diff-section-label > .diff-section-label.is-deleted').text()).endsWith( + '}' + ), + 'Removed object ends the JSON block' + ); + + assert.equal( + this.$('.diff-section-label > .diff-section-label.is-added > .diff-section-label').length, + this.get('diff').Objects[1].Objects.length + this.get('diff').Objects[1].Fields.length, + 'Edited block contains each nested field and object' + ); + + assert.equal( + this.$( + '.diff-section-label > .diff-section-label.is-added > .diff-section-label > .diff-section-label .diff-section-table-row' + ).length, + this.get('diff').Objects[1].Objects[0].Fields.length, + 'Objects within objects are rendered' + ); +}); + +function field(name, type, newVal, oldVal) { + switch (type) { + case 'added': + return { + Annotations: null, + New: newVal, + Old: '', + Type: 'Added', + Name: name, + }; + case 'deleted': + return { + Annotations: null, + New: '', + Old: newVal, + Type: 'Deleted', + Name: name, + }; + case 'edited': + return { + Annotations: null, + New: newVal, + Old: oldVal, + Type: 'Edited', + Name: name, + }; + } + return { + Annotations: null, + New: newVal, + Old: oldVal, + Type: 'None', + Name: name, + }; +} + +function cleanWhitespace(string) { + return string + .replace(/\n/g, '') + .replace(/ +/g, ' ') + .trim(); +} diff --git a/ui/tests/integration/list-pagination-test.js b/ui/tests/integration/list-pagination-test.js new file mode 100644 index 000000000..6e1c40529 --- /dev/null +++ b/ui/tests/integration/list-pagination-test.js @@ -0,0 +1,242 @@ +import { test, skip, moduleForComponent } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('list-pagination', 'Integration | Component | list pagination', { + integration: true, +}); + +const defaults = { + source: [], + size: 25, + page: 1, + spread: 2, +}; + +const list100 = Array(100).fill(null).map((_, i) => i); + +test('the source property', function(assert) { + this.set('source', list100); + this.render(hbs` + {{#list-pagination source=source as |p|}} + {{p.currentPage}} of {{p.totalPages}} + {{#p.first}}first{{/p.first}} + {{#p.prev}}prev{{/p.prev}} + {{#each p.pageLinks as |link|}} + {{link.pageNumber}} + {{/each}} + {{#p.next}}next{{/p.next}} + {{#p.last}}last{{/p.last}} + + {{#each p.list as |item|}} +
    {{item}}
    + {{/each}} + {{/list-pagination}} + `); + + assert.ok(!this.$('.first').length, 'On the first page, there is no first link'); + assert.ok(!this.$('.prev').length, 'On the first page, there is no prev link'); + + assert.equal( + this.$('.link').length, + defaults.spread + 1, + 'Pages links spread to the right by the spread amount' + ); + + for (var pageNumber = 1; pageNumber <= defaults.spread + 1; pageNumber++) { + assert.ok(this.$(`.link.page-${pageNumber}`).length, `Page link includes ${pageNumber}`); + } + + assert.ok(this.$('.next').length, 'While not on the last page, there is a next link'); + assert.ok(this.$('.last').length, 'While not on the last page, there is a last link'); + + assert.ok( + this.$('.item').length, + defaults.size, + `Only ${defaults.size} (the default) number of items are rendered` + ); + + for (var item = 0; item < defaults.size; item++) { + assert.equal( + this.$(`.item:eq(${item})`).text(), + item, + 'Rendered items are in the current page' + ); + } +}); + +test('the size property', function(assert) { + this.setProperties({ + size: 5, + source: list100, + }); + this.render(hbs` + {{#list-pagination source=source size=size as |p|}} + {{p.currentPage}} of {{p.totalPages}} + {{/list-pagination}} + `); + + const totalPages = Math.ceil(this.get('source').length / this.get('size')); + assert.equal(this.$('.page-info').text(), `1 of ${totalPages}`, `${totalPages} total pages`); +}); + +test('the spread property', function(assert) { + this.setProperties({ + source: list100, + spread: 1, + size: 10, + currentPage: 5, + }); + + this.render(hbs` + {{#list-pagination source=source spread=spread size=size page=currentPage as |p|}} + {{#each p.pageLinks as |link|}} + {{link.pageNumber}} + {{/each}} + {{/list-pagination}} + `); + + testSpread.call(this, assert); + this.set('spread', 4); + testSpread.call(this, assert); +}); + +test('page property', function(assert) { + this.setProperties({ + source: list100, + size: 5, + currentPage: 5, + }); + + this.render(hbs` + {{#list-pagination source=source size=size page=currentPage as |p|}} + {{#each p.list as |item|}} +
    {{item}}
    + {{/each}} + {{/list-pagination}} + `); + + testItems.call(this, assert); + this.set('currentPage', 2); + testItems.call(this, assert); +}); + +// Ember doesn't support query params (or controllers or routes) in integration tests, +// so links can only be tested in acceptance tests. +// Leaving this test here for posterity. +skip('pagination links link with query params', function() {}); + +test('there are no pagination links when source is less than page size', function(assert) { + this.set('source', list100.slice(0, 10)); + this.render(hbs` + {{#list-pagination source=source as |p|}} + {{p.currentPage}} of {{p.totalPages}} + {{#p.first}}first{{/p.first}} + {{#p.prev}}prev{{/p.prev}} + {{#each p.pageLinks as |link|}} + {{link.pageNumber}} + {{/each}} + {{#p.next}}next{{/p.next}} + {{#p.last}}last{{/p.last}} + + {{#each p.list as |item|}} +
    {{item}}
    + {{/each}} + {{/list-pagination}} + `); + + assert.ok(!this.$('.first').length, 'No first link'); + assert.ok(!this.$('.prev').length, 'No prev link'); + assert.ok(!this.$('.next').length, 'No next link'); + assert.ok(!this.$('.last').length, 'No last link'); + + assert.equal(this.$('.page-info').text(), '1 of 1', 'Only one page'); + assert.equal( + this.$('.item').length, + this.get('source.length'), + 'Number of items equals length of source' + ); +}); + +// when there are no items in source +test('when there are no items in source', function(assert) { + this.set('source', []); + this.render(hbs` + {{#list-pagination source=source as |p|}} + {{p.currentPage}} of {{p.totalPages}} + {{#p.first}}first{{/p.first}} + {{#p.prev}}prev{{/p.prev}} + {{#each p.pageLinks as |link|}} + {{link.pageNumber}} + {{/each}} + {{#p.next}}next{{/p.next}} + {{#p.last}}last{{/p.last}} + + {{#each p.list as |item|}} +
    {{item}}
    + {{/each}} + {{else}} +
    Empty State
    + {{/list-pagination}} + `); + + assert.ok( + !this.$('.page-info, .first, .prev, .link, .next, .last, .item').length, + 'Nothing in the yield renders' + ); + assert.ok(this.$('.empty-state').length, 'Empty state is rendered'); +}); + +// when there is less pages than the total spread amount +test('when there is less pages than the total spread amount', function(assert) { + this.setProperties({ + source: list100, + spread: 4, + size: 20, + page: 3, + }); + + const totalPages = Math.ceil(this.get('source.length') / this.get('size')); + + this.render(hbs` + {{#list-pagination source=source page=page spread=spread size=size as |p|}} + {{p.currentPage}} of {{p.totalPages}} + {{#p.first}}first{{/p.first}} + {{#p.prev}}prev{{/p.prev}} + {{#each p.pageLinks as |link|}} + {{link.pageNumber}} + {{/each}} + {{#p.next}}next{{/p.next}} + {{#p.last}}last{{/p.last}} + {{/list-pagination}} + `); + + assert.ok(this.$('.first').length, 'First page still exists'); + assert.ok(this.$('.prev').length, 'Prev page still exists'); + assert.ok(this.$('.next').length, 'Next page still exists'); + assert.ok(this.$('.last').length, 'Last page still exists'); + assert.equal(this.$('.link').length, totalPages, 'Every page gets a page link'); + for (var pageNumber = 1; pageNumber < totalPages; pageNumber++) { + assert.ok(this.$(`.link.page-${pageNumber}`).length, `Page link for ${pageNumber} exists`); + } +}); + +function testSpread(assert) { + const { spread, currentPage } = this.getProperties('spread', 'currentPage'); + for (var pageNumber = currentPage - spread; pageNumber <= currentPage + spread; pageNumber++) { + assert.ok( + this.$(`.link.page-${pageNumber}`).length, + `Page links for currentPage (${currentPage}) +/- spread of ${spread} (${pageNumber})` + ); + } +} + +function testItems(assert) { + const { currentPage, size } = this.getProperties('currentPage', 'size'); + for (var item = 0; item < size; item++) { + assert.equal( + this.$(`.item:eq(${item})`).text(), + item + (currentPage - 1) * size, + `Rendered items are in the current page, ${currentPage} (${item + (currentPage - 1) * size})` + ); + } +} diff --git a/ui/tests/integration/list-table-test.js b/ui/tests/integration/list-table-test.js new file mode 100644 index 000000000..a9c688035 --- /dev/null +++ b/ui/tests/integration/list-table-test.js @@ -0,0 +1,80 @@ +import { test, skip, moduleForComponent } from 'ember-qunit'; +import { faker } from 'ember-cli-mirage'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('list-table', 'Integration | Component | list table', { + integration: true, +}); + +const commonTable = Array(10).fill(null).map(() => ({ + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + age: faker.random.number({ min: 18, max: 60 }), +})); + +// thead +test('component exposes a thead contextual component', function(assert) { + this.set('source', commonTable); + this.render(hbs` + {{#list-table source=source sortProperty=sortProperty sortDescending=sortDescending as |t|}} + {{#t.head class="head"}} + First Name + Last Name + Age + {{/t.head}} + {{/list-table}} + `); + + assert.ok(this.$('.head').length, 'Table head is rendered'); + assert.equal( + this.$('.head').prop('tagName').toLowerCase(), + 'thead', + 'Table head is a thead element' + ); +}); + +// tbody +test('component exposes a tbody contextual component', function(assert) { + this.setProperties({ + source: commonTable, + sortProperty: 'firstName', + sortDescending: false, + }); + this.render(hbs` + {{#list-table source=source sortProperty=sortProperty sortDescending=sortDescending as |t|}} + {{#t.body class="body" as |row|}} + + {{row.model.firstName}} + {{row.model.lastName}} + {{row.model.age}} + + {{/t.body}} + {{/list-table}} + `); + + assert.ok(this.$('.body').length, 'Table body is rendered'); + assert.equal( + this.$('.body').prop('tagName').toLowerCase(), + 'tbody', + 'Table body is a tbody element' + ); + + assert.equal(this.$('.item').length, this.get('source.length'), 'Each item gets its own row'); + + // list-table is not responsible for sorting, only dispatching sort events. The table is still + // rendered in index-order. + this.get('source').forEach((item, index) => { + const $item = this.$(`.item:eq(${index})`); + assert.equal($item.find('td:eq(0)').text().trim(), item.firstName, 'First name'); + assert.equal($item.find('td:eq(1)').text().trim(), item.lastName, 'Last name'); + assert.equal($item.find('td:eq(2)').text().trim(), item.age, 'Age'); + }); +}); + +// Ember doesn't support query params (or controllers or routes) in integration tests, +// so sorting links can only be tested in acceptance tests. +// Leaving this test here for posterity. +skip( + 'sort-by creates links using the appropriate links given sort property and sort descending', + function() {} +); diff --git a/ui/tests/test-helper.js b/ui/tests/test-helper.js new file mode 100644 index 000000000..6bec495ff --- /dev/null +++ b/ui/tests/test-helper.js @@ -0,0 +1,7 @@ +import 'npm:core-js'; +import resolver from './helpers/resolver'; +import { setResolver } from 'ember-qunit'; +import { start } from 'ember-cli-qunit'; + +setResolver(resolver); +start(); diff --git a/ui/tests/unit/.gitkeep b/ui/tests/unit/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/tests/unit/adapters/job-test.js b/ui/tests/unit/adapters/job-test.js new file mode 100644 index 000000000..ce64cd03f --- /dev/null +++ b/ui/tests/unit/adapters/job-test.js @@ -0,0 +1,62 @@ +import { test, moduleFor } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleFor('adapter:job', 'Unit | Adapter | Job', { + unit: true, + needs: ['service:token'], + beforeEach() { + window.sessionStorage.clear(); + + this.server = startMirage(); + this.server.create('node'); + this.server.create('job', { id: 'job-1' }); + }, + afterEach() { + this.server.shutdown(); + }, +}); + +test('The job summary is stitched into the job request', function(assert) { + const { pretender } = this.server; + const jobId = 'job-1'; + + this.subject().findRecord(null, { modelName: 'job' }, jobId); + + assert.deepEqual( + pretender.handledRequests.mapBy('url'), + [`/v1/job/${jobId}`, `/v1/job/${jobId}/summary`], + 'The two requests made are /job/:id and /job/:id/summary' + ); +}); + +test('When there is no token set in the token service, no x-nomad-token header is set', function( + assert +) { + const { pretender } = this.server; + const jobId = 'job-1'; + + this.subject().findRecord(null, { modelName: 'job' }, jobId); + + assert.notOk( + pretender.handledRequests.mapBy('requestHeaders').some(headers => headers['X-Nomad-Token']), + 'No token header present on either job request' + ); +}); + +test('When a token is set in the token service, then x-nomad-token header is set', function( + assert +) { + const { pretender } = this.server; + const jobId = 'job-1'; + const secret = 'here is the secret'; + + this.subject().set('token.secret', secret); + this.subject().findRecord(null, { modelName: 'job' }, jobId); + + assert.ok( + pretender.handledRequests + .mapBy('requestHeaders') + .every(headers => headers['X-Nomad-Token'] === secret), + 'The token header is present on both job requests' + ); +}); diff --git a/ui/tests/unit/mixins/searchable-test.js b/ui/tests/unit/mixins/searchable-test.js new file mode 100644 index 000000000..dc17ecdb3 --- /dev/null +++ b/ui/tests/unit/mixins/searchable-test.js @@ -0,0 +1,69 @@ +import Ember from 'ember'; +import { moduleFor, test } from 'ember-qunit'; +import Searchable from 'nomad-ui/mixins/searchable'; + +const { getOwner, computed } = Ember; + +moduleFor('mixin:searchable', 'Unit | Mixin | Searchable', { + subject() { + const SearchableObject = Ember.Object.extend(Searchable, { + source: null, + searchProps: computed(() => ['id', 'name']), + listToSearch: computed.alias('source'), + }); + + this.register('test-container:searchable-object', SearchableObject); + return getOwner(this).lookup('test-container:searchable-object'); + }, +}); + +test('the searchable mixin does nothing when there is no search term', function(assert) { + const subject = this.subject(); + subject.set('source', [{ id: '1', name: 'hello' }, { id: '2', name: 'world' }]); + + assert.deepEqual(subject.get('listSearched'), subject.get('source')); +}); + +test('the searchable mixin allows for fuzzy match search', function(assert) { + const subject = this.subject(); + subject.set('source', [{ id: '1', name: 'hello' }, { id: '2', name: 'world' }]); + + subject.set('searchTerm', 'helo'); + assert.deepEqual( + subject.get('listSearched'), + [{ id: '1', name: 'hello' }], + 'hello matched for the term helo' + ); +}); + +test('the searchable mixin allows for regex search', function(assert) { + const subject = this.subject(); + subject.set('source', [ + { id: '1', name: 'hello' }, + { id: '2', name: 'world' }, + { id: '3', name: 'oranges' }, + ]); + + subject.set('searchTerm', '/.+l+[A-Z]$'); + assert.deepEqual( + subject.get('listSearched'), + [{ id: '1', name: 'hello' }, { id: '2', name: 'world' }], + 'hello and world matched for regex' + ); +}); + +test('the searchable mixin only searches the declared search props', function(assert) { + const subject = this.subject(); + subject.set('source', [ + { id: '1', name: 'United States of America', continent: 'North America' }, + { id: '2', name: 'Canada', continent: 'North America' }, + { id: '3', name: 'Mexico', continent: 'North America' }, + ]); + + subject.set('searchTerm', '/America'); + assert.deepEqual( + subject.get('listSearched'), + [{ id: '1', name: 'United States of America', continent: 'North America' }], + 'Only USA matched, since continent is not a search prop' + ); +}); diff --git a/ui/tests/unit/models/job-test.js b/ui/tests/unit/models/job-test.js new file mode 100644 index 000000000..709f9eb85 --- /dev/null +++ b/ui/tests/unit/models/job-test.js @@ -0,0 +1,99 @@ +import { moduleForModel, test } from 'ember-qunit'; + +moduleForModel('job', 'Unit | Model | job', { + needs: ['model:task-group', 'model:task', 'model:task-group-summary'], +}); + +test('should expose aggregate allocations derived from task groups', function(assert) { + const job = this.subject({ + name: 'example', + taskGroups: [ + { + name: 'one', + count: 0, + tasks: [], + }, + { + name: 'two', + count: 0, + tasks: [], + }, + { + name: 'three', + count: 0, + tasks: [], + }, + ], + taskGroupSummaries: [ + { + name: 'one', + queuedAllocs: 1, + startingAllocs: 2, + runningAllocs: 3, + completeAllocs: 4, + failedAllocs: 5, + lostAllocs: 6, + }, + { + name: 'two', + queuedAllocs: 2, + startingAllocs: 4, + runningAllocs: 6, + completeAllocs: 8, + failedAllocs: 10, + lostAllocs: 12, + }, + { + name: 'three', + queuedAllocs: 3, + startingAllocs: 6, + runningAllocs: 9, + completeAllocs: 12, + failedAllocs: 15, + lostAllocs: 18, + }, + ], + }); + + assert.equal( + job.get('totalAllocs'), + job.get('taskGroups').mapBy('summary.totalAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'totalAllocs is the sum of all group totalAllocs' + ); + + assert.equal( + job.get('queuedAllocs'), + job.get('taskGroups').mapBy('summary.queuedAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'queuedAllocs is the sum of all group queuedAllocs' + ); + + assert.equal( + job.get('startingAllocs'), + job.get('taskGroups').mapBy('summary.startingAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'startingAllocs is the sum of all group startingAllocs' + ); + + assert.equal( + job.get('runningAllocs'), + job.get('taskGroups').mapBy('summary.runningAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'runningAllocs is the sum of all group runningAllocs' + ); + + assert.equal( + job.get('completeAllocs'), + job.get('taskGroups').mapBy('summary.completeAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'completeAllocs is the sum of all group completeAllocs' + ); + + assert.equal( + job.get('failedAllocs'), + job.get('taskGroups').mapBy('summary.failedAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'failedAllocs is the sum of all group failedAllocs' + ); + + assert.equal( + job.get('lostAllocs'), + job.get('taskGroups').mapBy('summary.lostAllocs').reduce((sum, allocs) => sum + allocs, 0), + 'lostAllocs is the sum of all group lostAllocs' + ); +}); diff --git a/ui/tests/unit/models/task-group-test.js b/ui/tests/unit/models/task-group-test.js new file mode 100644 index 000000000..b5262e4fa --- /dev/null +++ b/ui/tests/unit/models/task-group-test.js @@ -0,0 +1,63 @@ +import Ember from 'ember'; +import { moduleForModel, test } from 'ember-qunit'; + +const { get } = Ember; +const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); + +moduleForModel('task-group', 'Unit | Model | task-group', { + needs: ['model:task', 'model:task-group-summary'], +}); + +test("should expose reserved resource stats as aggregates of each task's reserved resources", function( + assert +) { + const taskGroup = this.subject({ + name: 'group-example', + tasks: [ + { + name: 'task-one', + driver: 'docker', + reservedMemory: 512, + reservedCPU: 500, + reservedDisk: 1024, + }, + { + name: 'task-two', + driver: 'docker', + reservedMemory: 256, + reservedCPU: 1000, + reservedDisk: 512, + }, + { + name: 'task-three', + driver: 'docker', + reservedMemory: 1024, + reservedCPU: 1500, + reservedDisk: 4096, + }, + { + name: 'task-four', + driver: 'docker', + reservedMemory: 2048, + reservedCPU: 500, + reservedDisk: 128, + }, + ], + }); + + assert.equal( + taskGroup.get('reservedCPU'), + sum(taskGroup.get('tasks'), 'reservedCPU'), + 'reservedCPU is an aggregate sum of task CPU reservations' + ); + assert.equal( + taskGroup.get('reservedMemory'), + sum(taskGroup.get('tasks'), 'reservedMemory'), + 'reservedMemory is an aggregate sum of task memory reservations' + ); + assert.equal( + taskGroup.get('reservedDisk'), + sum(taskGroup.get('tasks'), 'reservedDisk'), + 'reservedDisk is an aggregate sum of task disk reservations' + ); +}); diff --git a/ui/tests/unit/serializers/job-test.js b/ui/tests/unit/serializers/job-test.js new file mode 100644 index 000000000..9e4a2e2ad --- /dev/null +++ b/ui/tests/unit/serializers/job-test.js @@ -0,0 +1,132 @@ +import { test } from 'ember-qunit'; +import JobModel from 'nomad-ui/models/job'; +import moduleForSerializer from '../../helpers/module-for-serializer'; + +moduleForSerializer('job', 'Unit | Serializer | Job', { + needs: [ + 'serializer:job', + 'model:task-group-summary', + 'model:task-group', + 'transform:fragment-array', + ], +}); + +test('The JobSummary object is transformed from a map to a list', function(assert) { + const original = { + ID: 'example', + ParentID: '', + Name: 'example', + Type: 'service', + Priority: 50, + Periodic: false, + ParameterizedJob: false, + Stop: false, + Status: 'running', + StatusDescription: '', + JobSummary: { + JobID: 'example', + Summary: { + cache: { + Queued: 0, + Complete: 0, + Failed: 0, + Running: 1, + Starting: 0, + Lost: 0, + }, + something_else: { + Queued: 0, + Complete: 0, + Failed: 0, + Running: 2, + Starting: 0, + Lost: 0, + }, + }, + CreateIndex: 7, + ModifyIndex: 13, + }, + CreateIndex: 7, + ModifyIndex: 9, + JobModifyIndex: 7, + }; + + const normalized = this.subject().normalize(JobModel, original); + + assert.deepEqual(normalized.data.attributes, { + name: 'example', + type: 'service', + priority: 50, + periodic: false, + parameterized: false, + status: 'running', + statusDescription: '', + taskGroupSummaries: [ + { + name: 'cache', + queuedAllocs: 0, + completeAllocs: 0, + failedAllocs: 0, + runningAllocs: 1, + startingAllocs: 0, + lostAllocs: 0, + }, + { + name: 'something_else', + queuedAllocs: 0, + completeAllocs: 0, + failedAllocs: 0, + runningAllocs: 2, + startingAllocs: 0, + lostAllocs: 0, + }, + ], + createIndex: 7, + modifyIndex: 9, + }); +}); + +test('The children stats are lifted out of the JobSummary object', function(assert) { + const original = { + ID: 'example', + ParentID: '', + Name: 'example', + Type: 'service', + Priority: 50, + Periodic: false, + ParameterizedJob: false, + Stop: false, + Status: 'running', + StatusDescription: '', + JobSummary: { + JobID: 'example', + Summary: {}, + Children: { + Pending: 1, + Running: 2, + Dead: 3, + }, + }, + CreateIndex: 7, + ModifyIndex: 9, + JobModifyIndex: 7, + }; + + const normalized = this.subject().normalize(JobModel, original); + + assert.deepEqual(normalized.data.attributes, { + name: 'example', + type: 'service', + priority: 50, + periodic: false, + parameterized: false, + status: 'running', + statusDescription: '', + taskGroupSummaries: [], + pendingChildren: 1, + runningChildren: 2, + deadChildren: 3, + createIndex: 7, + modifyIndex: 9, + }); +}); diff --git a/ui/vendor/.gitkeep b/ui/vendor/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui/yarn.lock b/ui/yarn.lock new file mode 100644 index 000000000..84ee0a99e --- /dev/null +++ b/ui/yarn.lock @@ -0,0 +1,7964 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@glimmer/compiler@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/compiler/-/compiler-0.22.3.tgz#3aef9448460af1d320a82423323498a6ff38a0c6" + dependencies: + "@glimmer/syntax" "^0.22.3" + "@glimmer/util" "^0.22.3" + "@glimmer/wire-format" "^0.22.3" + simple-html-tokenizer "^0.3.0" + +"@glimmer/di@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@glimmer/di/-/di-0.2.0.tgz#73bfd4a6ee4148a80bf092e8a5d29bcac9d4ce7e" + +"@glimmer/interfaces@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/interfaces/-/interfaces-0.22.3.tgz#1c2e3289ae41a750f0c8ddcc64529b9e90dda604" + dependencies: + "@glimmer/wire-format" "^0.22.3" + +"@glimmer/node@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/node/-/node-0.22.3.tgz#ff33eea6e65147a20c1bd1f05fdc4a6c3595c54c" + dependencies: + "@glimmer/runtime" "^0.22.3" + simple-dom "^0.3.0" + +"@glimmer/object-reference@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/object-reference/-/object-reference-0.22.3.tgz#31db68c8912324c63509b1ef83213f7ad4ef312b" + dependencies: + "@glimmer/reference" "^0.22.3" + "@glimmer/util" "^0.22.3" + +"@glimmer/object@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/object/-/object-0.22.3.tgz#1fc9fd7465c7d12e5b92464ad40038b595de8ed0" + dependencies: + "@glimmer/object-reference" "^0.22.3" + "@glimmer/util" "^0.22.3" + +"@glimmer/reference@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/reference/-/reference-0.22.3.tgz#6f2ef8cd97fe756d89fef75f8c3c79003502a2a9" + dependencies: + "@glimmer/util" "^0.22.3" + +"@glimmer/resolver@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@glimmer/resolver/-/resolver-0.4.1.tgz#cd9644572c556e7e799de1cf8eff2b999cf5b878" + dependencies: + "@glimmer/di" "^0.2.0" + +"@glimmer/runtime@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/runtime/-/runtime-0.22.3.tgz#b8cb28efc9cc86c406ee996f5c2cf6730620d404" + dependencies: + "@glimmer/interfaces" "^0.22.3" + "@glimmer/object" "^0.22.3" + "@glimmer/object-reference" "^0.22.3" + "@glimmer/reference" "^0.22.3" + "@glimmer/util" "^0.22.3" + "@glimmer/wire-format" "^0.22.3" + +"@glimmer/syntax@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/syntax/-/syntax-0.22.3.tgz#8528d19324bf7f920f5cfd31925e452e51781b44" + dependencies: + handlebars "^4.0.6" + simple-html-tokenizer "^0.3.0" + +"@glimmer/util@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.22.3.tgz#8272f50905d1bb904ee371e8ade83fd779b51508" + +"@glimmer/wire-format@^0.22.3": + version "0.22.3" + resolved "https://registry.yarnpkg.com/@glimmer/wire-format/-/wire-format-0.22.3.tgz#19b226d9b93ba6ee54472d9ffb1d48e7c0d80a0d" + dependencies: + "@glimmer/util" "^0.22.3" + +JSONStream@^1.0.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +accepts@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +accepts@~1.3.3: + version "1.3.4" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + dependencies: + mime-types "~2.1.16" + negotiator "0.6.1" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.3, acorn@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" + +after@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.1.tgz#ab5d4fb883f596816d3515f8f791c0af486dd627" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0, ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alter@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/alter/-/alter-0.2.0.tgz#c7588808617572034aae62480af26b1d4d1cb3cd" + dependencies: + stable "~0.1.3" + +amd-name-resolver@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.6.tgz#d3e4ba2dfcaab1d820c1be9de947c67828cfe595" + dependencies: + ensure-posix-path "^1.0.1" + +amd-name-resolver@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/amd-name-resolver/-/amd-name-resolver-0.0.7.tgz#814301adfe8a2f109f6e84d5e935196efb669615" + dependencies: + ensure-posix-path "^1.0.1" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-escapes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + +ansi-styles@^2.1.0, ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +aot-test-generators@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/aot-test-generators/-/aot-test-generators-0.1.0.tgz#43f0f615f97cb298d7919c1b0b4e6b7310b03cd0" + dependencies: + jsesc "^2.5.0" + +app-root-path@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" + +aproba@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7, argparse@~1.0.2: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + +array-to-error@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-to-error/-/array-to-error-1.1.1.tgz#d68812926d14097a205579a667eeaf1856a44c07" + dependencies: + array-to-sentence "^1.1.0" + +array-to-sentence@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-to-sentence/-/array-to-sentence-1.1.0.tgz#c804956dafa53232495b205a9452753a258d39fc" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arraybuffer.slice@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +ast-traverse@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" + +ast-types@0.8.12: + version "0.8.12" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc" + +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + +astw@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917" + dependencies: + acorn "^4.0.3" + +async-disk-cache@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.3.2.tgz#ac53d6152843df202c9406e28d774362608d74dd" + dependencies: + debug "^2.1.3" + heimdalljs "^0.2.3" + istextorbinary "2.1.0" + mkdirp "^0.5.0" + rimraf "^2.5.3" + rsvp "^3.0.18" + username-sync "1.0.1" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async-promise-queue@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/async-promise-queue/-/async-promise-queue-1.0.4.tgz#308baafbc74aff66a0bb6e7f4a18d4fe8434440c" + dependencies: + async "^2.4.1" + debug "^2.6.8" + +async@^1.4.0, async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +async@~0.2.9: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^5.0.0: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-5.8.38.tgz#1fcaee79d7e61b750b00b8e54f6dfc9d0af86558" + dependencies: + babel-plugin-constant-folding "^1.0.1" + babel-plugin-dead-code-elimination "^1.0.2" + babel-plugin-eval "^1.0.1" + babel-plugin-inline-environment-variables "^1.0.1" + babel-plugin-jscript "^1.0.4" + babel-plugin-member-expression-literals "^1.0.1" + babel-plugin-property-literals "^1.0.1" + babel-plugin-proto-to-assign "^1.0.3" + babel-plugin-react-constant-elements "^1.0.3" + babel-plugin-react-display-name "^1.0.3" + babel-plugin-remove-console "^1.0.1" + babel-plugin-remove-debugger "^1.0.1" + babel-plugin-runtime "^1.0.7" + babel-plugin-undeclared-variables-check "^1.0.2" + babel-plugin-undefined-to-void "^1.1.6" + babylon "^5.8.38" + bluebird "^2.9.33" + chalk "^1.0.0" + convert-source-map "^1.1.0" + core-js "^1.0.0" + debug "^2.1.1" + detect-indent "^3.0.0" + esutils "^2.0.0" + fs-readdir-recursive "^0.1.0" + globals "^6.4.0" + home-or-tmp "^1.0.0" + is-integer "^1.0.4" + js-tokens "1.0.1" + json5 "^0.4.0" + lodash "^3.10.0" + minimatch "^2.0.3" + output-file-sync "^1.1.0" + path-exists "^1.0.0" + path-is-absolute "^1.0.0" + private "^0.1.6" + regenerator "0.8.40" + regexpu "^1.3.0" + repeating "^1.1.2" + resolve "^1.1.6" + shebang-regex "^1.0.0" + slash "^1.0.0" + source-map "^0.5.0" + source-map-support "^0.2.10" + to-fast-properties "^1.0.0" + trim-right "^1.0.0" + try-resolve "^1.0.0" + +babel-core@^6.14.0, babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-generator@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.6" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-constant-folding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-constant-folding/-/babel-plugin-constant-folding-1.0.1.tgz#8361d364c98e449c3692bdba51eff0844290aa8e" + +babel-plugin-dead-code-elimination@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-dead-code-elimination/-/babel-plugin-dead-code-elimination-1.0.2.tgz#5f7c451274dcd7cccdbfbb3e0b85dd28121f0f65" + +babel-plugin-debug-macros@^0.1.10, babel-plugin-debug-macros@^0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.1.11.tgz#6c562bf561fccd406ce14ab04f42c218cf956605" + dependencies: + semver "^5.3.0" + +babel-plugin-ember-modules-api-polyfill@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-1.6.0.tgz#abd1afa4237b3121cb51222f9bf3283cad8990aa" + dependencies: + ember-rfc176-data "^0.2.0" + +babel-plugin-eval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-eval/-/babel-plugin-eval-1.0.1.tgz#a2faed25ce6be69ade4bfec263f70169195950da" + +babel-plugin-feature-flags@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-feature-flags/-/babel-plugin-feature-flags-0.3.1.tgz#9c827cf9a4eb9a19f725ccb239e85cab02036fc1" + +babel-plugin-filter-imports@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-filter-imports/-/babel-plugin-filter-imports-0.3.1.tgz#e7859b56886b175dd2616425d277b219e209ea8b" + +babel-plugin-htmlbars-inline-precompile@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-0.2.3.tgz#cd365e278af409bfa6be7704c4354beee742446b" + +babel-plugin-inline-environment-variables@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz#1f58ce91207ad6a826a8bf645fafe68ff5fe3ffe" + +babel-plugin-jscript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/babel-plugin-jscript/-/babel-plugin-jscript-1.0.4.tgz#8f342c38276e87a47d5fa0a8bd3d5eb6ccad8fcc" + +babel-plugin-member-expression-literals@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-member-expression-literals/-/babel-plugin-member-expression-literals-1.0.1.tgz#cc5edb0faa8dc927170e74d6d1c02440021624d3" + +babel-plugin-property-literals@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-property-literals/-/babel-plugin-property-literals-1.0.1.tgz#0252301900192980b1c118efea48ce93aab83336" + +babel-plugin-proto-to-assign@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/babel-plugin-proto-to-assign/-/babel-plugin-proto-to-assign-1.0.4.tgz#c49e7afd02f577bc4da05ea2df002250cf7cd123" + dependencies: + lodash "^3.9.3" + +babel-plugin-react-constant-elements@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-react-constant-elements/-/babel-plugin-react-constant-elements-1.0.3.tgz#946736e8378429cbc349dcff62f51c143b34e35a" + +babel-plugin-react-display-name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-react-display-name/-/babel-plugin-react-display-name-1.0.3.tgz#754fe38926e8424a4e7b15ab6ea6139dee0514fc" + +babel-plugin-remove-console@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz#d8f24556c3a05005d42aaaafd27787f53ff013a7" + +babel-plugin-remove-debugger@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-remove-debugger/-/babel-plugin-remove-debugger-1.0.1.tgz#fd2ea3cd61a428ad1f3b9c89882ff4293e8c14c7" + +babel-plugin-runtime@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/babel-plugin-runtime/-/babel-plugin-runtime-1.0.7.tgz#bf7c7d966dd56ecd5c17fa1cb253c9acb7e54aaf" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-undeclared-variables-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-undeclared-variables-check/-/babel-plugin-undeclared-variables-check-1.0.2.tgz#5cf1aa539d813ff64e99641290af620965f65dee" + dependencies: + leven "^1.0.2" + +babel-plugin-undefined-to-void@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz#7f578ef8b78dfae6003385d8417a61eda06e2f81" + +babel-polyfill@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-preset-env@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babel6-plugin-strip-class-callcheck@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/babel6-plugin-strip-class-callcheck/-/babel6-plugin-strip-class-callcheck-6.0.0.tgz#de841c1abebbd39f78de0affb2c9a52ee228fddf" + +babel6-plugin-strip-heimdall@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/babel6-plugin-strip-heimdall/-/babel6-plugin-strip-heimdall-6.0.1.tgz#35f80eddec1f7fffdc009811dfbd46d9965072b6" + +babylon@^5.8.38: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +backbone@^1.1.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.3.3.tgz#4cc80ea7cb1631ac474889ce40f2f8bc683b2999" + dependencies: + underscore ">=1.8.3" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +base64id@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f" + +basic-auth@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + dependencies: + callsite "1.0.0" + +binary-extensions@^1.0.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" + +"binaryextensions@1 || 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.0.0.tgz#e597d1a7a6a3558a2d1c7241a16c99965e6aa40f" + +blank-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9" + +blob@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^2.9.33: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + +bluebird@^3.1.1, bluebird@^3.4.6: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069" + dependencies: + continuable-cache "^0.3.1" + error "^7.0.0" + raw-body "~1.1.0" + safe-json-parse "~1.0.1" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +bourbon@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/bourbon/-/bourbon-4.3.4.tgz#4da380029e92c0c8f9764c779451a134b11e7cc3" + +bower-config@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.0.tgz#16c38c1135f8071c19f25938d61b0d8cbf18d3f1" + dependencies: + graceful-fs "^4.1.3" + mout "^1.0.0" + optimist "^0.6.1" + osenv "^0.1.3" + untildify "^2.1.0" + +bower-endpoint-parser@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/bower-endpoint-parser/-/bower-endpoint-parser-0.2.2.tgz#00b565adbfab6f2d35addde977e97962acbcb3f6" + +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +breakable@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1" + +broccoli-asset-rev@^2.4.5: + version "2.6.0" + resolved "https://registry.yarnpkg.com/broccoli-asset-rev/-/broccoli-asset-rev-2.6.0.tgz#0633fc3a0b2ba0c2c1d56fa9feb7b331fc83be6d" + dependencies: + broccoli-asset-rewrite "^1.1.0" + broccoli-filter "^1.2.2" + json-stable-stringify "^1.0.0" + minimatch "^3.0.4" + rsvp "^3.0.6" + +broccoli-asset-rewrite@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-asset-rewrite/-/broccoli-asset-rewrite-1.1.0.tgz#77a5da56157aa318c59113245e8bafb4617f8830" + dependencies: + broccoli-filter "^1.2.3" + +broccoli-babel-transpiler@^5.6.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-5.7.2.tgz#756c30544775144e984333b7115f42c916ba08e0" + dependencies: + babel-core "^5.0.0" + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.0.0" + broccoli-persistent-filter "^1.4.2" + clone "^0.2.0" + hash-for-dep "^1.0.2" + heimdalljs-logger "^0.1.7" + json-stable-stringify "^1.0.0" + rsvp "^3.5.0" + workerpool "^2.2.1" + +broccoli-babel-transpiler@^6.0.0, broccoli-babel-transpiler@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.1.2.tgz#26019c045b5ea3e44cfef62821302f9bd483cabd" + dependencies: + babel-core "^6.14.0" + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.0.0" + broccoli-persistent-filter "^1.4.0" + clone "^2.0.0" + hash-for-dep "^1.0.2" + heimdalljs-logger "^0.1.7" + json-stable-stringify "^1.0.0" + rsvp "^3.5.0" + workerpool "^2.2.1" + +broccoli-brocfile-loader@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/broccoli-brocfile-loader/-/broccoli-brocfile-loader-0.18.0.tgz#2e86021c805c34ffc8d29a2fb721cf273e819e4b" + dependencies: + findup-sync "^0.4.2" + +broccoli-builder@^0.18.3: + version "0.18.8" + resolved "https://registry.yarnpkg.com/broccoli-builder/-/broccoli-builder-0.18.8.tgz#fe54694d544c3cdfdb01028e802eeca65749a879" + dependencies: + heimdalljs "^0.2.0" + promise-map-series "^0.2.1" + quick-temp "^0.1.2" + rimraf "^2.2.8" + rsvp "^3.0.17" + silent-error "^1.0.1" + +broccoli-caching-writer@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-2.3.1.tgz#b93cf58f9264f003075868db05774f4e7f25bd07" + dependencies: + broccoli-kitchen-sink-helpers "^0.2.5" + broccoli-plugin "1.1.0" + debug "^2.1.1" + rimraf "^2.2.8" + rsvp "^3.0.17" + walk-sync "^0.2.5" + +broccoli-caching-writer@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broccoli-caching-writer/-/broccoli-caching-writer-3.0.3.tgz#0bd2c96a9738d6a6ab590f07ba35c5157d7db476" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.2.1" + debug "^2.1.1" + rimraf "^2.2.8" + rsvp "^3.0.17" + walk-sync "^0.3.0" + +broccoli-clean-css@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-clean-css/-/broccoli-clean-css-1.1.0.tgz#9db143d9af7e0ae79c26e3ac5a9bb2d720ea19fa" + dependencies: + broccoli-persistent-filter "^1.1.6" + clean-css-promise "^0.1.0" + inline-source-map-comment "^1.0.5" + json-stable-stringify "^1.0.0" + +broccoli-concat@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/broccoli-concat/-/broccoli-concat-3.2.2.tgz#86ffdc52606eb590ba9f6b894c5ec7a016f5b7b9" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.3.0" + broccoli-stew "^1.3.3" + ensure-posix-path "^1.0.2" + fast-sourcemap-concat "^1.0.1" + find-index "^1.1.0" + fs-extra "^1.0.0" + fs-tree-diff "^0.5.6" + lodash.merge "^4.3.0" + lodash.omit "^4.1.0" + lodash.uniq "^4.2.0" + walk-sync "^0.3.1" + +broccoli-config-loader@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/broccoli-config-loader/-/broccoli-config-loader-1.0.1.tgz#d10aaf8ebc0cb45c1da5baa82720e1d88d28c80a" + dependencies: + broccoli-caching-writer "^3.0.3" + +broccoli-config-replace@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/broccoli-config-replace/-/broccoli-config-replace-1.1.2.tgz#6ea879d92a5bad634d11329b51fc5f4aafda9c00" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.2.0" + debug "^2.2.0" + fs-extra "^0.24.0" + +broccoli-debug@^0.6.1, broccoli-debug@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/broccoli-debug/-/broccoli-debug-0.6.3.tgz#1f33bb0eacb5db81366f0492524c82b1217eb578" + dependencies: + broccoli-plugin "^1.2.1" + fs-tree-diff "^0.5.2" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + minimatch "^3.0.3" + symlink-or-copy "^1.1.8" + tree-sync "^1.2.2" + +broccoli-file-creator@^1.0.0, broccoli-file-creator@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-1.1.1.tgz#1b35b67d215abdfadd8d49eeb69493c39e6c3450" + dependencies: + broccoli-kitchen-sink-helpers "~0.2.0" + broccoli-plugin "^1.1.0" + broccoli-writer "~0.1.1" + mkdirp "^0.5.1" + rsvp "~3.0.6" + symlink-or-copy "^1.0.1" + +broccoli-filter@^0.1.11: + version "0.1.14" + resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-0.1.14.tgz#23cae3891ff9ebb7b4d7db00c6dcf03535daf7ad" + dependencies: + broccoli-kitchen-sink-helpers "^0.2.6" + broccoli-writer "^0.1.1" + mkdirp "^0.3.5" + promise-map-series "^0.2.1" + quick-temp "^0.1.2" + rsvp "^3.0.16" + symlink-or-copy "^1.0.1" + walk-sync "^0.1.3" + +broccoli-filter@^1.2.2, broccoli-filter@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-1.2.4.tgz#409afb94b9a3a6da9fac8134e91e205f40cc7330" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^1.0.0" + copy-dereference "^1.0.0" + debug "^2.2.0" + mkdirp "^0.5.1" + promise-map-series "^0.2.1" + rsvp "^3.0.18" + symlink-or-copy "^1.0.1" + walk-sync "^0.3.1" + +broccoli-flatiron@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/broccoli-flatiron/-/broccoli-flatiron-0.0.0.tgz#e97504016b56eea04813b5d862fda18b6f11a77f" + dependencies: + broccoli-kitchen-sink-helpers "~0.2.4" + broccoli-writer "~0.1.1" + mkdirp "^0.3.5" + rsvp "~3.0.6" + +broccoli-funnel-reducer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/broccoli-funnel-reducer/-/broccoli-funnel-reducer-1.0.0.tgz#11365b2a785aec9b17972a36df87eef24c5cc0ea" + +broccoli-funnel@^1.0.0, broccoli-funnel@^1.0.1, broccoli-funnel@^1.0.2, broccoli-funnel@^1.0.6, broccoli-funnel@^1.1.0, broccoli-funnel@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.2.0.tgz#cddc3afc5ff1685a8023488fff74ce6fb5a51296" + dependencies: + array-equal "^1.0.0" + blank-object "^1.0.1" + broccoli-plugin "^1.3.0" + debug "^2.2.0" + exists-sync "0.0.4" + fast-ordered-set "^1.0.0" + fs-tree-diff "^0.5.3" + heimdalljs "^0.2.0" + minimatch "^3.0.0" + mkdirp "^0.5.0" + path-posix "^1.0.0" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + walk-sync "^0.3.1" + +broccoli-kitchen-sink-helpers@^0.2.5, broccoli-kitchen-sink-helpers@^0.2.6, broccoli-kitchen-sink-helpers@~0.2.0, broccoli-kitchen-sink-helpers@~0.2.4: + version "0.2.9" + resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.2.9.tgz#a5e0986ed8d76fb5984b68c3f0450d3a96e36ecc" + dependencies: + glob "^5.0.10" + mkdirp "^0.5.1" + +broccoli-kitchen-sink-helpers@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/broccoli-kitchen-sink-helpers/-/broccoli-kitchen-sink-helpers-0.3.1.tgz#77c7c18194b9664163ec4fcee2793444926e0c06" + dependencies: + glob "^5.0.10" + mkdirp "^0.5.1" + +broccoli-lint-eslint@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/broccoli-lint-eslint/-/broccoli-lint-eslint-3.3.1.tgz#35c675546a5a7ad8f3319edd732e3aad8ca241de" + dependencies: + aot-test-generators "^0.1.0" + broccoli-concat "^3.2.2" + broccoli-persistent-filter "^1.2.0" + eslint "^3.0.0" + json-stable-stringify "^1.0.1" + lodash.defaultsdeep "^4.6.0" + md5-hex "^2.0.0" + +broccoli-merge-trees@^1.0.0, broccoli-merge-trees@^1.1.0, broccoli-merge-trees@^1.1.1, broccoli-merge-trees@^1.1.2, broccoli-merge-trees@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5" + dependencies: + broccoli-plugin "^1.3.0" + can-symlink "^1.0.0" + fast-ordered-set "^1.0.2" + fs-tree-diff "^0.5.4" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + +broccoli-merge-trees@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-2.0.0.tgz#10aea46dd5cebcc8b8f7d5a54f0a84a4f0bb90b9" + dependencies: + broccoli-plugin "^1.3.0" + merge-trees "^1.0.1" + +broccoli-middleware@^1.0.0-beta.8: + version "1.0.0" + resolved "https://registry.yarnpkg.com/broccoli-middleware/-/broccoli-middleware-1.0.0.tgz#92f4e1fb9a791ea986245a7077f35cc648dab097" + dependencies: + handlebars "^4.0.4" + mime "^1.2.11" + +broccoli-persistent-filter@^1.0.3, broccoli-persistent-filter@^1.1.5, broccoli-persistent-filter@^1.1.6, broccoli-persistent-filter@^1.2.0, broccoli-persistent-filter@^1.4.0, broccoli-persistent-filter@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.3.tgz#3511bc52fc53740cda51621f58a28152d9911bc1" + dependencies: + async-disk-cache "^1.2.1" + async-promise-queue "^1.0.3" + broccoli-plugin "^1.0.0" + fs-tree-diff "^0.5.2" + hash-for-dep "^1.0.2" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + mkdirp "^0.5.1" + promise-map-series "^0.2.1" + rimraf "^2.6.1" + rsvp "^3.0.18" + symlink-or-copy "^1.0.1" + walk-sync "^0.3.1" + +broccoli-plugin@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02" + dependencies: + promise-map-series "^0.2.1" + quick-temp "^0.1.3" + rimraf "^2.3.4" + symlink-or-copy "^1.0.1" + +broccoli-plugin@^1.0.0, broccoli-plugin@^1.1.0, broccoli-plugin@^1.2.0, broccoli-plugin@^1.2.1, broccoli-plugin@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#bee704a8e42da08cb58e513aaa436efb7f0ef1ee" + dependencies: + promise-map-series "^0.2.1" + quick-temp "^0.1.3" + rimraf "^2.3.4" + symlink-or-copy "^1.1.8" + +broccoli-rollup@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/broccoli-rollup/-/broccoli-rollup-1.3.0.tgz#43a0a7798555bab54217009eb470a4ff5a056df0" + dependencies: + broccoli-plugin "^1.2.1" + es6-map "^0.1.4" + fs-extra "^0.30.0" + fs-tree-diff "^0.5.2" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + md5-hex "^1.3.0" + node-modules-path "^1.0.1" + rollup "^0.41.4" + symlink-or-copy "^1.1.8" + walk-sync "^0.3.1" + +broccoli-sass-source-maps@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/broccoli-sass-source-maps/-/broccoli-sass-source-maps-2.0.0.tgz#7f25f9f4b296918cec6e00672c63e75abce33d45" + dependencies: + broccoli-caching-writer "^3.0.3" + include-path-searcher "^0.1.0" + mkdirp "^0.3.5" + node-sass "^4.1.0" + object-assign "^2.0.0" + rsvp "^3.0.6" + +broccoli-slow-trees@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/broccoli-slow-trees/-/broccoli-slow-trees-3.0.1.tgz#9bf2a9e2f8eb3ed3a3f2abdde988da437ccdc9b4" + dependencies: + heimdalljs "^0.2.1" + +broccoli-source@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/broccoli-source/-/broccoli-source-1.1.0.tgz#54f0e82c8b73f46580cbbc4f578f0b32fca8f809" + +broccoli-sri-hash@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/broccoli-sri-hash/-/broccoli-sri-hash-2.1.2.tgz#bc69905ed7a381ad325cc0d02ded071328ebf3f3" + dependencies: + broccoli-caching-writer "^2.2.0" + mkdirp "^0.5.1" + rsvp "^3.1.0" + sri-toolbox "^0.2.0" + symlink-or-copy "^1.0.1" + +broccoli-stew@^1.2.0, broccoli-stew@^1.3.3, broccoli-stew@^1.4.0, broccoli-stew@^1.4.2, broccoli-stew@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/broccoli-stew/-/broccoli-stew-1.5.0.tgz#d7af8c18511dce510e49d308a62e5977f461883c" + dependencies: + broccoli-debug "^0.6.1" + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.0.0" + broccoli-persistent-filter "^1.1.6" + broccoli-plugin "^1.3.0" + chalk "^1.1.3" + debug "^2.4.0" + ensure-posix-path "^1.0.1" + fs-extra "^2.0.0" + minimatch "^3.0.2" + resolve "^1.1.6" + rsvp "^3.0.16" + symlink-or-copy "^1.1.8" + walk-sync "^0.3.0" + +broccoli-string-replace@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/broccoli-string-replace/-/broccoli-string-replace-0.1.2.tgz#1ed92f85680af8d503023925e754e4e33676b91f" + dependencies: + broccoli-persistent-filter "^1.1.5" + minimatch "^3.0.3" + +broccoli-templater@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/broccoli-templater/-/broccoli-templater-1.0.0.tgz#7c054aacf596d1868d1a44291f9ec7b907d30ecf" + dependencies: + broccoli-filter "^0.1.11" + broccoli-stew "^1.2.0" + lodash.template "^3.3.2" + +broccoli-uglify-sourcemap@^1.0.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/broccoli-uglify-sourcemap/-/broccoli-uglify-sourcemap-1.5.2.tgz#04f84ab0db539031fa868ccfa563c9932d50cedb" + dependencies: + broccoli-plugin "^1.2.1" + debug "^2.2.0" + lodash.merge "^4.5.1" + matcher-collection "^1.0.0" + mkdirp "^0.5.0" + source-map-url "^0.3.0" + symlink-or-copy "^1.0.1" + uglify-js "^2.7.0" + walk-sync "^0.1.3" + +broccoli-unwatched-tree@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/broccoli-unwatched-tree/-/broccoli-unwatched-tree-0.1.3.tgz#ab0fb820f613845bf67a803baad820f68b1e3aae" + dependencies: + broccoli-source "^1.1.0" + +broccoli-writer@^0.1.1, broccoli-writer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/broccoli-writer/-/broccoli-writer-0.1.1.tgz#d4d71aa8f2afbc67a3866b91a2da79084b96ab2d" + dependencies: + quick-temp "^0.1.0" + rsvp "^3.0.6" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-pack@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.2.tgz#f86cd6cef4f5300c8e63e07a4d512f65fbff4531" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + defined "^1.0.0" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^1.11.0, browser-resolve@^1.7.0: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserify@^13.0.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-13.3.0.tgz#b5a9c9020243f0c70e4675bec8223bc627e415ce" + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^1.11.0" + browserify-zlib "~0.1.2" + buffer "^4.1.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.1" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "~1.1.0" + duplexer2 "~0.1.2" + events "~1.1.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "~0.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + module-deps "^4.0.8" + os-browserify "~0.1.1" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^2.0.0" + string_decoder "~0.10.0" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "~0.0.0" + url "~0.11.0" + util "~0.10.1" + vm-browserify "~0.0.1" + xtend "^4.0.0" + +browserslist@^2.1.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8" + dependencies: + caniuse-lite "^1.0.30000718" + electron-to-chromium "^1.3.18" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.1.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + +bulma@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.4.2.tgz#3be8c832cf6658bfc421ecb41f6dc5a8a0c7c0e5" + +bytes@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" + +bytes@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" + +cached-path-relative@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" + +calculate-cache-key-for-tree@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/calculate-cache-key-for-tree/-/calculate-cache-key-for-tree-1.1.0.tgz#0c3e42c9c134f3c9de5358c0f16793627ea976d6" + dependencies: + json-stable-stringify "^1.0.1" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2, camelcase@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +can-symlink@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/can-symlink/-/can-symlink-1.0.0.tgz#97b607d8a84bb6c6e228b902d864ecb594b9d219" + dependencies: + tmp "0.0.28" + +caniuse-lite@^1.0.30000718: + version "1.0.30000718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000718.tgz#0dd24290beb11310b2d80f6b70a823c2a65a6fad" + +capture-exit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" + dependencies: + rsvp "^3.3.3" + +cardinal@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-0.5.0.tgz#00d5f661dbd4aabfdf7d41ce48a5a59bca35a291" + dependencies: + ansicolors "~0.2.1" + redeyed "~0.5.0" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +charm@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + dependencies: + inherits "^2.0.1" + +chokidar@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +ci-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +clean-base-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clean-base-url/-/clean-base-url-1.0.0.tgz#c901cf0a20b972435b0eccd52d056824a4351b7b" + +clean-css-promise@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/clean-css-promise/-/clean-css-promise-0.1.1.tgz#43f3d2c8dfcb2bf071481252cd9b76433c08eecb" + dependencies: + array-to-error "^1.0.0" + clean-css "^3.4.5" + pinkie-promise "^2.0.0" + +clean-css@^3.4.5: + version "3.4.28" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff" + dependencies: + commander "2.8.x" + source-map "0.4.x" + +cli-cursor@^1.0.1, cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + +cli-table2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" + dependencies: + lodash "^3.10.1" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" + +cli-table@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + +clone@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +colors@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combine-source-map@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@2.8.x: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commander@2.9.0, commander@^2.5.0, commander@^2.6.0, commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commoner@~0.10.3: + version "0.10.8" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.11.17" + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + +component-emitter@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3" + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + +compressible@~2.0.10: + version "2.0.11" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a" + dependencies: + mime-db ">= 1.29.0 < 2" + +compression@^1.4.4: + version "1.7.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" + dependencies: + accepts "~1.3.3" + bytes "2.5.0" + compressible "~2.0.10" + debug "2.6.8" + on-headers "~1.0.1" + safe-buffer "5.1.1" + vary "~1.1.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6, concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@~1.5.0, concat-stream@~1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +configstore@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +console-ui@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/console-ui/-/console-ui-1.0.3.tgz#31c524461b63422769f9e89c173495d91393721c" + dependencies: + chalk "^1.1.3" + inquirer "^1.2.3" + ora "^0.2.0" + through "^2.3.8" + +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +continuable-cache@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" + +convert-source-map@^1.1.0, convert-source-map@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +copy-dereference@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/copy-dereference/-/copy-dereference-1.0.0.tgz#6b131865420fd81b413ba994b44d3655311152b6" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" + +core-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-1.1.0.tgz#86d63918733cf9da1a5aae729e62c0a88e66ad0a" + +core-object@^3.0.0: + version "3.1.5" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-3.1.5.tgz#fa627b87502adc98045e44678e9a8ec3b9c0d2a9" + dependencies: + chalk "^2.0.0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37" + dependencies: + graceful-fs "^4.1.2" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.0.1" + os-homedir "^1.0.1" + parse-json "^2.2.0" + pinkie-promise "^2.0.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@^3.0.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d3-color@1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" + +d3-dispatch@1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" + +d3-ease@1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" + +d3-interpolate@1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.5.tgz#69e099ff39214716e563c9aec3ea9d1ea4b8a79f" + dependencies: + d3-color "1" + +d3-selection@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.1.0.tgz#1998684896488f839ca0372123da34f1d318809c" + +d3-timer@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.6.tgz#4044bf15d7025c06ce7d1149f73cd07b54dbd784" + +d3-transition@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.0.tgz#cfc85c74e5239324290546623572990560c3966f" + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.27.2: + version "1.28.5" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debug@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" + dependencies: + ms "0.7.2" + +debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.0, debug@^2.6.8, debug@~2.6.7: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +defs@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/defs/-/defs-1.1.1.tgz#b22609f2c7a11ba7a3db116805c139b1caffa9d2" + dependencies: + alter "~0.2.0" + ast-traverse "~0.1.1" + breakable "~1.0.0" + esprima-fb "~15001.1001.0-dev-harmony-fb" + simple-fmt "~0.1.0" + simple-is "~0.2.0" + stringmap "~0.2.2" + stringset "~0.2.1" + tryor "~0.1.2" + yargs "~3.27.0" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.1, depd@~1.1.0, depd@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +deps-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" + dependencies: + JSONStream "^1.0.3" + shasum "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +derequire@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/derequire/-/derequire-2.0.6.tgz#31a414bb7ca176239fa78b116636ef77d517e768" + dependencies: + acorn "^4.0.3" + concat-stream "^1.4.6" + escope "^3.6.0" + through2 "^2.0.0" + yargs "^6.5.0" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-3.0.1.tgz#9dc5e5ddbceef8325764b9451b02bc6d54084f75" + dependencies: + get-stdin "^4.0.1" + minimist "^1.1.0" + repeating "^1.1.0" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detective@^4.0.0, detective@^4.3.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1" + dependencies: + acorn "^4.0.3" + defined "^1.0.0" + +diff@^3.1.0, diff@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +doctrine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +domain-browser@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +editions@^1.1.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.3.tgz#0907101bdda20fac3cbe334c27cbd0688dc99a5b" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.3.18: + version "1.3.18" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.18.tgz#3dcc99da3e6b665f6abbc71c28ad51a2cd731a9c" + +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +ember-ajax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ember-ajax/-/ember-ajax-3.0.0.tgz#8f21e9da0c1d433cf879aa855fce464d517e9ab5" + dependencies: + ember-cli-babel "^6.0.0" + +ember-browserify@^1.1.13: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-browserify/-/ember-browserify-1.2.0.tgz#c774896dc44ab2c5c608226f31628994348a3fd5" + dependencies: + acorn "^5.0.3" + broccoli-caching-writer "^3.0.3" + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-merge-trees "^1.1.2" + broccoli-plugin "^1.2.1" + browserify "^13.0.0" + core-object "^1.1.0" + debug "^2.2.0" + derequire "^2.0.3" + ember-cli-version-checker "^1.1.4" + fs-tree "^1.0.0" + fs-tree-diff "^0.5.0" + lodash "^4.5.1" + md5-hex "^1.3.0" + mkdirp "^0.5.0" + promise-map-series "^0.2.0" + quick-temp "^0.1.2" + rimraf "^2.2.8" + rsvp "^3.0.14" + symlink-or-copy "^1.0.0" + through2 "^2.0.0" + walk-sync "^0.2.7" + +ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.2.4.tgz#5ce4f46b08ed6f6d21e878619fb689719d6e8e13" + dependencies: + broccoli-babel-transpiler "^5.6.2" + broccoli-funnel "^1.0.0" + clone "^2.0.0" + ember-cli-version-checker "^1.0.2" + resolve "^1.1.2" + +ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.0.0-beta.9, ember-cli-babel@^6.1.0, ember-cli-babel@^6.4.1, ember-cli-babel@^6.6.0, ember-cli-babel@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.8.1.tgz#695f94c57a9375c2a0e219306a41105d6b937991" + dependencies: + amd-name-resolver "0.0.7" + babel-plugin-debug-macros "^0.1.11" + babel-plugin-ember-modules-api-polyfill "^1.5.1" + babel-plugin-transform-es2015-modules-amd "^6.24.0" + babel-polyfill "^6.16.0" + babel-preset-env "^1.5.1" + broccoli-babel-transpiler "^6.1.2" + broccoli-debug "^0.6.2" + broccoli-funnel "^1.0.0" + broccoli-source "^1.1.0" + clone "^2.0.0" + ember-cli-version-checker "^2.0.0" + +ember-cli-bourbon@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/ember-cli-bourbon/-/ember-cli-bourbon-2.0.0-beta.1.tgz#9d9b07bd4c7da7b2806ea18fc5cb9b37dd15ad25" + dependencies: + bourbon "^4.3.3" + broccoli-funnel "^1.0.1" + ember-cli-babel "^5.1.7" + +ember-cli-broccoli-sane-watcher@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/ember-cli-broccoli-sane-watcher/-/ember-cli-broccoli-sane-watcher-2.0.4.tgz#f43f42f75b7509c212fb926cd9aea86ae19264c6" + dependencies: + broccoli-slow-trees "^3.0.1" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + rsvp "^3.0.18" + sane "^1.1.1" + +ember-cli-dependency-checker@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ember-cli-dependency-checker/-/ember-cli-dependency-checker-1.4.0.tgz#2b13f977e1eea843fc1a21a001be6ca5d4ef1942" + dependencies: + chalk "^0.5.1" + is-git-url "^0.2.0" + semver "^4.1.0" + +ember-cli-eslint@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ember-cli-eslint/-/ember-cli-eslint-3.1.0.tgz#8d9e2a867654835ac1b24858d9117e143fa54bd4" + dependencies: + broccoli-lint-eslint "^3.3.0" + ember-cli-version-checker "^1.3.1" + rsvp "^3.2.1" + walk-sync "^0.3.0" + +ember-cli-get-component-path-option@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771" + +ember-cli-get-dependency-depth@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-get-dependency-depth/-/ember-cli-get-dependency-depth-1.0.0.tgz#e0afecf82a2d52f00f28ab468295281aec368d11" + +ember-cli-htmlbars-inline-precompile@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars-inline-precompile/-/ember-cli-htmlbars-inline-precompile-0.4.4.tgz#24a7617152630d64a047e553b72e00963a4f8d73" + dependencies: + babel-plugin-htmlbars-inline-precompile "^0.2.3" + ember-cli-version-checker "^2.0.0" + hash-for-dep "^1.0.2" + silent-error "^1.1.0" + +ember-cli-htmlbars@^1.0.3, ember-cli-htmlbars@^1.1.1, ember-cli-htmlbars@^1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-1.3.4.tgz#461289724b34af372a6a0c4b6635819156963353" + dependencies: + broccoli-persistent-filter "^1.0.3" + ember-cli-version-checker "^1.0.2" + hash-for-dep "^1.0.2" + json-stable-stringify "^1.0.0" + strip-bom "^2.0.0" + +ember-cli-import-polyfill@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ember-cli-import-polyfill/-/ember-cli-import-polyfill-0.2.0.tgz#c1a08a8affb45c97b675926272fe78cf4ca166f2" + +ember-cli-inject-live-reload@^1.4.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-1.7.0.tgz#af94336e015336127dfb98080ad442bb233e37ed" + +ember-cli-is-package-missing@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-is-package-missing/-/ember-cli-is-package-missing-1.0.0.tgz#6e6184cafb92635dd93ca6c946b104292d4e3390" + +ember-cli-legacy-blueprints@^0.1.2: + version "0.1.5" + resolved "https://registry.yarnpkg.com/ember-cli-legacy-blueprints/-/ember-cli-legacy-blueprints-0.1.5.tgz#93c15ca242ec5107d62a8af7ec30f6ac538f3ad9" + dependencies: + chalk "^1.1.1" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-get-dependency-depth "^1.0.0" + ember-cli-is-package-missing "^1.0.0" + ember-cli-lodash-subset "^1.0.7" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.0.0" + ember-cli-test-info "^1.0.0" + ember-cli-valid-component-name "^1.0.0" + ember-cli-version-checker "^1.1.7" + ember-router-generator "^1.0.0" + exists-sync "0.0.3" + fs-extra "^0.24.0" + inflection "^1.7.1" + rsvp "^3.0.17" + silent-error "^1.0.0" + +ember-cli-lodash-subset@^1.0.11, ember-cli-lodash-subset@^1.0.7: + version "1.0.12" + resolved "https://registry.yarnpkg.com/ember-cli-lodash-subset/-/ember-cli-lodash-subset-1.0.12.tgz#af2e77eba5dcb0d77f3308d3a6fd7d3450f6e537" + +ember-cli-mirage@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/ember-cli-mirage/-/ember-cli-mirage-0.3.4.tgz#eeb9d6e02c0c49c81915762178bab9a42d86ada8" + dependencies: + broccoli-funnel "^1.0.2" + broccoli-merge-trees "^1.1.0" + broccoli-stew "^1.5.0" + chalk "^1.1.1" + ember-cli-babel "^5.1.7" + ember-cli-node-assets "^0.1.4" + ember-get-config "0.2.1" + ember-inflector "^2.0.0" + ember-lodash "^4.17.3" + exists-sync "0.0.3" + fake-xml-http-request "^1.4.0" + faker "^3.0.0" + pretender "^1.4.2" + route-recognizer "^0.2.3" + +ember-cli-moment-shim@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ember-cli-moment-shim/-/ember-cli-moment-shim-3.4.0.tgz#2fbe13d8654097da86abb64016cddf12ff19a3de" + dependencies: + broccoli-funnel "^1.1.0" + broccoli-merge-trees "^2.0.0" + broccoli-source "^1.1.0" + broccoli-stew "^1.4.0" + chalk "^1.1.3" + ember-cli-babel "^6.6.0" + ember-cli-import-polyfill "^0.2.0" + exists-sync "^0.0.4" + lodash.defaults "^4.2.0" + moment "^2.18.1" + moment-timezone "~0.5.11" + +ember-cli-node-assets@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.1.6.tgz#6488a2949048c801ad6d9e33753c7bce32fc1146" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.1.1" + broccoli-unwatched-tree "^0.1.1" + debug "^2.2.0" + lodash "^4.5.1" + resolve "^1.1.7" + +ember-cli-normalize-entity-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz#0b14f7bcbc599aa117b5fddc81e4fd03c4bad5b7" + dependencies: + silent-error "^1.0.0" + +ember-cli-path-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-path-utils/-/ember-cli-path-utils-1.0.0.tgz#4e39af8b55301cddc5017739b77a804fba2071ed" + +ember-cli-preprocess-registry@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/ember-cli-preprocess-registry/-/ember-cli-preprocess-registry-3.1.1.tgz#38456c21c4d2b64945850cf9ec68db6ba769288a" + dependencies: + broccoli-clean-css "^1.1.0" + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.0.0" + debug "^2.2.0" + ember-cli-lodash-subset "^1.0.7" + exists-sync "0.0.3" + process-relative-require "^1.0.0" + silent-error "^1.0.0" + +ember-cli-qunit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-qunit/-/ember-cli-qunit-4.0.0.tgz#1f0022469a5bd64f627b8102880a25e94e533a3b" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^2.0.0" + ember-cli-babel "^6.0.0-beta.7" + ember-cli-test-loader "^2.0.0" + ember-cli-version-checker "^1.1.4" + ember-qunit "^2.1.3" + qunit-notifications "^0.1.1" + qunitjs "^2.0.1" + resolve "^1.1.6" + silent-error "^1.0.0" + +ember-cli-sass@^6.1.3, ember-cli-sass@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/ember-cli-sass/-/ember-cli-sass-6.2.0.tgz#e1f81289678e1e22d9cf9dbf7fa2de76a0de9a2f" + dependencies: + broccoli-funnel "^1.0.0" + broccoli-merge-trees "^1.1.0" + broccoli-sass-source-maps "^2.0.0" + ember-cli-version-checker "^1.0.2" + merge "^1.2.0" + +ember-cli-shims@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ember-cli-shims/-/ember-cli-shims-1.1.0.tgz#0e3b8a048be865b4f81cc81d397ff1eeb13f75b6" + dependencies: + ember-cli-babel "^6.0.0-beta.7" + ember-cli-version-checker "^1.2.0" + silent-error "^1.0.1" + +ember-cli-showdown@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/ember-cli-showdown/-/ember-cli-showdown-3.2.2.tgz#a0b1539bf84bf2f09671514664defde64bdabb07" + dependencies: + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^1.1.1" + broccoli-source "^1.1.0" + broccoli-stew "^1.5.0" + ember-cli-babel "^6.0.0" + ember-cli-htmlbars "^1.1.1" + ember-cli-import-polyfill "^0.2.0" + ember-getowner-polyfill "^1.2.2" + showdown "^1.6.4" + +ember-cli-sri@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ember-cli-sri/-/ember-cli-sri-2.1.1.tgz#971620934a4b9183cf7923cc03e178b83aa907fd" + dependencies: + broccoli-sri-hash "^2.1.0" + +ember-cli-string-helpers@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ember-cli-string-helpers/-/ember-cli-string-helpers-1.4.0.tgz#f7142a9499a149f69a9f2662b94f5f21eae0ec48" + dependencies: + broccoli-funnel "^1.0.1" + ember-cli-babel "^5.1.7" + +ember-cli-string-utils@^1.0.0, ember-cli-string-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ember-cli-string-utils/-/ember-cli-string-utils-1.1.0.tgz#39b677fc2805f55173735376fcef278eaa4452a1" + +ember-cli-test-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-test-info/-/ember-cli-test-info-1.0.0.tgz#ed4e960f249e97523cf891e4aed2072ce84577b4" + dependencies: + ember-cli-string-utils "^1.0.0" + +ember-cli-test-loader@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ember-cli-test-loader/-/ember-cli-test-loader-2.2.0.tgz#3fb8d5d1357e4460d3f0a092f5375e71b6f7c243" + dependencies: + ember-cli-babel "^6.8.1" + +ember-cli-uglify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-cli-uglify/-/ember-cli-uglify-1.2.0.tgz#3208c32b54bc2783056e8bb0d5cfe9bbaf17ffb2" + dependencies: + broccoli-uglify-sourcemap "^1.0.0" + +ember-cli-valid-component-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-valid-component-name/-/ember-cli-valid-component-name-1.0.0.tgz#71550ce387e0233065f30b30b1510aa2dfbe87ef" + dependencies: + silent-error "^1.0.0" + +ember-cli-version-checker@^1.0.2, ember-cli-version-checker@^1.1.4, ember-cli-version-checker@^1.1.6, ember-cli-version-checker@^1.1.7, ember-cli-version-checker@^1.2.0, ember-cli-version-checker@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-1.3.1.tgz#0bc2d134c830142da64bf9627a0eded10b61ae72" + dependencies: + semver "^5.3.0" + +ember-cli-version-checker@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.0.0.tgz#e1f7d8e4cdcd752ac35f1611e4daa8836db4c4c7" + dependencies: + resolve "^1.3.3" + semver "^5.3.0" + +ember-cli@2.13.2: + version "2.13.2" + resolved "https://registry.yarnpkg.com/ember-cli/-/ember-cli-2.13.2.tgz#a561f08e69b184fa3175f706cced299c0d1684e5" + dependencies: + amd-name-resolver "0.0.6" + babel-plugin-transform-es2015-modules-amd "^6.24.0" + bower-config "^1.3.0" + bower-endpoint-parser "0.2.2" + broccoli-babel-transpiler "^6.0.0" + broccoli-brocfile-loader "^0.18.0" + broccoli-builder "^0.18.3" + broccoli-concat "^3.2.2" + broccoli-config-loader "^1.0.0" + broccoli-config-replace "^1.1.2" + broccoli-funnel "^1.0.6" + broccoli-funnel-reducer "^1.0.0" + broccoli-merge-trees "^2.0.0" + broccoli-middleware "^1.0.0-beta.8" + broccoli-source "^1.1.0" + broccoli-stew "^1.2.0" + calculate-cache-key-for-tree "^1.0.0" + capture-exit "^1.1.0" + chalk "^1.1.3" + clean-base-url "^1.0.0" + compression "^1.4.4" + configstore "^3.0.0" + console-ui "^1.0.2" + core-object "^3.0.0" + diff "^3.2.0" + ember-cli-broccoli-sane-watcher "^2.0.4" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-is-package-missing "^1.0.0" + ember-cli-legacy-blueprints "^0.1.2" + ember-cli-lodash-subset "^1.0.11" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-preprocess-registry "^3.1.0" + ember-cli-string-utils "^1.0.0" + ember-try "^0.2.14" + ensure-posix-path "^1.0.2" + escape-string-regexp "^1.0.3" + execa "^0.6.0" + exists-sync "0.0.4" + exit "^0.1.2" + express "^4.12.3" + filesize "^3.1.3" + find-up "^2.1.0" + fs-extra "2.0.0" + fs-tree-diff "^0.5.2" + get-caller-file "^1.0.0" + git-repo-info "^1.4.1" + glob "7.1.1" + heimdalljs "^0.2.3" + heimdalljs-fs-monitor "^0.1.0" + heimdalljs-graph "^0.3.1" + heimdalljs-logger "^0.1.7" + http-proxy "^1.9.0" + inflection "^1.7.0" + is-git-url "^0.2.0" + isbinaryfile "^3.0.0" + js-yaml "^3.6.1" + json-stable-stringify "^1.0.1" + leek "0.0.24" + lodash.template "^4.2.5" + markdown-it "^8.3.0" + markdown-it-terminal "0.0.4" + minimatch "^3.0.0" + morgan "^1.8.1" + node-modules-path "^1.0.0" + nopt "^3.0.6" + npm-package-arg "^4.1.1" + portfinder "^1.0.7" + promise-map-series "^0.2.1" + quick-temp "^0.1.8" + resolve "^1.3.0" + rsvp "^3.3.3" + sane "^1.6.0" + semver "^5.1.1" + silent-error "^1.0.0" + sort-package-json "^1.4.0" + symlink-or-copy "^1.1.8" + temp "0.8.3" + testem "^1.15.0" + tiny-lr "^1.0.3" + tree-sync "^1.2.1" + uuid "^3.0.0" + validate-npm-package-name "^3.0.0" + walk-sync "^0.3.0" + yam "0.0.22" + +ember-composable-helpers@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-2.0.3.tgz#9b5e595bf5a45bc4431adfe27821f23b1d534be0" + dependencies: + broccoli-funnel "^1.0.1" + ember-cli-babel "^6.1.0" + +ember-data-model-fragments@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/ember-data-model-fragments/-/ember-data-model-fragments-2.14.0.tgz#f31a03cdcf2449eaaaf84e0996324bf6af6c7b8e" + dependencies: + broccoli-file-creator "^1.1.1" + broccoli-merge-trees "^2.0.0" + ember-cli-babel "^6.0.0" + exists-sync "^0.0.4" + git-repo-info "^1.4.1" + npm-git-info "^1.0.3" + +ember-data@^2.14.0: + version "2.14.10" + resolved "https://registry.yarnpkg.com/ember-data/-/ember-data-2.14.10.tgz#acf66ffffb062a7fc999f9d989d0e0d2e3858cd3" + dependencies: + amd-name-resolver "0.0.7" + babel-plugin-feature-flags "^0.3.1" + babel-plugin-filter-imports "^0.3.1" + babel-plugin-transform-es2015-block-scoping "^6.24.1" + babel6-plugin-strip-class-callcheck "^6.0.0" + babel6-plugin-strip-heimdall "^6.0.1" + broccoli-babel-transpiler "^6.0.0" + broccoli-debug "^0.6.2" + broccoli-file-creator "^1.0.0" + broccoli-funnel "^1.2.0" + broccoli-merge-trees "^1.0.0" + broccoli-rollup "^1.2.0" + chalk "^1.1.1" + ember-cli-babel "^6.4.1" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.0.0" + ember-cli-test-info "^1.0.0" + ember-cli-version-checker "^1.1.4" + ember-inflector "^2.0.0" + ember-runtime-enumerable-includes-polyfill "^2.0.0" + exists-sync "0.0.3" + git-repo-info "^1.1.2" + heimdalljs "^0.3.0" + inflection "^1.8.0" + npm-git-info "^1.0.0" + semver "^5.1.0" + silent-error "^1.0.0" + testem "1.15.0" + +ember-export-application-global@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-2.0.0.tgz#8d6d7619ac8a1a3f8c43003549eb21ebed685bd2" + dependencies: + ember-cli-babel "^6.0.0-beta.7" + +ember-factory-for-polyfill@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-factory-for-polyfill/-/ember-factory-for-polyfill-1.2.0.tgz#e27752a7d9dbd5336e8b470341bc1c55bbe3e4d2" + dependencies: + ember-cli-version-checker "^1.2.0" + +ember-fetch@^3.2.7: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-3.3.1.tgz#47f7c0d282bfe61d4899f3a9d5af9f71e5558910" + dependencies: + broccoli-funnel "^1.2.0" + broccoli-stew "^1.4.2" + broccoli-templater "^1.0.0" + ember-cli-babel "^6.8.1" + node-fetch "^2.0.0-alpha.3" + whatwg-fetch "^2.0.3" + +ember-freestyle@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/ember-freestyle/-/ember-freestyle-0.4.2.tgz#ae1593be2106e4212b319e5474a3f9432fa4e329" + dependencies: + broccoli-flatiron "0.0.0" + broccoli-funnel "^1.0.1" + broccoli-merge-trees "^2.0.0" + broccoli-source "^1.1.0" + broccoli-stew "^1.2.0" + broccoli-writer "^0.1.1" + ember-cli-babel "^6.0.0" + ember-cli-htmlbars "^1.3.0" + ember-cli-sass "^6.1.3" + ember-cli-showdown "^3.2.1" + ember-runtime-enumerable-includes-polyfill "^2.0.0" + es6-promise "^4.1.0" + glob "^7.1.1" + highlight.js "^9.3.0" + +ember-get-config@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.1.tgz#a1325cceefcb4534c78fc6ccb2be87a3feda6817" + dependencies: + broccoli-file-creator "^1.1.1" + ember-cli-babel "^5.1.6" + +ember-getowner-polyfill@^1.2.2: + version "1.2.5" + resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-1.2.5.tgz#ceff8a09897d0d7e05c821bb71666a95eb26dc92" + dependencies: + ember-cli-babel "^5.1.6" + ember-cli-version-checker "^1.2.0" + ember-factory-for-polyfill "^1.1.0" + +ember-getowner-polyfill@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-2.0.1.tgz#9bfe2b4d527ed174e76fef2c8f30937d77cb66fb" + dependencies: + ember-cli-version-checker "^1.2.0" + ember-factory-for-polyfill "^1.1.0" + +ember-href-to@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/ember-href-to/-/ember-href-to-1.13.0.tgz#308ab4803d9d08e30a92af888cc67412a800468d" + dependencies: + ember-cli-babel "^5.1.6" + +ember-inflector@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-2.0.1.tgz#e9ac469ffa17992a43276bb1c9b8d87992b10d37" + dependencies: + ember-cli-babel "^6.0.0" + +ember-load-initializers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-1.0.0.tgz#4919eaf06f6dfeca7e134633d8c05a6c9921e6e7" + dependencies: + ember-cli-babel "^6.0.0-beta.7" + +ember-lodash@^4.17.3: + version "4.17.5" + resolved "https://registry.yarnpkg.com/ember-lodash/-/ember-lodash-4.17.5.tgz#bda557402facae144567d1ef530b3de7c38bcde1" + dependencies: + broccoli-debug "^0.6.1" + broccoli-funnel "^1.1.0" + broccoli-merge-trees "^2.0.0" + broccoli-string-replace "^0.1.1" + ember-cli-babel "^6.4.1" + lodash-es "^4.17.4" + +ember-macro-helpers@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/ember-macro-helpers/-/ember-macro-helpers-0.15.1.tgz#a402651fafb3a25b3612a8c09843bc4061553a13" + dependencies: + ember-cli-babel "^6.0.0" + ember-cli-string-utils "^1.1.0" + ember-cli-test-info "^1.0.0" + ember-weakmap "^2.0.0" + +ember-moment@^7.3.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-7.4.1.tgz#88018c4d981643bb4521ad39539f60ba3ec6bf42" + dependencies: + ember-cli-babel "^6.6.0" + ember-getowner-polyfill "^2.0.1" + ember-macro-helpers "^0.15.1" + +ember-qunit@^2.1.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-2.2.0.tgz#3cdf400031c93a38de781a7304819738753b7f99" + dependencies: + ember-test-helpers "^0.6.3" + +ember-resolver@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ember-resolver/-/ember-resolver-4.4.0.tgz#211ad00dea0ff2f3344aea156e556de42fc7ec74" + dependencies: + "@glimmer/resolver" "^0.4.1" + babel-plugin-debug-macros "^0.1.10" + broccoli-funnel "^1.1.0" + broccoli-merge-trees "^2.0.0" + ember-cli-babel "^6.8.1" + ember-cli-version-checker "^2.0.0" + resolve "^1.3.3" + +ember-rfc176-data@^0.2.0: + version "0.2.7" + resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.2.7.tgz#bd355bc9b473e08096b518784170a23388bc973b" + +ember-router-generator@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee" + dependencies: + recast "^0.11.3" + +ember-runtime-enumerable-includes-polyfill@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-runtime-enumerable-includes-polyfill/-/ember-runtime-enumerable-includes-polyfill-2.0.0.tgz#6e9ba118bc909d1d7762de1b03a550d8955308a9" + dependencies: + ember-cli-babel "^6.0.0" + ember-cli-version-checker "^1.1.6" + +ember-sinon@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/ember-sinon/-/ember-sinon-0.7.0.tgz#41b83b5b1c71626db26e8ffb4a52cdab9c039a29" + dependencies: + broccoli-funnel "^1.1.0" + broccoli-merge-trees "^1.2.1" + ember-cli-babel "^5.1.7" + sinon "^2.1.0" + +ember-source@~2.14.0: + version "2.14.1" + resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-2.14.1.tgz#4abf0b4c916f2da8bf317349df4750905df7e628" + dependencies: + "@glimmer/compiler" "^0.22.3" + "@glimmer/node" "^0.22.3" + "@glimmer/reference" "^0.22.3" + "@glimmer/runtime" "^0.22.3" + "@glimmer/util" "^0.22.3" + broccoli-funnel "^1.2.0" + broccoli-merge-trees "^2.0.0" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.1.0" + ember-cli-test-info "^1.0.0" + ember-cli-valid-component-name "^1.0.0" + ember-cli-version-checker "^1.3.1" + handlebars "^4.0.6" + jquery "^3.2.1" + resolve "^1.3.3" + rsvp "^3.5.0" + simple-dom "^0.3.0" + simple-html-tokenizer "^0.4.1" + +ember-test-helpers@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/ember-test-helpers/-/ember-test-helpers-0.6.3.tgz#f864cdf6f4e75f3f8768d6537785b5ab6e82d907" + +ember-truth-helpers@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-1.3.0.tgz#6ed9f83ce9a49f52bb416d55e227426339a64c60" + dependencies: + ember-cli-babel "^5.1.6" + +ember-try-config@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ember-try-config/-/ember-try-config-2.1.0.tgz#e0e156229a542346a58ee6f6ad605104c98edfe0" + dependencies: + lodash "^4.6.1" + node-fetch "^1.3.3" + rsvp "^3.2.1" + semver "^5.1.0" + +ember-try@^0.2.14: + version "0.2.16" + resolved "https://registry.yarnpkg.com/ember-try/-/ember-try-0.2.16.tgz#cf7092d8a8fea9701d7faa73cbdbff37a8ada330" + dependencies: + chalk "^1.0.0" + cli-table2 "^0.2.0" + core-object "^1.1.0" + debug "^2.2.0" + ember-cli-version-checker "^1.1.6" + ember-try-config "^2.0.1" + extend "^3.0.0" + fs-extra "^0.26.0" + promise-map-series "^0.2.1" + resolve "^1.1.6" + rimraf "^2.3.2" + rsvp "^3.0.17" + semver "^5.1.0" + +ember-weakmap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-weakmap/-/ember-weakmap-2.0.0.tgz#71c6819a8bfd0b077ae17ca1d9053fc5db06e4ac" + dependencies: + ember-cli-babel "^5.1.6" + +ember-welcome-page@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/ember-welcome-page/-/ember-welcome-page-3.1.1.tgz#dead338443f24257c552bfa83ca84b0704b6c184" + dependencies: + broccoli-funnel "^1.0.1" + ember-cli-babel "^6.0.0-beta.9" + ember-cli-htmlbars "^1.0.3" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +engine.io-client@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.0.tgz#7b730e4127414087596d9be3c88d2bc5fdb6cf5c" + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "2.3.3" + engine.io-parser "1.3.1" + has-cors "1.1.0" + indexof "0.0.1" + parsejson "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + ws "1.1.1" + xmlhttprequest-ssl "1.5.3" + yeast "0.1.2" + +engine.io-parser@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.1.tgz#9554f1ae33107d6fbd170ca5466d2f833f6a07cf" + dependencies: + after "0.8.1" + arraybuffer.slice "0.0.6" + base64-arraybuffer "0.1.5" + blob "0.0.4" + has-binary "0.1.6" + wtf-8 "1.0.0" + +engine.io@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.0.tgz#3eeb5f264cb75dbbec1baaea26d61f5a4eace2aa" + dependencies: + accepts "1.3.3" + base64id "0.1.0" + cookie "0.3.1" + debug "2.3.3" + engine.io-parser "1.3.1" + ws "1.1.1" + +ensure-posix-path@^1.0.0, ensure-posix-path@^1.0.1, ensure-posix-path@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz#a65b3e42d0b71cfc585eb774f9943c8d9b91b0c2" + +entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +error@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.30" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3, es6-map@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@^3.0.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.5.2" + debug "^2.1.1" + doctrine "^2.0.0" + escope "^3.6.0" + espree "^3.4.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +eslint@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.5.0.tgz#bb75d3b8bde97fb5e13efcd539744677feb019c3" + dependencies: + ajv "^5.2.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^2.6.8" + doctrine "^2.0.0" + eslint-scope "^3.7.1" + espree "^3.5.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^9.17.0" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^4.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "^4.0.1" + text-table "~0.2.0" + +espree@^3.4.0, espree@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d" + dependencies: + acorn "^5.1.1" + acorn-jsx "^3.0.0" + +esprima-fb@~12001.1.0-dev-harmony-fb: + version "12001.1.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-12001.1.0-dev-harmony-fb.tgz#d84400384ba95ce2678c617ad24a7f40808da915" + +esprima-fb@~15001.1001.0-dev-harmony-fb: + version "15001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events-to-array@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6" + +events@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.2.tgz#f66bb88ecd57f71a766821e20283ea38c68bf80a" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +exec-sh@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" + dependencies: + merge "^1.1.3" + +execa@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exists-stat@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/exists-stat/-/exists-stat-1.0.0.tgz#0660e3525a2e89d9e446129440c272edfa24b529" + +exists-sync@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.3.tgz#b910000bedbb113b378b82f5f5a7638107622dcf" + +exists-sync@0.0.4, exists-sync@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.4.tgz#9744c2c428cc03b01060db454d4b12f0ef3c8879" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" + +express@^4.10.7, express@^4.12.3: + version "4.15.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.8" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.4" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.5" + qs "6.5.0" + range-parser "~1.2.0" + send "0.15.4" + serve-static "1.12.4" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +external-editor@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b" + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + tmp "^0.0.29" + +external-editor@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" + dependencies: + iconv-lite "^0.4.17" + jschardet "^1.4.2" + tmp "^0.0.31" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +fake-xml-http-request@^1.4.0, fake-xml-http-request@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-1.6.0.tgz#bd0ac79ae3e2660098282048a12c730a6f64d550" + +faker@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/faker/-/faker-3.1.0.tgz#0f908faf4e6ec02524e54a57e432c5c013e08c9f" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fast-ordered-set@^1.0.0, fast-ordered-set@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-ordered-set/-/fast-ordered-set-1.0.3.tgz#3fbb36634f7be79e4f7edbdb4a357dee25d184eb" + dependencies: + blank-object "^1.0.1" + +fast-sourcemap-concat@^1.0.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/fast-sourcemap-concat/-/fast-sourcemap-concat-1.2.3.tgz#22f14e92d739e37920334376ec8433bf675eaa04" + dependencies: + chalk "^0.5.1" + fs-extra "^0.30.0" + heimdalljs-logger "^0.1.7" + memory-streams "^0.1.0" + mkdirp "^0.5.0" + rsvp "^3.0.14" + source-map "^0.4.2" + source-map-url "^0.3.0" + sourcemap-validator "^1.0.5" + +faye-websocket@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + +figures@^1.3.5, figures@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +filesize@^3.1.3: + version "3.5.10" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +finalhandler@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" + dependencies: + debug "2.6.8" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-index@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-1.1.0.tgz#53007c79cd30040d6816d79458e8837d5c5705ef" + +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +findup-sync@0.4.3, findup-sync@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" + +fireworm@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/fireworm/-/fireworm-0.7.1.tgz#ccf20f7941f108883fcddb99383dbe6e1861c758" + dependencies: + async "~0.2.9" + is-type "0.0.1" + lodash.debounce "^3.1.1" + lodash.flatten "^3.0.2" + minimatch "^3.0.2" + +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flat@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-2.0.1.tgz#70e29188a74be0c3c89409eed1fa9577907ae32f" + dependencies: + is-buffer "~1.1.2" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +formatio@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + +fs-extra@2.0.0, fs-extra@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.0.0.tgz#337352bded4a0b714f3eb84de8cea765e9d37600" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + +fs-extra@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^0.26.0: + version "0.26.7" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs-readdir-recursive@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz#315b4fb8c1ca5b8c47defef319d073dad3568059" + +fs-tree-diff@^0.5.0, fs-tree-diff@^0.5.2, fs-tree-diff@^0.5.3, fs-tree-diff@^0.5.4, fs-tree-diff@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-0.5.6.tgz#342665749e8dca406800b672268c8f5073f3e623" + dependencies: + heimdalljs-logger "^0.1.7" + object-assign "^4.1.0" + path-posix "^1.0.0" + symlink-or-copy "^1.1.8" + +fs-tree@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-tree/-/fs-tree-1.0.0.tgz#ef64da3e6dd32cc0df27c3b3e0c299ffa575c026" + dependencies: + mkdirp "~0.5.0" + rimraf "~2.2.8" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.36" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +fuse.js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.0.5.tgz#b58d85878802321de94461654947b93af1086727" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.0, get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +git-repo-info@^1.1.2, git-repo-info@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-1.4.1.tgz#2a072823254aaf62fcf0766007d7b6651bd41943" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@7.1.1, glob@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.10, glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.1.0, glob@^7.1.2, glob@~7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + +globals@^6.4.0: + version "6.4.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-6.4.1.tgz#8498032b3b6d1cc81eebc5f79690d8fe29fabf4f" + +globals@^9.14.0, globals@^9.17.0, globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" + dependencies: + glob "~7.1.1" + lodash "~4.17.4" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +handlebars@^4.0.4, handlebars@^4.0.6: + version "4.0.10" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-binary@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.6.tgz#25326f39cfa4f616ad8787894e3af2cfbc7b6e10" + dependencies: + isarray "0.0.1" + +has-binary@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" + dependencies: + isarray "0.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash-for-dep@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hash-for-dep/-/hash-for-dep-1.2.0.tgz#3bdb883aef0d34e82097ef2f7109b1b401cada6b" + dependencies: + broccoli-kitchen-sink-helpers "^0.3.1" + heimdalljs "^0.2.3" + heimdalljs-logger "^0.1.7" + resolve "^1.4.0" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +heimdalljs-fs-monitor@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/heimdalljs-fs-monitor/-/heimdalljs-fs-monitor-0.1.0.tgz#d404a65688c6714c485469ed3495da4853440272" + dependencies: + heimdalljs "^0.2.0" + heimdalljs-logger "^0.1.7" + +heimdalljs-graph@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/heimdalljs-graph/-/heimdalljs-graph-0.3.3.tgz#ea801dbba659c8d522fe1cb83b2d605726e4918f" + +heimdalljs-logger@^0.1.7: + version "0.1.9" + resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.9.tgz#d76ada4e45b7bb6f786fc9c010a68eb2e2faf176" + dependencies: + debug "^2.2.0" + heimdalljs "^0.2.0" + +heimdalljs@^0.2.0, heimdalljs@^0.2.1, heimdalljs@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.5.tgz#6aa54308eee793b642cff9cf94781445f37730ac" + dependencies: + rsvp "~3.2.1" + +heimdalljs@^0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.3.3.tgz#e92d2c6f77fd46d5bf50b610d28ad31755054d0b" + dependencies: + rsvp "~3.2.1" + +highlight.js@^9.3.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +home-or-tmp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-1.0.0.tgz#4b9f1e40800c3e50c6c27f781676afcce71f3985" + dependencies: + os-tmpdir "^1.0.1" + user-home "^1.1.1" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4, hosted-git-info@^2.1.5: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + +http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-proxy@^1.13.1, http-proxy@^1.9.0: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +husky@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.4.tgz#48785c5028de3452a51c48c12c4f94b2124a1407" + dependencies: + chalk "^1.1.3" + find-parent-dir "^0.3.0" + is-ci "^1.0.9" + normalize-path "^1.0.0" + +iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +ignore@^3.2.0, ignore@^3.3.3: + version "3.3.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.4.tgz#85ab6d0a9ca8b27b31604c09efe1c14dc21ab872" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +include-path-searcher@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/include-path-searcher/-/include-path-searcher-0.1.0.tgz#c0cf2ddfa164fb2eae07bc7ca43a7f191cb4d7bd" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflection@^1.7.0, inflection@^1.7.1, inflection@^1.8.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +inline-source-map-comment@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz#50a8a44c2a790dfac441b5c94eccd5462635faf6" + dependencies: + chalk "^1.0.0" + get-stdin "^4.0.1" + minimist "^1.1.1" + sum-up "^1.0.1" + xtend "^4.0.0" + +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + dependencies: + source-map "~0.5.3" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^3.0.6: + version "3.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.2.tgz#c2aaede1507cc54d826818737742d621bef2e823" + dependencies: + ansi-escapes "^2.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +insert-module-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + concat-stream "~1.5.1" + is-buffer "^1.1.0" + lexical-scope "^1.2.0" + process "~0.11.0" + through2 "^2.0.0" + xtend "^4.0.0" + +interpret@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" + +invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ipaddr.js@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-ci@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-git-url@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/is-git-url/-/is-git-url-0.2.3.tgz#445200d6fbd6da028fb5e01440d9afc93f3ccb64" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-integer@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" + dependencies: + is-finite "^1.0.0" + +is-my-json-valid@^2.10.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-type@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/is-type/-/is-type-0.0.1.tgz#f651d85c365d44955d14a51d8d7061f3f6b4779c" + dependencies: + core-util-is "~1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +isarray@0.0.1, isarray@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isbinaryfile@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istextorbinary@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.1.0.tgz#dbed2a6f51be2f7475b68f89465811141b758874" + dependencies: + binaryextensions "1 || 2" + editions "^1.1.1" + textextensions "1 || 2" + +jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +js-base64@^2.1.8: + version "2.1.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" + +js-reporters@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-1.2.0.tgz#7cf2cb698196684790350d0c4ca07f4aed9ec17e" + +js-tokens@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.1.tgz#cc435a5c8b94ad15acb7983140fc80182c89aeae" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.6.1, js-yaml@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jschardet@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + +jsesc@~0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.3.0.tgz#1bf5ee63b4539fe2e26d0c1e99c240b97a457972" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-formatter-js@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json-formatter-js/-/json-formatter-js-2.2.0.tgz#1ed987223ef2f1d945304597faae78b580a8212b" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d" + +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +labeled-stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" + dependencies: + inherits "^2.0.1" + isarray "~0.0.1" + stream-splicer "^2.0.0" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +leek@0.0.24: + version "0.0.24" + resolved "https://registry.yarnpkg.com/leek/-/leek-0.0.24.tgz#e400e57f0e60d8ef2bd4d068dc428a54345dbcda" + dependencies: + debug "^2.1.0" + lodash.assign "^3.2.0" + rsvp "^3.0.21" + +leven@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lexical-scope@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" + dependencies: + astw "^2.0.0" + +linkify-it@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" + dependencies: + uc.micro "^1.0.1" + +linkify-it@~1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a" + dependencies: + uc.micro "^1.0.1" + +lint-staged@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.6.1.tgz#24423c8b7bd99d96e15acd1ac8cb392a78e58582" + dependencies: + app-root-path "^2.0.0" + cosmiconfig "^1.1.0" + execa "^0.7.0" + listr "^0.12.0" + lodash.chunk "^4.2.0" + minimatch "^3.0.0" + npm-which "^3.0.1" + p-map "^1.1.1" + staged-git-files "0.0.4" + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + +listr-update-renderer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f" + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +listr@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + figures "^1.7.0" + indent-string "^2.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.2.0" + listr-verbose-renderer "^0.4.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + ora "^0.2.3" + p-map "^1.1.1" + rxjs "^5.0.0-beta.11" + stream-to-observable "^0.1.0" + strip-ansi "^3.0.1" + +livereload-js@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader.js@^4.2.3: + version "4.6.0" + resolved "https://registry.yarnpkg.com/loader.js/-/loader.js-4.6.0.tgz#b965663ddbe2d80da482454cb865efe496e93e22" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash-es@^4.17.4: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basebind@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basebind/-/lodash._basebind-2.3.0.tgz#2b5bc452a0e106143b21869f233bdb587417d248" + dependencies: + lodash._basecreate "~2.3.0" + lodash._setbinddata "~2.3.0" + lodash.isobject "~2.3.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basecreate@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz#9b88a86a4dcff7b7f3c61d83a2fcfc0671ec9de0" + dependencies: + lodash._renative "~2.3.0" + lodash.isobject "~2.3.0" + lodash.noop "~2.3.0" + +lodash._basecreatecallback@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz#37b2ab17591a339e988db3259fcd46019d7ac362" + dependencies: + lodash._setbinddata "~2.3.0" + lodash.bind "~2.3.0" + lodash.identity "~2.3.0" + lodash.support "~2.3.0" + +lodash._basecreatewrapper@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz#aa0c61ad96044c3933376131483a9759c3651247" + dependencies: + lodash._basecreate "~2.3.0" + lodash._setbinddata "~2.3.0" + lodash._slice "~2.3.0" + lodash.isobject "~2.3.0" + +lodash._baseflatten@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz#0770ff80131af6e34f3b511796a7ba5214e65ff7" + dependencies: + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._createwrapper@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz#d1aae1102dadf440e8e06fc133a6edd7fe146075" + dependencies: + lodash._basebind "~2.3.0" + lodash._basecreatewrapper "~2.3.0" + lodash.isfunction "~2.3.0" + +lodash._escapehtmlchar@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.3.0.tgz#d03da6bd82eedf38dc0a5b503d740ecd0e894592" + dependencies: + lodash._htmlescapes "~2.3.0" + +lodash._escapestringchar@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._escapestringchar/-/lodash._escapestringchar-2.3.0.tgz#cce73ae60fc6da55d2bf8a0679c23ca2bab149fc" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._htmlescapes@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._htmlescapes/-/lodash._htmlescapes-2.3.0.tgz#1ca98863cadf1fa1d82c84f35f31e40556a04f3a" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._objecttypes@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz#6a3ea3987dd6eeb8021b2d5c9c303549cc2bae1e" + +lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash._reinterpolate@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-2.3.0.tgz#03ee9d85c0e55cbd590d71608a295bdda51128ec" + +lodash._renative@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._renative/-/lodash._renative-2.3.0.tgz#77d8edd4ced26dd5971f9e15a5f772e4e317fbd3" + +lodash._reunescapedhtml@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.3.0.tgz#db920b55ac7f3ff825939aceb9ba2c231713d24d" + dependencies: + lodash._htmlescapes "~2.3.0" + lodash.keys "~2.3.0" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash._setbinddata@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz#e5610490acd13277d59858d95b5f2727f1508f04" + dependencies: + lodash._renative "~2.3.0" + lodash.noop "~2.3.0" + +lodash._shimkeys@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz#611f93149e3e6c721096b48769ef29537ada8ba9" + dependencies: + lodash._objecttypes "~2.3.0" + +lodash._slice@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash._slice/-/lodash._slice-2.3.0.tgz#147198132859972e4680ca29a5992c855669aa5c" + +lodash.assign@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.assignin@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" + +lodash.bind@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-2.3.0.tgz#c2a8e18b68e5ecc152e2b168266116fea5b016cc" + dependencies: + lodash._createwrapper "~2.3.0" + lodash._renative "~2.3.0" + lodash._slice "~2.3.0" + +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + +lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.4.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.debounce@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5" + dependencies: + lodash._getnative "^3.0.0" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.defaults@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-2.3.0.tgz#a832b001f138f3bb9721c2819a2a7cc5ae21ed25" + dependencies: + lodash._objecttypes "~2.3.0" + lodash.keys "~2.3.0" + +lodash.defaultsdeep@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81" + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + dependencies: + lodash._root "^3.0.0" + +lodash.escape@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-2.3.0.tgz#844c38c58f844e1362ebe96726159b62cf5f2a58" + dependencies: + lodash._escapehtmlchar "~2.3.0" + lodash._reunescapedhtml "~2.3.0" + lodash.keys "~2.3.0" + +lodash.find@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" + +lodash.flatten@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-3.0.2.tgz#de1cf57758f8f4479319d35c3e9cc60c4501938c" + dependencies: + lodash._baseflatten "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.foreach@~2.3.x: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-2.3.0.tgz#083404c91e846ee77245fdf9d76519c68b2af168" + dependencies: + lodash._basecreatecallback "~2.3.0" + lodash.forown "~2.3.0" + +lodash.forown@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-2.3.0.tgz#24fb4aaf800d45fc2dc60bfec3ce04c836a3ad7f" + dependencies: + lodash._basecreatecallback "~2.3.0" + lodash._objecttypes "~2.3.0" + lodash.keys "~2.3.0" + +lodash.identity@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.3.0.tgz#6b01a210c9485355c2a913b48b6711219a173ded" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isfunction@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz#6b2973e47a647cf12e70d676aea13643706e5267" + +lodash.isobject@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.3.0.tgz#2e16d3fc583da9831968953f2d8e6d73434f6799" + dependencies: + lodash._objecttypes "~2.3.0" + +lodash.isplainobject@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5" + dependencies: + lodash._basefor "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.keysin "^3.0.0" + +lodash.istypedarray@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.keys@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.3.0.tgz#b350f4f92caa9f45a4a2ecf018454cf2f28ae253" + dependencies: + lodash._renative "~2.3.0" + lodash._shimkeys "~2.3.0" + lodash.isobject "~2.3.0" + +lodash.keysin@^3.0.0: + version "3.0.8" + resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-3.0.8.tgz#22c4493ebbedb1427962a54b445b2c8a767fb47f" + dependencies: + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + +lodash.merge@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-3.3.2.tgz#0d90d93ed637b1878437bb3e21601260d7afe994" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._createassigner "^3.0.0" + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + lodash.isplainobject "^3.0.0" + lodash.istypedarray "^3.0.0" + lodash.keys "^3.0.0" + lodash.keysin "^3.0.0" + lodash.toplainobject "^3.0.0" + +lodash.merge@^4.3.0, lodash.merge@^4.4.0, lodash.merge@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" + +lodash.mergewith@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + +lodash.noop@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-2.3.0.tgz#3059d628d51bbf937cd2a0b6fc3a7f212a669c2c" + +lodash.omit@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.support@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.support/-/lodash.support-2.3.0.tgz#7eaf038af4f0d6aab776b44aa6dcfc80334c9bfd" + dependencies: + lodash._renative "~2.3.0" + +lodash.template@^3.3.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.template@^4.2.5: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.template@~2.3.x: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-2.3.0.tgz#4e3e29c433b4cfea675ec835e6f12391c61fd22b" + dependencies: + lodash._escapestringchar "~2.3.0" + lodash._reinterpolate "~2.3.0" + lodash.defaults "~2.3.0" + lodash.escape "~2.3.0" + lodash.keys "~2.3.0" + lodash.templatesettings "~2.3.0" + lodash.values "~2.3.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.templatesettings@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-2.3.0.tgz#303d132c342710040d5a18efaa2d572fd03f8cdc" + dependencies: + lodash._reinterpolate "~2.3.0" + lodash.escape "~2.3.0" + +lodash.toplainobject@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz#28790ad942d293d78aa663a07ecf7f52ca04198d" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keysin "^3.0.0" + +lodash.uniq@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash.uniqby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + +lodash.values@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-2.3.0.tgz#ca96fbe60a20b0b0ec2ba2ba5fc6a765bd14a3ba" + dependencies: + lodash.keys "~2.3.0" + +lodash@^3.10.0, lodash@^3.10.1, lodash@^3.9.3: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@~4.17.4: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + +log-update@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + dependencies: + ansi-escapes "^1.0.0" + cli-cursor "^1.0.2" + +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +markdown-it-terminal@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/markdown-it-terminal/-/markdown-it-terminal-0.0.4.tgz#3f2ce624ba2ca964a78b8b388d605ee330de9ced" + dependencies: + ansi-styles "^2.1.0" + cardinal "^0.5.0" + cli-table "^0.3.1" + lodash.merge "^3.3.2" + markdown-it "^4.4.0" + +markdown-it@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-4.4.0.tgz#3df373dbea587a9a7fef3e56311b68908f75c414" + dependencies: + argparse "~1.0.2" + entities "~1.1.1" + linkify-it "~1.2.0" + mdurl "~1.0.0" + uc.micro "^1.0.0" + +markdown-it@^8.3.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.0.tgz#e2400881bf171f7018ed1bd9da441dac8af6306d" + dependencies: + argparse "^1.0.7" + entities "~1.1.1" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.3" + +matcher-collection@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.0.4.tgz#2f66ae0869996f29e43d0b62c83dd1d43e581755" + dependencies: + minimatch "^3.0.2" + +md5-hex@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4" + dependencies: + md5-o-matic "^0.1.1" + +md5-hex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-2.0.0.tgz#d0588e9f1c74954492ecd24ac0ac6ce997d92e33" + dependencies: + md5-o-matic "^0.1.1" + +md5-o-matic@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mdurl@^1.0.1, mdurl@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-streams@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.2.tgz#273ff777ab60fec599b116355255282cca2c50c2" + dependencies: + readable-stream "~1.0.2" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge-trees@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-trees/-/merge-trees-1.0.1.tgz#ccbe674569787f9def17fd46e6525f5700bbd23e" + dependencies: + can-symlink "^1.0.0" + fs-tree-diff "^0.5.4" + heimdalljs "^0.2.1" + heimdalljs-logger "^0.1.7" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + +merge@^1.1.3, merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.29.0 < 2": + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-db@~1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" + dependencies: + mime-db "~1.29.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +mime@^1.2.11: + version "1.4.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.0.tgz#69e9e0db51d44f2a3b56e48b7817d7d137f1a343" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimatch@^2.0.3: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mkdirp@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + +mktemp@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" + +module-deps@^4.0.8: + version "4.1.1" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" + dependencies: + JSONStream "^1.0.3" + browser-resolve "^1.7.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.0" + defined "^1.0.0" + detective "^4.0.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.1.3" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +moment-timezone@~0.5.11: + version "0.5.13" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +morgan@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" + dependencies: + basic-auth "~1.1.0" + debug "2.6.8" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +mout@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mout/-/mout-1.0.0.tgz#9bdf1d4af57d66d47cb353a6335a3281098e1501" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mustache@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +mute-stream@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +nan@^2.3.0, nan@^2.3.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +native-promise-only@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-fetch@^1.3.3: + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-fetch@^2.0.0-alpha.3: + version "2.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.0.0-alpha.8.tgz#f586cf6730ce30431c7d4528ce561d81add8ba90" + +node-gyp@^3.3.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "2" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-modules-path@^1.0.0, node-modules-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/node-modules-path/-/node-modules-path-1.0.1.tgz#40096b08ce7ad0ea14680863af449c7c75a5d1c8" + +node-notifier@^5.0.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" + dependencies: + growly "^1.3.0" + semver "^5.3.0" + shellwords "^0.1.0" + which "^1.2.12" + +node-pre-gyp@^0.6.36: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-sass@^4.1.0: + version "4.5.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.3.2" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "^2.79.0" + sass-graph "^2.1.1" + stdout-stream "^1.4.0" + +"nopt@2 || 3", nopt@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + +normalize-path@^2.0.0, normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npm-git-info@^1.0.0, npm-git-info@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-git-info/-/npm-git-info-1.0.3.tgz#a933c42ec321e80d3646e0d6e844afe94630e1d5" + +npm-package-arg@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.1.tgz#593303fdea85f7c422775f17f9eb7670f680e3ec" + dependencies: + hosted-git-info "^2.1.5" + semver "^5.1.0" + +npm-path@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe" + dependencies: + which "^1.2.10" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npm-which@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa" + dependencies: + commander "^2.9.0" + npm-path "^2.0.2" + which "^1.2.10" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +ora@^0.2.0, ora@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + +os-browserify@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.3, osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +output-file-sync@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" + dependencies: + graceful-fs "^4.1.4" + mkdirp "^0.5.1" + object-assign "^4.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +parsejson@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab" + dependencies: + better-assert "~1.0.0" + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-exists@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + +path-posix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.13" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.13.tgz#c37d295531e786b1da3e3eadc840426accb0ae25" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +pluralize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762" + +portfinder@^1.0.7: + version "1.0.13" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +pretender@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-1.5.1.tgz#bd9098c03d39c3bc7dcb84a28ee27e096e2e32b8" + dependencies: + fake-xml-http-request "^1.6.0" + route-recognizer "^0.3.3" + +prettier@^1.4.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.6.0.tgz#23e9c68251f440feb847f558821bead21765919a" + +printf@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/printf/-/printf-0.2.5.tgz#c438ca2ca33e3927671db4ab69c0e52f936a4f0f" + +private@^0.1.6, private@^0.1.7, private@~0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process-relative-require@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/process-relative-require/-/process-relative-require-1.0.0.tgz#1590dfcf5b8f2983ba53e398446b68240b4cc68a" + dependencies: + node-modules-path "^1.0.0" + +process@~0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +promise-map-series@^0.2.0, promise-map-series@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.2.3.tgz#c2d377afc93253f6bd03dbb77755eb88ab20a847" + dependencies: + rsvp "^3.0.14" + +proxy-addr@~1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.4.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.3.2, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + +qs@6.5.0, qs@^6.4.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +quick-temp@^0.1.0, quick-temp@^0.1.2, quick-temp@^0.1.3, quick-temp@^0.1.5, quick-temp@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.8.tgz#bab02a242ab8fb0dd758a3c9776b32f9a5d94408" + dependencies: + mktemp "~0.4.0" + rimraf "^2.5.4" + underscore.string "~3.3.4" + +qunit-notifications@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/qunit-notifications/-/qunit-notifications-0.1.1.tgz#3001afc6a6a77dfbd962ccbcddde12dec5286c09" + +qunitjs@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/qunitjs/-/qunitjs-2.4.0.tgz#58f3a81e846687f2e7f637c5bedc9c267f887261" + dependencies: + chokidar "1.6.1" + commander "2.9.0" + exists-stat "1.0.0" + findup-sync "0.4.3" + js-reporters "1.2.0" + resolve "1.3.2" + walk-sync "0.3.1" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425" + dependencies: + bytes "1" + string_decoder "0.10" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + dependencies: + readable-stream "^2.0.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readable-stream@~1.0.2: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +recast@0.10.33, recast@^0.10.10: + version "0.10.33" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697" + dependencies: + ast-types "0.8.12" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + source-map "~0.5.0" + +recast@^0.11.17, recast@^0.11.3: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redeyed@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-0.5.0.tgz#7ab000e60ee3875ac115d29edb32c1403c6c25d1" + dependencies: + esprima-fb "~12001.1.0-dev-harmony-fb" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-runtime@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regenerator@0.8.40: + version "0.8.40" + resolved "https://registry.yarnpkg.com/regenerator/-/regenerator-0.8.40.tgz#a0e457c58ebdbae575c9f8cd75127e93756435d8" + dependencies: + commoner "~0.10.3" + defs "~1.1.0" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + recast "0.10.33" + through "~2.3.8" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexpu/-/regexpu-1.3.0.tgz#e534dc991a9e5846050c98de6d7dd4a55c9ea16d" + dependencies: + esprima "^2.6.0" + recast "^0.10.10" + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^1.1.0, repeating@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@2, request@^2.79.0, request@^2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2, require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +resolve@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235" + dependencies: + path-parse "^1.0.5" + +resolve@^1.1.2, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.0, resolve@^1.3.3, resolve@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.3.2, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@~2.2.6, rimraf@~2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +rollup@^0.41.4: + version "0.41.6" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.41.6.tgz#e0d05497877a398c104d816d2733a718a7a94e2a" + dependencies: + source-map-support "^0.4.0" + +route-recognizer@^0.2.3: + version "0.2.10" + resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.2.10.tgz#024b2283c2e68d13a7c7f5173a5924645e8902df" + +route-recognizer@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.3.tgz#1d365e27fa6995e091675f7dc940a8c00353bd29" + +rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1, rsvp@^3.3.3, rsvp@^3.5.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" + +rsvp@~3.0.6: + version "3.0.21" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.0.21.tgz#49c588fe18ef293bcd0ab9f4e6756e6ac433359f" + +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + +rxjs@^5.0.0-beta.11: + version "5.4.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" + dependencies: + symbol-observable "^1.0.1" + +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +safe-json-parse@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" + +samsam@1.x, samsam@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67" + +sane@^1.1.1, sane@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.7.0.tgz#b3579bccb45c94cf20355cc81124990dfd346e30" + dependencies: + anymatch "^1.3.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sass-graph@^2.1.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.1.1, semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +semver@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.15.4: + version "0.15.4" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" + dependencies: + debug "2.6.8" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.2" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-static@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.4" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +shelljs@^0.7.5: + version "0.7.8" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + +showdown@^1.6.4: + version "1.7.3" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.7.3.tgz#8d27f501a0850f33a0c25f1a6b7a10b9fc464633" + dependencies: + yargs "^8.0.1" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +silent-error@^1.0.0, silent-error@^1.0.1, silent-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/silent-error/-/silent-error-1.1.0.tgz#2209706f1c850a9f1d10d0d840918b46f26e1bc9" + dependencies: + debug "^2.2.0" + +simple-dom@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/simple-dom/-/simple-dom-0.3.2.tgz#0663d10f1556f1500551d518f56e3aba0871371d" + +simple-fmt@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b" + +simple-html-tokenizer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.3.0.tgz#9b8b5559d80e331a544dd13dd59382e5d0d94411" + +simple-html-tokenizer@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.4.1.tgz#028988bb7fe8b2e6645676d82052587d440b02d3" + +simple-is@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" + +sinon@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lolex "^1.6.0" + native-promise-only "^0.8.1" + path-to-regexp "^1.7.0" + samsam "^1.1.3" + text-encoding "0.6.4" + type-detect "^4.0.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +socket.io-adapter@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b" + dependencies: + debug "2.3.3" + socket.io-parser "2.3.1" + +socket.io-client@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.6.0.tgz#5b668f4f771304dfeed179064708386fa6717853" + dependencies: + backo2 "1.0.2" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "2.3.3" + engine.io-client "1.8.0" + has-binary "0.1.7" + indexof "0.0.1" + object-component "0.0.3" + parseuri "0.0.5" + socket.io-parser "2.3.1" + to-array "0.1.4" + +socket.io-parser@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0" + dependencies: + component-emitter "1.1.2" + debug "2.2.0" + isarray "0.0.1" + json3 "3.3.2" + +socket.io@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.6.0.tgz#3e40d932637e6bd923981b25caf7c53e83b6e2e1" + dependencies: + debug "2.3.3" + engine.io "1.8.0" + has-binary "0.1.7" + object-assign "4.1.0" + socket.io-adapter "0.5.0" + socket.io-client "1.6.0" + socket.io-parser "2.3.1" + +sort-object-keys@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.2.tgz#d3a6c48dc2ac97e6bc94367696e03f6d09d37952" + +sort-package-json@^1.4.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-1.7.0.tgz#13b362ff6400c5b4eaa9ba220f9ea7c3d6644b5f" + dependencies: + sort-object-keys "^1.1.1" + +source-map-support@^0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.2.10.tgz#ea5a3900a1c1cb25096a0ae8cc5c2b4b10ded3dc" + dependencies: + source-map "0.1.32" + +source-map-support@^0.4.0, source-map-support@^0.4.15: + version "0.4.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.16.tgz#16fecf98212467d017d586a2af68d628b9421cd8" + dependencies: + source-map "^0.5.6" + +source-map-url@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" + +source-map@0.1.32, source-map@~0.1.x: + version "0.1.32" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.32.tgz#c8b6c167797ba4740a8ea33252162ff08591b266" + dependencies: + amdefine ">=0.0.4" + +source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +sourcemap-validator@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sourcemap-validator/-/sourcemap-validator-1.0.5.tgz#f9b960f48c6469e288a19af305f005da3dc1df3a" + dependencies: + jsesc "~0.3.x" + lodash.foreach "~2.3.x" + lodash.template "~2.3.x" + source-map "~0.1.x" + +spawn-args@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb" + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sri-toolbox@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stable@~0.1.3: + version "0.1.6" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10" + +staged-git-files@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-http@^2.0.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +stream-to-observable@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" + +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@0.10, string_decoder@~0.10.0, string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringmap@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" + +stringset@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +styled_string@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/styled_string/-/styled_string-0.0.1.tgz#d22782bd81295459bc4f1df18c4bad8e94dd124a" + +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + +sum-up@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e" + dependencies: + chalk "^1.0.0" + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" + dependencies: + has-flag "^2.0.0" + +symbol-observable@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + +symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.1.8.tgz#cabe61e0010c1c023c173b25ee5108b37f4b4aa3" + +syntax-error@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.3.0.tgz#1ed9266c4d40be75dc55bf9bb1cb77062bb96ca1" + dependencies: + acorn "^4.0.3" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +table@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tap-parser@^5.1.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-5.4.0.tgz#6907e89725d7b7fa6ae41ee2c464c3db43188aec" + dependencies: + events-to-array "^1.0.1" + js-yaml "^3.2.7" + optionalDependencies: + readable-stream "^2" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.0.0, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +temp@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + +testem@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/testem/-/testem-1.15.0.tgz#2e3a9e7ac29f16a20f718eb0c4b12e7a44900675" + dependencies: + backbone "^1.1.2" + bluebird "^3.4.6" + charm "^1.0.0" + commander "^2.6.0" + consolidate "^0.14.0" + cross-spawn "^5.0.0" + express "^4.10.7" + fireworm "^0.7.0" + glob "^7.0.4" + http-proxy "^1.13.1" + js-yaml "^3.2.5" + lodash.assignin "^4.1.0" + lodash.clonedeep "^4.4.1" + lodash.find "^4.5.1" + mkdirp "^0.5.1" + mustache "^2.2.1" + node-notifier "^5.0.1" + npmlog "^4.0.0" + printf "^0.2.3" + rimraf "^2.4.4" + socket.io "1.6.0" + spawn-args "^0.2.0" + styled_string "0.0.1" + tap-parser "^5.1.0" + xmldom "^0.1.19" + +testem@^1.15.0: + version "1.18.4" + resolved "https://registry.yarnpkg.com/testem/-/testem-1.18.4.tgz#e45fed922bec2f54a616c43f11922598ac97eb41" + dependencies: + backbone "^1.1.2" + bluebird "^3.4.6" + charm "^1.0.0" + commander "^2.6.0" + consolidate "^0.14.0" + cross-spawn "^5.1.0" + express "^4.10.7" + fireworm "^0.7.0" + glob "^7.0.4" + http-proxy "^1.13.1" + js-yaml "^3.2.5" + lodash.assignin "^4.1.0" + lodash.clonedeep "^4.4.1" + lodash.find "^4.5.1" + lodash.uniqby "^4.7.0" + mkdirp "^0.5.1" + mustache "^2.2.1" + node-notifier "^5.0.1" + npmlog "^4.0.0" + printf "^0.2.3" + rimraf "^2.4.4" + socket.io "1.6.0" + spawn-args "^0.2.0" + styled_string "0.0.1" + tap-parser "^5.1.0" + xmldom "^0.1.19" + +text-encoding@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +"textextensions@1 || 2": + version "2.1.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.1.0.tgz#1be0dc2a0dc244d44be8a09af6a85afb93c4dbc3" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + dependencies: + process "~0.11.0" + +tiny-lr@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.0.5.tgz#21f40bf84ebd1f853056680375eef1670c334112" + dependencies: + body "^5.1.0" + debug "~2.6.7" + faye-websocket "~0.10.0" + livereload-js "^2.2.2" + object-assign "^4.1.0" + qs "^6.4.0" + +tmp@0.0.28: + version "0.0.28" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" + dependencies: + os-tmpdir "~1.0.1" + +tmp@^0.0.29: + version "0.0.29" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" + dependencies: + os-tmpdir "~1.0.1" + +tmp@^0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.0, to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tree-sync@^1.2.1, tree-sync@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.2.2.tgz#2cf76b8589f59ffedb58db5a3ac7cb013d0158b7" + dependencies: + debug "^2.2.0" + fs-tree-diff "^0.5.6" + mkdirp "^0.5.1" + quick-temp "^0.1.5" + walk-sync "^0.2.7" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.0, trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +try-resolve@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/try-resolve/-/try-resolve-1.0.1.tgz#cfde6fabd72d63e5797cfaab873abbe8e700e912" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tryor@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" + +tty-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-detect@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6, typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uc.micro@^1.0.0, uc.micro@^1.0.1, uc.micro@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" + +uglify-js@^2.6, uglify-js@^2.7.0: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +umd@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e" + +underscore.string@~3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" + dependencies: + sprintf-js "^1.0.3" + util-deprecate "^1.0.2" + +underscore@>=1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +untildify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" + dependencies: + os-homedir "^1.0.0" + +url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +username-sync@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/username-sync/-/username-sync-1.0.1.tgz#1cde87eefcf94b8822984d938ba2b797426dae1f" + +util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@~0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + dependencies: + builtins "^1.0.3" + +vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@~0.0.1: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +walk-sync@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.1.tgz#558a16aeac8c0db59c028b73c66f397684ece465" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walk-sync@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.1.3.tgz#8a07261a00bda6cfb1be25e9f100fad57546f583" + +walk-sync@^0.2.5, walk-sync@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walk-sync@^0.3.0, walk-sync@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.2.tgz#4827280afc42d0e035367c4a4e31eeac0d136f75" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + +whatwg-fetch@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@1, which@^1.2.10, which@^1.2.12, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +window-size@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +workerpool@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-2.2.4.tgz#c9dbe01e103e92df0e8f55356fc860135fbd43b0" + dependencies: + object-assign "4.1.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +ws@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +wtf-8@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + +xmldom@^0.1.19: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xmlhttprequest-ssl@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" + +xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.0, y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yam@0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/yam/-/yam-0.0.22.tgz#38a76cb79a19284d9206ed49031e359a1340bd06" + dependencies: + fs-extra "^0.30.0" + lodash.merge "^4.4.0" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^6.5.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +yargs@~3.27.0: + version "3.27.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.27.0.tgz#21205469316e939131d59f2da0c6d7f98221ea40" + dependencies: + camelcase "^1.2.1" + cliui "^2.1.0" + decamelize "^1.0.0" + os-locale "^1.4.0" + window-size "^0.1.2" + y18n "^3.2.0" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE b/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE new file mode 100644 index 000000000..5782c7269 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Elazar Leibovich +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/README.md b/vendor/github.com/elazarl/go-bindata-assetfs/README.md new file mode 100644 index 000000000..27ee48f09 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/README.md @@ -0,0 +1,46 @@ +# go-bindata-assetfs + +Serve embedded files from [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) with `net/http`. + +[GoDoc](http://godoc.org/github.com/elazarl/go-bindata-assetfs) + +### Installation + +Install with + + $ go get github.com/jteeuwen/go-bindata/... + $ go get github.com/elazarl/go-bindata-assetfs/... + +### Creating embedded data + +Usage is identical to [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) usage, +instead of running `go-bindata` run `go-bindata-assetfs`. + +The tool will create a `bindata_assetfs.go` file, which contains the embedded data. + +A typical use case is + + $ go-bindata-assetfs data/... + +### Using assetFS in your code + +The generated file provides an `assetFS()` function that returns a `http.Filesystem` +wrapping the embedded files. What you usually want to do is: + + http.Handle("/", http.FileServer(assetFS())) + +This would run an HTTP server serving the embedded files. + +## Without running binary tool + +You can always just run the `go-bindata` tool, and then + +use + + import "github.com/elazarl/go-bindata-assetfs" + ... + http.Handle("/", + http.FileServer( + &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "data"})) + +to serve files embedded from the `data` directory. diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go b/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go new file mode 100644 index 000000000..04f6d7a39 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go @@ -0,0 +1,167 @@ +package assetfs + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" +) + +var ( + defaultFileTimestamp = time.Now() +) + +// FakeFile implements os.FileInfo interface for a given path and size +type FakeFile struct { + // Path is the path of this file + Path string + // Dir marks of the path is a directory + Dir bool + // Len is the length of the fake file, zero if it is a directory + Len int64 + // Timestamp is the ModTime of this file + Timestamp time.Time +} + +func (f *FakeFile) Name() string { + _, name := filepath.Split(f.Path) + return name +} + +func (f *FakeFile) Mode() os.FileMode { + mode := os.FileMode(0644) + if f.Dir { + return mode | os.ModeDir + } + return mode +} + +func (f *FakeFile) ModTime() time.Time { + return f.Timestamp +} + +func (f *FakeFile) Size() int64 { + return f.Len +} + +func (f *FakeFile) IsDir() bool { + return f.Mode().IsDir() +} + +func (f *FakeFile) Sys() interface{} { + return nil +} + +// AssetFile implements http.File interface for a no-directory file with content +type AssetFile struct { + *bytes.Reader + io.Closer + FakeFile +} + +func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile { + if timestamp.IsZero() { + timestamp = defaultFileTimestamp + } + return &AssetFile{ + bytes.NewReader(content), + ioutil.NopCloser(nil), + FakeFile{name, false, int64(len(content)), timestamp}} +} + +func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, errors.New("not a directory") +} + +func (f *AssetFile) Size() int64 { + return f.FakeFile.Size() +} + +func (f *AssetFile) Stat() (os.FileInfo, error) { + return f, nil +} + +// AssetDirectory implements http.File interface for a directory +type AssetDirectory struct { + AssetFile + ChildrenRead int + Children []os.FileInfo +} + +func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory { + fileinfos := make([]os.FileInfo, 0, len(children)) + for _, child := range children { + _, err := fs.AssetDir(filepath.Join(name, child)) + fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}}) + } + return &AssetDirectory{ + AssetFile{ + bytes.NewReader(nil), + ioutil.NopCloser(nil), + FakeFile{name, true, 0, time.Time{}}, + }, + 0, + fileinfos} +} + +func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) { + if count <= 0 { + return f.Children, nil + } + if f.ChildrenRead+count > len(f.Children) { + count = len(f.Children) - f.ChildrenRead + } + rv := f.Children[f.ChildrenRead : f.ChildrenRead+count] + f.ChildrenRead += count + return rv, nil +} + +func (f *AssetDirectory) Stat() (os.FileInfo, error) { + return f, nil +} + +// AssetFS implements http.FileSystem, allowing +// embedded files to be served from net/http package. +type AssetFS struct { + // Asset should return content of file in path if exists + Asset func(path string) ([]byte, error) + // AssetDir should return list of files in the path + AssetDir func(path string) ([]string, error) + // AssetInfo should return the info of file in path if exists + AssetInfo func(path string) (os.FileInfo, error) + // Prefix would be prepended to http requests + Prefix string +} + +func (fs *AssetFS) Open(name string) (http.File, error) { + name = path.Join(fs.Prefix, name) + if len(name) > 0 && name[0] == '/' { + name = name[1:] + } + if b, err := fs.Asset(name); err == nil { + timestamp := defaultFileTimestamp + if fs.AssetInfo != nil { + if info, err := fs.AssetInfo(name); err == nil { + timestamp = info.ModTime() + } + } + return NewAssetFile(name, b, timestamp), nil + } + if children, err := fs.AssetDir(name); err == nil { + return NewAssetDirectory(name, children, fs), nil + } else { + // If the error is not found, return an error that will + // result in a 404 error. Otherwise the server returns + // a 500 error for files not found. + if strings.Contains(err.Error(), "not found") { + return nil, os.ErrNotExist + } + return nil, err + } +} diff --git a/vendor/github.com/elazarl/go-bindata-assetfs/doc.go b/vendor/github.com/elazarl/go-bindata-assetfs/doc.go new file mode 100644 index 000000000..a664249f3 --- /dev/null +++ b/vendor/github.com/elazarl/go-bindata-assetfs/doc.go @@ -0,0 +1,13 @@ +// assetfs allows packages to serve static content embedded +// with the go-bindata tool with the standard net/http package. +// +// See https://github.com/jteeuwen/go-bindata for more information +// about embedding binary data with go-bindata. +// +// Usage example, after running +// $ go-bindata data/... +// use: +// http.Handle("/", +// http.FileServer( +// &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "data"})) +package assetfs diff --git a/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go b/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go index 289ad5321..a9fe74646 100644 --- a/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go +++ b/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go @@ -19,6 +19,7 @@ import ( "regexp" "sort" "strings" + "sync" ) /******************************************************************************/ @@ -54,6 +55,7 @@ var ( var ( numberTokens = map[string]int{ "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, + "00": 0, "01": 1, "02": 2, "03": 3, "04": 4, "05": 5, "06": 6, "07": 7, "08": 8, "09": 9, "10": 10, "11": 11, "12": 12, "13": 13, "14": 14, "15": 15, "16": 16, "17": 17, "18": 18, "19": 19, "20": 20, "21": 21, "22": 22, "23": 23, "24": 24, "25": 25, "26": 26, "27": 27, "28": 28, "29": 29, "30": 30, "31": 31, "32": 32, "33": 33, "34": 34, "35": 35, "36": 36, "37": 37, "38": 38, "39": 39, @@ -119,7 +121,7 @@ var ( min: 0, max: 59, defaultList: genericDefaultList[0:60], - valuePattern: `[0-9]|[1-5][0-9]`, + valuePattern: `0?[0-9]|[1-5][0-9]`, atoi: atoi, } minuteDescriptor = fieldDescriptor{ @@ -127,7 +129,7 @@ var ( min: 0, max: 59, defaultList: genericDefaultList[0:60], - valuePattern: `[0-9]|[1-5][0-9]`, + valuePattern: `0?[0-9]|[1-5][0-9]`, atoi: atoi, } hourDescriptor = fieldDescriptor{ @@ -135,7 +137,7 @@ var ( min: 0, max: 23, defaultList: genericDefaultList[0:24], - valuePattern: `[0-9]|1[0-9]|2[0-3]`, + valuePattern: `0?[0-9]|1[0-9]|2[0-3]`, atoi: atoi, } domDescriptor = fieldDescriptor{ @@ -143,7 +145,7 @@ var ( min: 1, max: 31, defaultList: genericDefaultList[1:32], - valuePattern: `[1-9]|[12][0-9]|3[01]`, + valuePattern: `0?[1-9]|[12][0-9]|3[01]`, atoi: atoi, } monthDescriptor = fieldDescriptor{ @@ -151,7 +153,7 @@ var ( min: 1, max: 12, defaultList: genericDefaultList[1:13], - valuePattern: `[1-9]|1[012]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|march|april|june|july|august|september|october|november|december`, + valuePattern: `0?[1-9]|1[012]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|march|april|june|july|august|september|october|november|december`, atoi: func(s string) int { return monthTokens[s] }, @@ -161,7 +163,7 @@ var ( min: 0, max: 6, defaultList: genericDefaultList[0:7], - valuePattern: `[0-7]|sun|mon|tue|wed|thu|fri|sat|sunday|monday|tuesday|wednesday|thursday|friday|saturday`, + valuePattern: `0?[0-7]|sun|mon|tue|wed|thu|fri|sat|sunday|monday|tuesday|wednesday|thursday|friday|saturday`, atoi: func(s string) int { return dowTokens[s] }, @@ -193,6 +195,7 @@ var ( fieldFinder = regexp.MustCompile(`\S+`) entryFinder = regexp.MustCompile(`[^,]+`) layoutRegexp = make(map[string]*regexp.Regexp) + layoutRegexpLock sync.Mutex ) /******************************************************************************/ @@ -445,6 +448,9 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) directive.first = desc.min directive.last = desc.max directive.step = atoi(snormal[pairs[2]:pairs[3]]) + if directive.step < 1 || directive.step > desc.max { + return nil, fmt.Errorf("invalid interval %s", snormal) + } directives = append(directives, &directive) continue } @@ -455,6 +461,9 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) directive.last = desc.max directive.step = atoi(snormal[pairs[4]:pairs[5]]) + if directive.step < 1 || directive.step > desc.max { + return nil, fmt.Errorf("invalid interval %s", snormal) + } directives = append(directives, &directive) continue } @@ -465,6 +474,9 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) directive.last = desc.atoi(snormal[pairs[4]:pairs[5]]) directive.step = atoi(snormal[pairs[6]:pairs[7]]) + if directive.step < 1 || directive.step > desc.max { + return nil, fmt.Errorf("invalid interval %s", snormal) + } directives = append(directives, &directive) continue } @@ -478,6 +490,9 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) /******************************************************************************/ func makeLayoutRegexp(layout, value string) *regexp.Regexp { + layoutRegexpLock.Lock() + defer layoutRegexpLock.Unlock() + layout = strings.Replace(layout, `%value%`, value, -1) re := layoutRegexp[layout] if re == nil { diff --git a/vendor/vendor.json b/vendor/vendor.json index 45bb073ac..e7f4002f7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -517,6 +517,12 @@ "path": "github.com/dustin/go-humanize", "revision": "8929fe90cee4b2cb9deb468b51fb34eba64d1bf0" }, + { + "checksumSHA1": "7DxViusFRJ7UPH0jZqYatwDrOkY=", + "path": "github.com/elazarl/go-bindata-assetfs", + "revision": "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43", + "revisionTime": "2017-02-27T21:27:28Z" + }, { "checksumSHA1": "QBkOnLnM6zZ158NJSVLqoE4V6fI=", "path": "github.com/fatih/structs", @@ -648,9 +654,12 @@ "revisionTime": "2017-09-01T21:42:48Z" }, { + "checksumSHA1": "m8B3L3qJ3tFfP6BI9pIFr9oal3w=", "comment": "1.0.0", + "origin": "github.com/dadgar/cronexpr", "path": "github.com/gorhill/cronexpr", - "revision": "a557574d6c024ed6e36acc8b610f5f211c91568a" + "revision": "675cac9b2d182dccb5ba8d5f8a0d5988df8a4394", + "revisionTime": "2017-09-15T18:30:32Z" }, { "comment": "1.0.0", diff --git a/website/source/api/deployments.html.md b/website/source/api/deployments.html.md index 86b775002..409127ebf 100644 --- a/website/source/api/deployments.html.md +++ b/website/source/api/deployments.html.md @@ -3,7 +3,7 @@ layout: api page_title: Deployments - HTTP API sidebar_current: api-deployments description: |- - The /deployment are used to query for and interact with deployments. + The /deployment endpoints are used to query for and interact with deployments. --- # Deployments HTTP API diff --git a/website/source/api/jobs.html.md b/website/source/api/jobs.html.md index ba9abd560..5262e8267 100644 --- a/website/source/api/jobs.html.md +++ b/website/source/api/jobs.html.md @@ -96,11 +96,23 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `NO` | `none` | +| `NO` | `namespace:submit-job`
    `namespace:sentinel-override` if `PolicyOverride` set | ### Parameters -There are no parameters, but the request _body_ contains the entire job file. +- `Job` `(Job: )` - Specifies the JSON definition of the job. + +- `EnforceIndex` `(bool: false)` - If set, the job will only be registered if the + passed `JobModifyIndex` matches the current job's index. If the index is zero, + the register only occurs if the job is new. This paradigm allows check-and-set + style job updating. + +- `JobModifyIndex` `(int: 0)` - Specifies the `JobModifyIndex` to enforce the + current job is at. + +- `PolicyOverride` `(bool: false)` - If set, any soft mandatory Sentinel policies + will be overriden. This allows a job to be registered when it would be denied + by policy. ### Sample Payload @@ -1027,7 +1039,7 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `NO` | `none` | +| `NO` | `namespace:submit-job`
    `namespace:sentinel-override` if `PolicyOverride` set | ### Parameters @@ -1036,7 +1048,7 @@ The table below shows this endpoint's support for - `Job` `(Job: )` - Specifies the JSON definition of the job. -- `EnforceIndex` `(int: 0)` - If set, the job will only be registered if the +- `EnforceIndex` `(bool: false)` - If set, the job will only be registered if the passed `JobModifyIndex` matches the current job's index. If the index is zero, the register only occurs if the job is new. This paradigm allows check-and-set style job updating. @@ -1044,6 +1056,10 @@ The table below shows this endpoint's support for - `JobModifyIndex` `(int: 0)` - Specifies the `JobModifyIndex` to enforce the current job is at. +- `PolicyOverride` `(bool: false)` - If set, any soft mandatory Sentinel policies + will be overriden. This allows a job to be registered when it would be denied + by policy. + ### Sample Payload ```javascript @@ -1051,10 +1067,10 @@ The table below shows this endpoint's support for "Job": { // ... }, - "EnforceIndex": 1, + "EnforceIndex": true, "JobModifyIndex": 4 } -``` +``` ### Sample Request @@ -1298,7 +1314,7 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `NO` | `none` | +| `NO` | `namespace:submit-job`
    `namespace:sentinel-override` if `PolicyOverride` set | ### Parameters @@ -1311,12 +1327,17 @@ The table below shows this endpoint's support for submitted and server side version of the job should be included in the response. +- `PolicyOverride` `(bool: false)` - If set, any soft mandatory Sentinel policies + will be overriden. This allows a job to be registered when it would be denied + by policy. + ### Sample Payload ```json { "Job": "...", - "Diff": true + "Diff": true, + "PolicyOverride": false } ``` diff --git a/website/source/api/json-jobs.html.md b/website/source/api/json-jobs.html.md index 8e52d3ea4..6410e7105 100644 --- a/website/source/api/json-jobs.html.md +++ b/website/source/api/json-jobs.html.md @@ -152,6 +152,9 @@ The `Job` object supports the following keys: - `Meta` - Annotates the job with opaque metadata. +- `Namespace` - The namespace to execute the job in, defaults to "default". + Values other than default are not allowed in non-Enterprise versions of Nomad. + - `ParameterizedJob` - Specifies the job as a parameterized job such that it can be dispatched against. The `ParamaterizedJob` object supports the following attributes: diff --git a/website/source/api/namespaces.html.md b/website/source/api/namespaces.html.md new file mode 100644 index 000000000..601f00831 --- /dev/null +++ b/website/source/api/namespaces.html.md @@ -0,0 +1,182 @@ +--- +layout: api +page_title: Namespace - HTTP API +sidebar_current: api-namespaces +description: |- + The /namespace endpoints are used to query for and interact with namespaces. +--- + +# Namespace HTTP API + +The `/namespace` endpoints are used to query for and interact with namespaces. + +~> **Enterprise Only!** This API endpoint and functionality only exists in +Nomad Enterprise. This is not present in the open source version of Nomad. + +## List Namespaces + +This endpoint lists all namespaces. + +| Method | Path | Produces | +| ------ | ----------------- | ------------------ | +| `GET` | `/v1/namespaces` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | ------------- | +| `YES` | `namespace:*`
    Any capability on the namespace authorizes the endpoint | + +### Parameters + +- `prefix` `(string: "")`- Specifies a string to filter namespaces on based on + an index prefix. This is specified as a querystring parameter. + +### Sample Request + +```text +$ curl \ + https://nomad.rocks/v1/namespaces +``` + +```text +$ curl \ + https://nomad.rocks/v1/namespaces?prefix=prod +``` + +### Sample Response + +```json +[ + { + "CreateIndex": 31, + "Description": "Production API Servers", + "ModifyIndex": 31, + "Name": "api-prod" + }, + { + "CreateIndex": 5, + "Description": "Default shared namespace", + "ModifyIndex": 5, + "Name": "default" + } +] +``` + +## Read Namespace + +This endpoint reads information about a specific namespace. + +| Method | Path | Produces | +| ------ | --------------------------- | -------------------------- | +| `GET` | `/v1/namespace/:namespace` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | -------------------- | +| `YES` | `namespace:*`
    Any capability on the namespace authorizes the endpoint | + +### Parameters + +- `:namespace` `(string: )`- Specifies the namespace to query. + +### Sample Request + +```text +$ curl \ + https://nomad.rocks/v1/namespace/api-prod +``` + +### Sample Response + +```json +{ + "CreateIndex": 31, + "Description": "Production API Servers", + "Hash": "N8WvePwqkp6J354eLJMKyhvsFdPELAos0VuBfMoVKoU=", + "ModifyIndex": 31, + "Name": "api-prod" +} +``` + +## Create or Update Namespace + +This endpoint is used to create or update a namespace. + +| Method | Path | Produces | +| ------- | ----------------------------------------------- | -------------------------- | +| `POST` | `/v1/namespace/:namespace`
    `/v1/namespace` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | ------------ | +| `NO` | `management` | + +### Parameters + +- `Namespace` `(string: )`- Specifies the namespace to create or + update. + +- `Description` `(string: "")` - Specifies an optional human-readable + description of the namespace. + +### Sample Payload + +```javascript +{ + "Namespace": "api-prod", + "Description": "Production API Servers" +} +``` + +### Sample Request + +```text +$ curl \ + --request POST \ + --data @namespace.json \ + https://nomad.rocks/v1/namespace/api-prod +``` + +```text +$ curl \ + --request POST \ + --data @namespace.json \ + https://nomad.rocks/v1/namespace +``` + +## Delete Namespace + +This endpoint is used to delete a namespace. + +| Method | Path | Produces | +| ------- | -------------------------- | -------------------------- | +| `DELETE` | `/v1/namespace/:namespace` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | ------------ | +| `NO` | `management` | + +### Parameters + +- `:namespace` `(string: )`- Specifies the namespace to delete. + +### Sample Request + +```text +$ curl \ + --request DELETE \ + https://nomad.rocks/v1/namespace/api-prod +``` diff --git a/website/source/api/sentinel-policies.html.md b/website/source/api/sentinel-policies.html.md new file mode 100644 index 000000000..8a0314619 --- /dev/null +++ b/website/source/api/sentinel-policies.html.md @@ -0,0 +1,180 @@ +--- +layout: api +page_title: Sentinel Policies - HTTP API +sidebar_current: api-sentinel-policies +description: |- + The /sentinel/policy/ endpoints are used to configure and manage Sentinel policies. +--- + +# Sentinel Policies HTTP API + +The `/sentinel/policies` and `/sentinel/policy/` endpoints are used to manage Sentinel policies. +For more details about Sentinel policies, please see the [Sentinel Policy Guide](/guides/sentinel-policy.html). + +Sentinel endpoints are only available when ACLs are enabled. For more details about ACLs, please see the [ACL Guide](/guides/acl.html). + +~> **Enterprise Only!** This API endpoint and functionality only exists in +Nomad Enterprise. This is not present in the open source version of Nomad. + +## List Policies + +This endpoint lists all Sentinel policies. This lists the policies that have been replicated +to the region, and may lag behind the authoritative region. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/sentinel/policies` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), [consistency modes](/api/index.html#consistency-modes) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------ | +| `YES` | `all` | `management` | + + +### Sample Request + +```text +$ curl \ + https://nomad.rocks/v1/sentinel/policies +``` + +### Sample Response + +```json +[ + { + "Name": "foo", + "Description": "test policy", + "Scope": "submit-job", + "EnforcementLevel": "advisory", + "Hash": "CIs8aNX5OfFvo4D7ihWcQSexEJpHp+Za+dHSncVx5+8=", + "CreateIndex": 8, + "ModifyIndex": 8 + } +] +``` + +## Create or Update Policy + +This endpoint creates or updates an Sentinel Policy. This request is always forwarded to the +authoritative region. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `POST` | `/sentinel/policy/:policy_name` | `(empty body)` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | ------------------ | +| `NO` | `management` | + +### Parameters + +- `Name` `(string: )` - Specifies the name of the policy. + Creates the policy if the name does not exist, otherwise updates the existing policy. + +- `Description` `(string: )` - Specifies a human readable description. + +- `Scope` `(string: )` - Specifies the scope of when this policy applies. Only `submit-job` is currently supported. + +- `EnforcementLevel` `(string: )` - Specifies the enforcement level of the policy. Can be `advisory` which warns on failure, + `hard-mandatory` which prevents an operation on failure, and `soft-mandatory` which is like `hard-mandatory` but can be overridden. + +- `Policy` `(string: )` - Specifies the Sentinel policy itself. + +### Sample Payload + +```json +{ + "Name": "my-policy", + "Description": "This is a great policy", + "Scope": "submit-job", + "EnforcementLevel": "advisory", + "Policy": "main = rule { true }", +} +``` + +### Sample Request + +```text +$ curl \ + --request POST \ + --data @payload.json \ + https://nomad.rocks/v1/sentinel/policy/my-policy +``` + +## Read Policy + +This endpoint reads a Sentinel policy with the given name. This queries the policy that have been +replicated to the region, and may lag behind the authoritative region. + + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/sentinel/policy/:policy_name` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), [consistency modes](/api/index.html#consistency-modes) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------ | +| `YES` | `all` | `management` | + +### Sample Request + +```text +$ curl \ + https://nomad.rocks/v1/sentinel/policy/foo +``` + +### Sample Response + +```json +{ + "Name": "foo", + "Description": "test policy", + "Scope": "submit-job", + "EnforcementLevel": "advisory", + "Policy": "main = rule { true }\n", + "Hash": "CIs8aNX5OfFvo4D7ihWcQSexEJpHp+Za+dHSncVx5+8=", + "CreateIndex": 8, + "ModifyIndex": 8 +} +``` + +## Delete Policy + +This endpoint deletes the named Sentinel policy. This request is always forwarded to the +authoritative region. + +| Method | Path | Produces | +| -------- | ---------------------------- | -------------------------- | +| `DELETE` | `/sentinel/policy/:policy_name` | `(empty body)` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries) and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | ACL Required | +| ---------------- | ------------- | +| `NO` | `management` | + +### Parameters + +- `policy_name` `(string: )` - Specifies the policy name to delete. + +### Sample Request + +```text +$ curl \ + --request DELETE \ + https://nomad.rocks/v1/sentinel/policy/foo +``` + diff --git a/website/source/assets/images/sentinel.jpg b/website/source/assets/images/sentinel.jpg new file mode 100644 index 000000000..290678fc4 --- /dev/null +++ b/website/source/assets/images/sentinel.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c74876b7be5177a941f9e2a99c099c26957013710816cdbe8dba9c2b71d9cc26 +size 133585 diff --git a/website/source/docs/agent/configuration/index.html.md b/website/source/docs/agent/configuration/index.html.md index 854c7c395..794a822bc 100644 --- a/website/source/docs/agent/configuration/index.html.md +++ b/website/source/docs/agent/configuration/index.html.md @@ -194,6 +194,8 @@ testing. with potentially multiple zones, which map to [datacenters](#datacenter) such as `us-west` and `us-east`. +- `sentinel` ([Sentinel][sentinel]: nil) - Specifies configuration for Sentinel policies. + - `server` ([Server][server]: nil) - Specifies configuration which is specific to the Nomad server. - `syslog_facility` `(string: "LOCAL0")` - Specifies the syslog facility to write to. This has no effect unless `enable_syslog` is true. @@ -231,5 +233,6 @@ http_api_response_headers { [vault]: /docs/agent/configuration/vault.html "Nomad Agent vault Configuration" [tls]: /docs/agent/configuration/tls.html "Nomad Agent tls Configuration" [client]: /docs/agent/configuration/client.html "Nomad Agent client Configuration" +[sentinel]: /docs/agent/configuration/sentinel.html "Nomad Agent sentinel Configuration" [server]: /docs/agent/configuration/server.html "Nomad Agent server Configuration" [acl]: /docs/agent/configuration/acl.html "Nomad Agent ACL Configuration" diff --git a/website/source/docs/agent/configuration/sentinel.html.md b/website/source/docs/agent/configuration/sentinel.html.md new file mode 100644 index 000000000..05769b453 --- /dev/null +++ b/website/source/docs/agent/configuration/sentinel.html.md @@ -0,0 +1,42 @@ +--- +layout: "docs" +page_title: "sentinel Stanza - Agent Configuration" +sidebar_current: "docs-agent-configuration-sentinel" +description: |- + The "sentinel" stanza configures the Nomad agent for Sentinel policies and tune various parameters. +--- + +# `sentinel` Stanza + + + + + + +
    Placement + **sentinel** +
    + +The `sentinel` stanza configures the Sentinel policy engine and tunes various parameters. + +```hcl +sentinel { + import "custom-plugin" { + path = "/usr/bin/sentinel-custom-plugin" + args = ["-verbose", "foo"] + } +} +``` + +## `sentinel` Parameters + +- `import` ([Import](#import-parameters): nil) - + Specifies a plugin that should be made available for importing by Sentinel policies. + The name of the import matches the name that can be imported. + +### `import` Parameters + +- `path` `(string: "")` - Specifies the path to the import plugin. Must be executable by Nomad. + +- `args` `(array: [])` - Specifies arguments to pass to the plugin when starting it. + diff --git a/website/source/docs/commands/namespace.html.md.erb b/website/source/docs/commands/namespace.html.md.erb new file mode 100644 index 000000000..4680f76a6 --- /dev/null +++ b/website/source/docs/commands/namespace.html.md.erb @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "Commands: namespace" +sidebar_current: "docs-commands-namespace" +description: > + The namespace command is used to interact with namespaces. +--- + +# Nomad Namespace + +Command: `nomad namespace` + +The `namespace` command is used to interact with namespaces. + +## Usage + +Usage: `nomad namespace [options]` + +Run `nomad namespace -h` for help on that subcommand. The following +subcommands are available: + +* [`namespace apply`][apply] - Create or update a namespace +* [`namespace delete`][delete] - Delete a namespace +* [`namespace list`][list] - List available namespaces + +[apply]: /docs/commands/namespace/apply.html "Create or update a namespace" +[delete]: /docs/commands/namespace/delete.html "Delete a namespace" +[list]: /docs/commands/namespace/list.html "List available namespaces" diff --git a/website/source/docs/commands/namespace/apply.html.md.erb b/website/source/docs/commands/namespace/apply.html.md.erb new file mode 100644 index 000000000..073e900c8 --- /dev/null +++ b/website/source/docs/commands/namespace/apply.html.md.erb @@ -0,0 +1,40 @@ +--- +layout: "docs" +page_title: "Commands: namespace apply" +sidebar_current: "docs-commands-namespace-apply" +description: > + The namespace apply command is used create or update a namespace. +--- + +# Command: namespace apply + +The `namespace apply` command is used create or update a namespace. + +~> Namespace commands are new in Nomad 0.7 and are only available with Nomad +Enterprise. + +## Usage + +``` +nomad namespace apply [options] +``` + +The `namespace apply` command requires the name of the namespace to be created +or updated. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Apply Options + +* `-description` : An optional human readable description for the namespace. + +## Examples + +Create a namespace + +``` +$ nomad namespace apply -description "Prod API servers" api-prod +Successfully applied namespace "api-prod"! +``` diff --git a/website/source/docs/commands/namespace/delete.html.md.erb b/website/source/docs/commands/namespace/delete.html.md.erb new file mode 100644 index 000000000..8a16681f7 --- /dev/null +++ b/website/source/docs/commands/namespace/delete.html.md.erb @@ -0,0 +1,35 @@ +--- +layout: "docs" +page_title: "Commands: namespace delete" +sidebar_current: "docs-commands-namespace-delete" +description: > + The namespace delete command is used to delete a namespace. +--- + +# Command: namespace delete + +The `namespace delete` command is used delete a namespace. + +~> Namespace commands are new in Nomad 0.7 and are only available with Nomad +Enterprise. + +## Usage + +``` +nomad namespace delete [options] +``` + +The `namespace delete` command requires the name of the namespace to be deleted. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Examples + +Delete a namespace + +``` +$ nomad namespace delete api-prod +Successfully deleted namespace "api-prod"! +``` diff --git a/website/source/docs/commands/namespace/list.html.md.erb b/website/source/docs/commands/namespace/list.html.md.erb new file mode 100644 index 000000000..e5dc1d7ee --- /dev/null +++ b/website/source/docs/commands/namespace/list.html.md.erb @@ -0,0 +1,46 @@ +--- +layout: "docs" +page_title: "Commands: namespace list" +sidebar_current: "docs-commands-namespace-list" +description: > + The namespace list command is used to list namespaces. +--- + +# Command: namespace list + +The `namespace list` command is used list available namespaces. + +~> Namespace commands are new in Nomad 0.7 and are only available with Nomad +Enterprise. + +## Usage + +``` +nomad namespace list [options] +``` + +The `namespace list` command requires no arguments. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## List Options + +* `-json` : Output the namespaces in their JSON format. + +* `-t` : Format and display the namespaces using a Go template. + +## Examples + +List all namespaces: + +``` +$ nomad namespace list +Name Description +default Default shared namespace +api-prod Production instances of backend API servers +api-qa QA instances of backend API servers +web-prod Production instances of webservers +web-qa QA instances of webservers +``` diff --git a/website/source/docs/commands/plan.html.md.erb b/website/source/docs/commands/plan.html.md.erb index e5105a964..686aa57c8 100644 --- a/website/source/docs/commands/plan.html.md.erb +++ b/website/source/docs/commands/plan.html.md.erb @@ -60,6 +60,8 @@ Plan will return one of the following exit codes: * `-diff`: Determines whether the diff between the remote job and planned job is shown. Defaults to true. +* `-policy-override`: Sets the flag to force override any soft mandatory Sentinel policies. + * `-verbose`: Increase diff verbosity. ## Examples diff --git a/website/source/docs/commands/run.html.md.erb b/website/source/docs/commands/run.html.md.erb index 41d17a3a4..2ea69810e 100644 --- a/website/source/docs/commands/run.html.md.erb +++ b/website/source/docs/commands/run.html.md.erb @@ -60,14 +60,16 @@ precedence, going from highest to lowest: the `-vault-token` flag, the will be output, which can be used to examine the evaluation using the [eval-status](/docs/commands/eval-status.html) command +* `-output`: Output the JSON that would be submitted to the HTTP API without + submitting the job. + +* `-policy-override`: Sets the flag to force override any soft mandatory Sentinel policies. + * `-vault-token`: If set, the passed Vault token is stored in the job before sending to the Nomad servers. This allows passing the Vault token without storing it in the job file. This overrides the token found in $VAULT_TOKEN environment variable and that found in the job. -* `-output`: Output the JSON that would be submitted to the HTTP API without - submitting the job. - * `-verbose`: Show full information. ## Examples diff --git a/website/source/docs/commands/sentinel.html.md.erb b/website/source/docs/commands/sentinel.html.md.erb new file mode 100644 index 000000000..92bcf621f --- /dev/null +++ b/website/source/docs/commands/sentinel.html.md.erb @@ -0,0 +1,30 @@ +--- +layout: "docs" +page_title: "Commands: sentinel" +sidebar_current: "docs-commands-sentinel" +description: > + The sentinel command is used to interact with Sentinel policies. +--- + +# Nomad Sentinel + +Command: `nomad sentinel` + +The `sentinel` command is used to interact with Sentinel policies. + +## Usage + +Usage: `nomad sentinel [options]` + +Run `nomad sentinel -h` for help on that subcommand. The following +subcommands are available: + +* [`sentinel apply`][apply] - Create a new or update existing Sentinel policies +* [`sentinel delete`][delete] - Delete an existing Sentinel policies +* [`sentinel list`][list] - Display all Sentinel policies +* [`sentinel read`][read] - Inspects an existing Sentinel policies + +[delete]: /docs/commands/sentinel/delete.html +[list]: /docs/commands/sentinel/list.html +[read]: /docs/commands/sentinel/read.html +[apply]: /docs/commands/sentinel/apply.html diff --git a/website/source/docs/commands/sentinel/apply.html.md.erb b/website/source/docs/commands/sentinel/apply.html.md.erb new file mode 100644 index 000000000..7743e433b --- /dev/null +++ b/website/source/docs/commands/sentinel/apply.html.md.erb @@ -0,0 +1,42 @@ +--- +layout: "docs" +page_title: "Commands: sentinel apply" +sidebar_current: "docs-commands-sentinel-apply" +description: > + The sentinel apply command is used to write a new, or update an existing, Sentinel policy. +--- + +# Command: sentinel apply + +The `sentinel apply` command is used to write a new, or update an existing, Sentinel policy. + +## Usage + +``` +nomad sentinel apply [options] +``` + +The `sentinel apply` command requires two arguments, the policy name and the policy file. +The policy file can be read from stdin by specifying "-" as the file name. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Apply Options + +* `-description` : Sets a human readable description for the policy + +* `-scope` : (default: submit-job) Sets the scope of the policy and when it should be enforced. + +* `-level` : (default: advisory) Sets the enforcment level of the policy. Must be one of advisory, + soft-mandatory, hard-mandatory. + +## Examples + +Write a policy: + +``` +$ nomad sentinel write -description "My test policy" foo test.sentinel +Successfully wrote "foo" Sentinel policy! +``` diff --git a/website/source/docs/commands/sentinel/delete.html.md.erb b/website/source/docs/commands/sentinel/delete.html.md.erb new file mode 100644 index 000000000..62fa2b0e1 --- /dev/null +++ b/website/source/docs/commands/sentinel/delete.html.md.erb @@ -0,0 +1,32 @@ +--- +layout: "docs" +page_title: "Commands: sentinel delete" +sidebar_current: "docs-commands-sentinel-delete" +description: > + The sentinel delete command is used to delete a Sentinel policy. +--- + +# Command: sentinel delete + +The `sentinel delete` command is used to delete a Sentinel policy. + +## Usage + +``` +nomad sentinel delete [options] +``` + +The `sentinel delete` command requires a single argument, the policy name. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Examples + +Delete a policy: + +``` +$ nomad sentinel delete foo +Successfully deleted "foo" Sentinel policy! +``` diff --git a/website/source/docs/commands/sentinel/list.html.md.erb b/website/source/docs/commands/sentinel/list.html.md.erb new file mode 100644 index 000000000..d8a3a8679 --- /dev/null +++ b/website/source/docs/commands/sentinel/list.html.md.erb @@ -0,0 +1,33 @@ +--- +layout: "docs" +page_title: "Commands: sentinel list" +sidebar_current: "docs-commands-sentinel-list" +description: > + The sentinel list command is used to list all installed Sentinel policies. +--- + +# Command: sentinel list + +The `sentinel list` command is used to display all the installed Sentinel policies. + +## Usage + +``` +nomad sentinel list [options] +``` + +The `sentinel list` command requires no arguments. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Examples + +List all policies: + +``` +$ nomad sentinel list +Name Scope Enforcement Level Description +foo submit-job advisory my test policy +``` diff --git a/website/source/docs/commands/sentinel/read.html.md.erb b/website/source/docs/commands/sentinel/read.html.md.erb new file mode 100644 index 000000000..76644251a --- /dev/null +++ b/website/source/docs/commands/sentinel/read.html.md.erb @@ -0,0 +1,43 @@ +--- +layout: "docs" +page_title: "Commands: sentinel read" +sidebar_current: "docs-commands-sentinel-read" +description: > + The sentinel read command is used to inspect a Sentinel policies. +--- + +# Command: sentinel read + +The `sentinel read` command is used to inspect a Sentinel policy. + +## Usage + +``` +nomad sentinel read [options] +``` + +The `sentinel read` command requires a single argument, the policy name. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Read Options + +* `-raw` : Output the raw policy only. + +## Examples + +Read all policies: + +``` +$ nomad sentinel read foo +Name = foo +Scope = submit-job +Enforcement Level = advisory +Description = my test policy +Policy: + +main = rule { true } + +``` diff --git a/website/source/docs/enterprise/index.html.md b/website/source/docs/enterprise/index.html.md new file mode 100644 index 000000000..7b6ad5fa8 --- /dev/null +++ b/website/source/docs/enterprise/index.html.md @@ -0,0 +1,20 @@ +--- +layout: "docs" +page_title: "Nomad Enterprise" +sidebar_current: "docs-enterprise" +description: |- + Nomad Enterprise features a number of capabilities beyond the open source + offering that may be beneficial in certain workflows. +--- + +# Nomad Enterprise + +Nomad Enterprise simplifies operations by automating workflows. It adds support +for microservices deployments across complex network topologies. It also +increases both scalability and resilience. Features include: + +- [Namespaces](/docs/enterprise/namespaces/index.html) +- [Namespace Quotas](/docs/enterprise/quotas/index.html) +- [Sentinel Policy Enforcement](/docs/enterprise/sentinel/index.html) + +These features are part of [Nomad Enterprise](https://www.hashicorp.com/products/nomad/). diff --git a/website/source/docs/enterprise/namespaces/index.html.md b/website/source/docs/enterprise/namespaces/index.html.md new file mode 100644 index 000000000..e8c0dd603 --- /dev/null +++ b/website/source/docs/enterprise/namespaces/index.html.md @@ -0,0 +1,22 @@ +--- +layout: "docs" +page_title: "Nomad Enterprise Namespaces" +sidebar_current: "docs-enterprise-namespaces" +description: |- + Nomad Enterprise provides support for namespaces, which allows jobs and their + associated objects to be segmented from each other and other users of the + cluster. +--- + +# Nomad Enterprise Namespaces + +In [Nomad Enterprise](https://www.hashicorp.com/products/nomad/), a shared +cluster can be partioned into [namespaces](/guides/namespaces.html) which allows +jobs and their associated objects to be isolated from each other and other users +of the cluster. + +Namespaces enhance the usability of a shared cluster by isolating teams from the +jobs of others, provide fine grain access control to jobs when coupled with +[ACLs](/guides/acl.html), and can prevent bad actors from negatively impacting +the whole cluster when used in conjunction with +[quotas](/docs/enterprise/quotas/index.html). diff --git a/website/source/docs/enterprise/quotas/index.html.md b/website/source/docs/enterprise/quotas/index.html.md new file mode 100644 index 000000000..25289e14c --- /dev/null +++ b/website/source/docs/enterprise/quotas/index.html.md @@ -0,0 +1,19 @@ +--- +layout: "docs" +page_title: "Nomad Enterprise Namespace Quotas" +sidebar_current: "docs-enterprise-quotas" +description: |- + Nomad Enterprise provides support for applying quotas to namespaces which + restricts the overall resources that jobs within the namespace are allowed to + consume. +--- + +# Nomad Enterprise Namespace Quotas + +In [Nomad Enterprise](https://www.hashicorp.com/products/nomad/), operators can +define quota specifications and apply them to namespaces. When a quota is +attached to a namespace, the jobs within the namespace may not consume more +resources than the quota specification allows. + +This allows operators to partition a shared cluster and ensure that no single +actor can consume the whole resources of the cluster. diff --git a/website/source/docs/enterprise/sentinel/index.html.md b/website/source/docs/enterprise/sentinel/index.html.md new file mode 100644 index 000000000..b8a1349df --- /dev/null +++ b/website/source/docs/enterprise/sentinel/index.html.md @@ -0,0 +1,31 @@ +--- +layout: "docs" +page_title: "Nomad Enterprise Sentinel Policy Enforcement" +sidebar_current: "docs-enterprise-sentinel" +description: |- + Nomad Enterprise provides support for policy enforcement using Sentinel. +--- + +# Nomad Enterprise Sentinel Policy Enforcement + +In [Nomad Enterprise](https://www.hashicorp.com/products/nomad/), operators can +create [Sentinel policies](/guides/sentinel-policy.html) for fine grain policy +enforcement. Sentinel policies build on top of the ACL system and allow operators to define +fine grain policies such as disallowing jobs to be submitted to production on +Fridays. These extremely rich policies are defined as code. For example, to +restrict jobs to only using the Docker driver, the operator would define and apply +the following policy: + +``` +# Only allows Docker based tasks +main = rule { all_drivers_docker } + +# all_drivers_docker checks that all the drivers in use are Docker +all_drivers_docker = rule { + all job.task_groups as tg { + all tg.tasks as task { + task.driver is "docker" + } + } +} +``` diff --git a/website/source/docs/job-specification/job.html.md b/website/source/docs/job-specification/job.html.md index f1a4f23ee..47cffdd6f 100644 --- a/website/source/docs/job-specification/job.html.md +++ b/website/source/docs/job-specification/job.html.md @@ -64,10 +64,6 @@ job "docs" { ## `job` Parameters -- `all_at_once` `(bool: false)` - Controls if the entire set of tasks in the job - must be placed atomically or if they can be scheduled incrementally. This - should only be used for special circumstances. - - `all_at_once` `(bool: false)` - Controls whether the scheduler can make partial placements if optimistic scheduling resulted in an oversubscribed node. This does not control whether all allocations for the job, where all @@ -89,6 +85,9 @@ job "docs" { - `meta` ([Meta][]: nil) - Specifies a key-value map that annotates with user-defined metadata. +- `namespace` `(string: "default")` - The namespace in which to execute the job. + Values other than default are not allowed in non-Enterprise versions of Nomad. + - `parameterized` ([Parameterized][parameterized]: nil) - Specifies the job as a parameterized job such that it can be dispatched against. diff --git a/website/source/downloads.html.erb b/website/source/downloads.html.erb index 4156f48ae..cf235899b 100644 --- a/website/source/downloads.html.erb +++ b/website/source/downloads.html.erb @@ -31,6 +31,10 @@ description: |-

    Check out the v<%= latest_version %> CHANGELOG for information on the latest release.

    +

    Nomad 0.7 Beta

    +

    + A beta release of Nomad 0.7 is also available! The beta can be downloaded on the Nomad releases page. +

    diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index ca6759a5e..7f5f94597 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -225,12 +225,13 @@ Namespace rules are keyed by the namespace name they apply to. When no namespace * `submit-job` - Allows jobs to be submitted or modified. * `read-logs` - Allows the logs associated with a job to be viewed. * `read-fs` - Allows the filesystem of allocations associated to be viewed. +* `sentinel-override` - Allows soft mandatory policies to be overriden. The coarse grained policy dispositions are shorthand for the fine grained capabilities: * `deny` policy - ["deny"] -* `read` policy - ["list-jobs", "read-jobs"] -* `write` policy - ["list-jobs", "read-jobs", "submit-job", "read-logs", "read-fs"] +* `read` policy - ["list-jobs", "read-job"] +* `write` policy - ["list-jobs", "read-job", "submit-job", "read-logs", "read-fs"] When both the policy short hand and a capabilities list are provided, the capabilities are merged: diff --git a/website/source/guides/namespaces.html.markdown b/website/source/guides/namespaces.html.markdown new file mode 100644 index 000000000..c4677ac3a --- /dev/null +++ b/website/source/guides/namespaces.html.markdown @@ -0,0 +1,121 @@ +--- +layout: "guides" +page_title: "Namespaces" +sidebar_current: "guides-namespaces" +description: |- + Nomad Enterprise provides support for namespaces, which allows jobs and their + associated objects to be segmented from each other and other users of the + cluster. +--- + +# Namespaces + +Nomad Enterprise has support for namespaces, which allows jobs and their +associated objects to be segmented from each other and other users of the +cluster. + +~> **Enterprise Only!** This functionality only exists in Nomad Enterprise. +This is not present in the open source version of Nomad. + +## Use Case + +Namespaces allow a single cluster to be shared by many teams and projects +without conflict. Nomad requires job IDs to be unique within namespaces but not +across namespaces. This allows each team to operate independently of others. + +When combined with ACLs, the isolation of namespaces can be enforced, only +allowing designated users access to read or modify the jobs and associated +objects in a namespace. + +When quotas are applied to a namespace they provide a means to limit resource +consumption by the jobs in the namespace. This can prevent a single actor from +consuming excessive cluster resources and negatively impacting other teams and +applications sharing the cluster. + +## Namespaced Objects + +Nomad places all jobs and their derived objects into namespaces. These include +jobs, allocations, deployments, and evaluations. + +Nomad does not namespace objects that are shared across multiple namespaces. +This includes nodes, [ACL policies](/guides/acl.html), [Sentinel +policies](/guides/sentinel-policy.html), and quota specifications. + +## Working with Namespaces + +For specific details about working with namespaces, see the [namespace +commands](/docs/commands/namespace.html) and [HTTP API](/api/namespaces.html) +documentation. + +### Creating and viewing namespaces: + +Namespaces can be interacted with using the `nomad namespace` subcommand. The +following creates and lists the namespaces of a cluster: + +``` +$ nomad namespace apply -description "QA instances of webservers" web-qa +Successfully applied namespace "web-qa"! + +$ nomad namespace list +Name Description +default Default shared namespace +api-prod Production instances of backend API servers +api-qa QA instances of backend API servers +web-prod Production instances of webservers +web-qa QA instances of webservers +``` + +### Running jobs + +To run a job in a specific namespace, we annotate the job with the `namespace` +parameter. If ommitted, the job will be run in the `default` namespace. Below is +an example of running the job in the newly created `web-qa` namespace: + +``` +job "rails-www" { + + # Run in the QA environmet + namespace = "web-qa" + + # Only run in one datacenter when QAing + datacenters = ["us-west1"] + ... +} +``` + +### Specifying desired namespace + +When using commands that operate on objects that are namespaced, the namespace +can be specified either with the flag `-namespace` or read from the +`NOMAD_NAMESPACE` environment variable: + +``` +$ nomad job status -namespace=web-qa +ID Type Priority Status Submit Date +rails-www service 50 running 09/17/17 19:17:46 UTC + +$ export NOMAD_NAMESPACE=web-qa + +$ nomad job status +ID Type Priority Status Submit Date +rails-www service 50 running 09/17/17 19:17:46 UTC +``` + +### ACLs + +Access to namespaces can be restricted using [ACLs](/guides/acl.html). As an +example we could create an ACL policy that allows full access to the QA +environment for our web namespaces but restrict the production access by +creating the following policy: + +``` +# Allow read only access to the production namespace +namespace "web-prod" { + policy = "read" +} + +# Allow writing to the QA namespace +namespace "web-qa" { + policy = "write" +} +``` diff --git a/website/source/guides/sentinel-policy.html.markdown b/website/source/guides/sentinel-policy.html.markdown new file mode 100644 index 000000000..1e81bca80 --- /dev/null +++ b/website/source/guides/sentinel-policy.html.markdown @@ -0,0 +1,207 @@ +--- +layout: "guides" +page_title: "Sentinel Policies" +sidebar_current: "guides-sentinel" +description: |- + Nomad integrates with Sentinel for fine grained policy enforcement. Sentinel allows operators to express their policies as code, and have their policies automatically enforced. This allows operators to define a "sandbox" and restrict actions to only those compliant with policy. The Sentinel integration builds on the ACL System. +--- + +# Sentinel Policies + +Nomad integrates with Sentinel for fine grained policy enforcement. Sentinel allows operators to express their policies as code, and have their policies automatically enforced. This allows operators to define a "sandbox" and restrict actions to only those compliant with policy. The Sentinel integration builds on the [ACL System](/guides/acl.html). + +~> **Enterprise Only!** This functionality only exists in Nomad Enterprise. +This is not present in the open source version of Nomad. + +# Sentinel Overview + +Sentinel integrates with the ACL system, and provides the ability to do fine grained policy enforcment. Users must have appropriate permissions to perform an action, and then are subject to any applicable Sentinel policies: + +![Sentinel Overview](/assets/images/sentinel.jpg) + + * **Sentinel Policies**. Policies are able to introspect on request arguments and use complex logic to determine if the request meets policy requirements. For example, a Sentinel policy may restrict Nomad jobs to only using the "docker" driver, or prevent jobs from being modified outside of business hours. + + * **Policy Scope**. Sentinel policies declare a "scope", which determines when the policies apply. Currently the only supported scope is "submit-job", which applies to any new jobs being submitted, or existing jobs being updated. + + * **Enforcement Level**. Sentinel policies support multiple enforcement levels. The `advisory` level emits a warning when the policy fails, while `soft-mandatory` and `hard-mandatory` will prevent the operation. A `soft-mandatory` policy can be overriden if the user has necessary permissions. + +### Sentinel Policies + +Each Sentinel policy has a unique name, an optional description, applicable scope, enforcement level, and a Sentinel rule definition. +If multiple policies are installed for the same scope, all of them are enforced and must pass. + +Sentinel policies _cannot_ be used unless the ACL system is enabled. + +### Policy Scope + +Sentinel policies specify an applicable scope, which limits when the policy is enforced. This allows policies to govern various aspects of the system. + +The following table summarizes the scopes that are available for Sentinel policies: + +| Scope | Description | +| ---------- | ----------------------------------------------------- | +| submit-job | Applies to any jobs (new or updated) being registered | + + +### Enforcement Level + +Sentinel policies specify an enforcement level which changes how a policy is enforced. This allows for more flexibility in policy enforcement. + +The following table summarizes the enforcement levels that are available: + +| Enforcement Level | Description | +| ----------------- | ---------------------------------------------------------------------- | +| advisory | Issues a warning when a policy fails | +| soft-mandatory | Prevents operation when a policy fails, issues a warning if overridden | +| hard-mandatory | Prevents operation when a policy fails | + +The [`sentinel-override` capability](/guides/acl.html#sentinel-override) is required to override a `soft-mandatory` policy. This allows a restricted set of users to have override capability when necessary. + +## Multi-Region Configuration + +Nomad supports multi-datacenter and multi-region configurations. A single region is able to service multiple datacenters, and all servers in a region replicate their state between each other. In a multi-region configuration, there is a set of servers per region. Each region operates independently and is loosely coupled to allow jobs to be scheduled in any region and requests to flow transparently to the correct region. + +When ACLs are enabled, Nomad depends on an "authoritative region" to act as a single source of truth for ACL policies, global ACL tokens, and Sentinel policies. The authoritative region is configured in the [`server` stanza](/docs/agent/configuration/server.html) of agents, and all regions must share a single a single authoritative source. Any Sentinel policies are created in the authoritative region first. All other regions replicate Sentinel policies, ACL policies, and global ACL tokens to act as local mirrors. This allows policies to be administered centrally, and for enforcement to be local to each region for low latency. + +## Configuring Sentinel Policies + +Sentinel policies are tied to the ACL system, which is not enabled by default. +See the [ACL guide](/guides/acl.html) for details on how to configure ACLs. + +## Example: Installing Sentinel Policies + +This example shows how to install a Sentinel policy. It assumes that ACLs have already +been bootstrapped (see the [ACL guide](/guides/acl.html)), and that a `NOMAD_TOKEN` environment variable +is set to a management token. + +First, create a Sentinel policy, named `test.sentinel`: + +``` +# Test policy always fails for demonstration purposes +main = rule { false } +``` + +Then, install this as an `advisory` policy which issues a warning on failure: + +``` +$ nomad sentinel apply -level=advisory test-policy test.sentinel +Successfully wrote "test-policy" Sentinel policy! +``` + +Use `nomad init` to create a job file and attempt to submit it: + +``` +$ nomad init +Example job file written to example.nomad + +$ nomad run example.nomad +Job Warnings: +1 warning(s): + +* test-policy : Result: false (allowed failure based on level) + +FALSE - test-policy:2:1 - Rule "main" + + +==> Monitoring evaluation "f43ac28d" + Evaluation triggered by job "example" + Evaluation within deployment: "11e01124" + Allocation "2618f3b4" created: node "add8ce93", group "cache" + Allocation "5c2674f2" created: node "add8ce93", group "cache" + Allocation "9937811f" created: node "add8ce93", group "cache" + Evaluation status changed: "pending" -> "complete" +==> Evaluation "f43ac28d" finished with status "complete" +``` + +We can see our policy failed, but the job was accepted because of an `advisory` enforcement level. + +Next, let's change `test.sentinel` to only allow "exec" based drivers: + +``` +# Test policy only allows exec based tasks +main = rule { all_drivers_exec } + +# all_drivers_exec checks that all the drivers in use are exec +all_drivers_exec = rule { + all job.task_groups as tg { + all tg.tasks as task { + task.driver is "exec" + } + } +} +``` + +Then install the updated policy at a soft mandatory level: + +``` +$ nomad sentinel apply -level=soft-mandatory test-policy test.sentinel +Successfully wrote "test-policy" Sentinel policy! +``` + +With our new policy, attempt to submit the same job, which uses the "docker" driver: + +``` +$ nomad run example.nomad +Error submitting job: Unexpected response code: 500 (1 error(s) occurred: + +* test-policy : Result: false + +FALSE - test-policy:2:1 - Rule "main" + FALSE - test-policy:6:5 - all job.task_groups as tg { + all tg.tasks as task { + task.driver is "exec" + } +} + +FALSE - test-policy:5:1 - Rule "all_drivers_exec" +) +``` + +Because our policy is failing, the job was rejected. Since this is a `soft-mandatory` policy, +submit with the `-policy-override` flag set: + +``` +$ nomad run -policy-override example.nomad +Job Warnings: +1 warning(s): + +* test-policy : Result: false (allowed failure based on level) + +FALSE - test-policy:2:1 - Rule "main" + FALSE - test-policy:6:5 - all job.task_groups as tg { + all tg.tasks as task { + task.driver is "exec" + } +} + +FALSE - test-policy:5:1 - Rule "all_drivers_exec" + + +==> Monitoring evaluation "16195b50" + Evaluation triggered by job "example" + Evaluation within deployment: "11e01124" + Evaluation status changed: "pending" -> "complete" +==> Evaluation "16195b50" finished with status "complete" +``` + +This time, the job was accepted but with a warning that our policy is failing but was overriden. + +# Policy Specification + +Sentinel policies are specified in the [Sentinel Language](#). The language is designed to be easy to read and write, +while being fast to evaluate. There is no limitation on how complex policies can be, but they are in the execution path so +care should be taken to avoid adversely impacting performance. + +In each scope, there are different objects made available for introspection, such a job being submitted. Policies can +inspect these objects to apply fine-grained policies. + +### Scope `submit-job` + +The following objects are made available in the `submit-job` scope: + +| Object | Description | +| ------ | ------------------------- | +| `job` | The job being submitted | + +See the [Sentinel Job Object](/guides/sentinel/job.html) for details on the fields that are available. + diff --git a/website/source/guides/sentinel/job.html.md b/website/source/guides/sentinel/job.html.md new file mode 100644 index 000000000..440440cae --- /dev/null +++ b/website/source/guides/sentinel/job.html.md @@ -0,0 +1,23 @@ +--- +layout: "guides" +page_title: "Sentinel Job Object" +sidebar_current: "guides-sentinel-job" +description: |- + Job objects can be introspected to apply fine grained Sentinel policies. +--- + +# Sentinel Job Objects + +The `job` object is made available to policies in the `submit-job` scope automatically, without an explicit import. +The object maps to the [JSON Specification of jobs](/api/json-jobs.html), but fields differ slightly for better readability. + +Sentinel convention for identifiers is lower case and seperated by underscores. All fields on the job are accessed by the same name, converted to lower case and seperating camal case to underscores. Here are some examples: + +| Job Field | Sentinel Accessor | +| --------------------------------------- | ---------------------- | +| `job.ID ` | `job.id` | +| `job.AllAtOnce` | `job.all_at_once` | +| `job.ParentID` | `job.parent_id` | +| `job.TaskGroups` | `job.task_groups` | +| `job.TaskGroups[0].EphemeralDisk.SizeMB`| `job.task_groups[0].ephemeral_disk.size_mb` | + diff --git a/website/source/layouts/_sidebar.erb b/website/source/layouts/_sidebar.erb index bee68ba1e..da3237669 100644 --- a/website/source/layouts/_sidebar.erb +++ b/website/source/layouts/_sidebar.erb @@ -11,6 +11,7 @@
  • Docs
  • API
  • Community
  • +
  • Enterprise
  • Security
  • Press Kit
  • diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index a19e0c511..c311a92e6 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -47,6 +47,10 @@ Jobs + > + Namespaces + + > Nodes @@ -67,6 +71,10 @@ Search + > + Sentinel Policies + + > Status diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 39b98d366..2df48c082 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -263,6 +263,20 @@ > logs + > + namespace + + > node-drain @@ -286,6 +300,23 @@ > run + > + sentinel + + > server-force-leave @@ -326,6 +357,9 @@
  • > consul
  • +
  • > + sentinel +
  • > server
  • @@ -387,6 +421,24 @@ > FAQ + +
    + + > + Nomad Enterprise + + + <% end %> diff --git a/website/source/layouts/guides.erb b/website/source/layouts/guides.erb index c9fc3e5a9..8594f81af 100644 --- a/website/source/layouts/guides.erb +++ b/website/source/layouts/guides.erb @@ -54,14 +54,27 @@ - > - Securing Nomad + > + Namespaces > Outage Recovery + > + Securing Nomad + + + > + Sentinel Policies + + + > Nomad UI diff --git a/website/source/layouts/layout.erb b/website/source/layouts/layout.erb index ffffd6747..6318cd065 100644 --- a/website/source/layouts/layout.erb +++ b/website/source/layouts/layout.erb @@ -85,6 +85,7 @@
  • Docs
  • API
  • Community
  • +
  • Enterprise
  • <%= inline_svg "download.svg" %> Download @@ -117,6 +118,7 @@
  • Docs
  • API
  • Community
  • +
  • Enterprise
  • Security
  • Press Kit