Преглед изворни кода

Merge branch 'simplified-view' of github.com:porter-dev/porter into simplified-view

Feroze Mohideen пре 3 година
родитељ
комит
3500527312

+ 7 - 0
api/server/handlers/infra/forms.go

@@ -735,6 +735,13 @@ tabs:
     - type: array-input
       variable: azs
       label: Availability Zones
+  - name: net_settings_single_az_nat_gateway
+    contents:
+    - type: checkbox
+      variable: single_az_nat_gateway
+      label: "Place a NAT gateway inside a single AZ. Disabling this will place a NAT gateway in each AZ, for each subnet in your cluster's VPC."
+      settings:
+        default: true
   - name: nginx_settings
     contents:
     - type: heading

+ 35 - 0
api/server/handlers/stacks/create_porter_app.go

@@ -0,0 +1,35 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+type CreatePorterAppHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewCreatePorterAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreatePorterAppHandler {
+	return &CreatePorterAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// ctx := r.Context()
+	// cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+	fmt.Println("congrats on making it!")
+
+	w.WriteHeader(http.StatusCreated)
+}

+ 29 - 0
api/server/router/stack.go

@@ -53,6 +53,35 @@ func getStackRoutes(
 
 	var routes []*router.Route
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/update_config -> stacks.NewCreateStackHandler
+	createPorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/update_config",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createPorterAppHandler := stacks.NewCreatePorterAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createPorterAppEndpoint,
+		Handler:  createPorterAppHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks -> stacks.NewCreateStackHandler
 	createEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 4
api/server/shared/config/env/envconfs.go

@@ -46,10 +46,7 @@ type ServerConf struct {
 	GithubAppWebhookSecret string `env:"GITHUB_APP_WEBHOOK_SECRET"`
 	GithubAppID            string `env:"GITHUB_APP_ID"`
 	GithubAppSecretPath    string `env:"GITHUB_APP_SECRET_PATH"`
-	// GithubAppSecretBase64 is a base64 encoded version of the GithubAppSecret. This can be used instead of GithubAppSecretPath to pass in a key, allowing for support in systems where mounting the secret is not possible.
-	// If GithubAppSecretBase64 is set, it will check for a file at GithubAppSecretPath. If a file is found, the file will NOT be overwritten. If no file it found, then GithubAppSecretBase64 will be decoded and written to GithubAppSecretPath.
-	GithubAppSecretBase64 string `env:"GITHUB_APP_SECRET_BASE64"`
-	GithubAppSecret       []byte
+	GithubAppSecret        []byte
 
 	GoogleClientID         string `env:"GOOGLE_CLIENT_ID"`
 	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`

+ 8 - 39
api/server/shared/config/loader/loader.go

@@ -1,10 +1,8 @@
 package loader
 
 import (
-	"encoding/base64"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -186,51 +184,22 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.Logger.Info().Msg("Created Github client")
 	}
 
-	if sc.GithubAppSecretBase64 != "" {
-		if sc.GithubAppSecretPath == "" {
-			sc.GithubAppSecretPath = "github-app-secret-key"
-		}
-		_, err := os.Stat(sc.GithubAppSecretPath)
-		if err != nil {
-			if !errors.Is(err, os.ErrNotExist) {
-				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error checking if GITHUB_APP_SECRET_PATH exists: %w", err)
-			}
-			secret, err := base64.StdEncoding.DecodeString(sc.GithubAppSecretBase64)
-			if err != nil {
-				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error decoding: %w", err)
-			}
-			_, err = createDirectoryRecursively(sc.GithubAppSecretPath)
-			if err != nil {
-				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error creating directory for GITHUB_APP_SECRET_PATH: %w", err)
-			}
-			err = os.WriteFile(sc.GithubAppSecretPath, secret, os.ModePerm)
-			if err != nil {
-				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error writing to GITHUB_APP_SECRET_PATH: %w", err)
-			}
-		}
-	}
-
 	if sc.GithubAppClientID != "" &&
 		sc.GithubAppClientSecret != "" &&
 		sc.GithubAppName != "" &&
 		sc.GithubAppWebhookSecret != "" &&
 		sc.GithubAppSecretPath != "" &&
 		sc.GithubAppID != "" {
-		if AppID, err := strconv.ParseInt(sc.GithubAppID, 10, 64); err == nil {
-			res.GithubAppConf = oauth.NewGithubAppClient(&oauth.Config{
-				ClientID:     sc.GithubAppClientID,
-				ClientSecret: sc.GithubAppClientSecret,
-				Scopes:       []string{"read:user"},
-				BaseURL:      sc.ServerURL,
-			}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, AppID)
-		}
-
-		secret, err := ioutil.ReadFile(sc.GithubAppSecretPath)
+		AppID, err := strconv.Atoi(sc.GithubAppID)
 		if err != nil {
-			return nil, fmt.Errorf("could not read github app secret: %s", err)
+			return nil, fmt.Errorf("could not read github App ID: %s", err)
 		}
-
-		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
+		res.GithubAppConf = oauth.NewGithubAppClient(&oauth.Config{
+			ClientID:     sc.GithubAppClientID,
+			ClientSecret: sc.GithubAppClientSecret,
+			Scopes:       []string{"read:user"},
+			BaseURL:      sc.ServerURL,
+		}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, int64(AppID))
 	}
 
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {

+ 3 - 2
dashboard/src/components/porter/VerticalSteps.tsx

@@ -16,7 +16,7 @@ const VerticalSteps: React.FC<Props> = ({
     <StyledVerticalSteps>
       {steps.map((step, i) => {
         return (
-          <StepWrapper>
+          <StepWrapper isLast={i === steps.length - 1}>
             {
               (i !== steps.length - 1) && (
                 <Line isActive={i + 1 <= currentStep} />
@@ -83,10 +83,11 @@ const OpacityWrapper = styled.div<{
 `;
 
 const StepWrapper = styled.div<{
+  isLast: boolean;
 }>`
   padding-left: 30px;
   position: relative;
-  margin-bottom: 35px;
+  margin-bottom: ${props => props.isLast ? "" : "35px"};
 `;
 
 const StyledVerticalSteps = styled.div<{

+ 12 - 45
dashboard/src/components/repo-selector/ActionConfBranchSelector.tsx

@@ -5,8 +5,6 @@ import { ActionConfigType } from "shared/types";
 
 import RepoList from "./RepoList";
 import BranchList from "./BranchList";
-import ContentsList from "./ContentsList";
-import ActionDetails from "./ActionDetails";
 import InputRow from "../form-components/InputRow";
 
 type Props = {
@@ -14,31 +12,18 @@ type Props = {
   branch: string;
   setActionConfig: (x: ActionConfigType) => void;
   setBranch: (x: string) => void;
-  reset: any;
-  dockerfilePath: string;
-  procfilePath: string;
-  procfileProcess: string;
   setDockerfilePath: (x: string) => void;
-  setProcfileProcess: (x: string) => void;
-  setProcfilePath: (x: string) => void;
-  folderPath: string;
   setFolderPath: (x: string) => void;
-  setSelectedRegistry: (x: any) => void;
-  selectedRegistry: any;
-  setBuildConfig: (x: any) => void;
 };
 
-const defaultActionConfig: ActionConfigType = {
-  git_repo: "",
-  image_repo_uri: "",
-  git_branch: "",
-  git_repo_id: 0,
-  kind: "github",
-};
-
-const ActionConfEditorStack: React.FC<Props> = (props) => {
-  const { actionConfig, setBranch, setActionConfig, branch } = props;
-
+const ActionConfEditorStack: React.FC<Props> = ({
+  actionConfig,
+  setBranch,
+  setActionConfig,
+  branch,
+  setFolderPath,
+  setDockerfilePath,
+}) => {
   if (!actionConfig.git_repo) {
     return (
       <ExpandedWrapperAlt>
@@ -50,6 +35,7 @@ const ActionConfEditorStack: React.FC<Props> = (props) => {
       </ExpandedWrapperAlt>
     );
   } else if (!branch) {
+    setFolderPath("./");
     return (
       <>
         <ExpandedWrapperAlt>
@@ -69,13 +55,14 @@ const ActionConfEditorStack: React.FC<Props> = (props) => {
         label="Branch"
         type="text"
         width="100%"
-        value={props?.branch}
+        value={branch}
       />
       <BackButton
         width="145px"
         onClick={() => {
+          setFolderPath("");
           setBranch("");
-          props.setDockerfilePath("");
+          setDockerfilePath("");
         }}
       >
         <i className="material-icons">keyboard_backspace</i>
@@ -92,26 +79,6 @@ const Br = styled.div`
   height: 8px;
 `;
 
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const HeaderButton = styled.div`
-  margin-bottom: 5px;
-  padding: 5px 10px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-weight: 500;
-  margin-right: 10px;
-`;
-
-const RepoHeader = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
 const ExpandedWrapper = styled.div`
   margin-top: 10px;
   width: 100%;

+ 9 - 42
dashboard/src/components/repo-selector/ActionConfEditorStack.tsx

@@ -4,28 +4,14 @@ import styled from "styled-components";
 import { ActionConfigType } from "shared/types";
 
 import RepoList from "./RepoList";
-import BranchList from "./BranchList";
-import ContentsList from "./ContentsList";
-import ActionDetails from "./ActionDetails";
 import InputRow from "../form-components/InputRow";
 
 type Props = {
   actionConfig: ActionConfigType | null;
-  branch: string;
   setActionConfig: (x: ActionConfigType) => void;
   setBranch: (x: string) => void;
-  reset: any;
-  dockerfilePath: string;
-  procfilePath: string;
-  procfileProcess: string;
   setDockerfilePath: (x: string) => void;
-  setProcfileProcess: (x: string) => void;
-  setProcfilePath: (x: string) => void;
-  folderPath: string;
   setFolderPath: (x: string) => void;
-  setSelectedRegistry: (x: any) => void;
-  selectedRegistry: any;
-  setBuildConfig: (x: any) => void;
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -36,8 +22,13 @@ const defaultActionConfig: ActionConfigType = {
   kind: "github",
 };
 
-const ActionConfEditorStack: React.FC<Props> = (props) => {
-  const { actionConfig, setBranch, setActionConfig, branch } = props;
+const ActionConfEditorStack: React.FC<Props> = ({
+  actionConfig,
+  setBranch,
+  setActionConfig,
+  setFolderPath,
+  setDockerfilePath,
+}) => {
 
   if (!actionConfig.git_repo) {
     return (
@@ -64,7 +55,8 @@ const ActionConfEditorStack: React.FC<Props> = (props) => {
           onClick={() => {
             setActionConfig({ ...defaultActionConfig });
             setBranch("");
-            props.setDockerfilePath("");
+            setFolderPath("");
+            setDockerfilePath("");
           }}
         >
           <i className="material-icons">keyboard_backspace</i>
@@ -77,31 +69,6 @@ const ActionConfEditorStack: React.FC<Props> = (props) => {
 
 export default ActionConfEditorStack;
 
-const Br = styled.div`
-  width: 100%;
-  height: 8px;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const HeaderButton = styled.div`
-  margin-bottom: 5px;
-  padding: 5px 10px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-weight: 500;
-  margin-right: 10px;
-`;
-
-const RepoHeader = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
 const ExpandedWrapper = styled.div`
   margin-top: 10px;
   width: 100%;

+ 740 - 0
dashboard/src/components/repo-selector/DetectContentsList.tsx

@@ -0,0 +1,740 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import file from "assets/file.svg";
+import folder from "assets/folder.svg";
+import info from "assets/info.svg";
+import close from "assets/close.png";
+
+import api from "../../shared/api";
+import { Context } from "../../shared/Context";
+import { ActionConfigType, FileType } from "../../shared/types";
+
+import Loading from "../Loading";
+import Spacer from "components/porter/Spacer";
+import AdvancedBuildSettings from "main/home/app-dashboard/new-app-flow/AdvancedBuildSettings";
+
+interface AutoBuildpack {
+  name?: string;
+  valid: boolean;
+}
+
+type PropsType = {
+  actionConfig: ActionConfigType | null;
+  branch: string;
+  dockerfilePath?: string;
+  folderPath: string;
+  procfilePath?: string;
+  setActionConfig: (x: ActionConfigType) => void;
+  setProcfileProcess?: (x: string) => void;
+  setDockerfilePath: (x: string) => void;
+  setProcfilePath: (x: string) => void;
+  setFolderPath: (x: string) => void;
+};
+
+type StateType = {
+  loading: boolean;
+  error: boolean;
+  contents: FileType[];
+  currentDir: string;
+  dockerfiles: string[];
+  processes: Record<string, string>;
+  autoBuildpack: AutoBuildpack;
+  showingBuildContextPrompt: boolean;
+};
+
+export default class DetectContentsList extends Component<
+  PropsType,
+  StateType
+> {
+  state = {
+    loading: true,
+    error: false,
+    contents: [] as FileType[],
+    currentDir: "",
+    dockerfiles: [] as string[],
+    processes: null as Record<string, string>,
+    autoBuildpack: {
+      valid: false,
+      name: "",
+    },
+    showingBuildContextPrompt: true,
+  };
+
+  componentDidMount() {
+    this.updateContents();
+  }
+
+  setSubdirectory = (x: string) => {
+    this.setState({ currentDir: x }, () => this.updateContents());
+  };
+
+  fetchContents = () => {
+    let { currentProject } = this.context;
+    const { actionConfig, branch } = this.props;
+
+    if (actionConfig.kind === "gitlab") {
+      return api
+        .getGitlabFolderContent(
+          "<token>",
+          { dir: this.state.currentDir || "./" },
+          {
+            project_id: currentProject.id,
+            integration_id: actionConfig.gitlab_integration_id,
+            repo_owner: actionConfig.git_repo.split("/")[0],
+            repo_name: actionConfig.git_repo.split("/")[1],
+            branch: branch,
+          }
+        )
+        .then((res) => {
+          const { data } = res;
+
+          return {
+            data: data.map((x: FileType) => ({
+              ...x,
+              type: x.type === "tree" ? "dir" : "file",
+            })),
+          };
+        });
+    }
+    return api.getBranchContents(
+      "<token>",
+      { dir: this.state.currentDir || "./" },
+      {
+        project_id: currentProject.id,
+        git_repo_id: actionConfig.git_repo_id,
+        kind: "github",
+        owner: actionConfig.git_repo.split("/")[0],
+        name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  detectBuildpacks = () => {
+    let { currentProject } = this.context;
+    let { actionConfig, branch } = this.props;
+
+    if (actionConfig.kind === "github") {
+      return api.detectBuildpack(
+        "<token>",
+        {
+          dir: this.state.currentDir || ".",
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+          branch: branch,
+        }
+      );
+    }
+
+    return api.detectGitlabBuildpack(
+      "<token>",
+      { dir: this.state.currentDir || "." },
+      {
+        project_id: currentProject.id,
+        integration_id: actionConfig.gitlab_integration_id,
+
+        repo_owner: actionConfig.git_repo.split("/")[0],
+        repo_name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  fetchProcfileContent = (procfilePath: string) => {
+    let { currentProject } = this.context;
+    let { actionConfig, branch } = this.props;
+    if (actionConfig.kind === "github") {
+      return api.getProcfileContents(
+        "<token>",
+        {
+          path: procfilePath,
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+          branch: branch,
+        }
+      );
+    }
+
+    return api.getGitlabProcfileContents(
+      "<token>",
+      { path: procfilePath },
+      {
+        project_id: currentProject.id,
+        integration_id: actionConfig.gitlab_integration_id,
+        owner: actionConfig.git_repo.split("/")[0],
+        name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  updateContents = () => {
+    // Get branch contents
+    this.fetchContents()
+      .then((res) => {
+        let files = [] as FileType[];
+        let folders = [] as FileType[];
+        res.data.map((x: FileType, i: number) => {
+          x.type === "dir" ? folders.push(x) : files.push(x);
+        });
+
+        folders.sort((a: FileType, b: FileType) => {
+          return a.path < b.path ? 1 : 0;
+        });
+        files.sort((a: FileType, b: FileType) => {
+          return a.path < b.path ? 1 : 0;
+        });
+        let contents = folders.concat(files);
+
+        this.setState({ contents, loading: false, error: false });
+      })
+      .catch((err) => {
+        console.log(err);
+
+        this.setState({ loading: false, error: true });
+      });
+
+    this.detectBuildpacks()
+      .then(({ data }) => {
+        this.setState({
+          autoBuildpack: data,
+        });
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({
+          autoBuildpack: {
+            valid: false,
+          },
+        });
+      });
+
+    let ppath =
+      this.props.procfilePath ||
+      `${this.state.currentDir ? this.state.currentDir : "."}/Procfile`;
+    this.fetchProcfileContent(ppath)
+      .then(({ data }) => {
+        this.setState({ processes: data });
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  };
+
+  renderContentList = () => {
+    let { contents, loading, error } = this.state;
+    if (loading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    } else if (error || !contents) {
+      return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>;
+    }
+    return contents.map((item: FileType, i: number) => {
+      let splits = item.path.split("/");
+      let fileName = splits[splits.length - 1];
+      console.log(fileName);
+      if (fileName.includes("Dockerfile")) {
+        this.props.setDockerfilePath(item.path);
+        return (
+          <DetectedBuildMessage>
+            <i className="material-icons">check</i>
+            Detected Dockerfile at ./{item.path}
+          </DetectedBuildMessage>
+        );
+      }
+    });
+  };
+
+  renderJumpToParent = () => {
+    if (this.state.currentDir !== "") {
+      let splits = this.state.currentDir.split("/");
+      let subdir = "";
+      if (splits.length !== 1) {
+        subdir = this.state.currentDir.replace(splits[splits.length - 1], "");
+        if (subdir.charAt(subdir.length - 1) === "/") {
+          subdir = subdir.slice(0, subdir.length - 1);
+        }
+      }
+
+      return (
+        <Item lastItem={false} onClick={() => this.setSubdirectory(subdir)}>
+          <BackLabel>..</BackLabel>
+        </Item>
+      );
+    }
+
+    return (
+      <FileItem lastItem={false}>
+        <img src={info} />
+        Select{" "}
+        {this.props.dockerfilePath
+          ? "Docker Build Context"
+          : "Application Folder"}
+      </FileItem>
+    );
+  };
+
+  renderOverlay = () => {
+    if (this.props.procfilePath) {
+      let processes = this.state.processes
+        ? Object.keys(this.state.processes)
+        : [];
+      if (this.state.processes == null) {
+        return (
+          <Overlay>
+            <BgOverlay>
+              <LoadingWrapper>
+                <Loading />
+              </LoadingWrapper>
+            </BgOverlay>
+          </Overlay>
+        );
+      }
+
+      if (processes.length == 0) {
+        this.props.setProcfilePath("");
+      }
+
+      return (
+        <Overlay>
+          <BgOverlay
+            onClick={() =>
+              this.setState({ dockerfiles: [] }, () => {
+                this.props.setFolderPath("");
+                this.props.setProcfilePath("");
+              })
+            }
+          />
+          <CloseButton
+            onClick={() =>
+              this.setState({ dockerfiles: [] }, () => {
+                this.props.setProcfilePath("");
+              })
+            }
+          >
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Porter has detected a Procfile in this folder. Which process would
+            you like to run?
+          </Label>
+          <DockerfileList>
+            {processes.map((process: string, i: number) => {
+              return (
+                <Row
+                  key={i}
+                  onClick={() => {
+                    if (
+                      !this.props.folderPath ||
+                      this.props.folderPath === ""
+                    ) {
+                      this.props.setFolderPath("./");
+                    }
+                    this.props.setProcfileProcess(process);
+                  }}
+                  isLast={processes.length - 1 === i}
+                >
+                  <Indicator selected={false} />
+                  {process}
+                </Row>
+              );
+            })}
+          </DockerfileList>
+        </Overlay>
+      );
+    }
+    if (this.state.dockerfiles.length > 0 && !this.props.dockerfilePath) {
+      return (
+        <Overlay>
+          <BgOverlay onClick={() => this.setState({ dockerfiles: [] })} />
+          <CloseButton onClick={() => this.setState({ dockerfiles: [] })}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Porter has detected at least one Dockerfile in this folder. Would
+            you like to use an existing Dockerfile?
+          </Label>
+          <DockerfileList>
+            {this.state.dockerfiles.map((dockerfile: string, i: number) => {
+              return (
+                <Row
+                  key={i}
+                  onClick={() =>
+                    this.props.setDockerfilePath(
+                      `${this.state.currentDir || "."}/${dockerfile}`
+                    )
+                  }
+                  isLast={this.state.dockerfiles.length - 1 === i}
+                >
+                  <Indicator selected={false}></Indicator>
+                  {dockerfile}
+                </Row>
+              );
+            })}
+          </DockerfileList>
+          <ConfirmButton
+            onClick={() => {
+              this.props.setFolderPath(this.state.currentDir || "./");
+              if (
+                this.state.processes &&
+                Object.keys(this.state.processes).length > 0
+              ) {
+                this.props.setProcfilePath("./Procfile");
+              }
+            }}
+          >
+            No, I don't want to use a Dockerfile
+          </ConfirmButton>
+        </Overlay>
+      );
+    }
+    if (
+      this.props.dockerfilePath &&
+      !this.props.folderPath &&
+      this.state.showingBuildContextPrompt
+    ) {
+      return (
+        <Overlay>
+          <BgOverlay onClick={() => this.props.setDockerfilePath("")} />
+          <CloseButton
+            onClick={() =>
+              this.props.setFolderPath(this.state.currentDir || "./")
+            }
+          >
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Would you like to set the Docker build context to a different
+            directory?
+          </Label>
+          <MultiSelectRow>
+            <ConfirmButton
+              onClick={() => {
+                this.setState({ showingBuildContextPrompt: false });
+                this.setSubdirectory("");
+              }}
+            >
+              Yes
+            </ConfirmButton>
+            <ConfirmButton
+              onClick={() =>
+                this.props.setFolderPath(this.state.currentDir || "./")
+              }
+            >
+              No
+            </ConfirmButton>
+          </MultiSelectRow>
+        </Overlay>
+      );
+    }
+  };
+
+  render() {
+    return (
+      <>
+        {this.renderContentList()}
+
+        {this.state.autoBuildpack && this.state.autoBuildpack.valid ? (
+          <Banner>
+            <i className="material-icons">info</i>{" "}
+            <p>
+              <b>{this.state.autoBuildpack.name}</b> buildpack was{" "}
+              <a
+                href="https://docs.porter.run/deploying-applications/deploying-from-github/selecting-application-and-build-method#customizing-buildpacks"
+                target="_blank"
+              >
+                detected automatically
+              </a>
+              . Alternatively, select an application folder below:
+            </p>
+          </Banner>
+        ) : (
+          <>
+            <Spacer y={1} />
+            <AdvancedBuildSettings />
+          </>
+        )}
+      </>
+    );
+  }
+}
+
+DetectContentsList.contextType = Context;
+
+const FlexWrapper = styled.div`
+  position: absolute;
+  bottom: 28px;
+  left: 195px;
+  display: flex;
+  align-items: center;
+`;
+
+const StatusWrapper = styled.a<{ successful?: boolean }>`
+  display: flex;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #949eff;
+  margin-right: 25px;
+  margin-left: 20px;
+  cursor: pointer;
+  text-decoration: none;
+
+  > i {
+    font-size: 18px;
+    margin-right: 8px;
+  }
+`;
+
+const BgOverlay = styled.div`
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.8);
+  z-index: -1;
+`;
+
+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 Indicator = styled.div<{ selected: boolean }>`
+  border-radius: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  margin-right: 13px;
+  background: ${(props) => (props.selected ? "#ffffff22" : "#ffffff11")};
+`;
+
+const Label = styled.div`
+  max-width: 500px;
+  line-height: 1.5em;
+  text-align: center;
+  font-size: 14px;
+`;
+
+const MultiSelectRow = styled.div`
+  display: flex;
+  min-width: 150px;
+  justify-content: space-between;
+`;
+
+const DockerfileList = styled.div`
+  border-radius: 3px;
+  margin-top: 20px;
+  border: 1px solid #aaaabb;
+  background: #ffffff22;
+  width: 100%;
+  max-width: 500px;
+  max-height: 140px;
+  overflow-y: auto;
+`;
+
+const Row = styled.div<{ isLast: boolean }>`
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  align-items: center;
+  border-bottom: ${(props) => !props.isLast && "1px solid #aaaabb"};
+  cursor: pointer;
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const ConfirmButton = styled.div`
+  font-size: 18px;
+  padding: 7px 12px;
+  outline: none;
+  border: 1px solid white;
+  margin-top: 25px;
+  border-radius: 10px;
+  text-align: center;
+  cursor: pointer;
+  opacity: 0;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  font-weight: 500;
+  animation: linEnter 0.3s 0.1s;
+  animation-fill-mode: forwards;
+  @keyframes linEnter {
+    from {
+      transform: translateY(20px);
+      opacity: 0;
+    }
+    to {
+      transform: translateY(0px);
+      opacity: 1;
+    }
+  }
+  :hover {
+    background: white;
+    color: #232323;
+  }
+`;
+
+const Overlay = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 0 90px;
+`;
+
+const UseButton = styled.div`
+  height: 35px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #616feecc;
+  font-weight: 500;
+  padding: 10px 15px;
+  border-radius: 100px;
+  cursor: pointer;
+  :hover {
+    filter: brightness(120%);
+  }
+`;
+
+const BackLabel = styled.div`
+  font-size: 16px;
+  padding-left: 16px;
+  margin-top: -4px;
+  padding-bottom: 4px;
+`;
+
+const Item = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected?: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: pointer;
+  background: ${(props: { isSelected?: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+`;
+
+const FileItem = styled(Item)`
+  cursor: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "pointer" : "default"};
+  color: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "#fff" : "#ffffff55"};
+  :hover {
+    background: ${(props: { isADocker?: boolean }) =>
+      props.isADocker ? "#ffffff22" : "#ffffff11"};
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)``;
+
+const Banner = styled.div`
+  height: 40px;
+  width: 100%;
+  margin: 5px 0 10px;
+  font-size: 13px;
+  display: flex;
+  border-radius: 8px;
+  padding-left: 15px;
+  align-items: center;
+  background: #ffffff11;
+  > i {
+    margin-right: 10px;
+    font-size: 18px;
+  }
+`;
+const DetectedBuildMessage = styled.div`
+  color: #0f872b;
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  margin-right: 10px;
+
+  > i {
+    margin-right: 6px;
+    font-size: 20px;
+    border-radius: 20px;
+    transform: none;
+  }
+`;

+ 2 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -226,10 +226,10 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   setValue={(e) => {}}
                 />
               </>,
+              <Button onClick={() => setShowGHAModal(true)}>Deploy app</Button>
             ]}
           />
-          <Spacer y={1} />
-          <Button onClick={() => setShowGHAModal(true)}>DEPLYOY</Button>
+          <Spacer y={3} />
         </StyledConfigureTemplate>
       </Div>
       {showGHAModal && (

+ 89 - 340
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -11,8 +11,9 @@ import { ActionConfigType } from "shared/types";
 import { RouteComponentProps } from "react-router";
 import { Context } from "shared/Context";
 import ActionConfBranchSelector from "components/repo-selector/ActionConfBranchSelector";
+import DetectContentsList from "components/repo-selector/DetectContentsList";
 
-type PropsType = RouteComponentProps & {
+type Props = {
   source: SourceType | undefined;
   templateName: string;
   setTemplateName: (x: string) => void;
@@ -48,6 +49,7 @@ type PropsType = RouteComponentProps & {
   setSelectedRegistry: (x: string) => void;
   setBuildConfig: (x: any) => void;
 };
+
 const defaultActionConfig: ActionConfigType = {
   git_repo: "",
   image_repo_uri: "",
@@ -55,252 +57,141 @@ const defaultActionConfig: ActionConfigType = {
   git_repo_id: 0,
   kind: "github",
 };
-type StateType = {};
-class SourceSettings extends Component<PropsType, StateType> {
-  renderGithubSettings = () => {
+
+const SourceSettings: React.FC<Props> = ({
+  source,
+  imageUrl,
+  setImageUrl,
+  imageTag,
+  setImageTag,
+  actionConfig,
+  setActionConfig,
+  setProcfileProcess,
+  branch,
+  setBranch,
+  dockerfilePath,
+  setDockerfilePath,
+  procfilePath,
+  setProcfilePath,
+  folderPath,
+  setFolderPath,
+}) => {
+  const renderGithubSettings = () => {
     return (
       <>
         <Text size={16}>Build settings</Text>
         <Spacer y={0.5} />
         <Text color="helper">Select your Github repository.</Text>
         <Spacer y={0.5} />
-        {/* <Input
-                    placeholder="ex: academic-sophon"
-                    value=""
-                    width="100%"
-                    setValue={(e) => { }}
-                /> */}
-        {
-          <>
-            {" "}
-            {/* <CloseButton
-        onClick={() => {
-          setSourceType("");
-          setDockerfilePath("");
-          setFolderPath("");
-          setProcfilePath("");
-          setProcfileProcess("");
-        }}
-      >
-        <i className="material-icons">close</i>
-      </CloseButton> */}
-            <Subtitle>
-              Provide a repo folder to use as source.
-              {/* <Highlight
-                onClick={() => this.context.setCurrentModal("AccountSettingsModal", {})}
-              >
-                Manage Git repos
-              </Highlight> */}
-              <Required>*</Required>
-              <ActionConfEditorStack
-                actionConfig={this.props.actionConfig}
-                branch={this.props.branch}
-                setActionConfig={(actionConfig: ActionConfigType) => {
-                  this.props.setActionConfig(
-                    (currentActionConfig: ActionConfigType) => ({
-                      ...currentActionConfig,
-                      ...actionConfig,
-                    })
-                  );
-                  this.props.setImageUrl(actionConfig.image_repo_uri);
-                  /*
-            setParentState({ actionConfig }, () =>
-              setParentState({ imageUrl: actionConfig.image_repo_uri })
-            )
-            */
-                }}
-                procfileProcess={this.props.procfileProcess}
-                setProcfileProcess={(procfileProcess: string) => {
-                  this.props.setProcfileProcess(procfileProcess);
-                  this.props.setValuesToOverride((v: any) => ({
-                    ...v,
-                    "container.command": procfileProcess || "",
-                    showStartCommand: !procfileProcess,
-                  }));
-                }}
-                setBranch={this.props.setBranch}
-                setDockerfilePath={this.props.setDockerfilePath}
-                setProcfilePath={this.props.setProcfilePath}
-                procfilePath={this.props.procfilePath}
-                dockerfilePath={this.props.dockerfilePath}
-                folderPath={this.props.folderPath}
-                setFolderPath={this.props.setFolderPath}
-                reset={() => {
-                  this.props.setActionConfig({ ...defaultActionConfig });
-                  this.props.setBranch("");
-                  this.props.setDockerfilePath(null);
-                  this.props.setFolderPath(null);
-                }}
-                setSelectedRegistry={this.props.setSelectedRegistry}
-                selectedRegistry={this.props.selectedRegistry}
-                setBuildConfig={this.props.setBuildConfig}
-              />
-            </Subtitle>
-            <DarkMatter antiHeight="-4px" />
-            <br />
-          </>
-        }
+        <Subtitle>
+          Provide a repo folder to use as source.
+          <Required>*</Required>
+          <ActionConfEditorStack
+            actionConfig={actionConfig}
+            setActionConfig={(actionConfig: ActionConfigType) => {
+              setActionConfig(
+                (currentActionConfig: ActionConfigType) => ({
+                  ...currentActionConfig,
+                  ...actionConfig,
+                })
+              );
+              setImageUrl(actionConfig.image_repo_uri);
+            }}
+            setBranch={setBranch}
+            setDockerfilePath={setDockerfilePath}
+            setFolderPath={setFolderPath}
+          />
+        </Subtitle>
+        <DarkMatter antiHeight="-4px" />
+        <br />
         <Spacer y={0.5} />
-        {this.props.actionConfig.git_repo ? (
+        {actionConfig.git_repo && (
           <>
             <Text color="helper">Select your branch.</Text>
             <ActionConfBranchSelector
-              actionConfig={this.props.actionConfig}
-              branch={this.props.branch}
+              actionConfig={actionConfig}
+              branch={branch}
               setActionConfig={(actionConfig: ActionConfigType) => {
-                this.props.setActionConfig(
+                setActionConfig(
                   (currentActionConfig: ActionConfigType) => ({
                     ...currentActionConfig,
                     ...actionConfig,
                   })
                 );
-                this.props.setImageUrl(actionConfig.image_repo_uri);
-                /*
-      setParentState({ actionConfig }, () =>
-        setParentState({ imageUrl: actionConfig.image_repo_uri })
-      )
-      */
-              }}
-              procfileProcess={this.props.procfileProcess}
-              setProcfileProcess={(procfileProcess: string) => {
-                this.props.setProcfileProcess(procfileProcess);
-                this.props.setValuesToOverride((v: any) => ({
-                  ...v,
-                  "container.command": procfileProcess || "",
-                  showStartCommand: !procfileProcess,
-                }));
+                setImageUrl(actionConfig.image_repo_uri);
               }}
-              setBranch={this.props.setBranch}
-              setDockerfilePath={this.props.setDockerfilePath}
-              setProcfilePath={this.props.setProcfilePath}
-              procfilePath={this.props.procfilePath}
-              dockerfilePath={this.props.dockerfilePath}
-              folderPath={this.props.folderPath}
-              setFolderPath={this.props.setFolderPath}
-              reset={() => {
-                this.props.setActionConfig({ ...defaultActionConfig });
-                this.props.setBranch("");
-                this.props.setDockerfilePath(null);
-                this.props.setFolderPath(null);
-              }}
-              setSelectedRegistry={this.props.setSelectedRegistry}
-              selectedRegistry={this.props.selectedRegistry}
-              setBuildConfig={this.props.setBuildConfig}
+              setBranch={setBranch}
+              setDockerfilePath={setDockerfilePath}
+              setFolderPath={setFolderPath}
             />
           </>
-        ) : (
-          <></>
         )}
-        <Spacer y={0.5} />
-        <Spacer y={0.5} />
+        <Spacer y={1} />
         <Text color="helper">Specify your application root path.</Text>
         <Spacer y={0.5} />
         <Input
+          disabled={!branch ? true : false}
           placeholder="ex: ./"
-          value=""
+          value={folderPath}
           width="100%"
-          setValue={(e) => {}}
+          setValue={setFolderPath}
         />
-        <Spacer y={0.5} />
-        <Text color="helper">
-          Specify your porter.yaml path. <a>&nbsp;What is this?</a>
-        </Text>
-        <Spacer y={0.5} />
-        <Input
-          placeholder="ex: ./porter.yaml"
-          value=""
-          width="100%"
-          setValue={(e) => {}}
-        />
-        <Spacer y={1} />
-        <DetectedBuildMessage>
-          <i className="material-icons">check</i>
-          Detected Dockerfile at ./Dockerfile
-        </DetectedBuildMessage>
-        <Spacer y={1} />
-        <AdvancedBuildSettings />
+        {actionConfig.git_repo && branch && (
+          <DetectContentsList
+            actionConfig={actionConfig}
+            branch={branch}
+            dockerfilePath={dockerfilePath}
+            procfilePath={procfilePath}
+            folderPath={folderPath}
+            setActionConfig={setActionConfig}
+            setDockerfilePath={setDockerfilePath}
+            setProcfilePath={setProcfilePath}
+            setProcfileProcess={setProcfileProcess}
+            setFolderPath={setFolderPath}
+          />
+        )}
       </>
     );
   };
 
-  renderDockerSettings = () => {
+  const renderDockerSettings = () => {
     return (
       <>
         <Text size={16}>Registry settings</Text>
         <Spacer y={0.5} />
-        <Text color="helper">Select your Github repository.</Text>
+        <Text color="helper">
+          Specify the complete registry URL for your Docker image:
+        </Text>
         <Spacer height="20px" />
         <Input
-          placeholder="ex: academic-sophon"
-          value=""
+          placeholder="ex: nginx"
+          value={imageUrl}
           width="100%"
-          setValue={(e) => {}}
+          setValue={setImageUrl}
         />
-        <Spacer y={0.5} />
-        <Text color="helper">Select your branch.</Text>
       </>
     );
   };
 
-  render() {
-    return (
-      <SourceSettingsContainer source={this.props.source}>
-        <AnimateHeight height={this.props.source ? "auto" : 0}>
+  return (
+      <SourceSettingsContainer>
+        {source && <Spacer y={1} />}
+        <AnimateHeight height={source ? "auto" : 0}>
           <div>
-            {this.props.source === "github"
-              ? this.renderGithubSettings()
-              : this.renderDockerSettings()}
+            {source === "github" ? (
+              renderGithubSettings()
+            ) : (
+              renderDockerSettings()
+            )}
           </div>
         </AnimateHeight>
       </SourceSettingsContainer>
-    );
-  }
-}
+  )
+};
 
 export default SourceSettings;
 
 const SourceSettingsContainer = styled.div`
-  margin-top: ${(props: { source: SourceType | undefined }) =>
-    props.source && "20px"};
-`;
-const DetectedBuildMessage = styled.div`
-  color: #0f872b;
-  display: flex;
-  align-items: center;
-  border-radius: 5px;
-  margin-right: 10px;
-
-  > i {
-    margin-right: 6px;
-    font-size: 20px;
-    border-radius: 20px;
-    transform: none;
-  }
-`;
-
-const Heading = styled.div<{ isAtTop?: boolean }>`
-  color: white;
-  font-weight: 500;
-  font-size: 16px;
-  margin-bottom: 5px;
-  margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSourcePage = styled.div`
-  position: relative;
-  margin-top: -5px;
-`;
-
-const Buffer = styled.div`
-  width: 100%;
-  height: 15px;
-`;
-
-const Br = styled.div`
-  width: 100%;
-  height: 5px;
 `;
 
 const DarkMatter = styled.div<{ antiHeight?: string }>`
@@ -316,150 +207,8 @@ const Subtitle = styled.div`
   line-height: 1.6em;
 `;
 
-const CloseButton = styled.div`
-  position: absolute;
-  display: block;
-  width: 40px;
-  height: 40px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1;
-  border-radius: 50%;
-  right: 12px;
-  top: 10px;
-  cursor: pointer;
-  :hover {
-    background-color: #ffffff11;
-  }
-
-  > i {
-    font-size: 20px;
-    color: #aaaabb;
-  }
-`;
-
-const CloseButtonImg = styled.img`
-  width: 14px;
-  margin: 0 auto;
-`;
-
-const BlockIcon = styled.img<{ bw?: boolean }>`
-  height: 38px;
-  padding: 2px;
-  margin-top: 30px;
-  margin-bottom: 15px;
-  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
-`;
-
-const BlockDescription = styled.div`
-  margin-bottom: 12px;
-  color: #ffffff66;
-  text-align: center;
-  font-weight: default;
-  font-size: 13px;
-  padding: 0px 25px;
-  height: 2.4em;
-  font-size: 12px;
-  display: -webkit-box;
-  overflow: hidden;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-`;
-
-const BlockTitle = styled.div`
-  margin-bottom: 12px;
-  width: 80%;
-  text-align: center;
-  font-size: 14px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const Block = styled.div<{ disabled?: boolean }>`
-  align-items: center;
-  user-select: none;
-  display: flex;
-  font-size: 13px;
-  overflow: hidden;
-  font-weight: 500;
-  padding: 3px 0px 12px;
-  flex-direction: column;
-  align-item: center;
-  justify-content: space-between;
-  height: 170px;
-  cursor: ${(props) => (props.disabled ? "" : "pointer")};
-  color: #ffffff;
-  position: relative;
-
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-  }
-  :hover {
-    border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};
-  }
-
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const BlockList = styled.div`
-  overflow: visible;
-  margin-top: 6px;
-  margin-bottom: 27px;
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
-`;
-
 const Required = styled.div`
   margin-left: 8px;
   color: #fc4976;
   display: inline-block;
-`;
-
-const InputWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-top: -15px;
-  margin-bottom: -6px;
-`;
-
-const Warning = styled.span`
-  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
-    props.highlight ? "#f5cb42" : ""};
-  margin-left: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
-    props.makeFlush ? "" : "5px"};
-`;
-
-const Highlight = styled.a`
-  color: #8590ff;
-  text-decoration: none;
-  margin-left: 5px;
-  cursor: pointer;
-  display: inline;
-`;
-
-const StyledSourceBox = styled.div`
-  width: 100%;
-  color: #ffffff;
-  padding: 14px 35px 20px;
-  position: relative;
-  font-size: 13px;
-  margin-top: 6px;
-  margin-bottom: 25px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-`;
+`;

+ 1 - 5
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx

@@ -153,11 +153,7 @@ const EnvGroupArray = ({
                   {!disabled && (
                     <DeleteButton
                       onClick={() => {
-                        let _values = values;
-                        _values = _values.filter(
-                          (val) => val.key !== entry.key
-                        );
-                        setValues(_values);
+                        setValues(values.filter((val, index) => index !== i));
                       }}
                     >
                       <i className="material-icons">cancel</i>

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

@@ -164,6 +164,19 @@ const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
   return `/api/email/verify/initiate`;
 });
 
+const createPorterApp = baseApi<
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>("POST", (pathParams) => {
+  let { project_id, cluster_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/update_config`;
+});
+
 const createEnvironment = baseApi<
   {
     name: string;
@@ -2443,6 +2456,7 @@ export default {
   createPasswordResetVerify,
   createPasswordResetFinalize,
   createProject,
+  createPorterApp,
   createConfigMap,
   deleteCluster,
   deleteConfigMap,

+ 5 - 4
docs/developing/setup.md

@@ -28,14 +28,15 @@ First, in `/dashboard/.env`:
 
 ```
 NODE_ENV=development
-API_SERVER=localhost:8080
+API_SERVER=http://localhost:8081
+ENABLE_PROXY=true
 ```
 
 Next, in `/docker/.env`:
 
 ```
-SERVER_URL=http://localhost:8080
-SERVER_PORT=8080
+SERVER_URL=http://localhost:8081
+SERVER_PORT=8081
 DB_HOST=postgres
 DB_PORT=5432
 DB_USER=porter
@@ -44,7 +45,7 @@ DB_NAME=porter
 SQL_LITE=false
 ```
 
-Once you've done this, go to the root repository, and run `docker-compose -f docker-compose.dev.yaml up`. You should see postgres, webpack, and porter containers spin up. When the webpack and porter containers have finished compiling and have spun up successfully (this will take 5-10 minutes after the containers start), .
+Finally, run `make start-dev` from the root of the project. If you have all the dependencies set up correctly, things should just work.
 
 At this point, you can make a change to any `.go` file to trigger a backend rebuild, and any file in `/dashboard/src` to trigger a hot reload.
 

+ 1 - 1
scripts/dev-environment/SetupEnvironment.sh

@@ -31,7 +31,7 @@ fi
 
 if [[ ! -d ./dashboard/node_modules ]]; then 
   echo "Couldn't find node_modules, installing npm packages"
-  cd ./dashboard && npm install;
+  cd ./dashboard && npm install --legacy-peer-deps;
   cd ../;  
 else
   echo "Node modules found! Proceeding to start server"