Przeglądaj źródła

updated agent with install

Alexander Belanger 5 lat temu
rodzic
commit
0f31d71b52
39 zmienionych plików z 2128 dodań i 440 usunięć
  1. 2 0
      .darwin.goreleaser.yml
  2. 3 1
      .gitignore
  3. 2 0
      .goreleaser.yml
  4. 3 2
      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. 269 109
      dashboard/package-lock.json
  16. 4 0
      dashboard/package.json
  17. 1 0
      dashboard/src/components/ResourceTab.tsx
  18. 12 26
      dashboard/src/components/TabRegion.tsx
  19. 30 18
      dashboard/src/components/image-selector/ImageSelector.tsx
  20. 11 0
      dashboard/src/components/values-form/ValuesForm.tsx
  21. 9 0
      dashboard/src/main/home/Home.tsx
  22. 15 15
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  23. 134 115
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  24. 1 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  25. 2 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  26. 4 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  27. 1 1
      dashboard/src/main/home/integrations/Integrations.tsx
  28. 1 1
      dashboard/src/main/home/modals/ClusterInstructionsModal.tsx
  29. 197 0
      dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx
  30. 3 2
      dashboard/src/main/home/sidebar/Sidebar.tsx
  31. 1 0
      dashboard/src/main/home/templates/Templates.tsx
  32. 82 27
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  33. 2 0
      dashboard/src/shared/api.tsx
  34. 1 0
      dashboard/src/shared/types.tsx
  35. 26 0
      docs/GCR.md
  36. 1 0
      internal/forms/release.go
  37. 29 27
      internal/helm/agent.go
  38. 854 0
      package-lock.json
  39. 17 3
      server/api/deploy_handler.go

+ 2 - 0
.darwin.goreleaser.yml

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

+ 3 - 1
.gitignore

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

+ 2 - 0
.goreleaser.yml

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

+ 3 - 2
README.md

@@ -1,10 +1,11 @@
 # Porter
 Porter is a **dashboard for Helm** with support for the following features:
