Explorar o código

Merge branch 'master' of https://github.com/porter-dev/porter

jusrhee %!s(int64=5) %!d(string=hai) anos
pai
achega
bd7aaea64c
Modificáronse 40 ficheiros con 2019 adicións e 219 borrados
  1. 2 0
      .darwin.goreleaser.yml
  2. 3 1
      .gitignore
  3. 2 0
      .goreleaser.yml
  4. 1 1
      README.md
  5. 29 0
      cli/cmd/api/project.go
  6. 61 0
      cli/cmd/api/registry.go
  7. 105 6
      cli/cmd/cluster.go
  8. 21 0
      cli/cmd/config.go
  9. 12 6
      cli/cmd/connect.go
  10. 28 25
      cli/cmd/connect/kubeconfig.go
  11. 3 37
      cli/cmd/project.go
  12. 123 10
      cli/cmd/registry.go
  13. 6 5
      cli/cmd/server.go
  14. 23 0
      cli/cmd/version.go
  15. 264 109
      dashboard/package-lock.json
  16. 2 0
      dashboard/package.json
  17. 1 0
      dashboard/src/components/values-form/ValuesForm.tsx
  18. 4 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  19. 1 0
      dashboard/src/main/home/templates/Templates.tsx
  20. 63 7
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  21. 2 0
      dashboard/src/shared/api.tsx
  22. 21 0
      internal/forms/cluster.go
  23. 22 0
      internal/forms/registry.go
  24. 1 0
      internal/forms/release.go
  25. 15 5
      internal/helm/agent.go
  26. 1 0
      internal/repository/cluster.go
  27. 11 0
      internal/repository/gorm/cluster.go
  28. 42 0
      internal/repository/gorm/cluster_test.go
  29. 11 0
      internal/repository/gorm/registry.go
  30. 39 0
      internal/repository/gorm/registry_test.go
  31. 1 0
      internal/repository/registry.go
  32. 18 0
      internal/repository/test/cluster.go
  33. 18 0
      internal/repository/test/registry.go
  34. 854 0
      package-lock.json
  35. 58 0
      server/api/cluster_handler.go
  36. 24 0
      server/api/cluster_handler_test.go
  37. 17 3
      server/api/deploy_handler.go
  38. 58 0
      server/api/registry_handler.go
  39. 24 0
      server/api/registry_handler_test.go
  40. 28 0
      server/router/router.go

+ 2 - 0
.darwin.goreleaser.yml

@@ -4,6 +4,8 @@ before:
 builds:
 builds:
   - id: "porter-cli"
   - id: "porter-cli"
     binary: porter
     binary: porter
+    ldflags:
+    - -X 'github.com/porter-dev/porter/cli/cmd.Version={{.Version}}'
     env:
     env:
       - CGO_ENABLED=1
       - CGO_ENABLED=1
     dir: cli
     dir: cli

+ 3 - 1
.gitignore

@@ -5,5 +5,7 @@ app
 *.db
 *.db
 test.yaml
 test.yaml
 dist
 dist
+gon.hcl
+internal/local_templates
 gon*.hcl
 gon*.hcl
-*prod.Dockerfile
+*prod.Dockerfile

+ 2 - 0
.goreleaser.yml

@@ -3,6 +3,8 @@ before:
     - go mod download
     - go mod download
 builds:
 builds:
   - id: "porter-cli"
   - id: "porter-cli"
+    ldflags:
+    - -X 'github.com/porter-dev/porter/cli/cmd.Version={{.Version}}'
     binary: porter
     binary: porter
     dir: cli
     dir: cli
     main: ./main.go
     main: ./main.go

+ 1 - 1
README.md

@@ -4,7 +4,7 @@ Porter is a **dashboard for Helm** with support for the following features:
 - In-depth view of releases, including revision histories and component graphs
 - In-depth view of releases, including revision histories and component graphs
 - Rollback/update of existing releases, including editing of `values.yaml`
 - Rollback/update of existing releases, including editing of `values.yaml`
 
 
-![Graph View](https://user-images.githubusercontent.com/65516095/96605367-221abe00-12c4-11eb-8915-25e70fe7929a.png)
+![Graph View](https://user-images.githubusercontent.com/22849518/101073320-43322800-356d-11eb-9b69-a68bd951992e.png)
 **What's next for Porter?** View our [roadmap](https://github.com/porter-dev/porter/projects/1), or read our [mission statement](#mission-statement). 
 **What's next for Porter?** View our [roadmap](https://github.com/porter-dev/porter/projects/1), or read our [mission statement](#mission-statement). 
 
 
 ## Quick Start
 ## Quick Start

+ 29 - 0
cli/cmd/api/project.go

@@ -272,6 +272,35 @@ func (c *Client) CreateProjectCluster(
 	return bodyResp, nil
 	return bodyResp, nil
 }
 }
 
 
+// DeleteProjectCluster deletes a cluster given a project id and cluster id
+func (c *Client) DeleteProjectCluster(
+	ctx context.Context,
+	projectID uint,
+	clusterID uint,
+) error {
+	req, err := http.NewRequest(
+		"DELETE",
+		fmt.Sprintf("%s/projects/%d/clusters/%d", c.BaseURL, projectID, clusterID),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 // DeleteProjectResponse is the object returned after project deletion
 // DeleteProjectResponse is the object returned after project deletion
 type DeleteProjectResponse models.ProjectExternal
 type DeleteProjectResponse models.ProjectExternal
 
 

+ 61 - 0
cli/cmd/api/registry.go

@@ -104,6 +104,67 @@ func (c *Client) CreateGCR(
 	return bodyResp, nil
 	return bodyResp, nil
 }
 }
 
 
+// ListRegistryResponse is the list of registries for a project
+type ListRegistryResponse []models.RegistryExternal
+
+// ListRegistries returns a list of registries for a project
+func (c *Client) ListRegistries(
+	ctx context.Context,
+	projectID uint,
+) (ListRegistryResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListRegistryResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return *bodyResp, nil
+}
+
+// DeleteProjectRegistry deletes a registry given a project id and registry id
+func (c *Client) DeleteProjectRegistry(
+	ctx context.Context,
+	projectID uint,
+	registryID uint,
+) error {
+	req, err := http.NewRequest(
+		"DELETE",
+		fmt.Sprintf("%s/projects/%d/registries/%d", c.BaseURL, projectID, registryID),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 type ListRegistryRepositoryResponse []registry.Repository
 type ListRegistryRepositoryResponse []registry.Repository
 
 

+ 105 - 6
cli/cmd/cluster.go

@@ -4,22 +4,58 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 	"text/tabwriter"
 
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
 // clusterCmd represents the "porter cluster" base command when called
 // clusterCmd represents the "porter cluster" base command when called
 // without any subcommands
 // without any subcommands
 var clusterCmd = &cobra.Command{
 var clusterCmd = &cobra.Command{
-	Use:   "cluster",
-	Short: "Commands that read from a connected cluster",
+	Use:     "cluster",
+	Aliases: []string{"clusters"},
+	Short:   "Commands that read from a connected cluster",
 }
 }
 
 
-var listClusterNSCmd = &cobra.Command{
-	Use:   "namespace list",
-	Short: "Lists the namespaces in a cluster (used for testing connection)",
+var clusterListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the linked clusters in the current project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listClusters)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var clusterDeleteCmd = &cobra.Command{
+	Use:   "delete [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Deletes the cluster with the given id",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deleteCluster)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var clusterNamespaceCmd = &cobra.Command{
+	Use:     "namespace",
+	Aliases: []string{"namespaces"},
+	Short:   "Commands that perform operations on cluster namespaces",
+}
+
+var clusterNamespaceListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the namespaces in a cluster",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listNamespaces)
 		err := checkLoginAndRun(args, listNamespaces)
 
 
@@ -39,7 +75,70 @@ func init() {
 		"id of the cluster",
 		"id of the cluster",
 	)
 	)
 
 
-	clusterCmd.AddCommand(listClusterNSCmd)
+	clusterCmd.AddCommand(clusterNamespaceCmd)
+	clusterCmd.AddCommand(clusterListCmd)
+	clusterCmd.AddCommand(clusterDeleteCmd)
+
+	clusterNamespaceCmd.AddCommand(clusterNamespaceListCmd)
+}
+
+func listClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVER")
+
+	currClusterID := getClusterID()
+
+	for _, cluster := range clusters {
+		if currClusterID == cluster.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current cluster)\n", cluster.ID, cluster.Name, cluster.Server)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\n", cluster.ID, cluster.Name, cluster.Server)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func deleteCluster(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the cluster with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			return err
+		}
+
+		err = client.DeleteProjectCluster(context.Background(), getProjectID(), uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted cluster with id %d\n", id)
+	}
+
+	return nil
 }
 }
 
 
 func listNamespaces(user *api.AuthCheckResponse, client *api.Client, args []string) error {
 func listNamespaces(user *api.AuthCheckResponse, client *api.Client, args []string) error {

+ 21 - 0
cli/cmd/config.go

@@ -1,7 +1,10 @@
 package cmd
 package cmd
 
 
 import (
 import (
+	"fmt"
+	"io/ioutil"
 	"os"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strconv"
 
 
 	"github.com/fatih/color"
 	"github.com/fatih/color"
@@ -21,6 +24,12 @@ var (
 var configCmd = &cobra.Command{
 var configCmd = &cobra.Command{
 	Use:   "config",
 	Use:   "config",
 	Short: "Commands that control local configuration settings",
 	Short: "Commands that control local configuration settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		if err := printConfig(); err != nil {
+			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
 }
 }
 
 
 var setProjectCmd = &cobra.Command{
 var setProjectCmd = &cobra.Command{
@@ -128,6 +137,18 @@ func getDriver() string {
 	return viper.GetString("driver")
 	return viper.GetString("driver")
 }
 }
 
 
+func printConfig() error {
+	config, err := ioutil.ReadFile(filepath.Join(home, ".porter", "porter.yaml"))
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf(string(config))
+
+	return nil
+}
+
 func setProject(id uint) error {
 func setProject(id uint) error {
 	viper.Set("project", id)
 	viper.Set("project", id)
 	color.New(color.FgGreen).Printf("Set the current project id as %d\n", id)
 	color.New(color.FgGreen).Printf("Set the current project id as %d\n", id)

+ 12 - 6
cli/cmd/connect.go

@@ -21,7 +21,7 @@ var connectCmd = &cobra.Command{
 
 
 var connectKubeconfigCmd = &cobra.Command{
 var connectKubeconfigCmd = &cobra.Command{
 	Use:   "kubeconfig",
 	Use:   "kubeconfig",
-	Short: "Uses the local kubeconfig to connect to a cluster",
+	Short: "Uses the local kubeconfig to add a cluster",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectKubeconfig)
 		err := checkLoginAndRun(args, runConnectKubeconfig)
 
 
@@ -33,7 +33,7 @@ var connectKubeconfigCmd = &cobra.Command{
 
 
 var connectECRCmd = &cobra.Command{
 var connectECRCmd = &cobra.Command{
 	Use:   "ecr",
 	Use:   "ecr",
-	Short: "Connects an ECR instance to a project",
+	Short: "Adds an ECR instance to a project",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectECR)
 		err := checkLoginAndRun(args, runConnectECR)
 
 
@@ -45,7 +45,7 @@ var connectECRCmd = &cobra.Command{
 
 
 var connectGCRCmd = &cobra.Command{
 var connectGCRCmd = &cobra.Command{
 	Use:   "gcr",
 	Use:   "gcr",
-	Short: "Connects a GCR instance to a project",
+	Short: "Adds a GCR instance to a project",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, runConnectGCR)
 		err := checkLoginAndRun(args, runConnectGCR)
 
 
@@ -83,9 +83,9 @@ func init() {
 	)
 	)
 
 
 	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
 	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
-		"contexts",
+		"context",
 		nil,
 		nil,
-		"the list of contexts to connect (defaults to the current context)",
+		"the context to connect (defaults to the current context)",
 	)
 	)
 
 
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectECRCmd)
@@ -99,13 +99,19 @@ func runConnectKubeconfig(_ *api.AuthCheckResponse, client *api.Client, _ []stri
 		isLocal = true
 		isLocal = true
 	}
 	}
 
 
-	return connect.Kubeconfig(
+	id, err := connect.Kubeconfig(
 		client,
 		client,
 		kubeconfigPath,
 		kubeconfigPath,
 		*contexts,
 		*contexts,
 		getProjectID(),
 		getProjectID(),
 		isLocal,
 		isLocal,
 	)
 	)
+
+	if err != nil {
+		return err
+	}
+
+	return setCluster(id)
 }
 }
 
 
 func runConnectECR(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
 func runConnectECR(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {

+ 28 - 25
cli/cmd/connect/kubeconfig.go

@@ -27,17 +27,17 @@ func Kubeconfig(
 	contexts []string,
 	contexts []string,
 	projectID uint,
 	projectID uint,
 	isLocal bool,
 	isLocal bool,
-) error {
+) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
 	if projectID == 0 {
 	if projectID == 0 {
-		return fmt.Errorf("no project set, please run porter project set [id]")
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
 	}
 	}
 
 
 	// get the kubeconfig
 	// get the kubeconfig
 	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
 	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return 0, err
 	}
 	}
 
 
 	// send kubeconfig to client
 	// send kubeconfig to client
@@ -51,9 +51,11 @@ func Kubeconfig(
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return 0, err
 	}
 	}
 
 
+	var lastClusterID uint
+
 	for _, cc := range ccs {
 	for _, cc := range ccs {
 		var cluster *models.ClusterExternal
 		var cluster *models.ClusterExternal
 
 
@@ -66,7 +68,7 @@ func Kubeconfig(
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -75,25 +77,25 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					err = resolveClusterCAAction(filename, allResolver)
 					err = resolveClusterCAAction(filename, allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.ClusterLocalhost:
 				case models.ClusterLocalhost:
 					err := resolveLocalhostAction(allResolver)
 					err := resolveLocalhostAction(allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.ClientCertData:
 				case models.ClientCertData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -102,19 +104,19 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					err = resolveClientCertAction(filename, allResolver)
 					err = resolveClientCertAction(filename, allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.ClientKeyData:
 				case models.ClientKeyData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -123,19 +125,19 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					err = resolveClientKeyAction(filename, allResolver)
 					err = resolveClientKeyAction(filename, allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.OIDCIssuerData:
 				case models.OIDCIssuerData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -144,19 +146,19 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					err = resolveOIDCIssuerAction(filename, allResolver)
 					err = resolveOIDCIssuerAction(filename, allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.TokenData:
 				case models.TokenData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -165,13 +167,13 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 
 
 					err = resolveTokenDataAction(filename, allResolver)
 					err = resolveTokenDataAction(filename, allResolver)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.GCPKeyData:
 				case models.GCPKeyData:
 					err := resolveGCPKeyAction(
 					err := resolveGCPKeyAction(
@@ -181,7 +183,7 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				case models.AWSData:
 				case models.AWSData:
 					err := resolveAWSAction(
 					err := resolveAWSAction(
@@ -194,7 +196,7 @@ func Kubeconfig(
 					)
 					)
 
 
 					if err != nil {
 					if err != nil {
-						return err
+						return 0, err
 					}
 					}
 				}
 				}
 			}
 			}
@@ -207,7 +209,7 @@ func Kubeconfig(
 			)
 			)
 
 
 			if err != nil {
 			if err != nil {
-				return err
+				return 0, err
 			}
 			}
 
 
 			clExt := models.ClusterExternal(*resp)
 			clExt := models.ClusterExternal(*resp)
@@ -221,7 +223,7 @@ func Kubeconfig(
 			)
 			)
 
 
 			if err != nil {
 			if err != nil {
-				return err
+				return 0, err
 			}
 			}
 
 
 			clExt := models.ClusterExternal(*resp)
 			clExt := models.ClusterExternal(*resp)
@@ -230,9 +232,10 @@ func Kubeconfig(
 		}
 		}
 
 
 		color.New(color.FgGreen).Printf("created cluster %s with id %d\n", cluster.Name, cluster.ID)
 		color.New(color.FgGreen).Printf("created cluster %s with id %d\n", cluster.Name, cluster.ID)
+		lastClusterID = cluster.ID
 	}
 	}
 
 
-	return nil
+	return lastClusterID, nil
 }
 }
 
 
 // resolves a cluster ca data action
 // resolves a cluster ca data action

+ 3 - 37
cli/cmd/project.go

@@ -17,8 +17,9 @@ import (
 // projectCmd represents the "porter project" base command when called
 // projectCmd represents the "porter project" base command when called
 // without any subcommands
 // without any subcommands
 var projectCmd = &cobra.Command{
 var projectCmd = &cobra.Command{
-	Use:   "project",
-	Short: "Commands that control Porter project settings",
+	Use:     "project",
+	Aliases: []string{"projects"},
+	Short:   "Commands that control Porter project settings",
 }
 }
 
 
 var createProjectCmd = &cobra.Command{
 var createProjectCmd = &cobra.Command{
@@ -59,18 +60,6 @@ var listProjectCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
-var listProjectClustersCmd = &cobra.Command{
-	Use:   "clusters list",
-	Short: "Lists the linked clusters for a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listProjectClusters)
-
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
 func init() {
 func init() {
 	rootCmd.AddCommand(projectCmd)
 	rootCmd.AddCommand(projectCmd)
 
 
@@ -86,8 +75,6 @@ func init() {
 	projectCmd.AddCommand(deleteProjectCmd)
 	projectCmd.AddCommand(deleteProjectCmd)
 
 
 	projectCmd.AddCommand(listProjectCmd)
 	projectCmd.AddCommand(listProjectCmd)
-
-	projectCmd.AddCommand(listProjectClustersCmd)
 }
 }
 
 
 func createProject(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 func createProject(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
@@ -162,24 +149,3 @@ func deleteProject(_ *api.AuthCheckResponse, client *api.Client, args []string)
 
 
 	return nil
 	return nil
 }
 }
-
-func listProjectClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
-	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
-
-	if err != nil {
-		return err
-	}
-
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
-
-	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVER")
-
-	for _, cluster := range clusters {
-		fmt.Fprintf(w, "%d\t%s\t%s\n", cluster.ID, cluster.Name, cluster.Server)
-	}
-
-	w.Flush()
-
-	return nil
-}

+ 123 - 10
cli/cmd/registry.go

@@ -4,22 +4,58 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 	"text/tabwriter"
 
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
 // registryCmd represents the "porter registry" base command when called
 // registryCmd represents the "porter registry" base command when called
 // without any subcommands
 // without any subcommands
 var registryCmd = &cobra.Command{
 var registryCmd = &cobra.Command{
-	Use:   "registry",
-	Short: "Commands that read from a connected registry",
+	Use:     "registry",
+	Aliases: []string{"registries"},
+	Short:   "Commands that read from a connected registry",
 }
 }
 
 
-var registryCmdListRepos = &cobra.Command{
-	Use:   "repos list",
-	Short: "Lists the repositories in a registry",
+var registryListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the registries linked to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listRegistries)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var registryDeleteCmd = &cobra.Command{
+	Use:   "delete [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Deletes the registry with the given id",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deleteRegistry)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var registryReposCmd = &cobra.Command{
+	Use:     "repo",
+	Aliases: []string{"repos", "repository", "repositories"},
+	Short:   "Commands that perform operations on image registry repositories",
+}
+
+var registryReposListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the repositories in an image registry",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listRepos)
 		err := checkLoginAndRun(args, listRepos)
 
 
@@ -29,9 +65,16 @@ var registryCmdListRepos = &cobra.Command{
 	},
 	},
 }
 }
 
 
-var registryCmdListImages = &cobra.Command{
-	Use:   "images list [REPO_NAME]",
-	Short: "Lists the images in an image repository",
+var registryImageCmd = &cobra.Command{
+	Use:     "image",
+	Aliases: []string{"images"},
+	Short:   "Commands that perform operations on image in a repository",
+}
+
+var registryImageListCmd = &cobra.Command{
+	Use:   "list [repo_name]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Lists the images the specified image repository",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, listImages)
 		err := checkLoginAndRun(args, listImages)
 
 
@@ -51,9 +94,79 @@ func init() {
 		"id of the registry",
 		"id of the registry",
 	)
 	)
 
 
-	registryCmd.AddCommand(registryCmdListRepos)
+	registryCmd.AddCommand(registryReposCmd)
+	registryCmd.AddCommand(registryListCmd)
+	registryCmd.AddCommand(registryDeleteCmd)
+
+	registryReposCmd.AddCommand(registryReposListCmd)
+
+	registryCmd.AddCommand(registryImageCmd)
+	registryImageCmd.AddCommand(registryImageListCmd)
+}
+
+func listRegistries(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := getProjectID()
+
+	// get the list of namespaces
+	registries, err := client.ListRegistries(
+		context.Background(),
+		pID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVICE")
+
+	currRegistryID := getRegistryID()
+
+	for _, registry := range registries {
+		if currRegistryID == registry.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current registry)\n", registry.ID, registry.Name, registry.Service)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\n", registry.ID, registry.Name, registry.Service)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func deleteRegistry(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the registry with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			return err
+		}
+
+		err = client.DeleteProjectRegistry(context.Background(), getProjectID(), uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted registry with id %d\n", id)
+	}
 
 
-	registryCmd.AddCommand(registryCmdListImages)
+	return nil
 }
 }
 
 
 func listRepos(user *api.AuthCheckResponse, client *api.Client, args []string) error {
 func listRepos(user *api.AuthCheckResponse, client *api.Client, args []string) error {

+ 6 - 5
cli/cmd/server.go

@@ -23,14 +23,15 @@ type startOps struct {
 var opts = &startOps{}
 var opts = &startOps{}
 
 
 var serverCmd = &cobra.Command{
 var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "Commands to control a local Porter server",
+	Use:     "server",
+	Aliases: []string{"svr"},
+	Short:   "Commands to control a local Porter server",
 }
 }
 
 
 // startCmd represents the start command
 // startCmd represents the start command
 var startCmd = &cobra.Command{
 var startCmd = &cobra.Command{
 	Use:   "start",
 	Use:   "start",
-	Short: "Starts a Porter instance using the Docker engine",
+	Short: "Starts a Porter server instance on the host",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		if getDriver() == "docker" {
 		if getDriver() == "docker" {
 			setDriver("docker")
 			setDriver("docker")
@@ -100,14 +101,14 @@ func init() {
 		&opts.driver,
 		&opts.driver,
 		"driver",
 		"driver",
 		"local",
 		"local",
-		"the db to use, one of local or docker",
+		"the driver to use, one of \"local\" or \"docker\"",
 	)
 	)
 
 
 	startCmd.PersistentFlags().StringVar(
 	startCmd.PersistentFlags().StringVar(
 		&opts.imageTag,
 		&opts.imageTag,
 		"image-tag",
 		"image-tag",
 		"latest",
 		"latest",
-		"the Porter image tag to use",
+		"the Porter image tag to use (if using docker driver)",
 	)
 	)
 
 
 	opts.port = startCmd.PersistentFlags().IntP(
 	opts.port = startCmd.PersistentFlags().IntP(

+ 23 - 0
cli/cmd/version.go

@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+// Version will be linked by an ldflag during build
+var Version string = "dev"
+
+var versionCmd = &cobra.Command{
+	Use:     "version",
+	Aliases: []string{"v", "--version"},
+	Short:   "Prints the version of the Porter CLI",
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(Version)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(versionCmd)
+}

+ 264 - 109
dashboard/package-lock.json

@@ -453,6 +453,12 @@
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
       "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
       "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
     },
     },
+    "@types/random-words": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.0.tgz",
+      "integrity": "sha512-YZqkHIAGoXv6mlyEOwluhMjF9aotH2m9HrCCR+PNtES/ED00u5u4W7X1lWxc5AaFRKdKx+5orWTFW7iyGrZpOQ==",
+      "dev": true
+    },
     "@types/react": {
     "@types/react": {
       "version": "16.9.49",
       "version": "16.9.49",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz",
@@ -971,8 +977,7 @@
     "ansi-regex": {
     "ansi-regex": {
       "version": "4.1.0",
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-      "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-      "dev": true
+      "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
     },
     },
     "ansi-styles": {
     "ansi-styles": {
       "version": "3.2.1",
       "version": "3.2.1",
@@ -986,8 +991,6 @@
       "version": "3.1.1",
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
       "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
       "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
-      "dev": true,
-      "optional": true,
       "requires": {
       "requires": {
         "normalize-path": "^3.0.0",
         "normalize-path": "^3.0.0",
         "picomatch": "^2.0.4"
         "picomatch": "^2.0.4"
@@ -1177,8 +1180,7 @@
     "balanced-match": {
     "balanced-match": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
-      "dev": true
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
     },
     },
     "base": {
     "base": {
       "version": "0.11.2",
       "version": "0.11.2",
@@ -1256,9 +1258,7 @@
     "binary-extensions": {
     "binary-extensions": {
       "version": "2.1.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
-      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
-      "dev": true,
-      "optional": true
+      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
     },
     },
     "bindings": {
     "bindings": {
       "version": "1.5.0",
       "version": "1.5.0",
@@ -1362,7 +1362,6 @@
       "version": "1.1.11",
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "requires": {
       "requires": {
         "balanced-match": "^1.0.0",
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
         "concat-map": "0.0.1"
@@ -1372,7 +1371,6 @@
       "version": "3.0.2",
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
       "requires": {
       "requires": {
         "fill-range": "^7.0.1"
         "fill-range": "^7.0.1"
       }
       }
@@ -1383,6 +1381,11 @@
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
       "dev": true
       "dev": true
     },
     },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+    },
     "browserify-aes": {
     "browserify-aes": {
       "version": "1.2.0",
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -1566,8 +1569,7 @@
     "camelcase": {
     "camelcase": {
       "version": "5.3.1",
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-      "dev": true
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
     },
     },
     "camelize": {
     "camelize": {
       "version": "1.0.0",
       "version": "1.0.0",
@@ -1675,7 +1677,6 @@
       "version": "5.0.0",
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
       "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
       "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
-      "dev": true,
       "requires": {
       "requires": {
         "string-width": "^3.1.0",
         "string-width": "^3.1.0",
         "strip-ansi": "^5.2.0",
         "strip-ansi": "^5.2.0",
@@ -1686,7 +1687,6 @@
           "version": "5.2.0",
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
           "requires": {
             "ansi-regex": "^4.1.0"
             "ansi-regex": "^4.1.0"
           }
           }
@@ -1789,8 +1789,7 @@
     "concat-map": {
     "concat-map": {
       "version": "0.0.1",
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-      "dev": true
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
     },
     },
     "concat-stream": {
     "concat-stream": {
       "version": "1.6.2",
       "version": "1.6.2",
@@ -2098,8 +2097,7 @@
     "decamelize": {
     "decamelize": {
       "version": "1.2.0",
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
-      "dev": true
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
     },
     },
     "decode-uri-component": {
     "decode-uri-component": {
       "version": "0.2.0",
       "version": "0.2.0",
@@ -2135,7 +2133,6 @@
       "version": "1.1.3",
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "object-keys": "^1.0.12"
         "object-keys": "^1.0.12"
       }
       }
@@ -2230,6 +2227,11 @@
       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
       "dev": true
       "dev": true
     },
     },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
+    },
     "diff-match-patch": {
     "diff-match-patch": {
       "version": "1.0.5",
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
       "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
@@ -2446,8 +2448,7 @@
     "emoji-regex": {
     "emoji-regex": {
       "version": "7.0.3",
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
-      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
-      "dev": true
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
     },
     },
     "emojis-list": {
     "emojis-list": {
       "version": "3.0.0",
       "version": "3.0.0",
@@ -2500,7 +2501,6 @@
       "version": "1.17.7",
       "version": "1.17.7",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
       "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
       "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
-      "dev": true,
       "requires": {
       "requires": {
         "es-to-primitive": "^1.2.1",
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
         "function-bind": "^1.1.1",
@@ -2519,7 +2519,6 @@
       "version": "1.2.1",
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
       "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
       "requires": {
       "requires": {
         "is-callable": "^1.1.4",
         "is-callable": "^1.1.4",
         "is-date-object": "^1.0.1",
         "is-date-object": "^1.0.1",
@@ -2904,7 +2903,6 @@
       "version": "7.0.1",
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "to-regex-range": "^5.0.1"
         "to-regex-range": "^5.0.1"
       }
       }
@@ -2956,7 +2954,6 @@
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
       "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
       "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-      "dev": true,
       "requires": {
       "requires": {
         "locate-path": "^3.0.0"
         "locate-path": "^3.0.0"
       }
       }
@@ -3078,6 +3075,21 @@
         }
         }
       }
       }
     },
     },
+    "flat": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz",
+      "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==",
+      "requires": {
+        "is-buffer": "~2.0.3"
+      },
+      "dependencies": {
+        "is-buffer": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+          "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
+        }
+      }
+    },
     "flush-write-stream": {
     "flush-write-stream": {
       "version": "1.1.1",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -3259,27 +3271,23 @@
     "fs.realpath": {
     "fs.realpath": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
-      "dev": true
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
     },
     },
     "fsevents": {
     "fsevents": {
       "version": "2.1.3",
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
       "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
       "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
-      "dev": true,
       "optional": true
       "optional": true
     },
     },
     "function-bind": {
     "function-bind": {
       "version": "1.1.1",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     },
     "get-caller-file": {
     "get-caller-file": {
       "version": "2.0.5",
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
     },
     },
     "get-stream": {
     "get-stream": {
       "version": "4.1.0",
       "version": "4.1.0",
@@ -3314,8 +3322,6 @@
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
       "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
       "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
-      "dev": true,
-      "optional": true,
       "requires": {
       "requires": {
         "is-glob": "^4.0.1"
         "is-glob": "^4.0.1"
       }
       }
@@ -3387,6 +3393,11 @@
       "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
       "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
       "dev": true
       "dev": true
     },
     },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
+    },
     "handle-thing": {
     "handle-thing": {
       "version": "2.0.1",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -3397,7 +3408,6 @@
       "version": "1.0.3",
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "requires": {
       "requires": {
         "function-bind": "^1.1.1"
         "function-bind": "^1.1.1"
       }
       }
@@ -3410,8 +3420,7 @@
     "has-symbols": {
     "has-symbols": {
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
-      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
-      "dev": true
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
     },
     },
     "has-value": {
     "has-value": {
       "version": "1.0.0",
       "version": "1.0.0",
@@ -3489,8 +3498,7 @@
     "he": {
     "he": {
       "version": "1.2.0",
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
-      "dev": true
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
     },
     },
     "history": {
     "history": {
       "version": "4.10.1",
       "version": "4.10.1",
@@ -3879,7 +3887,6 @@
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true,
       "requires": {
       "requires": {
         "once": "^1.3.0",
         "once": "^1.3.0",
         "wrappy": "1"
         "wrappy": "1"
@@ -3888,8 +3895,7 @@
     "inherits": {
     "inherits": {
       "version": "2.0.4",
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     },
     "ini": {
     "ini": {
       "version": "1.3.5",
       "version": "1.3.5",
@@ -3967,8 +3973,6 @@
       "version": "2.1.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
-      "optional": true,
       "requires": {
       "requires": {
         "binary-extensions": "^2.0.0"
         "binary-extensions": "^2.0.0"
       }
       }
@@ -3981,8 +3985,7 @@
     "is-callable": {
     "is-callable": {
       "version": "1.2.2",
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
-      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
-      "dev": true
+      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
     },
     },
     "is-data-descriptor": {
     "is-data-descriptor": {
       "version": "0.1.4",
       "version": "0.1.4",
@@ -4007,8 +4010,7 @@
     "is-date-object": {
     "is-date-object": {
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
-      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
-      "dev": true
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
     },
     },
     "is-descriptor": {
     "is-descriptor": {
       "version": "0.1.6",
       "version": "0.1.6",
@@ -4038,20 +4040,17 @@
     "is-extglob": {
     "is-extglob": {
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     },
     "is-fullwidth-code-point": {
     "is-fullwidth-code-point": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-      "dev": true
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
     },
     },
     "is-glob": {
     "is-glob": {
       "version": "4.0.1",
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "dev": true,
       "requires": {
       "requires": {
         "is-extglob": "^2.1.1"
         "is-extglob": "^2.1.1"
       }
       }
@@ -4059,14 +4058,12 @@
     "is-negative-zero": {
     "is-negative-zero": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
-      "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=",
-      "dev": true
+      "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
     },
     },
     "is-number": {
     "is-number": {
       "version": "7.0.0",
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
     },
     },
     "is-path-cwd": {
     "is-path-cwd": {
       "version": "2.2.0",
       "version": "2.2.0",
@@ -4105,7 +4102,6 @@
       "version": "1.1.1",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
       "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
       "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
-      "dev": true,
       "requires": {
       "requires": {
         "has-symbols": "^1.0.1"
         "has-symbols": "^1.0.1"
       }
       }
@@ -4125,7 +4121,6 @@
       "version": "1.0.3",
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
       "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
       "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "has-symbols": "^1.0.1"
         "has-symbols": "^1.0.1"
       }
       }
@@ -4150,8 +4145,7 @@
     "isexe": {
     "isexe": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
     },
     },
     "isobject": {
     "isobject": {
       "version": "3.0.1",
       "version": "3.0.1",
@@ -4278,7 +4272,6 @@
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
       "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
       "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-      "dev": true,
       "requires": {
       "requires": {
         "p-locate": "^3.0.0",
         "p-locate": "^3.0.0",
         "path-exists": "^3.0.0"
         "path-exists": "^3.0.0"
@@ -4299,6 +4292,14 @@
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
       "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
       "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
     },
     },
+    "log-symbols": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+      "requires": {
+        "chalk": "^2.4.2"
+      }
+    },
     "loglevel": {
     "loglevel": {
       "version": "1.7.0",
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
@@ -4536,7 +4537,6 @@
       "version": "3.0.4",
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "dev": true,
       "requires": {
       "requires": {
         "brace-expansion": "^1.1.7"
         "brace-expansion": "^1.1.7"
       }
       }
@@ -4544,8 +4544,7 @@
     "minimist": {
     "minimist": {
       "version": "1.2.5",
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     },
     "mississippi": {
     "mississippi": {
       "version": "3.0.0",
       "version": "3.0.0",
@@ -4590,11 +4589,125 @@
       "version": "0.5.5",
       "version": "0.5.5",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "minimist": "^1.2.5"
         "minimist": "^1.2.5"
       }
       }
     },
     },
+    "mocha": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
+      "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
+      "requires": {
+        "ansi-colors": "3.2.3",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.3.0",
+        "debug": "3.2.6",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "find-up": "3.0.0",
+        "glob": "7.1.3",
+        "growl": "1.10.5",
+        "he": "1.2.0",
+        "js-yaml": "3.13.1",
+        "log-symbols": "3.0.0",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.5",
+        "ms": "2.1.1",
+        "node-environment-flags": "1.0.6",
+        "object.assign": "4.1.0",
+        "strip-json-comments": "2.0.1",
+        "supports-color": "6.0.0",
+        "which": "1.3.1",
+        "wide-align": "1.1.3",
+        "yargs": "13.3.2",
+        "yargs-parser": "13.1.2",
+        "yargs-unparser": "1.6.0"
+      },
+      "dependencies": {
+        "ansi-colors": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+          "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw=="
+        },
+        "chokidar": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
+          "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+          "requires": {
+            "anymatch": "~3.1.1",
+            "braces": "~3.0.2",
+            "fsevents": "~2.1.1",
+            "glob-parent": "~5.1.0",
+            "is-binary-path": "~2.1.0",
+            "is-glob": "~4.0.1",
+            "normalize-path": "~3.0.0",
+            "readdirp": "~3.2.0"
+          }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "requires": {
+            "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"
+          }
+        },
+        "js-yaml": {
+          "version": "3.13.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+        },
+        "object.assign": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+          "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+          "requires": {
+            "define-properties": "^1.1.2",
+            "function-bind": "^1.1.1",
+            "has-symbols": "^1.0.0",
+            "object-keys": "^1.0.11"
+          }
+        },
+        "readdirp": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
+          "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+          "requires": {
+            "picomatch": "^2.0.4"
+          }
+        },
+        "supports-color": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+          "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
     "move-concurrently": {
     "move-concurrently": {
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -4684,6 +4797,22 @@
         "tslib": "^1.10.0"
         "tslib": "^1.10.0"
       }
       }
     },
     },
+    "node-environment-flags": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+      "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3",
+        "semver": "^5.7.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+        }
+      }
+    },
     "node-forge": {
     "node-forge": {
       "version": "0.10.0",
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@@ -4770,8 +4899,7 @@
     "normalize-path": {
     "normalize-path": {
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
     },
     },
     "npm-run-path": {
     "npm-run-path": {
       "version": "2.0.2",
       "version": "2.0.2",
@@ -4830,8 +4958,7 @@
     "object-inspect": {
     "object-inspect": {
       "version": "1.8.0",
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
-      "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
-      "dev": true
+      "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
     },
     },
     "object-is": {
     "object-is": {
       "version": "1.1.3",
       "version": "1.1.3",
@@ -4868,8 +4995,7 @@
     "object-keys": {
     "object-keys": {
       "version": "1.1.1",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
     },
     },
     "object-visit": {
     "object-visit": {
       "version": "1.0.1",
       "version": "1.0.1",
@@ -4884,7 +5010,6 @@
       "version": "4.1.1",
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
       "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
       "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
-      "dev": true,
       "requires": {
       "requires": {
         "define-properties": "^1.1.3",
         "define-properties": "^1.1.3",
         "es-abstract": "^1.18.0-next.0",
         "es-abstract": "^1.18.0-next.0",
@@ -4896,7 +5021,6 @@
           "version": "1.18.0-next.1",
           "version": "1.18.0-next.1",
           "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
           "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
           "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
           "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
-          "dev": true,
           "requires": {
           "requires": {
             "es-to-primitive": "^1.2.1",
             "es-to-primitive": "^1.2.1",
             "function-bind": "^1.1.1",
             "function-bind": "^1.1.1",
@@ -4918,7 +5042,6 @@
       "version": "2.1.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
       "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
       "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
       "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
-      "dev": true,
       "requires": {
       "requires": {
         "define-properties": "^1.1.3",
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.0-next.1"
         "es-abstract": "^1.17.0-next.1"
@@ -4958,7 +5081,6 @@
       "version": "1.4.0",
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
       "requires": {
       "requires": {
         "wrappy": "1"
         "wrappy": "1"
       }
       }
@@ -4997,7 +5119,6 @@
       "version": "2.3.0",
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
       "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
       "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "dev": true,
       "requires": {
       "requires": {
         "p-try": "^2.0.0"
         "p-try": "^2.0.0"
       }
       }
@@ -5006,7 +5127,6 @@
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
       "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
       "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "p-limit": "^2.0.0"
         "p-limit": "^2.0.0"
       }
       }
@@ -5029,8 +5149,7 @@
     "p-try": {
     "p-try": {
       "version": "2.2.0",
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
     },
     },
     "pako": {
     "pako": {
       "version": "1.0.11",
       "version": "1.0.11",
@@ -5153,14 +5272,12 @@
     "path-exists": {
     "path-exists": {
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
-      "dev": true
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
     },
     },
     "path-is-absolute": {
     "path-is-absolute": {
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-      "dev": true
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
     },
     },
     "path-is-inside": {
     "path-is-inside": {
       "version": "1.0.2",
       "version": "1.0.2",
@@ -5198,8 +5315,7 @@
     "picomatch": {
     "picomatch": {
       "version": "2.2.2",
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
-      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
-      "dev": true
+      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
     },
     },
     "pify": {
     "pify": {
       "version": "4.0.1",
       "version": "4.0.1",
@@ -5462,6 +5578,14 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "dev": true
       "dev": true
     },
     },
+    "random-words": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.1.1.tgz",
+      "integrity": "sha512-Rdk5EoQePyt9Tz3RjeMELi2BSaCI+jDiOkBr4U+3fyBRiiW3qqEuaegGAUMOZ4yGWlQscFQGqQpdic3mAbNkrw==",
+      "requires": {
+        "mocha": "^7.1.1"
+      }
+    },
     "randombytes": {
     "randombytes": {
       "version": "2.1.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -5702,14 +5826,12 @@
     "require-directory": {
     "require-directory": {
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
-      "dev": true
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
     },
     },
     "require-main-filename": {
     "require-main-filename": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-      "dev": true
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
     },
     },
     "requires-port": {
     "requires-port": {
       "version": "1.0.0",
       "version": "1.0.0",
@@ -5994,8 +6116,7 @@
     "set-blocking": {
     "set-blocking": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
-      "dev": true
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
     },
     },
     "set-value": {
     "set-value": {
       "version": "2.0.1",
       "version": "2.0.1",
@@ -6501,7 +6622,6 @@
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
       "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
       "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-      "dev": true,
       "requires": {
       "requires": {
         "emoji-regex": "^7.0.1",
         "emoji-regex": "^7.0.1",
         "is-fullwidth-code-point": "^2.0.0",
         "is-fullwidth-code-point": "^2.0.0",
@@ -6512,7 +6632,6 @@
           "version": "5.2.0",
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
           "requires": {
             "ansi-regex": "^4.1.0"
             "ansi-regex": "^4.1.0"
           }
           }
@@ -6523,7 +6642,6 @@
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
       "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
       "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
-      "dev": true,
       "requires": {
       "requires": {
         "define-properties": "^1.1.3",
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
         "es-abstract": "^1.17.5"
@@ -6533,7 +6651,6 @@
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
       "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
       "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
-      "dev": true,
       "requires": {
       "requires": {
         "define-properties": "^1.1.3",
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
         "es-abstract": "^1.17.5"
@@ -6580,6 +6697,11 @@
         "min-indent": "^1.0.0"
         "min-indent": "^1.0.0"
       }
       }
     },
     },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+    },
     "styled-components": {
     "styled-components": {
       "version": "5.2.0",
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
       "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
@@ -6792,7 +6914,6 @@
       "version": "5.0.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "is-number": "^7.0.0"
         "is-number": "^7.0.0"
       }
       }
@@ -8071,7 +8192,6 @@
       "version": "1.3.1",
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
       "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
       "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
       "requires": {
       "requires": {
         "isexe": "^2.0.0"
         "isexe": "^2.0.0"
       }
       }
@@ -8079,8 +8199,39 @@
     "which-module": {
     "which-module": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
-      "dev": true
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
     },
     },
     "worker-farm": {
     "worker-farm": {
       "version": "1.7.0",
       "version": "1.7.0",
@@ -8095,7 +8246,6 @@
       "version": "5.1.0",
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
       "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
       "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
-      "dev": true,
       "requires": {
       "requires": {
         "ansi-styles": "^3.2.0",
         "ansi-styles": "^3.2.0",
         "string-width": "^3.0.0",
         "string-width": "^3.0.0",
@@ -8106,7 +8256,6 @@
           "version": "5.2.0",
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
           "requires": {
             "ansi-regex": "^4.1.0"
             "ansi-regex": "^4.1.0"
           }
           }
@@ -8116,8 +8265,7 @@
     "wrappy": {
     "wrappy": {
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
     },
     "ws": {
     "ws": {
       "version": "6.2.1",
       "version": "6.2.1",
@@ -8137,8 +8285,7 @@
     "y18n": {
     "y18n": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
-      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
-      "dev": true
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
     },
     },
     "yallist": {
     "yallist": {
       "version": "3.1.1",
       "version": "3.1.1",
@@ -8150,7 +8297,6 @@
       "version": "13.3.2",
       "version": "13.3.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
       "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
       "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
-      "dev": true,
       "requires": {
       "requires": {
         "cliui": "^5.0.0",
         "cliui": "^5.0.0",
         "find-up": "^3.0.0",
         "find-up": "^3.0.0",
@@ -8168,11 +8314,20 @@
       "version": "13.1.2",
       "version": "13.1.2",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
       "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
       "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
-      "dev": true,
       "requires": {
       "requires": {
         "camelcase": "^5.0.0",
         "camelcase": "^5.0.0",
         "decamelize": "^1.2.0"
         "decamelize": "^1.2.0"
       }
       }
+    },
+    "yargs-unparser": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
+      "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+      "requires": {
+        "flat": "^4.1.0",
+        "lodash": "^4.17.15",
+        "yargs": "^13.3.0"
+      }
     }
     }
   }
   }
 }
 }

+ 2 - 0
dashboard/package.json

@@ -17,6 +17,7 @@
     "markdown-to-jsx": "^7.0.1",
     "markdown-to-jsx": "^7.0.1",
     "posthog-node": "^1.0.6",
     "posthog-node": "^1.0.6",
     "qs": "^6.9.4",
     "qs": "^6.9.4",
+    "random-words": "^1.1.1",
     "react": "^16.13.1",
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-ace": "^9.1.3",
     "react-dom": "^16.13.1",
     "react-dom": "^16.13.1",
@@ -37,6 +38,7 @@
     "@types/js-base64": "^3.0.0",
     "@types/js-base64": "^3.0.0",
     "@types/node": "^12.12.62",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
     "@types/qs": "^6.9.5",
+    "@types/random-words": "^1.1.0",
     "@types/react": "^16.9.49",
     "@types/react": "^16.9.49",
     "@types/react-dom": "^16.9.8",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/react-modal": "^3.10.6",

+ 1 - 0
dashboard/src/components/values-form/ValuesForm.tsx

@@ -148,6 +148,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   }
   }
 
 
   render() {
   render() {
+    console.log('save values status', this.props.saveValuesStatus)
     return (
     return (
       <Wrapper>
       <Wrapper>
         <StyledValuesForm>
         <StyledValuesForm>

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -69,13 +69,13 @@ export default class ControllerTab extends Component<PropsType, StateType> {
       case "deployment":
       case "deployment":
       case "replicaset":
       case "replicaset":
         return [
         return [
-          c.status?.availableReplicas || c.status?.replicas - c.status?.unavailableReplicas, 
-          c.status?.replicas
+          c.status?.availableReplicas || c.status?.replicas - c.status?.unavailableReplicas || 0, 
+          c.status?.replicas || 0
         ]
         ]
       case "statefulset":
       case "statefulset":
-       return [c.status?.readyReplicas, c.status?.replicas]
+       return [c.status?.readyReplicas || 0, c.status?.replicas || 0]
       case "daemonset":
       case "daemonset":
-        return [c.status?.numberAvailable, c.status?.desiredNumberScheduled]
+        return [c.status?.numberAvailable || 0, c.status?.desiredNumberScheduled || 0]
       }
       }
   }
   }
 
 

+ 1 - 0
dashboard/src/main/home/templates/Templates.tsx

@@ -41,6 +41,7 @@ export default class Templates extends Component<PropsType, StateType> {
       if (err) {
       if (err) {
         this.setState({ loading: false, error: true });
         this.setState({ loading: false, error: true });
       } else {
       } else {
+        console.log(res.data)
         this.setState({ porterCharts: res.data, loading: false, error: false });
         this.setState({ porterCharts: res.data, loading: false, error: false });
       }
       }
     });
     });

+ 63 - 7
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import styled from 'styled-components';
 import styled from 'styled-components';
-
+import randomWords from 'random-words';
 import { Context } from '../../../../shared/Context';
 import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 import api from '../../../../shared/api';
 
 
@@ -19,39 +19,52 @@ type PropsType = {
 type StateType = {
 type StateType = {
   currentView: string,
   currentView: string,
   clusterOptions: { label: string, value: string }[],
   clusterOptions: { label: string, value: string }[],
+  saveValuesStatus: string | null
+  selectedNamespace: string,
   selectedCluster: string,
   selectedCluster: string,
   selectedImageUrl: string | null,
   selectedImageUrl: string | null,
   selectedTag: string | null,
   selectedTag: string | null,
   tabOptions: ChoiceType[],
   tabOptions: ChoiceType[],
   currentTab: string | null,
   currentTab: string | null,
+  tabContents: any
+  namespaceOptions: { label: string, value: string }[],
 };
 };
 
 
 export default class LaunchTemplate extends Component<PropsType, StateType> {
 export default class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
   state = {
     currentView: 'repo',
     currentView: 'repo',
     clusterOptions: [] as { label: string, value: string }[],
     clusterOptions: [] as { label: string, value: string }[],
+    saveValuesStatus: null as (string | null),
     selectedCluster: this.context.currentCluster.name,
     selectedCluster: this.context.currentCluster.name,
+    selectedNamespace: "default",
     selectedImageUrl: '' as string | null,
     selectedImageUrl: '' as string | null,
     selectedTag: '' as string | null,
     selectedTag: '' as string | null,
     tabOptions: [] as ChoiceType[],
     tabOptions: [] as ChoiceType[],
     currentTab: null as string | null,
     currentTab: null as string | null,
+    tabContents: [] as any,
+    namespaceOptions: [] as { label: string, value: string }[],
   };
   };
 
 
   onSubmit = (formValues: any) => {
   onSubmit = (formValues: any) => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
+    let name = randomWords({ exactly: 3, join: '-' })
+    this.setState({ saveValuesStatus: 'loading' });
+
     api.deployTemplate('<token>', {
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
       templateName: this.props.currentTemplate.name,
-      imageURL: "index.docker.io/bitnami/redis",
+      imageURL: "",
       storage: StorageType.Secret,
       storage: StorageType.Secret,
       formValues,
       formValues,
+      namespace: this.state.selectedNamespace,
+      name,
     }, {
     }, {
       id: currentProject.id,
       id: currentProject.id,
       cluster_id: currentCluster.id,
       cluster_id: currentCluster.id,
     }, (err: any, res: any) => {
     }, (err: any, res: any) => {
       if (err) {
       if (err) {
-        console.log(err)
+        this.setState({ saveValuesStatus: 'error' });
       } else {
       } else {
-        console.log(res.data)
+        this.setState({ saveValuesStatus: 'successful' });
       }
       }
     });
     });
   }
   }
@@ -64,8 +77,12 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         return (
         return (
           <ValuesFormWrapper>
           <ValuesFormWrapper>
             <ValuesForm 
             <ValuesForm 
+              key={tab.name}
               sections={tab.sections} 
               sections={tab.sections} 
               onSubmit={this.onSubmit}
               onSubmit={this.onSubmit}
+              // disabled={!this.state.selectedImageUrl || this.state.selectedImageUrl === ''}
+              disabled={false}
+              saveValuesStatus={this.state.saveValuesStatus}
             />
             />
           </ValuesFormWrapper>
           </ValuesFormWrapper>
         );
         );
@@ -83,17 +100,33 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     this.setState({ tabOptions });
     this.setState({ tabOptions });
 
 
     // TODO: query with selected filter once implemented
     // TODO: query with selected filter once implemented
-    let { currentProject } = this.context;
+    let { currentProject, currentCluster } = this.context;
     api.getClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
     api.getClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
       if (err) {
       if (err) {
         // console.log(err)
         // console.log(err)
       } else if (res.data) {
       } else if (res.data) {
+        console.log(res.data)
         let clusterOptions = res.data.map((x: Cluster) => { return { label: x.name, value: x.name } });
         let clusterOptions = res.data.map((x: Cluster) => { return { label: x.name, value: x.name } });
         if (res.data.length > 0) {
         if (res.data.length > 0) {
           this.setState({ clusterOptions });
           this.setState({ clusterOptions });
         }
         }
       }
       }
     });
     });
+
+    api.getNamespaces('<token>', {
+      cluster_id: currentCluster.id,
+    }, { id: currentProject.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else if (res.data) {
+        let namespaceOptions = res.data.items.map((x: { metadata: {name: string}}) => { 
+          return { label: x.metadata.name, value: x.metadata.name } 
+        });
+        if (res.data.items.length > 0) {
+          this.setState({ namespaceOptions });
+        }
+      }
+    });
   }
   }
 
 
   renderIcon = (icon: string) => {
   renderIcon = (icon: string) => {
@@ -138,9 +171,21 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             dropdownWidth='335px'
             dropdownWidth='335px'
             closeOverlay={true}
             closeOverlay={true}
           />
           />
+          <NamespaceLabel>
+            <i className="material-icons">view_list</i>Namespace
+          </NamespaceLabel>
+          <Selector
+            key={'namespace'}
+            activeValue={this.state.selectedNamespace}
+            setActiveValue={(namespace: string) => this.setState({ selectedNamespace: namespace })}
+            options={this.state.namespaceOptions}
+            width='250px'
+            dropdownWidth='335px'
+            closeOverlay={true}
+          />
         </ClusterSection>
         </ClusterSection>
 
 
-        <Subtitle>Select the container image you would like to connect to this template (optional).</Subtitle>
+        {/* <Subtitle>Select the container image you would like to connect to this template (optional).</Subtitle>
         <Br />
         <Br />
         <ImageSelector
         <ImageSelector
           selectedTag={this.state.selectedTag}
           selectedTag={this.state.selectedTag}
@@ -151,7 +196,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           setCurrentView={this.props.setCurrentView}
           setCurrentView={this.props.setCurrentView}
         />
         />
 
 
-        <br />
+        <br /> */}
         <Subtitle>Configure additional settings for this template (optional).</Subtitle>
         <Subtitle>Configure additional settings for this template (optional).</Subtitle>
         <TabRegion
         <TabRegion
           options={this.state.tabOptions}
           options={this.state.tabOptions}
@@ -198,6 +243,17 @@ const ClusterLabel = styled.div`
   }
   }
 `;
 `;
 
 
+const NamespaceLabel = styled.div`
+  margin-left: 15px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  > i {
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
 const Icon = styled.img`
 const Icon = styled.img`
   width: 21px;
   width: 21px;
   margin-right: 10px;
   margin-right: 10px;

+ 2 - 0
dashboard/src/shared/api.tsx

@@ -158,6 +158,8 @@ const deployTemplate = baseApi<{
   imageURL: string,
   imageURL: string,
   formValues: any,
   formValues: any,
   storage: StorageType,
   storage: StorageType,
+  namespace: string,
+  name: string,
 }, { id: number, cluster_id: number }>('POST', pathParams => {
 }, { id: number, cluster_id: number }>('POST', pathParams => {
   let { cluster_id, id } = pathParams;
   let { cluster_id, id } = pathParams;
   return `/api/projects/${id}/deploy?cluster_id=${cluster_id}`;
   return `/api/projects/${id}/deploy?cluster_id=${cluster_id}`;

+ 21 - 0
internal/forms/cluster.go

@@ -69,6 +69,27 @@ func (ccf *CreateClusterForm) ToCluster() (*models.Cluster, error) {
 	}, nil
 	}, nil
 }
 }
 
 
+// UpdateClusterForm represents the accepted values for updating a
+// cluster (only name for now)
+type UpdateClusterForm struct {
+	ID uint
+
+	Name string `json:"name" form:"required"`
+}
+
+// ToCluster converts the form to a cluster
+func (ucf *UpdateClusterForm) ToCluster(repo repository.ClusterRepository) (*models.Cluster, error) {
+	cluster, err := repo.ReadCluster(ucf.ID)
+
+	if err != nil {
+		return nil, err
+	}
+
+	cluster.Name = ucf.Name
+
+	return cluster, nil
+}
+
 // ResolveClusterForm will resolve a cluster candidate and create a new cluster
 // ResolveClusterForm will resolve a cluster candidate and create a new cluster
 type ResolveClusterForm struct {
 type ResolveClusterForm struct {
 	Resolver *models.ClusterResolverAll `form:"required"`
 	Resolver *models.ClusterResolverAll `form:"required"`

+ 22 - 0
internal/forms/registry.go

@@ -2,6 +2,7 @@ package forms
 
 
 import (
 import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 )
 
 
 // CreateRegistry represents the accepted values for creating a
 // CreateRegistry represents the accepted values for creating a
@@ -22,3 +23,24 @@ func (cr *CreateRegistry) ToRegistry() (*models.Registry, error) {
 		AWSIntegrationID: cr.AWSIntegrationID,
 		AWSIntegrationID: cr.AWSIntegrationID,
 	}, nil
 	}, nil
 }
 }
+
+// UpdateRegistryForm represents the accepted values for updating a
+// registry (only name for now)
+type UpdateRegistryForm struct {
+	ID uint
+
+	Name string `json:"name" form:"required"`
+}
+
+// ToRegistry converts the form to a cluster
+func (urf *UpdateRegistryForm) ToRegistry(repo repository.RegistryRepository) (*models.Registry, error) {
+	registry, err := repo.ReadRegistry(urf.ID)
+
+	if err != nil {
+		return nil, err
+	}
+
+	registry.Name = urf.Name
+
+	return registry, nil
+}

+ 1 - 0
internal/forms/release.go

@@ -119,6 +119,7 @@ type ChartTemplateForm struct {
 	TemplateName string                 `json:"templateName" form:"required"`
 	TemplateName string                 `json:"templateName" form:"required"`
 	ImageURL     string                 `json:"imageURL" form:"required"`
 	ImageURL     string                 `json:"imageURL" form:"required"`
 	FormValues   map[string]interface{} `json:"formValues"`
 	FormValues   map[string]interface{} `json:"formValues"`
+	Name         string                 `json:"name"`
 }
 }
 
 
 // InstallChartTemplateForm represents the accepted values for installing a new chart from a template.
 // InstallChartTemplateForm represents the accepted values for installing a new chart from a template.

+ 15 - 5
internal/helm/agent.go

@@ -88,8 +88,17 @@ func (a *Agent) UpgradeRelease(
 func (a *Agent) InstallChart(
 func (a *Agent) InstallChart(
 	cp string,
 	cp string,
 	values []byte,
 	values []byte,
+	name string,
+	namespace string,
 ) (*release.Release, error) {
 ) (*release.Release, error) {
-	cmd := action.NewInstall(a.ActionConfig)
+	client := action.NewInstall(a.ActionConfig)
+
+	if client.Version == "" && client.Devel {
+		client.Version = ">0.0.0-0"
+	}
+
+	client.ReleaseName = name
+	client.Namespace = namespace
 	valuesYaml, err := chartutil.ReadValues(values)
 	valuesYaml, err := chartutil.ReadValues(values)
 
 
 	if err != nil {
 	if err != nil {
@@ -99,6 +108,7 @@ func (a *Agent) InstallChart(
 	// Only supports filepaths for now, URL option WIP.
 	// Only supports filepaths for now, URL option WIP.
 	// Check chart dependencies to make sure all are present in /charts
 	// Check chart dependencies to make sure all are present in /charts
 	chartRequested, err := loader.Load(cp)
 	chartRequested, err := loader.Load(cp)
+
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -107,9 +117,9 @@ func (a *Agent) InstallChart(
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if chartRequested.Metadata.Deprecated {
-		return nil, fmt.Errorf("This chart is deprecated")
-	}
+	// if chartRequested.Metadata.Deprecated {
+	// 	return nil, fmt.Errorf("This chart is deprecated")
+	// }
 
 
 	if req := chartRequested.Metadata.Dependencies; req != nil {
 	if req := chartRequested.Metadata.Dependencies; req != nil {
 		if err := action.CheckDependencies(chartRequested, req); err != nil {
 		if err := action.CheckDependencies(chartRequested, req); err != nil {
@@ -118,7 +128,7 @@ func (a *Agent) InstallChart(
 		}
 		}
 	}
 	}
 
 
-	return cmd.Run(chartRequested, valuesYaml)
+	return client.Run(chartRequested, valuesYaml)
 }
 }
 
 
 // RollbackRelease rolls a release back to a specified revision/version
 // RollbackRelease rolls a release back to a specified revision/version

+ 1 - 0
internal/repository/cluster.go

@@ -16,6 +16,7 @@ type ClusterRepository interface {
 	CreateCluster(cluster *models.Cluster) (*models.Cluster, error)
 	CreateCluster(cluster *models.Cluster) (*models.Cluster, error)
 	ReadCluster(id uint) (*models.Cluster, error)
 	ReadCluster(id uint) (*models.Cluster, error)
 	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
 	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
+	UpdateCluster(cluster *models.Cluster) (*models.Cluster, error)
 	UpdateClusterTokenCache(tokenCache *ints.TokenCache) (*models.Cluster, error)
 	UpdateClusterTokenCache(tokenCache *ints.TokenCache) (*models.Cluster, error)
 	DeleteCluster(cluster *models.Cluster) error
 	DeleteCluster(cluster *models.Cluster) error
 }
 }

+ 11 - 0
internal/repository/gorm/cluster.go

@@ -193,6 +193,17 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 	return clusters, nil
 	return clusters, nil
 }
 }
 
 
+// UpdateCluster modifies an existing Cluster in the database
+func (repo *ClusterRepository) UpdateCluster(
+	cluster *models.Cluster,
+) (*models.Cluster, error) {
+	if err := repo.db.Save(cluster).Error; err != nil {
+		return nil, err
+	}
+
+	return cluster, nil
+}
+
 // UpdateClusterTokenCache updates the token cache for a cluster
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 	tokenCache *ints.TokenCache,
 	tokenCache *ints.TokenCache,

+ 42 - 0
internal/repository/gorm/cluster_test.go

@@ -299,6 +299,48 @@ func TestListClustersByProjectID(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestUpdateCluster(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_update_cluster.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initCluster(tester, t)
+	defer cleanup(tester, t)
+
+	cluster := tester.initClusters[0]
+
+	cluster.Name = "cluster-new-name"
+
+	cluster, err := tester.repo.Cluster.UpdateCluster(
+		cluster,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	cluster, err = tester.repo.Cluster.ReadCluster(tester.initClusters[0].ID)
+
+	// make sure data is correct
+	expCluster := models.Cluster{
+		ProjectID:                tester.initProjects[0].ID,
+		Name:                     "cluster-new-name",
+		Server:                   "https://localhost",
+		KubeIntegrationID:        tester.initKIs[0].ID,
+		CertificateAuthorityData: []byte("-----BEGIN"),
+	}
+
+	// reset fields for reflect.DeepEqual
+	cluster.Model = orm.Model{}
+
+	if diff := deep.Equal(expCluster, *cluster); diff != nil {
+		t.Errorf("incorrect cluster")
+		t.Error(diff)
+	}
+}
+
 func TestUpdateClusterToken(t *testing.T) {
 func TestUpdateClusterToken(t *testing.T) {
 	tester := &tester{
 	tester := &tester{
 		dbFileName: "./porter_test_update_cluster_token.db",
 		dbFileName: "./porter_test_update_cluster_token.db",

+ 11 - 0
internal/repository/gorm/registry.go

@@ -94,6 +94,17 @@ func (repo *RegistryRepository) ListRegistriesByProjectID(
 	return regs, nil
 	return regs, nil
 }
 }
 
 
+// UpdateRegistry modifies an existing Registry in the database
+func (repo *RegistryRepository) UpdateRegistry(
+	reg *models.Registry,
+) (*models.Registry, error) {
+	if err := repo.db.Save(reg).Error; err != nil {
+		return nil, err
+	}
+
+	return reg, nil
+}
+
 // UpdateRegistryTokenCache updates the token cache for a registry
 // UpdateRegistryTokenCache updates the token cache for a registry
 func (repo *RegistryRepository) UpdateRegistryTokenCache(
 func (repo *RegistryRepository) UpdateRegistryTokenCache(
 	tokenCache *ints.RegTokenCache,
 	tokenCache *ints.RegTokenCache,

+ 39 - 0
internal/repository/gorm/registry_test.go

@@ -86,6 +86,45 @@ func TestListRegistriesByProjectID(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestUpdateRegistry(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_update_registry.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initRegistry(tester, t)
+	defer cleanup(tester, t)
+
+	reg := tester.initRegs[0]
+
+	reg.Name = "registry-new-name"
+
+	reg, err := tester.repo.Registry.UpdateRegistry(
+		reg,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	reg, err = tester.repo.Registry.ReadRegistry(tester.initRegs[0].ID)
+
+	// make sure data is correct
+	expRegistry := models.Registry{
+		ProjectID: tester.initProjects[0].ID,
+		Name:      "registry-new-name",
+	}
+
+	// reset fields for reflect.DeepEqual
+	reg.Model = orm.Model{}
+
+	if diff := deep.Equal(expRegistry, *reg); diff != nil {
+		t.Errorf("incorrect registry")
+		t.Error(diff)
+	}
+}
+
 func TestUpdateRegistryToken(t *testing.T) {
 func TestUpdateRegistryToken(t *testing.T) {
 	tester := &tester{
 	tester := &tester{
 		dbFileName: "./porter_test_update_registry_token.db",
 		dbFileName: "./porter_test_update_registry_token.db",

+ 1 - 0
internal/repository/registry.go

@@ -10,6 +10,7 @@ type RegistryRepository interface {
 	CreateRegistry(reg *models.Registry) (*models.Registry, error)
 	CreateRegistry(reg *models.Registry) (*models.Registry, error)
 	ReadRegistry(id uint) (*models.Registry, error)
 	ReadRegistry(id uint) (*models.Registry, error)
 	ListRegistriesByProjectID(projectID uint) ([]*models.Registry, error)
 	ListRegistriesByProjectID(projectID uint) ([]*models.Registry, error)
+	UpdateRegistry(reg *models.Registry) (*models.Registry, error)
 	UpdateRegistryTokenCache(tokenCache *ints.RegTokenCache) (*models.Registry, error)
 	UpdateRegistryTokenCache(tokenCache *ints.RegTokenCache) (*models.Registry, error)
 	DeleteRegistry(reg *models.Registry) error
 	DeleteRegistry(reg *models.Registry) error
 }
 }

+ 18 - 0
internal/repository/test/cluster.go

@@ -144,6 +144,24 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 	return res, nil
 	return res, nil
 }
 }
 
 
+// UpdateCluster modifies an existing Cluster in the database
+func (repo *ClusterRepository) UpdateCluster(
+	cluster *models.Cluster,
+) (*models.Cluster, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(cluster.ID-1) >= len(repo.clusters) || repo.clusters[cluster.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(cluster.ID - 1)
+	repo.clusters[index] = cluster
+
+	return cluster, nil
+}
+
 // UpdateClusterTokenCache updates the token cache for a cluster
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 	tokenCache *ints.TokenCache,
 	tokenCache *ints.TokenCache,

+ 18 - 0
internal/repository/test/registry.go

@@ -73,6 +73,24 @@ func (repo *RegistryRepository) ListRegistriesByProjectID(
 	return res, nil
 	return res, nil
 }
 }
 
 
+// UpdateRegistry modifies an existing Registry in the database
+func (repo *RegistryRepository) UpdateRegistry(
+	reg *models.Registry,
+) (*models.Registry, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(reg.ID-1) >= len(repo.registries) || repo.registries[reg.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(reg.ID - 1)
+	repo.registries[index] = reg
+
+	return reg, nil
+}
+
 // UpdateRegistryTokenCache updates the token cache for a registry
 // UpdateRegistryTokenCache updates the token cache for a registry
 func (repo *RegistryRepository) UpdateRegistryTokenCache(
 func (repo *RegistryRepository) UpdateRegistryTokenCache(
 	tokenCache *ints.RegTokenCache,
 	tokenCache *ints.RegTokenCache,

+ 854 - 0
package-lock.json

@@ -0,0 +1,854 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@types/random-words": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.0.tgz",
+      "integrity": "sha512-YZqkHIAGoXv6mlyEOwluhMjF9aotH2m9HrCCR+PNtES/ED00u5u4W7X1lWxc5AaFRKdKx+5orWTFW7iyGrZpOQ==",
+      "dev": true
+    },
+    "ansi-colors": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw=="
+    },
+    "ansi-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+      "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
+      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "binary-extensions": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+    },
+    "call-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
+      "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
+      "requires": {
+        "function-bind": "^1.1.1",
+        "get-intrinsic": "^1.0.0"
+      }
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "chokidar": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
+      "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+      "requires": {
+        "anymatch": "~3.1.1",
+        "braces": "~3.0.2",
+        "fsevents": "~2.1.1",
+        "glob-parent": "~5.1.0",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.2.0"
+      }
+    },
+    "cliui": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+      "requires": {
+        "string-width": "^3.1.0",
+        "strip-ansi": "^5.2.0",
+        "wrap-ansi": "^5.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "debug": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+      "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
+    },
+    "emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+    },
+    "es-abstract": {
+      "version": "1.18.0-next.1",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+      "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+      "requires": {
+        "es-to-primitive": "^1.2.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-symbols": "^1.0.1",
+        "is-callable": "^1.2.2",
+        "is-negative-zero": "^2.0.0",
+        "is-regex": "^1.1.1",
+        "object-inspect": "^1.8.0",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.1",
+        "string.prototype.trimend": "^1.0.1",
+        "string.prototype.trimstart": "^1.0.1"
+      },
+      "dependencies": {
+        "object.assign": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+          "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+          "requires": {
+            "call-bind": "^1.0.0",
+            "define-properties": "^1.1.3",
+            "has-symbols": "^1.0.1",
+            "object-keys": "^1.1.1"
+          }
+        }
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-up": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+      "requires": {
+        "locate-path": "^3.0.0"
+      }
+    },
+    "flat": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz",
+      "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==",
+      "requires": {
+        "is-buffer": "~2.0.3"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "fsevents": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+      "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ=="
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+    },
+    "get-intrinsic": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
+      "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
+      "requires": {
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-symbols": "^1.0.1"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "requires": {
+        "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"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "has-symbols": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+      "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
+    },
+    "is-callable": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+    },
+    "is-date-object": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+    },
+    "is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-negative-zero": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+      "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+    },
+    "is-regex": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+      "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+      "requires": {
+        "has-symbols": "^1.0.1"
+      }
+    },
+    "is-symbol": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+      "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+      "requires": {
+        "has-symbols": "^1.0.1"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "locate-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+      "requires": {
+        "p-locate": "^3.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.20",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+    },
+    "log-symbols": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+      "requires": {
+        "chalk": "^2.4.2"
+      }
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "mocha": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
+      "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
+      "requires": {
+        "ansi-colors": "3.2.3",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.3.0",
+        "debug": "3.2.6",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "find-up": "3.0.0",
+        "glob": "7.1.3",
+        "growl": "1.10.5",
+        "he": "1.2.0",
+        "js-yaml": "3.13.1",
+        "log-symbols": "3.0.0",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.5",
+        "ms": "2.1.1",
+        "node-environment-flags": "1.0.6",
+        "object.assign": "4.1.0",
+        "strip-json-comments": "2.0.1",
+        "supports-color": "6.0.0",
+        "which": "1.3.1",
+        "wide-align": "1.1.3",
+        "yargs": "13.3.2",
+        "yargs-parser": "13.1.2",
+        "yargs-unparser": "1.6.0"
+      }
+    },
+    "ms": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+      "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+    },
+    "node-environment-flags": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+      "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3",
+        "semver": "^5.7.0"
+      }
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+    },
+    "object-inspect": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
+      "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
+      "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==",
+      "requires": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.18.0-next.1"
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "requires": {
+        "p-try": "^2.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+      "requires": {
+        "p-limit": "^2.0.0"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "picomatch": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
+    },
+    "random-words": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.1.1.tgz",
+      "integrity": "sha512-Rdk5EoQePyt9Tz3RjeMELi2BSaCI+jDiOkBr4U+3fyBRiiW3qqEuaegGAUMOZ4yGWlQscFQGqQpdic3mAbNkrw==",
+      "requires": {
+        "mocha": "^7.1.1"
+      }
+    },
+    "readdirp": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
+      "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+      "requires": {
+        "picomatch": "^2.0.4"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
+    },
+    "require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+    },
+    "semver": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      }
+    },
+    "string.prototype.trimend": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
+      "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
+      "requires": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3"
+      }
+    },
+    "string.prototype.trimstart": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
+      "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
+      "requires": {
+        "call-bind": "^1.0.0",
+        "define-properties": "^1.1.3"
+      }
+    },
+    "strip-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+      "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+      "requires": {
+        "ansi-regex": "^3.0.0"
+      }
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+    },
+    "supports-color": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+      "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "wrap-ansi": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+      "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "string-width": "^3.0.0",
+        "strip-ansi": "^5.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "y18n": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
+      "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ=="
+    },
+    "yargs": {
+      "version": "13.3.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+      "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+      "requires": {
+        "cliui": "^5.0.0",
+        "find-up": "^3.0.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^3.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^13.1.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "13.1.2",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+      "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    },
+    "yargs-unparser": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
+      "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+      "requires": {
+        "flat": "^4.1.0",
+        "lodash": "^4.17.15",
+        "yargs": "^13.3.0"
+      }
+    }
+  }
+}

+ 58 - 0
server/api/cluster_handler.go

@@ -119,6 +119,64 @@ func (app *App) HandleListProjectClusters(w http.ResponseWriter, r *http.Request
 	}
 	}
 }
 }
 
 
+// HandleUpdateProjectCluster updates a project's cluster
+func (app *App) HandleUpdateProjectCluster(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	clusterID, err := strconv.ParseUint(chi.URLParam(r, "cluster_id"), 0, 64)
+
+	if err != nil || clusterID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.UpdateClusterForm{
+		ID: uint(clusterID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a registry
+	cluster, err := form.ToCluster(app.repo.Cluster)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	cluster, err = app.repo.Cluster.UpdateCluster(cluster)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	clusterExt := cluster.Externalize()
+
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleDeleteProjectCluster handles the deletion of a Cluster via the cluster ID
 // HandleDeleteProjectCluster handles the deletion of a Cluster via the cluster ID
 func (app *App) HandleDeleteProjectCluster(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleDeleteProjectCluster(w http.ResponseWriter, r *http.Request) {
 	id, err := strconv.ParseUint(chi.URLParam(r, "cluster_id"), 0, 64)
 	id, err := strconv.ParseUint(chi.URLParam(r, "cluster_id"), 0, 64)

+ 24 - 0
server/api/cluster_handler_test.go

@@ -145,6 +145,30 @@ func TestHandleListProjectClusters(t *testing.T) {
 	testClusterRequests(t, listProjectClustersTest, true)
 	testClusterRequests(t, listProjectClustersTest, true)
 }
 }
 
 
+var updateClusterTests = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initProjectClusterDefault,
+		},
+		msg:       "Update cluster name",
+		method:    "POST",
+		endpoint:  "/api/projects/1/clusters/1",
+		body:      `{"name":"cluster-new-name"}`,
+		expStatus: http.StatusOK,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-new-name","server":"https://10.10.10.10","service":"kube"}`,
+		useCookie: true,
+		validators: []func(c *clusterTest, tester *tester, t *testing.T){
+			projectClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleUpdateCluster(t *testing.T) {
+	testClusterRequests(t, updateClusterTests, true)
+}
+
 var deleteClusterTests = []*clusterTest{
 var deleteClusterTests = []*clusterTest{
 	&clusterTest{
 	&clusterTest{
 		initializers: []func(t *tester){
 		initializers: []func(t *tester){

+ 17 - 3
server/api/deploy_handler.go

@@ -22,6 +22,7 @@ import (
 // HandleDeployTemplate triggers a chart deployment from a template
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 	vals, err := url.ParseQuery(r.URL.RawQuery)
+
 	if err != nil {
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
 		return
@@ -63,7 +64,9 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	// Set image URL
 	// Set image URL
-	(*values)["image"].(map[interface{}]interface{})["repository"] = form.ChartTemplateForm.ImageURL
+	if form.ChartTemplateForm.ImageURL != "" {
+		(*values)["image"].(map[interface{}]interface{})["repository"] = form.ChartTemplateForm.ImageURL
+	}
 
 
 	// Loop through form params to override
 	// Loop through form params to override
 	for k := range form.ChartTemplateForm.FormValues {
 	for k := range form.ChartTemplateForm.FormValues {
@@ -98,13 +101,24 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	var tgz string
+	switch form.ChartTemplateForm.TemplateName {
+	case "redis":
+		tgz = "redis-0.0.1.tgz"
+	}
+
 	// Output values.yaml string
 	// Output values.yaml string
-	_, err = agent.InstallChart(baseURL+"react-0.1.5.tgz", v)
+	_, err = agent.InstallChart(
+		"./internal/local_templates/"+tgz,
+		v,
+		form.ChartTemplateForm.Name,
+		form.ReleaseForm.Form.Namespace,
+	)
 
 
 	if err != nil {
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 			Code:   ErrReleaseDeploy,
 			Code:   ErrReleaseDeploy,
-			Errors: []string{"error installing a new chart" + err.Error()},
+			Errors: []string{"error installing a new chart: " + err.Error()},
 		}, w)
 		}, w)
 
 
 		return
 		return

+ 58 - 0
server/api/registry_handler.go

@@ -95,6 +95,64 @@ func (app *App) HandleListProjectRegistries(w http.ResponseWriter, r *http.Reque
 	}
 	}
 }
 }
 
 
+// HandleUpdateProjectRegistry updates a registry
+func (app *App) HandleUpdateProjectRegistry(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	registryID, err := strconv.ParseUint(chi.URLParam(r, "registry_id"), 0, 64)
+
+	if err != nil || registryID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.UpdateRegistryForm{
+		ID: uint(registryID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a registry
+	registry, err := form.ToRegistry(app.repo.Registry)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	registry, err = app.repo.Registry.UpdateRegistry(registry)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	regExt := registry.Externalize()
+
+	if err := json.NewEncoder(w).Encode(regExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleDeleteProjectRegistry handles the deletion of a Registry via the registry ID
 // HandleDeleteProjectRegistry handles the deletion of a Registry via the registry ID
 func (app *App) HandleDeleteProjectRegistry(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleDeleteProjectRegistry(w http.ResponseWriter, r *http.Request) {
 	id, err := strconv.ParseUint(chi.URLParam(r, "registry_id"), 0, 64)
 	id, err := strconv.ParseUint(chi.URLParam(r, "registry_id"), 0, 64)

+ 24 - 0
server/api/registry_handler_test.go

@@ -171,6 +171,30 @@ func TestHandleListRegistries(t *testing.T) {
 	testRegistryRequests(t, listRegistryTests, true)
 	testRegistryRequests(t, listRegistryTests, true)
 }
 }
 
 
+var updateRegistryTests = []*regTest{
+	&regTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initRegistry,
+		},
+		msg:       "Update registry name",
+		method:    "POST",
+		endpoint:  "/api/projects/1/registries/1",
+		body:      `{"name":"registry-new-name"}`,
+		expStatus: http.StatusOK,
+		expBody:   `{"id":1,"name":"registry-new-name","project_id":1,"service":"ecr"}`,
+		useCookie: true,
+		validators: []func(c *regTest, tester *tester, t *testing.T){
+			regBodyValidator,
+		},
+	},
+}
+
+func TestHandleUpdateRegistry(t *testing.T) {
+	testRegistryRequests(t, updateRegistryTests, true)
+}
+
 var deleteRegTests = []*regTest{
 var deleteRegTests = []*regTest{
 	&regTest{
 	&regTest{
 		initializers: []func(t *tester){
 		initializers: []func(t *tester){

+ 28 - 0
server/router/router.go

@@ -202,6 +202,20 @@ func New(
 			),
 			),
 		)
 		)
 
 
+		r.Method(
+			"POST",
+			"/projects/{project_id}/clusters/{cluster_id}",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleUpdateProjectCluster, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 		r.Method(
 			"DELETE",
 			"DELETE",
 			"/projects/{project_id}/clusters/{cluster_id}",
 			"/projects/{project_id}/clusters/{cluster_id}",
@@ -289,6 +303,20 @@ func New(
 			),
 			),
 		)
 		)
 
 
+		r.Method(
+			"POST",
+			"/projects/{project_id}/registries/{registry_id}",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveRegistryAccess(
+					requestlog.NewHandler(a.HandleUpdateProjectRegistry, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 		r.Method(
 			"DELETE",
 			"DELETE",
 			"/projects/{project_id}/registries/{registry_id}",
 			"/projects/{project_id}/registries/{registry_id}",