+- User-generated [form overlays](https://docs.getporter.dev/docs/porter-templates) for managing `values.yaml`
 - Visualization of all Helm releases with filtering by namespace
 - 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 raw `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). 
 
 ## Quick Start

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

@@ -272,6 +272,35 @@ func (c *Client) CreateProjectCluster(
 	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
 type DeleteProjectResponse models.ProjectExternal
 

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

@@ -104,6 +104,67 @@ func (c *Client) CreateGCR(
 	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
 type ListRegistryRepositoryResponse []registry.Repository
 

+ 105 - 6
cli/cmd/cluster.go

@@ -4,22 +4,58 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 )
 
 // clusterCmd represents the "porter cluster" base command when called
 // without any subcommands
 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) {
 		err := checkLoginAndRun(args, listNamespaces)
 
@@ -39,7 +75,70 @@ func init() {
 		"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 {

+ 21 - 0
cli/cmd/config.go

@@ -1,7 +1,10 @@
 package cmd
 
 import (
+	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strconv"
 
 	"github.com/fatih/color"
@@ -21,6 +24,12 @@ var (
 var configCmd = &cobra.Command{
 	Use:   "config",
 	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{
@@ -128,6 +137,18 @@ func getDriver() string {
 	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 {
 	viper.Set("project", 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{
 	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) {
 		err := checkLoginAndRun(args, runConnectKubeconfig)
 
@@ -33,7 +33,7 @@ var connectKubeconfigCmd = &cobra.Command{
 
 var connectECRCmd = &cobra.Command{
 	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) {
 		err := checkLoginAndRun(args, runConnectECR)
 
@@ -45,7 +45,7 @@ var connectECRCmd = &cobra.Command{
 
 var connectGCRCmd = &cobra.Command{
 	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) {
 		err := checkLoginAndRun(args, runConnectGCR)
 
@@ -83,9 +83,9 @@ func init() {
 	)
 
 	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
-		"contexts",
+		"context",
 		nil,
-		"the list of contexts to connect (defaults to the current context)",
+		"the context to connect (defaults to the current context)",
 	)
 
 	connectCmd.AddCommand(connectECRCmd)
@@ -99,13 +99,19 @@ func runConnectKubeconfig(_ *api.AuthCheckResponse, client *api.Client, _ []stri
 		isLocal = true
 	}
 
-	return connect.Kubeconfig(
+	id, err := connect.Kubeconfig(
 		client,
 		kubeconfigPath,
 		*contexts,
 		getProjectID(),
 		isLocal,
 	)
+
+	if err != nil {
+		return err
+	}
+
+	return setCluster(id)
 }
 
 func runConnectECR(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {

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

@@ -27,17 +27,17 @@ func Kubeconfig(
 	contexts []string,
 	projectID uint,
 	isLocal bool,
-) error {
+) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	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
 	rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
 	// send kubeconfig to client
@@ -51,9 +51,11 @@ func Kubeconfig(
 	)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
+	var lastClusterID uint
+
 	for _, cc := range ccs {
 		var cluster *models.ClusterExternal
 
@@ -66,7 +68,7 @@ func Kubeconfig(
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -75,25 +77,25 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClusterCAAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClusterLocalhost:
 					err := resolveLocalhostAction(allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClientCertData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -102,19 +104,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClientCertAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.ClientKeyData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -123,19 +125,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveClientKeyAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.OIDCIssuerData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -144,19 +146,19 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveOIDCIssuerAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.TokenData:
 					absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					filename, err := utils.GetFileReferenceFromKubeconfig(
@@ -165,13 +167,13 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 
 					err = resolveTokenDataAction(filename, allResolver)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.GCPKeyData:
 					err := resolveGCPKeyAction(
@@ -181,7 +183,7 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				case models.AWSData:
 					err := resolveAWSAction(
@@ -194,7 +196,7 @@ func Kubeconfig(
 					)
 
 					if err != nil {
-						return err
+						return 0, err
 					}
 				}
 			}
@@ -207,7 +209,7 @@ func Kubeconfig(
 			)
 
 			if err != nil {
-				return err
+				return 0, err
 			}
 
 			clExt := models.ClusterExternal(*resp)
@@ -221,7 +223,7 @@ func Kubeconfig(
 			)
 
 			if err != nil {
-				return err
+				return 0, err
 			}
 
 			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)
+		lastClusterID = cluster.ID
 	}
 
-	return nil
+	return lastClusterID, nil
 }
 
 // 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
 // without any subcommands
 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{
@@ -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() {
 	rootCmd.AddCommand(projectCmd)
 
@@ -86,8 +75,6 @@ func init() {
 	projectCmd.AddCommand(deleteProjectCmd)
 
 	projectCmd.AddCommand(listProjectCmd)
-
-	projectCmd.AddCommand(listProjectClustersCmd)
 }
 
 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
 }
-
-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"
 	"fmt"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 
+	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 )
 
 // registryCmd represents the "porter registry" base command when called
 // without any subcommands
 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) {
 		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) {
 		err := checkLoginAndRun(args, listImages)
 
@@ -51,9 +94,79 @@ func init() {
 		"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 {

+ 6 - 5
cli/cmd/server.go

@@ -23,14 +23,15 @@ type startOps struct {
 var opts = &startOps{}
 
 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
 var startCmd = &cobra.Command{
 	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) {
 		if getDriver() == "docker" {
 			setDriver("docker")
@@ -100,14 +101,14 @@ func init() {
 		&opts.driver,
 		"driver",
 		"local",
-		"the db to use, one of local or docker",
+		"the driver to use, one of \"local\" or \"docker\"",
 	)
 
 	startCmd.PersistentFlags().StringVar(
 		&opts.imageTag,
 		"image-tag",
 		"latest",
-		"the Porter image tag to use",
+		"the Porter image tag to use (if using docker driver)",
 	)
 
 	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)
+}

+ 269 - 109
dashboard/package-lock.json

@@ -418,6 +418,11 @@
       "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.14.165",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
+      "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg=="
+    },
     "@types/markdown-to-jsx": {
       "version": "6.11.3",
       "resolved": "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz",
@@ -448,6 +453,12 @@
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
       "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": {
       "version": "16.9.49",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz",
@@ -966,8 +977,7 @@
     "ansi-regex": {
       "version": "4.1.0",
       "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": {
       "version": "3.2.1",
@@ -981,8 +991,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
       "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
-      "dev": true,
-      "optional": true,
       "requires": {
         "normalize-path": "^3.0.0",
         "picomatch": "^2.0.4"
@@ -1172,8 +1180,7 @@
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
-      "dev": true
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
     },
     "base": {
       "version": "0.11.2",
@@ -1251,9 +1258,7 @@
     "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==",
-      "dev": true,
-      "optional": true
+      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
     },
     "bindings": {
       "version": "1.5.0",
@@ -1357,7 +1362,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "requires": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -1367,7 +1371,6 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
       "requires": {
         "fill-range": "^7.0.1"
       }
@@ -1378,6 +1381,11 @@
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
       "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": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -1561,8 +1569,7 @@
     "camelcase": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-      "dev": true
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
     },
     "camelize": {
       "version": "1.0.0",
@@ -1670,7 +1677,6 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
       "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
-      "dev": true,
       "requires": {
         "string-width": "^3.1.0",
         "strip-ansi": "^5.2.0",
@@ -1681,7 +1687,6 @@
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
             "ansi-regex": "^4.1.0"
           }
@@ -1784,8 +1789,7 @@
     "concat-map": {
       "version": "0.0.1",
       "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": {
       "version": "1.6.2",
@@ -2093,8 +2097,7 @@
     "decamelize": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
-      "dev": true
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
     },
     "decode-uri-component": {
       "version": "0.2.0",
@@ -2130,7 +2133,6 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
       "requires": {
         "object-keys": "^1.0.12"
       }
@@ -2225,6 +2227,11 @@
       "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
       "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": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
@@ -2441,8 +2448,7 @@
     "emoji-regex": {
       "version": "7.0.3",
       "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": {
       "version": "3.0.0",
@@ -2495,7 +2501,6 @@
       "version": "1.17.7",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
       "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
-      "dev": true,
       "requires": {
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
@@ -2514,7 +2519,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
       "requires": {
         "is-callable": "^1.1.4",
         "is-date-object": "^1.0.1",
@@ -2899,7 +2903,6 @@
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
       "requires": {
         "to-regex-range": "^5.0.1"
       }
@@ -2951,7 +2954,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
       "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-      "dev": true,
       "requires": {
         "locate-path": "^3.0.0"
       }
@@ -3073,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": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@@ -3254,27 +3271,23 @@
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
-      "dev": true
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
     },
     "fsevents": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
       "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
-      "dev": true,
       "optional": true
     },
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "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==",
-      "dev": true
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
     },
     "get-stream": {
       "version": "4.1.0",
@@ -3309,8 +3322,6 @@
       "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==",
-      "dev": true,
-      "optional": true,
       "requires": {
         "is-glob": "^4.0.1"
       }
@@ -3382,6 +3393,11 @@
       "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
       "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": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -3392,7 +3408,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "requires": {
         "function-bind": "^1.1.1"
       }
@@ -3405,8 +3420,7 @@
     "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==",
-      "dev": true
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
     },
     "has-value": {
       "version": "1.0.0",
@@ -3484,8 +3498,7 @@
     "he": {
       "version": "1.2.0",
       "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": {
       "version": "4.10.1",
@@ -3874,7 +3887,6 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true,
       "requires": {
         "once": "^1.3.0",
         "wrappy": "1"
@@ -3883,8 +3895,7 @@
     "inherits": {
       "version": "2.0.4",
       "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": {
       "version": "1.3.5",
@@ -3962,8 +3973,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
-      "optional": true,
       "requires": {
         "binary-extensions": "^2.0.0"
       }
@@ -3976,8 +3985,7 @@
     "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==",
-      "dev": true
+      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
     },
     "is-data-descriptor": {
       "version": "0.1.4",
@@ -4002,8 +4010,7 @@
     "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==",
-      "dev": true
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
     },
     "is-descriptor": {
       "version": "0.1.6",
@@ -4033,20 +4040,17 @@
     "is-extglob": {
       "version": "2.1.1",
       "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": {
       "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=",
-      "dev": true
+      "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==",
-      "dev": true,
       "requires": {
         "is-extglob": "^2.1.1"
       }
@@ -4054,14 +4058,12 @@
     "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=",
-      "dev": true
+      "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==",
-      "dev": true
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
     },
     "is-path-cwd": {
       "version": "2.2.0",
@@ -4100,7 +4102,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
       "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.1"
       }
@@ -4120,7 +4121,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
       "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.1"
       }
@@ -4145,8 +4145,7 @@
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
     },
     "isobject": {
       "version": "3.0.1",
@@ -4273,7 +4272,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
       "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-      "dev": true,
       "requires": {
         "p-locate": "^3.0.0",
         "path-exists": "^3.0.0"
@@ -4294,6 +4292,14 @@
       "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
       "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": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
@@ -4531,7 +4537,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "dev": true,
       "requires": {
         "brace-expansion": "^1.1.7"
       }
@@ -4539,8 +4544,7 @@
     "minimist": {
       "version": "1.2.5",
       "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": {
       "version": "3.0.0",
@@ -4585,11 +4589,125 @@
       "version": "0.5.5",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-      "dev": true,
       "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"
+      },
+      "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": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -4679,6 +4797,22 @@
         "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": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@@ -4765,8 +4899,7 @@
     "normalize-path": {
       "version": "3.0.0",
       "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": {
       "version": "2.0.2",
@@ -4825,8 +4958,7 @@
     "object-inspect": {
       "version": "1.8.0",
       "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": {
       "version": "1.1.3",
@@ -4863,8 +4995,7 @@
     "object-keys": {
       "version": "1.1.1",
       "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": {
       "version": "1.0.1",
@@ -4879,7 +5010,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
       "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.18.0-next.0",
@@ -4891,7 +5021,6 @@
           "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==",
-          "dev": true,
           "requires": {
             "es-to-primitive": "^1.2.1",
             "function-bind": "^1.1.1",
@@ -4913,7 +5042,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
       "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.0-next.1"
@@ -4953,7 +5081,6 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
       "requires": {
         "wrappy": "1"
       }
@@ -4992,7 +5119,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
       "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "dev": true,
       "requires": {
         "p-try": "^2.0.0"
       }
@@ -5001,7 +5127,6 @@
       "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==",
-      "dev": true,
       "requires": {
         "p-limit": "^2.0.0"
       }
@@ -5024,8 +5149,7 @@
     "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==",
-      "dev": true
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
     },
     "pako": {
       "version": "1.0.11",
@@ -5148,14 +5272,12 @@
     "path-exists": {
       "version": "3.0.0",
       "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": {
       "version": "1.0.1",
       "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": {
       "version": "1.0.2",
@@ -5193,8 +5315,7 @@
     "picomatch": {
       "version": "2.2.2",
       "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": {
       "version": "4.0.1",
@@ -5457,6 +5578,14 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "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": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -5697,14 +5826,12 @@
     "require-directory": {
       "version": "2.1.1",
       "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": {
       "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==",
-      "dev": true
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
     },
     "requires-port": {
       "version": "1.0.0",
@@ -5989,8 +6116,7 @@
     "set-blocking": {
       "version": "2.0.0",
       "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": {
       "version": "2.0.1",
@@ -6496,7 +6622,6 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
       "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-      "dev": true,
       "requires": {
         "emoji-regex": "^7.0.1",
         "is-fullwidth-code-point": "^2.0.0",
@@ -6507,7 +6632,6 @@
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
             "ansi-regex": "^4.1.0"
           }
@@ -6518,7 +6642,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
       "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -6528,7 +6651,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
       "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -6575,6 +6697,11 @@
         "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": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.0.tgz",
@@ -6787,7 +6914,6 @@
       "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==",
-      "dev": true,
       "requires": {
         "is-number": "^7.0.0"
       }
@@ -8066,7 +8192,6 @@
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
       "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
       "requires": {
         "isexe": "^2.0.0"
       }
@@ -8074,8 +8199,39 @@
     "which-module": {
       "version": "2.0.0",
       "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": {
       "version": "1.7.0",
@@ -8090,7 +8246,6 @@
       "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==",
-      "dev": true,
       "requires": {
         "ansi-styles": "^3.2.0",
         "string-width": "^3.0.0",
@@ -8101,7 +8256,6 @@
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
           "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
           "requires": {
             "ansi-regex": "^4.1.0"
           }
@@ -8111,8 +8265,7 @@
     "wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
     "ws": {
       "version": "6.2.1",
@@ -8132,8 +8285,7 @@
     "y18n": {
       "version": "4.0.0",
       "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": {
       "version": "3.1.1",
@@ -8145,7 +8297,6 @@
       "version": "13.3.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
       "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
-      "dev": true,
       "requires": {
         "cliui": "^5.0.0",
         "find-up": "^3.0.0",
@@ -8163,11 +8314,20 @@
       "version": "13.1.2",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
       "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
-      "dev": true,
       "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"
+      }
     }
   }
 }

+ 4 - 0
dashboard/package.json

@@ -5,6 +5,7 @@
   "dependencies": {
     "@fullstory/browser": "^1.4.5",
     "@types/js-yaml": "^3.12.5",
+    "@types/lodash": "^4.14.165",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/qs": "^6.9.5",
     "ace-builds": "^1.4.12",
@@ -12,9 +13,11 @@
     "dotenv": "^8.2.0",
     "js-base64": "^3.6.0",
     "js-yaml": "^3.14.0",
+    "lodash": "^4.17.20",
     "markdown-to-jsx": "^7.0.1",
     "posthog-node": "^1.0.6",
     "qs": "^6.9.4",
+    "random-words": "^1.1.1",
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-dom": "^16.13.1",
@@ -35,6 +38,7 @@
     "@types/js-base64": "^3.0.0",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
+    "@types/random-words": "^1.1.0",
     "@types/react": "^16.9.49",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",

+ 1 - 0
dashboard/src/components/ResourceTab.tsx

@@ -162,6 +162,7 @@ const ResourceHeader = styled.div`
   display: flex;
   align-items: center;
   color: #ffffff66;
+  user-select: none;
   padding: 8px 18px;
   padding-left: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? '10px' : '22px'};
   text-transform: capitalize;

+ 12 - 26
dashboard/src/components/TabRegion.tsx

@@ -6,27 +6,21 @@ import Loading from './Loading';
 
 type PropsType = {
   options: { label: string, value: string }[],
-  tabContents: any,
+  currentTab: string,
+  setCurrentTab: (x: string) => void,
   defaultTab?: string,
   addendum?: any,
-  checkTabExists?: boolean, // Handles the currently selected tab disappearing
   color?: string | null,
 };
 
 type StateType = {
-  currentTab: string
 };
 
 // Manages a tab selector and renders the associated view
-// TODO: consider rearchitecturing to support standard re-render
 export default class TabRegion extends Component<PropsType, StateType> {
-  state = {
-    currentTab: this.props.defaultTab
-  }
-
   setDefaultTab = () => {
     if (!this.props.defaultTab && this.props.options[0]) {
-      this.setState({ currentTab: this.props.options[0].value });
+      this.props.setCurrentTab(this.props.options[0].value);
     }
   }
 
@@ -35,24 +29,16 @@ export default class TabRegion extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    let { options, checkTabExists } = this.props;
-    if (prevProps.options !== options && !this.state.currentTab) {
-      this.setDefaultTab();
-    } else if (prevProps.checkTabExists !== checkTabExists
-      && !options.some((e: any) => e.value === this.state.currentTab)) {
-      this.setDefaultTab();
-    }
-  }
-
-  renderTabContents = () => {
-    let found = this.props.tabContents.find((el: any) => el.value === this.state.currentTab);
-    if (found) {
-      return found.component;
+    let { options, currentTab } = this.props;
+    if (prevProps.options !== options) {
+      if (options.filter(x => x.value === currentTab).length === 0) {
+        this.setDefaultTab();
+      }
     }
   }
 
   renderContents = () => {
-    if (!this.state.currentTab) {
+    if (!this.props.currentTab) {
       return (
         <Loading />
       );
@@ -63,13 +49,13 @@ export default class TabRegion extends Component<PropsType, StateType> {
         <TabSelector
           options={this.props.options}
           color={this.props.color}
-          currentTab={this.state.currentTab}
-          setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+          currentTab={this.props.currentTab}
+          setCurrentTab={(x: string) => this.props.setCurrentTab(x)}
           addendum={this.props.addendum}
         />
         <Gap />
         <TabContents>
-          {this.renderTabContents()}
+          {this.props.children}
         </TabContents>
       </Div>
     );

+ 30 - 18
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -26,7 +26,6 @@ type StateType = {
   error: boolean,
   images: ImageType[],
   clickedImage: ImageType | null,
-  registryId: number | null, // For passing registry ID to tag list
 };
 
 export default class ImageSelector extends Component<PropsType, StateType> {
@@ -36,17 +35,21 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     error: false,
     images: [] as ImageType[],
     clickedImage: null as ImageType | null,
-    registryId: null as number | null,
   }
 
   componentDidMount() {
     const { currentProject, setCurrentError } = this.context;
     let images = [] as ImageType[]
+    let errors = [] as number[]
     api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
       if (err) {
         console.log(err);
+        this.setState({ error: true });
       } else {
         let registries = res.data;
+        if (registries.length === 0) {
+          this.setState({ loading: false });
+        }
         registries.forEach(async (registry: any, i: number) => {
           await new Promise((nextController: (res?: any) => void) => {           
             api.getImageRepos('<token>', {}, 
@@ -54,26 +57,33 @@ export default class ImageSelector extends Component<PropsType, StateType> {
                 project_id: currentProject.id,
                 registry_id: registry.id,
               }, (err: any, res: any) => {
-              if (err && this.state.loading) {
-                this.setState({ error: true, loading: false });
+              if (err) {
+                errors.push(1);
               } else {
                 let newImg = res.data.map((img: any) => {
                   return {
                     kind: registry.service, 
-                    source: img.name
+                    source: img.name,
+                    registryId: registry.id,
                   }
                 })
                 images.push(...newImg)
-                if (i == registries.length - 1) {
-                  this.setState({
-                    images,
-                    registryId: registry.id,
-                    loading: false,
-                    error: false,
-                  });
-                }
-                nextController()
+                errors.push(0);
               }
+              
+              if (i == registries.length - 1) {
+                let error = errors.reduce((a, b) => {
+                  return a + b;
+                }) == registries.length ? true : false; 
+
+                this.setState({
+                  images,
+                  loading: false,
+                  error,
+                });
+              }
+
+              nextController()
             });    
           })
         });
@@ -81,6 +91,11 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     });
   }
 
+  /*
+  <Highlight onClick={() => this.props.setCurrentView('integrations')}>
+    Link your registry.
+  </Highlight>
+  */
   renderImageList = () => {
     let { images, loading, error } = this.state;
     if (loading) {
@@ -91,9 +106,6 @@ export default class ImageSelector extends Component<PropsType, StateType> {
       return (
         <LoadingWrapper>
           No registries found. 
-          <Highlight onClick={() => this.props.setCurrentView('integrations')}>
-            Link your registry.
-          </Highlight>
         </LoadingWrapper>
       );
     }
@@ -156,7 +168,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
               selectedTag={selectedTag}
               selectedImageUrl={selectedImageUrl}
               setSelectedTag={setSelectedTag}
-              registryId={this.state.registryId}
+              registryId={this.state.clickedImage.registryId}
             />
           </ExpandedWrapper>
           {this.renderBackButton()}

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

@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
+import _ from 'lodash';
 
 import { Section, FormElement } from '../../shared/types';
 import { Context } from '../../shared/Context';
@@ -17,6 +18,7 @@ type PropsType = {
   sections?: Section[],
   disabled?: boolean,
   saveValuesStatus?: string | null,
+  config?: any, // Chart config object containing existing values
 };
 
 type StateType = any;
@@ -32,6 +34,13 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         let key = item.name || item.variable;
         
         let def = item.settings && item.settings.default;
+
+        // Set default value from chart config if available
+        if (this.props.config) {
+          let retrievedValue = _.get(this.props.config, key)
+          retrievedValue ? def = retrievedValue : null;
+        }
+
         switch (item.type) {
           case 'checkbox':
             formState[key] = def ? def : false;
@@ -53,6 +62,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
 
   // Initialize corresponding state fields for form blocks
   componentDidMount() {
+    console.log(this.props.sections)
     this.updateFormState();
   }
 
@@ -139,6 +149,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   }
 
   render() {
+    console.log('save values status', this.props.saveValuesStatus)
     return (
       <Wrapper>
         <StyledValuesForm>

+ 9 - 0
dashboard/src/main/home/Home.tsx

@@ -15,6 +15,7 @@ import CreateProjectModal from './modals/CreateProjectModal';
 import UpdateProjectModal from './modals/UpdateProjectModal';
 import ClusterInstructionsModal from './modals/ClusterInstructionsModal';
 import IntegrationsModal from './modals/IntegrationsModal';
+import IntegrationsInstructionsModal from './modals/IntegrationsInstructionsModal';
 
 type PropsType = {
   logOut: () => void
@@ -140,6 +141,14 @@ export default class Home extends Component<PropsType, StateType> {
         >
           <IntegrationsModal />
         </ReactModal>
+        <ReactModal
+          isOpen={currentModal === 'IntegrationsInstructionsModal'}
+          onRequestClose={() => setCurrentModal(null, null)}
+          style={TallModalStyles}
+          ariaHideApp={false}
+        >
+          <IntegrationsInstructionsModal />
+        </ReactModal>
 
         <Sidebar
           logOut={this.props.logOut}

+ 15 - 15
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -63,50 +63,50 @@ export default class ChartList extends Component<PropsType, StateType> {
 
   setupWebsocket = (kind: string) => {
       let { currentCluster, currentProject } = this.context;
-      let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
-      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
+      let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws';
+      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`);
       ws.onopen = () => {
-        console.log('connected to websocket')
+        console.log('connected to websocket');
       }
   
       ws.onmessage = (evt: MessageEvent) => {
-        let event = JSON.parse(evt.data)
-        let object = event.Object
-        let chartKey = this.state.chartLookupTable[object.metadata.uid]
+        let event = JSON.parse(evt.data);
+        let object = event.Object;
+        let chartKey = this.state.chartLookupTable[object.metadata.uid];
 
         // ignore if updated object does not belong to any chart in the list.
         if (!chartKey) {
           return;
         }
 
-        let chartControllers = this.state.controllers[chartKey]
-        chartControllers[object.metadata.uid] = object
+        let chartControllers = this.state.controllers[chartKey];
+        chartControllers[object.metadata.uid] = object;
 
         this.setState({
           controllers: {
             ...this.state.controllers,
             [chartKey] : chartControllers
           }
-        })
+        });
       }
   
       ws.onclose = () => {
-        console.log('closing websocket')
+        console.log('closing websocket');
       }
   
       ws.onerror = (err: ErrorEvent) => {
-        console.log(err)
-        ws.close()
+        console.log(err);
+        ws.close();
       }
 
-      return ws
+      return ws;
   }
 
   setControllerWebsockets = (controllers: any[]) => {
     let websockets = controllers.map((kind: string) => {
-      return this.setupWebsocket(kind)
+      return this.setupWebsocket(kind);
     })
-    this.setState({websockets})
+    this.setState({ websockets });
   }
 
   getControllers = (charts: any[]) => {

+ 134 - 115
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -3,6 +3,7 @@ import styled from 'styled-components';
 import yaml from 'js-yaml';
 import { Base64 } from 'js-base64';
 import close from '../../../../assets/close.png';
+import _ from 'lodash';
 
 import { ResourceType, ChartType, StorageType, ChoiceType } from '../../../../shared/types';
 import { Context } from '../../../../shared/Context';
@@ -16,6 +17,7 @@ import ListSection from './ListSection';
 import StatusSection from './status/StatusSection';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 import SettingsSection from './SettingsSection';
+import { NavLink } from 'react-router-dom';
 
 type PropsType = {
   currentChart: ChartType,
@@ -31,15 +33,12 @@ type StateType = {
   podSelectors: string[]
   revisionPreview: ChartType | null,
   devOpsMode: boolean,
-  tabOptions: ChoiceType[],
+  tabOptions: any[],
   tabContents: any,
-  checkTabExists: boolean,
+  currentTab: string | null,
   saveValuesStatus: string | null,
 };
 
-// Tabs not display when previewing an old revision
-const excludedTabs = ['status', 'settings', 'deploy'];
-
 /*
   TODO: consolidate revisionPreview and currentChart (currentChart can just be the initial state)
   In general, tab management for ExpandedChart should be refactored. Cases to handle:
@@ -55,9 +54,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     podSelectors: [] as string[],
     revisionPreview: null as (ChartType | null),
     devOpsMode: localStorage.getItem('devOpsMode') === 'true',
-    tabOptions: [] as ChoiceType[],
+    tabOptions: [] as any[],
     tabContents: [] as any,
-    checkTabExists: false,
+    currentTab: null as string | null,
     saveValuesStatus: null as (string | null),
   }
 
@@ -77,34 +76,51 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       if (err) {
         console.log(err)
       } else {
-        this.setState({ components: res.data.Objects, podSelectors: res.data.PodSelectors }, this.refreshTabs);
+        this.setState({ components: res.data.Objects, podSelectors: res.data.PodSelectors });
       }
     });
   }
 
+  componentDidMount() {
+    this.updateTabs();
+    this.updateResources();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (this.props.currentChart !== prevProps.currentChart) {
+      this.updateResources();
+    }
+  }
+
   getFormData = (): any => {
     let { files } = this.props.currentChart.chart;
     for (const file of files) { 
       if (file.name === 'form.yaml') {
         let formData = yaml.load(Base64.decode(file.data));
-        /*
-        if (this.props.currentChart.config) {
-          console.log(formData)
-        }
-        */
         return formData;
       }
     };
     return null;
   }
 
-  upgradeValues = (values: any) => {
+  upgradeValues = (rawValues: any) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
-    values = yaml.dump(values);
+
+    // Convert dotted keys to nested objects
+    let values = {};
+    for (let key in rawValues) {
+      _.set(values, key, rawValues[key]);
+    }
+
+    // Weave in preexisting values and convert to yaml
+    let valuesYaml = yaml.dump({ ...(this.props.currentChart.config as Object), ...values });
+    
+    this.setState({ saveValuesStatus: 'loading' });
+    this.props.refreshChart();
     api.upgradeChartValues('<token>', {
       namespace: this.props.currentChart.namespace,
       storage: StorageType.Secret,
-      values,
+      values: valuesYaml,
     }, {
       id: currentProject.id, 
       name: this.props.currentChart.name,
@@ -119,116 +135,126 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       }
     });
   }
-
-  refreshTabs = () => {
-    let formData = this.getFormData();
-    let tabOptions = [] as ChoiceType[];
-    let tabContents = [] as any;
-
-    // Generate form tabs if form.yaml exists
-    if (formData && formData.tabs) {
-      formData.tabs.map((tab: any, i: number) => {
-        tabOptions.push({ value: '@' + tab.name, label: tab.label });
-        tabContents.push({
-          value: '@' + tab.name,
-          component: (
-            <ValuesFormWrapper>
-              <ValuesForm 
-                sections={tab.sections} 
-                onSubmit={this.upgradeValues}
-                saveValuesStatus={this.state.saveValuesStatus}
-              />
-            </ValuesFormWrapper>
-          ),
-        });
-      });
-    }
-
-    // Append universal tabs
-    tabOptions.push(
-      { label: 'Status', value: 'status' },
-      //{ label: 'Deploy', value: 'deploy' },
-      { label: 'Chart Overview', value: 'graph' },
-      { label: 'Settings', value: 'settings' },
-    );
-
-    if (this.state.devOpsMode) {
-      tabOptions.push(
-        { label: 'Manifests', value: 'list' },
-        { label: 'Raw Values', value: 'values' }
-      );
-    }
-
+  
+  renderTabContents = () => {
+    let { 
+      currentTab, 
+      podSelectors, 
+      components, 
+      showRevisions,
+      saveValuesStatus,
+      tabOptions,
+      revisionPreview,
+    } = this.state;
     let { currentChart, refreshChart, setSidebar, setCurrentView } = this.props;
-    let chart = this.state.revisionPreview || currentChart;
-    tabContents.push(
-      {
-        value: 'status', component: (
-          <StatusSection currentChart={chart} selectors={this.state.podSelectors} />
-        ),
-      },
-      {
-        value: 'deploy', component: (
+    let chart = revisionPreview || currentChart;
+
+    switch (currentTab) {
+      case 'status': 
+        return (
+          <StatusSection currentChart={chart} selectors={podSelectors} />
+        );
+      case 'deploy': 
+        return (
           <Unimplemented>Coming soon.</Unimplemented> 
-        ),
-      },
-      {
-        value: 'settings', component: (
+        );
+      case 'settings': 
+        return (
           <SettingsSection
             currentChart={chart}
             refreshChart={refreshChart}
             setCurrentView={setCurrentView}
           /> 
-        ),
-      },
-      {
-        value: 'graph', component: (
+        );
+      case 'graph': 
+        return (
           <GraphSection
-            components={this.state.components}
+            components={components}
             currentChart={chart}
             setSidebar={setSidebar}
 
             // Handle resize YAML wrapper
-            showRevisions={this.state.showRevisions}
+            showRevisions={showRevisions}
           />
-        ),
-      },
-      {
-        value: 'list', component: (
+        );
+      case 'list': 
+        return (
           <ListSection
             currentChart={chart}
-            components={this.state.components}
+            components={components}
 
             // Handle resize YAML wrapper
-            showRevisions={this.state.showRevisions}
+            showRevisions={showRevisions}
           />
-        ),
-      },
-      {
-        value: 'values', component: (
+        );
+      case 'values': 
+        return (
           <ValuesYaml
             currentChart={chart}
             refreshChart={refreshChart}
           />
-        ),
-      },
-    );
-    this.setState({ tabOptions, tabContents });
+        );
+      default:
+        if (currentTab && currentTab.includes('@')) {
+          return tabOptions.map((tab: any, i: number) => {
+
+            // If tab is current, render
+            if (tab.value === currentTab) {
+              
+              return (
+                <ValuesFormWrapper>
+                  <ValuesForm 
+                    sections={tab.sections} 
+                    onSubmit={this.upgradeValues}
+                    saveValuesStatus={saveValuesStatus}
+                    config={chart.config}
+                  />
+                </ValuesFormWrapper>
+              );
+            }
+          });
+        }
+    }
   }
 
-  componentDidMount() {
-    this.updateResources();
-  }
+  updateTabs = () => {
+    let formData = this.getFormData();
+    let tabOptions = [] as any[];
 
-  componentDidUpdate(prevProps: PropsType) {
-    if (this.props.currentChart !== prevProps.currentChart) {
-      this.updateResources();
+    // Generate form tabs if form.yaml exists
+    if (formData && formData.tabs) {
+      formData.tabs.map((tab: any, i: number) => {
+        tabOptions.push({ value: '@' + tab.name, label: tab.label, sections: tab.sections });
+      });
+    }
+
+    // Append universal tabs
+    tabOptions.push(
+      { label: 'Status', value: 'status' },
+      //{ label: 'Deploy', value: 'deploy' },
+      { label: 'Chart Overview', value: 'graph' },
+      { label: 'Settings', value: 'settings' },
+    );
+
+    if (this.state.devOpsMode) {
+      tabOptions.push(
+        { label: 'Manifests', value: 'list' },
+        { label: 'Raw Values', value: 'values' }
+      );
     }
+
+    // Filter tabs if previewing an old revision
+    if (this.state.revisionPreview) {
+      let liveTabs = ['status', 'settings', 'deploy'];
+      tabOptions = tabOptions.filter((tab: any) => !liveTabs.includes(tab.value));
+    }
+    
+    this.setState({ tabOptions });
   }
 
   setRevisionPreview = (oldChart: ChartType) => {
     let { currentCluster, currentProject } = this.context;
-    this.setState({ revisionPreview: oldChart, checkTabExists: true });
+    this.setState({ revisionPreview: oldChart }, () => this.updateTabs());
 
     if (oldChart) {
       api.getChartComponents('<token>', {
@@ -243,11 +269,10 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         if (err) {
           console.log(err)
         } else {
-          this.setState({ components: res.data.Objects, podSelectors: res.data.PodSelectors }, this.refreshTabs);
+          this.setState({ components: res.data.Objects, podSelectors: res.data.PodSelectors });
         }
       });
     } else {
-      this.setState({ checkTabExists: false });
       this.updateResources();
     }
   }
@@ -255,19 +280,13 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   // TODO: consolidate with pop + push in refreshTabs
   toggleDevOpsMode = () => {
     if (this.state.devOpsMode) {
-      let { tabOptions } = this.state;
-      tabOptions.pop();
-      tabOptions.pop();
-      this.setState({ devOpsMode: false, checkTabExists: true, tabOptions }, () => {
-        localStorage.setItem('devOpsMode', 'false')
+      this.setState({ devOpsMode: false }, () => {
+        this.updateTabs();
+        localStorage.setItem('devOpsMode', 'false');
       });
     } else {
-      let { tabOptions } = this.state;
-      tabOptions.push(
-        { label: 'Manifests', value: 'list' },
-        { label: 'Raw Values', value: 'values' }
-      );
-      this.setState({ devOpsMode: true, tabOptions, checkTabExists: false }, () => {
+      this.setState({ devOpsMode: true }, () => {
+        this.updateTabs();
         localStorage.setItem('devOpsMode', 'true');
       });
     }
@@ -333,18 +352,18 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           </HeaderWrapper>
 
           <TabRegion
-            options={this.state.tabOptions.filter((opt: any) => {
-              return !this.state.revisionPreview || !excludedTabs.includes(opt.value);
-            })}
-            tabContents={this.state.tabContents}
-            checkTabExists={this.state.checkTabExists}
+            currentTab={this.state.currentTab}
+            setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+            options={this.state.tabOptions}
             color={this.state.revisionPreview ? '#f5cb42' : null}
             addendum={
               <TabButton onClick={this.toggleDevOpsMode} devOpsMode={this.state.devOpsMode}>
                 <i className="material-icons">offline_bolt</i> DevOps Mode
               </TabButton>
             }
-          />
+          >
+            {this.renderTabContents()}
+          </TabRegion>
         </StyledExpandedChart>
       </div>
     );

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx

@@ -45,6 +45,7 @@ export default class ListSection extends Component<PropsType, StateType> {
       let rawYaml = yaml.dump(resource.RawYAML);
       return (
         <ResourceTab
+          key={i}
           handleClick={() => this.setState({ yaml: rawYaml })}
           selected={this.state.yaml === rawYaml}
           kind={resource.Kind}

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -31,6 +31,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   }
 
   redeployWithNewImage = (img: string, tag: string) => {
+    this.setState({saveValuesStatus: 'loading'})
     let { currentCluster, currentProject } = this.context;
     let image = {
       image: {
@@ -78,6 +79,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
           onClick={() => this.redeployWithNewImage(this.state.selectedImageUrl, this.state.selectedTag)}
           status={this.state.saveValuesStatus}
           makeFlush={true}
+          disabled={this.state.selectedImageUrl && this.state.selectedTag ? false : true}
         />
       </Wrapper>
     );

+ 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 "replicaset":
         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":
-       return [c.status?.readyReplicas, c.status?.replicas]
+       return [c.status?.readyReplicas || 0, c.status?.replicas || 0]
       case "daemonset":
-        return [c.status?.numberAvailable, c.status?.desiredNumberScheduled]
+        return [c.status?.numberAvailable || 0, c.status?.desiredNumberScheduled || 0]
       }
   }
 

+ 1 - 1
dashboard/src/main/home/integrations/Integrations.tsx

@@ -36,7 +36,7 @@ export default class Integrations extends Component<PropsType, StateType> {
           if (err) {
             console.log(err);
           } else {
-            console.log(res.data)
+            // console.log(res.data)
           }
         });
         break;

+ 1 - 1
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -31,7 +31,7 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
             1. To install the Porter CLI, first retrieve the latest binary:
             <Code>
               &#123;<br />
-              name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*_Darwin_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")<br />
+              name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*porter_.*_Darwin_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")<br />
               name=$(basename $name)<br />
               curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name<br />
               unzip -a $name<br />

+ 197 - 0
dashboard/src/main/home/modals/IntegrationsInstructionsModal.tsx

@@ -0,0 +1,197 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import close from '../../../assets/close.png';
+import TabSelector from '../../../components/TabSelector';
+
+import { Context } from '../../../shared/Context';
+
+type PropsType = {
+};
+
+type StateType = {
+  currentTab: string,
+  currentPage: number,
+};
+
+const tabOptions = [
+  { label: 'MacOS', value: 'mac' }
+];
+
+export default class ClusterInstructionsModal extends Component<PropsType, StateType> {
+  state = {
+    currentTab: 'mac',
+    currentPage: 0,
+  }
+
+  renderPage = () => {
+    switch (this.state.currentPage) {
+      case 0:
+        return (
+          <Placeholder>
+            <Bold>Elastic Container Registry (ECR):</Bold>
+            1. Run the following command on the Porter CLI.
+            <Code>
+              porter connect ecr
+            </Code>
+            2. Enter the region your ECR instance belongs to. For example:
+            <Code>
+              AWS Region: us-west-2
+            </Code>
+            3. Porter will automatically set up an IAM user in your AWS account to grant ECR access. Once this is done, it will prompt you to enter a name for the registry. Here you may enter any name you'd like.
+            <Code>
+              Give this registry a name: my-awesome-registry
+            </Code>
+          </Placeholder>
+        );
+      default:
+        return
+    }
+  }
+ 
+  render() {
+    let { currentPage, currentTab } = this.state;
+    return (
+      <StyledClusterInstructionsModal>
+        <CloseButton onClick={() => {
+          this.context.setCurrentModal(null, null);
+        }}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+
+        <ModalTitle>Connecting to an Image Registry</ModalTitle>
+
+        <TabSelector
+          options={tabOptions}
+          currentTab={currentTab}
+          setCurrentTab={(value: string) => this.setState({ currentTab: value })}
+        />
+
+        {this.renderPage()}
+      </StyledClusterInstructionsModal>
+    );
+  }
+}
+
+ClusterInstructionsModal.contextType = Context;
+
+const PageCount = styled.div`
+  margin-right: 9px;
+  user-select: none;
+  letter-spacing: 2px;
+`;
+
+const PageSection = styled.div`
+  position: absolute;
+  bottom: 22px;
+  right: 20px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #ffffff;
+  justify-content: flex-end;
+  user-select: none;
+  
+  > i {
+    font-size: 18px;
+    margin-left: 2px;
+    cursor: pointer;
+    border-radius: 20px;
+    padding: 5px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Code = styled.div`
+  background: #181B21;
+  padding: 10px 15px;
+  border: 1px solid #ffffff44;
+  border-radius: 5px;
+  margin: 10px 0px 15px;
+  color: #ffffff;
+  font-size: 13px;
+  user-select: text;
+  line-height: 1em;
+  font-family: monospace;
+`;
+
+const A = styled.a`
+  color: #ffffff;
+  text-decoration: underline;
+  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+`;
+
+const Placeholder = styled.div`
+  color: #aaaabb;
+  font-size: 13px;
+  margin-left: 0px;
+  margin-top: 25px;
+  line-height: 1.5em;
+  user-select: none;
+`;
+
+const Bold = styled.div`
+  font-weight: 600;
+  margin-bottom: 7px;
+`;
+
+const Subtitle = styled.div`
+  padding: 10px 0px 20px;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  margin-top: 3px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const ModalTitle = styled.div`
+  margin: 0px 0px 13px;
+  display: flex;
+  flex: 1;
+  font-family: 'Assistant';
+  font-size: 18px;
+  color: #ffffff;
+  user-select: none;
+  font-weight: 700;
+  align-items: center;
+  position: relative;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  z-index: 1;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const StyledClusterInstructionsModal= styled.div`
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  padding: 25px 32px;
+  overflow: hidden;
+  border-radius: 6px;
+  background: #202227;
+`;

+ 3 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -124,8 +124,9 @@ export default class Sidebar extends Component<PropsType, StateType> {
             Templates
           </NavButton>
           <NavButton
-            onClick={() => this.props.setCurrentView('integrations')}
-            selected={this.props.currentView === 'integrations'}
+            // onClick={() => this.props.setCurrentView('integrations')}
+            // selected={this.props.currentView === 'integrations'}
+            onClick={() => this.context.setCurrentModal('IntegrationsInstructionsModal', {})}
           >
             <img src={integrations} />
             Integrations

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

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

+ 82 - 27
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-
+import randomWords from 'random-words';
 import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 
@@ -19,85 +19,114 @@ type PropsType = {
 type StateType = {
   currentView: string,
   clusterOptions: { label: string, value: string }[],
+  saveValuesStatus: string | null
+  selectedNamespace: string,
   selectedCluster: string,
   selectedImageUrl: string | null,
   selectedTag: string | null,
   tabOptions: ChoiceType[],
+  currentTab: string | null,
   tabContents: any
+  namespaceOptions: { label: string, value: string }[],
 };
 
 export default class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
     currentView: 'repo',
     clusterOptions: [] as { label: string, value: string }[],
+    saveValuesStatus: null as (string | null),
     selectedCluster: this.context.currentCluster.name,
+    selectedNamespace: "default",
     selectedImageUrl: '' as string | null,
     selectedTag: '' as string | null,
     tabOptions: [] as ChoiceType[],
+    currentTab: null as string | null,
     tabContents: [] as any,
+    namespaceOptions: [] as { label: string, value: string }[],
   };
 
   onSubmit = (formValues: any) => {
     let { currentCluster, currentProject } = this.context;
+    let name = randomWords({ exactly: 3, join: '-' })
+    this.setState({ saveValuesStatus: 'loading' });
+
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
-      imageURL: "index.docker.io/bitnami/redis",
+      imageURL: "",
       storage: StorageType.Secret,
       formValues,
+      namespace: this.state.selectedNamespace,
+      name,
     }, {
       id: currentProject.id,
       cluster_id: currentCluster.id,
     }, (err: any, res: any) => {
       if (err) {
-        console.log(err)
+        this.setState({ saveValuesStatus: 'error' });
       } else {
-        console.log(res.data)
+        this.setState({ saveValuesStatus: 'successful' });
       }
     });
   }
 
-  refreshTabs = () => {
-    // Generate settings tabs from the provided form
-    let tabOptions = [] as ChoiceType[];
-    let tabContents = [] as any;
-    this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
-      tabOptions.push({ value: tab.name, label: tab.label });
-      tabContents.push({
-        value: tab.name, component: (
+  renderTabContents = () => {
+    return this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
+
+      // If tab is current, render
+      if (tab.name === this.state.currentTab) {
+        return (
           <ValuesFormWrapper>
             <ValuesForm 
+              key={tab.name}
               sections={tab.sections} 
               onSubmit={this.onSubmit}
-              disabled={!this.state.selectedImageUrl || this.state.selectedImageUrl === ''}
+              // disabled={!this.state.selectedImageUrl || this.state.selectedImageUrl === ''}
+              disabled={false}
+              saveValuesStatus={this.state.saveValuesStatus}
             />
           </ValuesFormWrapper>
-        ),
-      });
+        );
+      }
     });
-    this.setState({ tabOptions, tabContents });
   }
 
   componentDidMount() {
-    this.refreshTabs();
+
+    // Retrieve tab options
+    let tabOptions = [] as ChoiceType[];
+    this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
+      tabOptions.push({ value: tab.name, label: tab.label });
+    });
+    this.setState({ tabOptions });
 
     // 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) => {
       if (err) {
         // console.log(err)
       } else if (res.data) {
+        console.log(res.data)
         let clusterOptions = res.data.map((x: Cluster) => { return { label: x.name, value: x.name } });
         if (res.data.length > 0) {
           this.setState({ clusterOptions });
         }
       }
     });
-  }
 
-  componentDidUpdate(prevProps: PropsType, prevState: StateType) {
-    if (this.state.selectedImageUrl != prevState.selectedImageUrl) {
-      this.refreshTabs();
-    }
+    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) => {
@@ -142,9 +171,21 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             dropdownWidth='335px'
             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>
 
-        <Subtitle>Select the container image you would like to connect to this template.</Subtitle>
+        {/* <Subtitle>Select the container image you would like to connect to this template (optional).</Subtitle>
         <Br />
         <ImageSelector
           selectedTag={this.state.selectedTag}
@@ -155,12 +196,15 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           setCurrentView={this.props.setCurrentView}
         />
 
-        <br />
+        <br /> */}
         <Subtitle>Configure additional settings for this template (optional).</Subtitle>
         <TabRegion
           options={this.state.tabOptions}
-          tabContents={this.state.tabContents}
-        />
+          currentTab={this.state.currentTab}
+          setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+        >
+          {this.renderTabContents()}
+        </TabRegion>
       </StyledLaunchTemplate>
     );
   }
@@ -199,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`
   width: 21px;
   margin-right: 10px;

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

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

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -136,4 +136,5 @@ export interface ChoiceType {
 export interface ImageType {
   kind: string,
   source: string,
+  registryId: number,
 }

+ 26 - 0
docs/GCR.md

@@ -0,0 +1,26 @@
+# Google Container Registry (GCR) Connection
+
+To authenticate a private GCR registry, you will first need a Google Cloud service account with registry viewing permissions. To create a new service account, go to your Google Cloud console and navigate to the **IAM & Admin** tab in the navigation menu and select **Service Accounts**:
+
+<img src="https://files.readme.io/a0c0c75-Screen_Shot_2020-06-24_at_2.51.46_PM.png" width="80%">
+
+Select **Create Service Account** and provide a name and brief description for the new service account. Next, choose the role **Viewer** when you are prompted to grant permissions to your service account:
+
+<img src="https://files.readme.io/aa8cda5-Screen_Shot_2020-06-24_at_4.03.33_PM.png" width="80%">
+
+After the service account has been created, you need to create a JSON key for your service account by going to **Actions** -> **Create key** and then selecting JSON as your key type. Once your JSON key file has downloaded, use the `porter connect gcr` command to add the registry to your project. 
+
+For example, for a key named `gcp-key-file.json` on Mac:
+
+```diff
+$ cd ~/Downloads
+$ porter connect gcr 
+Please provide the full path to a service account key file.
+Key file location: ./gcp-key-file.json
++ created gcp integration with id 3
+Give this registry a name: gcr-registry
++ created registry with id 16 and name gcr-test
++ Set the current registry id as 16
+```
+
+Having issues authenticating your private registry? You can reach us at [contact@getporter.dev](mailto:contact@getporter.dev).

+ 1 - 0
internal/forms/release.go

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

+ 29 - 27
internal/helm/agent.go

@@ -6,7 +6,6 @@ import (
 	"github.com/pkg/errors"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/release"
 	"k8s.io/helm/pkg/chartutil"
 )
@@ -88,9 +87,17 @@ func (a *Agent) UpgradeReleaseByValues(
 	return res, nil
 }
 
-// InstallChart reads the raw values and calls Agent.InstallChartByValues
-func (a *Agent) InstallChart(
-	cp string,
+// InstallChartConfig is the config required to install a chart
+type InstallChartConfig struct {
+	Chart     *chart.Chart
+	Name      string
+	Namespace string
+	Values    map[string]interface{}
+}
+
+// InstallChartFromValuesBytes reads the raw values and calls Agent.InstallChart
+func (a *Agent) InstallChartFromValuesBytes(
+	conf *InstallChartConfig,
 	values []byte,
 ) (*release.Release, error) {
 	valuesYaml, err := chartutil.ReadValues(values)
@@ -99,45 +106,40 @@ func (a *Agent) InstallChart(
 		return nil, fmt.Errorf("Values could not be parsed: %v", err)
 	}
 
-	return a.InstallChartByValues(cp, valuesYaml)
+	conf.Values = valuesYaml
+
+	return a.InstallChart(conf)
 }
 
-// InstallChartByValues installs a new chart by unmarshalled values via
-// URL, absolute or relative filepaths.
-//
-// Equivalent to `helm install [CHART_NAME] [cp]` where cp is one of the following:
-//  1) Absolute URL: https://example.com/charts/nginx-1.2.3.tgz
-//  2) path to packaged chart ./nginx-1.2.3.tgz
-//  3) path to unpacked chart ./nginx
-func (a *Agent) InstallChartByValues(
-	cp string,
-	values map[string]interface{},
+// InstallChart installs a new chart
+func (a *Agent) InstallChart(
+	conf *InstallChartConfig,
 ) (*release.Release, error) {
 	cmd := action.NewInstall(a.ActionConfig)
 
-	// Only supports filepaths for now, URL option WIP.
-	// Check chart dependencies to make sure all are present in /charts
-	chartRequested, err := loader.Load(cp)
-	if err != nil {
-		return nil, err
+	if cmd.Version == "" && cmd.Devel {
+		cmd.Version = ">0.0.0-0"
 	}
 
-	if err := checkIfInstallable(chartRequested); err != nil {
+	cmd.ReleaseName = conf.Name
+	cmd.Namespace = conf.Namespace
+
+	if err := checkIfInstallable(conf.Chart); err != nil {
 		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 err := action.CheckDependencies(chartRequested, req); err != nil {
+	if req := conf.Chart.Metadata.Dependencies; req != nil {
+		if err := action.CheckDependencies(conf.Chart, req); err != nil {
 			// TODO: Handle dependency updates.
 			return nil, err
 		}
 	}
 
-	return cmd.Run(chartRequested, values)
+	return cmd.Run(conf.Chart, conf.Values)
 }
 
 // RollbackRelease rolls a release back to a specified revision/version

+ 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"
+      }
+    }
+  }
+}

+ 17 - 3
server/api/deploy_handler.go

@@ -22,6 +22,7 @@ import (
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
+
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
@@ -63,7 +64,9 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// 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
 	for k := range form.ChartTemplateForm.FormValues {
@@ -98,13 +101,24 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	var tgz string
+	switch form.ChartTemplateForm.TemplateName {
+	case "redis":
+		tgz = "redis-0.0.1.tgz"
+	}
+
 	// 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 {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 			Code:   ErrReleaseDeploy,
-			Errors: []string{"error installing a new chart" + err.Error()},
+			Errors: []string{"error installing a new chart: " + err.Error()},
 		}, w)
 
 		return