Просмотр исходного кода

Refactoring stacks build settings (#3173)

Feroze Mohideen 3 лет назад
Родитель
Сommit
27fd9298e1
35 измененных файлов с 2939 добавлено и 3016 удалено
  1. 1 1
      api/types/porter_app.go
  2. 220 50
      dashboard/package-lock.json
  3. 2 0
      dashboard/package.json
  4. 4 0
      dashboard/src/assets/stars-white.svg
  5. 4 0
      dashboard/src/assets/stars.svg
  6. 19 19
      dashboard/src/components/image-selector/ImageSelector.tsx
  7. 0 151
      dashboard/src/components/repo-selector/ActionConfBranchSelector.tsx
  8. 0 117
      dashboard/src/components/repo-selector/ActionConfEditorStack.tsx
  9. 0 656
      dashboard/src/components/repo-selector/BuildpackStack.tsx
  10. 0 678
      dashboard/src/components/repo-selector/DetectContentsList.tsx
  11. 28 28
      dashboard/src/components/repo-selector/RepoList.tsx
  12. 150 0
      dashboard/src/main/home/app-dashboard/build-settings/AddCustomBuildpackComponent.tsx
  13. 137 0
      dashboard/src/main/home/app-dashboard/build-settings/AdvancedBuildSettings.tsx
  14. 200 0
      dashboard/src/main/home/app-dashboard/build-settings/BranchSelector.tsx
  15. 189 0
      dashboard/src/main/home/app-dashboard/build-settings/BuildSettingsTab.tsx
  16. 166 0
      dashboard/src/main/home/app-dashboard/build-settings/BuildpackCard.tsx
  17. 145 0
      dashboard/src/main/home/app-dashboard/build-settings/BuildpackList.tsx
  18. 362 0
      dashboard/src/main/home/app-dashboard/build-settings/BuildpackStack.tsx
  19. 253 0
      dashboard/src/main/home/app-dashboard/build-settings/DetectContentsList.tsx
  20. 146 0
      dashboard/src/main/home/app-dashboard/build-settings/ProviderSelector.tsx
  21. 408 0
      dashboard/src/main/home/app-dashboard/build-settings/RepositorySelector.tsx
  22. 184 0
      dashboard/src/main/home/app-dashboard/build-settings/SharedBuildSettings.tsx
  23. 0 427
      dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx
  24. 38 41
      dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx
  25. 0 159
      dashboard/src/main/home/app-dashboard/expanded-app/SharedBuildSettings.tsx
  26. 0 284
      dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx
  27. 0 9
      dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx
  28. 3 21
      dashboard/src/main/home/app-dashboard/new-app-flow/GithubConnectModal.tsx
  29. 72 206
      dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx
  30. 44 161
      dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx
  31. 1 3
      dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx
  32. 2 1
      dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx
  33. 41 0
      dashboard/src/main/home/app-dashboard/types/porterApp.ts
  34. 4 4
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx
  35. 116 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx

+ 1 - 1
api/types/porter_app.go

@@ -19,7 +19,7 @@ type PorterApp struct {
 	// Build settings (optional)
 	BuildContext   string `json:"build_context,omitempty"`
 	Builder        string `json:"builder,omitempty"`
-	Buildpacks     string `json:"build_packs,omitempty"`
+	Buildpacks     string `json:"buildpacks,omitempty"`
 	Dockerfile     string `json:"dockerfile,omitempty"`
 	PullRequestURL string `json:"pull_request_url,omitempty"`
 

+ 220 - 50
dashboard/package-lock.json

@@ -100,6 +100,7 @@
         "@types/node": "^12.12.62",
         "@types/qs": "^6.9.5",
         "@types/react": "^18.0.0",
+        "@types/react-beautiful-dnd": "^13.1.4",
         "@types/react-color": "^3.0.6",
         "@types/react-datepicker": "^4.4.2",
         "@types/react-dom": "^18.0.0",
@@ -121,6 +122,7 @@
         "html-webpack-plugin": "^4.5.0",
         "prettier": "2.2.1",
         "qs": "^6.9.4",
+        "react-beautiful-dnd": "^13.1.1",
         "react-refresh": "^0.10.0",
         "source-map-loader": "^1.1.0",
         "style-loader": "^2.0.0",
@@ -1968,17 +1970,17 @@
       "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
     },
     "node_modules/@emotion/is-prop-valid": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
-      "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+      "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
       "dependencies": {
-        "@emotion/memoize": "^0.8.0"
+        "@emotion/memoize": "^0.8.1"
       }
     },
     "node_modules/@emotion/memoize": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
-      "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+      "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
     },
     "node_modules/@emotion/serialize": {
       "version": "0.11.16",
@@ -2423,9 +2425,9 @@
       "dev": true
     },
     "node_modules/@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/popperjs"
@@ -3098,7 +3100,6 @@
       "version": "18.0.28",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
       "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-      "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -3114,6 +3115,15 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-beautiful-dnd": {
+      "version": "13.1.4",
+      "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz",
+      "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==",
+      "dev": true,
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/react-color": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz",
@@ -3154,6 +3164,18 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-redux": {
+      "version": "7.1.25",
+      "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
+      "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
+      "dev": true,
+      "dependencies": {
+        "@types/hoist-non-react-statics": "^3.3.0",
+        "@types/react": "*",
+        "hoist-non-react-statics": "^3.3.0",
+        "redux": "^4.0.0"
+      }
+    },
     "node_modules/@types/react-router": {
       "version": "5.1.20",
       "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
@@ -3185,23 +3207,13 @@
       }
     },
     "node_modules/@types/react-transition-group": {
-      "version": "4.4.5",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
-      "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+      "version": "4.4.6",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
+      "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
       "dependencies": {
         "@types/react": "*"
       }
     },
-    "node_modules/@types/react-transition-group/node_modules/@types/react": {
-      "version": "18.0.28",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
-      "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-      "dependencies": {
-        "@types/prop-types": "*",
-        "@types/scheduler": "*",
-        "csstype": "^3.0.2"
-      }
-    },
     "node_modules/@types/reactcss": {
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
@@ -5690,6 +5702,15 @@
         "urix": "^0.1.0"
       }
     },
+    "node_modules/css-box-model": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+      "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+      "dev": true,
+      "dependencies": {
+        "tiny-invariant": "^1.0.6"
+      }
+    },
     "node_modules/css-color-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -10705,6 +10726,12 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "dev": true
     },
+    "node_modules/raf-schd": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
+      "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
+      "dev": true
+    },
     "node_modules/random-word-slugs": {
       "version": "0.1.6",
       "resolved": "https://registry.npmjs.org/random-word-slugs/-/random-word-slugs-0.1.6.tgz",
@@ -10813,6 +10840,25 @@
         "react-dom": ">=16.8.0"
       }
     },
+    "node_modules/react-beautiful-dnd": {
+      "version": "13.1.1",
+      "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
+      "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.9.2",
+        "css-box-model": "^1.2.0",
+        "memoize-one": "^5.1.1",
+        "raf-schd": "^4.0.2",
+        "react-redux": "^7.2.0",
+        "redux": "^4.0.4",
+        "use-memo-one": "^1.1.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8.5 || ^17.0.0 || ^18.0.0",
+        "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/react-color": {
       "version": "2.19.3",
       "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
@@ -10980,6 +11026,37 @@
         "react-dom": "^16.8.0 || ^17 || ^18"
       }
     },
+    "node_modules/react-redux": {
+      "version": "7.2.9",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
+      "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.15.4",
+        "@types/react-redux": "^7.1.20",
+        "hoist-non-react-statics": "^3.3.2",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.7.2",
+        "react-is": "^17.0.2"
+      },
+      "peerDependencies": {
+        "react": "^16.8.3 || ^17 || ^18"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-redux/node_modules/react-is": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+      "dev": true
+    },
     "node_modules/react-refresh": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
@@ -11145,6 +11222,15 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/redux": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -13238,6 +13324,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/use-memo-one": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+      "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+      "dev": true,
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -16260,17 +16355,17 @@
       "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
     },
     "@emotion/is-prop-valid": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
-      "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+      "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
       "requires": {
-        "@emotion/memoize": "^0.8.0"
+        "@emotion/memoize": "^0.8.1"
       }
     },
     "@emotion/memoize": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
-      "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+      "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
     },
     "@emotion/serialize": {
       "version": "0.11.16",
@@ -16571,9 +16666,9 @@
       "dev": true
     },
     "@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
       "version": "0.0.66",
@@ -17138,7 +17233,6 @@
       "version": "18.0.28",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
       "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-      "dev": true,
       "requires": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -17154,6 +17248,15 @@
         "@types/react": "*"
       }
     },
+    "@types/react-beautiful-dnd": {
+      "version": "13.1.4",
+      "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz",
+      "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/react-color": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz",
@@ -17194,6 +17297,18 @@
         "@types/react": "*"
       }
     },
+    "@types/react-redux": {
+      "version": "7.1.25",
+      "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
+      "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
+      "dev": true,
+      "requires": {
+        "@types/hoist-non-react-statics": "^3.3.0",
+        "@types/react": "*",
+        "hoist-non-react-statics": "^3.3.0",
+        "redux": "^4.0.0"
+      }
+    },
     "@types/react-router": {
       "version": "5.1.20",
       "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
@@ -17225,23 +17340,11 @@
       }
     },
     "@types/react-transition-group": {
-      "version": "4.4.5",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
-      "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+      "version": "4.4.6",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
+      "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
       "requires": {
         "@types/react": "*"
-      },
-      "dependencies": {
-        "@types/react": {
-          "version": "18.0.28",
-          "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
-          "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-          "requires": {
-            "@types/prop-types": "*",
-            "@types/scheduler": "*",
-            "csstype": "^3.0.2"
-          }
-        }
       }
     },
     "@types/reactcss": {
@@ -19400,6 +19503,15 @@
         }
       }
     },
+    "css-box-model": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+      "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+      "dev": true,
+      "requires": {
+        "tiny-invariant": "^1.0.6"
+      }
+    },
     "css-color-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -23357,6 +23469,12 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "dev": true
     },
+    "raf-schd": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
+      "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
+      "dev": true
+    },
     "random-word-slugs": {
       "version": "0.1.6",
       "resolved": "https://registry.npmjs.org/random-word-slugs/-/random-word-slugs-0.1.6.tgz",
@@ -23441,6 +23559,21 @@
       "resolved": "https://registry.npmjs.org/react-animate-height/-/react-animate-height-3.1.1.tgz",
       "integrity": "sha512-UkC6+V3ZlCneBRaSM7aUctDJ+PRP6ztcGtxvU7MTeoMMWPhz8BQNaX7QWaZrkzp1ih1G8uZZ+DI9nfLvtD6OdQ=="
     },
+    "react-beautiful-dnd": {
+      "version": "13.1.1",
+      "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
+      "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.9.2",
+        "css-box-model": "^1.2.0",
+        "memoize-one": "^5.1.1",
+        "raf-schd": "^4.0.2",
+        "react-redux": "^7.2.0",
+        "redux": "^4.0.4",
+        "use-memo-one": "^1.1.1"
+      }
+    },
     "react-color": {
       "version": "2.19.3",
       "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
@@ -23554,6 +23687,28 @@
         "warning": "^4.0.2"
       }
     },
+    "react-redux": {
+      "version": "7.2.9",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
+      "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.15.4",
+        "@types/react-redux": "^7.1.20",
+        "hoist-non-react-statics": "^3.3.2",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.7.2",
+        "react-is": "^17.0.2"
+      },
+      "dependencies": {
+        "react-is": {
+          "version": "17.0.2",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+          "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+          "dev": true
+        }
+      }
+    },
     "react-refresh": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
@@ -23693,6 +23848,15 @@
         }
       }
     },
+    "redux": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -25362,6 +25526,12 @@
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "dev": true
     },
+    "use-memo-one": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+      "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+      "dev": true
+    },
     "use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",

+ 2 - 0
dashboard/package.json

@@ -101,6 +101,7 @@
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
     "@types/react": "^18.0.0",
+    "@types/react-beautiful-dnd": "^13.1.4",
     "@types/react-color": "^3.0.6",
     "@types/react-datepicker": "^4.4.2",
     "@types/react-dom": "^18.0.0",
@@ -122,6 +123,7 @@
     "html-webpack-plugin": "^4.5.0",
     "prettier": "2.2.1",
     "qs": "^6.9.4",
+    "react-beautiful-dnd": "^13.1.1",
     "react-refresh": "^0.10.0",
     "source-map-loader": "^1.1.0",
     "style-loader": "^2.0.0",

+ 4 - 0
dashboard/src/assets/stars-white.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.8236 2.3999L16.6538 7.34611L21.6 9.17637L16.6538 11.0066L14.8236 15.9528L12.9933 11.0066L8.04708 9.17637L12.9933 7.34611L14.8236 2.3999Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
+<path d="M6.35297 13.694L7.95179 16.0481L10.3059 17.647L7.95179 19.2458L6.35297 21.5999L4.75414 19.2458L2.40002 17.647L4.75414 16.0481L6.35297 13.694Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/stars.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.8236 2.3999L16.6538 7.34611L21.6 9.17637L16.6538 11.0066L14.8236 15.9528L12.9933 11.0066L8.04708 9.17637L12.9933 7.34611L14.8236 2.3999Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
+<path d="M6.35297 13.694L7.95179 16.0481L10.3059 17.647L7.95179 19.2458L6.35297 21.5999L4.75414 19.2458L2.40002 17.647L4.75414 16.0481L6.35297 13.694Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
+</svg>

+ 19 - 19
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -12,25 +12,25 @@ import ImageList from "./ImageList";
 
 type PropsType =
   | {
-      forceExpanded?: boolean;
-      selectedImageUrl: string | null;
-      selectedTag: string | null;
-      setSelectedImageUrl: (x: string) => void;
-      setSelectedTag: (x: string) => void;
-      noTagSelection?: boolean;
-      disableImageSelect?: boolean;
-      readOnly?: boolean;
-    }
+    forceExpanded?: boolean;
+    selectedImageUrl: string | null;
+    selectedTag: string | null;
+    setSelectedImageUrl: (x: string) => void;
+    setSelectedTag: (x: string) => void;
+    noTagSelection?: boolean;
+    disableImageSelect?: boolean;
+    readOnly?: boolean;
+  }
   | {
-      forceExpanded?: boolean;
-      selectedImageUrl: string | null;
-      selectedTag: string | null;
-      setSelectedImageUrl?: (x: string) => void;
-      setSelectedTag?: (x: string) => void;
-      noTagSelection?: boolean;
-      disableImageSelect?: boolean;
-      readOnly: true;
-    };
+    forceExpanded?: boolean;
+    selectedImageUrl: string | null;
+    selectedTag: string | null;
+    setSelectedImageUrl?: (x: string) => void;
+    setSelectedTag?: (x: string) => void;
+    noTagSelection?: boolean;
+    disableImageSelect?: boolean;
+    readOnly: true;
+  };
 
 type StateType = {
   isExpanded: boolean;
@@ -243,7 +243,7 @@ const ImageItem = styled.div`
   font-size: 13px;
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;

+ 0 - 151
dashboard/src/components/repo-selector/ActionConfBranchSelector.tsx

@@ -1,151 +0,0 @@
-import React from "react";
-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";
-import Input from "components/porter/Input";
-
-type Props = {
-  actionConfig: ActionConfigType | null;
-  branch: string;
-  setActionConfig: (x: ActionConfigType) => void;
-  setBranch: (x: string) => void;
-  setDockerfilePath: (x: string) => void;
-  setFolderPath: (x: string) => void;
-  setBuildView?: (x: string) => void;
-  setPorterYamlPath?: (x: string) => void;
-};
-
-const ActionConfEditorStack: React.FC<Props> = (props) => {
-  const {
-    actionConfig,
-    setBranch,
-    setActionConfig,
-    branch,
-    setPorterYamlPath,
-  } = props;
-
-  if (!actionConfig.git_repo) {
-    return (
-      <ExpandedWrapperAlt>
-        <RepoList
-          actionConfig={actionConfig}
-          setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
-          readOnly={false}
-        />
-      </ExpandedWrapperAlt>
-    );
-  } else if (!branch) {
-    props.setFolderPath("./");
-    return (
-      <>
-        <ExpandedWrapperAlt>
-          <BranchList
-            actionConfig={actionConfig}
-            setBranch={(branch: string) => setBranch(branch)}
-          />
-        </ExpandedWrapperAlt>
-        <Br />
-      </>
-    );
-  }
-  return (
-    <>
-      <Input
-        disabled={true}
-        label="GitHub branch:"
-        type="text"
-        width="100%"
-        value={props?.branch}
-        setValue={() => {}}
-        placeholder=""
-      />
-      <BackButton
-        width="145px"
-        onClick={() => {
-          setBranch ? setBranch("") : null;
-          props.setFolderPath ? props.setFolderPath("") : null;
-          props.setDockerfilePath ? props.setDockerfilePath("") : null;
-          props.setActionConfig ? props.setActionConfig(actionConfig) : null;
-          props.setBuildView ? props.setBuildView("buildpacks") : null;
-          setPorterYamlPath("");
-        }}
-      >
-        <i className="material-icons">keyboard_backspace</i>
-        Select branch
-      </BackButton>
-    </>
-  );
-};
-
-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%;
-  border-radius: 3px;
-  border: 1px solid #ffffff44;
-  max-height: 275px;
-`;
-
-const ExpandedWrapperAlt = styled(ExpandedWrapper)`
-  border: 0;
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-top: 22px;
-  cursor: pointer;
-  font-size: 13px;
-  height: 35px;
-  padding: 5px 13px;
-  margin-bottom: -7px;
-  padding-right: 15px;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  width: ${(props: { width: string }) => props.width};
-  color: white;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: white;
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;

+ 0 - 117
dashboard/src/components/repo-selector/ActionConfEditorStack.tsx

@@ -1,117 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-import { ActionConfigType } from "shared/types";
-
-import RepoList from "./RepoList";
-import InputRow from "../form-components/InputRow";
-import Input from "components/porter/Input";
-
-type Props = {
-  actionConfig: ActionConfigType | null;
-  setActionConfig: (x: ActionConfigType) => void;
-  setBranch?: (x: string) => void;
-  setDockerfilePath?: (x: string) => void;
-  setFolderPath?: (x: string) => void;
-  setBuildView?: (x: string) => void;
-  setPorterYamlPath?: (x: string) => void;
-};
-
-const defaultActionConfig: ActionConfigType = {
-  git_repo: null,
-  image_repo_uri: null,
-  git_branch: null,
-  git_repo_id: 0,
-  kind: "github",
-};
-
-const ActionConfEditorStack: React.FC<Props> = ({
-  actionConfig,
-  setBranch,
-  setActionConfig,
-  setFolderPath,
-  setDockerfilePath,
-  setBuildView,
-  setPorterYamlPath,
-}) => {
-  if (!actionConfig.git_repo) {
-    return (
-      <ExpandedWrapperAlt>
-        <RepoList
-          actionConfig={actionConfig}
-          setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
-          readOnly={false}
-        />
-      </ExpandedWrapperAlt>
-    );
-  } else {
-    return (
-      <>
-        <Input
-          disabled={true}
-          label="GitHub repository:"
-          width="100%"
-          value={actionConfig?.git_repo}
-          setValue={() => {}}
-          placeholder=""
-        />
-        <BackButton
-          width="135px"
-          onClick={() => {
-            setActionConfig({ ...defaultActionConfig });
-            setBranch ? setBranch("") : null;
-            setFolderPath ? setFolderPath("") : null;
-            setDockerfilePath ? setDockerfilePath("") : null;
-            setBuildView ? setBuildView("buildpacks") : null;
-            setPorterYamlPath("");
-          }}
-        >
-          <i className="material-icons">keyboard_backspace</i>
-          Select repo
-        </BackButton>
-      </>
-    );
-  }
-};
-
-export default ActionConfEditorStack;
-
-const ExpandedWrapper = styled.div`
-  margin-top: 10px;
-  width: 100%;
-  border-radius: 3px;
-  border: 1px solid #ffffff44;
-  max-height: 275px;
-`;
-
-const ExpandedWrapperAlt = styled(ExpandedWrapper)`
-  border: 0;
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-top: 22px;
-  cursor: pointer;
-  font-size: 13px;
-  height: 35px;
-  padding: 5px 13px;
-  margin-bottom: -7px;
-  padding-right: 15px;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  width: ${(props: { width: string }) => props.width};
-  color: white;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: white;
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;

+ 0 - 656
dashboard/src/components/repo-selector/BuildpackStack.tsx

@@ -1,656 +0,0 @@
-import { DeviconsNameList } from "assets/devicons-name-list";
-import Helper from "components/form-components/Helper";
-import InputRow from "components/form-components/InputRow";
-import Select from "components/porter/Select";
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { ActionConfigType } from "shared/types";
-import styled, { keyframes } from "styled-components";
-// Add the following imports
-import { Button as MuiButton, Modal as MuiModal } from "@material-ui/core";
-import { makeStyles, withStyles } from "@material-ui/core/styles";
-import Button from "components/porter/Button";
-import Modal from "components/porter/Modal";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-
-const DEFAULT_BUILDER_NAME = "heroku";
-const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
-const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
-
-type BuildConfig = {
-  builder: string;
-  buildpacks: string[];
-  config: null | {
-    [key: string]: string;
-  };
-};
-
-type Buildpack = {
-  name: string;
-  buildpack: string;
-  config: {
-    [key: string]: string;
-  };
-};
-
-type DetectedBuildpack = {
-  name: string;
-  builders: string[];
-  detected: Buildpack[];
-  others: Buildpack[];
-  buildConfig: BuildConfig;
-};
-
-type DetectBuildpackResponse = DetectedBuildpack[];
-
-export const BuildpackStack: React.FC<{
-  actionConfig: ActionConfigType;
-  folderPath: string;
-  branch: string;
-  hide: boolean;
-  onChange: (config: BuildConfig) => void;
-  currentBuildConfig?: BuildConfig;
-  setBuildConfig?: (config: BuildConfig) => void;
-}> = ({
-  actionConfig,
-  folderPath,
-  branch,
-  hide,
-  onChange,
-  currentBuildConfig,
-  setBuildConfig,
-}) => {
-  const { currentProject } = useContext(Context);
-
-  const [builders, setBuilders] = useState<DetectedBuildpack[]>(null);
-
-  const [stacks, setStacks] = useState<string[]>(null);
-  const [selectedStack, setSelectedStack] = useState<string>(
-    currentBuildConfig?.builder || null
-  );
-  const [isModalOpen, setIsModalOpen] = useState(false);
-
-  const [selectedBuildpacks, setSelectedBuildpacks] = useState<Buildpack[]>([]);
-  const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>(
-    []
-  );
-  const renderModalContent = () => {
-    return (
-      <>
-        <Text size={16}>Buildpack Configuration</Text>
-        <Spacer y={1} />
-        <Scrollable>
-          <Text color="helper">Selected buildpacks:</Text>
-          <Spacer y={1} />
-          {!!selectedBuildpacks?.length &&
-            renderBuildpacksList(selectedBuildpacks, "remove")}
-
-          <Spacer y={1} />
-          {!!availableBuildpacks?.length && (
-            <>
-              <Text color="helper">Available buildpacks:</Text>
-              <Spacer y={1} />
-              <>{renderBuildpacksList(availableBuildpacks, "add")}</>
-            </>
-          )}
-          <Spacer y={1} />
-          <Text color="helper">
-            You may also add buildpacks by directly providing their GitHub links
-            or links to ZIP files that contain the buildpack source code.
-          </Text>
-          <Spacer y={1} />
-          <AddCustomBuildpackForm onAdd={handleAddCustomBuildpack} />
-          <Spacer y={2} />
-        </Scrollable>
-        <Footer>
-          <Shade />
-          <Spacer y={1} />
-          <Button onClick={() => setIsModalOpen(false)}>Save buildpacks</Button>
-        </Footer>
-      </>
-    );
-  };
-  useEffect(() => {
-    let buildConfig: BuildConfig = {} as BuildConfig;
-    buildConfig.builder = selectedStack;
-    buildConfig.buildpacks = selectedBuildpacks?.map((buildpack) => {
-      return buildpack.buildpack;
-    });
-
-    if (typeof onChange === "function") {
-      onChange(buildConfig);
-
-      if (currentBuildConfig) {
-        setBuildConfig(buildConfig);
-      }
-    }
-  }, [selectedStack, selectedBuildpacks]);
-
-  const detectBuildpack = () => {
-    if (actionConfig.kind === "gitlab") {
-      return api.detectGitlabBuildpack<DetectBuildpackResponse>(
-        "<token>",
-        {
-          repo_path: actionConfig.git_repo,
-          branch: branch,
-          dir: folderPath || ".",
-        },
-        {
-          project_id: currentProject.id,
-          integration_id: actionConfig.gitlab_integration_id,
-        }
-      );
-    }
-
-    return api.detectBuildpack<DetectBuildpackResponse>(
-      "<token>",
-      {
-        dir: folderPath || ".",
-      },
-      {
-        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,
-      }
-    );
-  };
-
-  const classes = useStyles();
-
-  useEffect(() => {
-    detectBuildpack()
-      // getMockData()
-      .then(({ data }) => {
-        const builders = data;
-
-        const defaultBuilder = builders.find(
-          (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
-        );
-
-        var detectedBuildpacks = defaultBuilder.detected;
-        var availableBuildpacks = defaultBuilder.others;
-        var defaultStack = "";
-        if (currentBuildConfig && currentBuildConfig.buildpacks.length != 0) {
-          if (!detectedBuildpacks) {
-            detectedBuildpacks = [];
-          }
-
-          defaultStack = currentBuildConfig.builder;
-          for (const buildpackName of currentBuildConfig.buildpacks) {
-            const matchingBuildpackIndex = availableBuildpacks.findIndex(
-              (buildpack) => buildpack.buildpack === buildpackName
-            );
-
-            if (matchingBuildpackIndex >= 0) {
-              const matchingBuildpack = availableBuildpacks.splice(
-                matchingBuildpackIndex,
-                1
-              )[0];
-              const existingBuildpackIndex = detectedBuildpacks.findIndex(
-                (buildpack) => buildpack.buildpack === buildpackName
-              );
-              if (existingBuildpackIndex < 0) {
-                detectedBuildpacks.push(matchingBuildpack);
-              }
-            } else {
-              const newBuildpack: Buildpack = {
-                name: buildpackName,
-                buildpack: buildpackName,
-                config: null,
-              };
-              const existingBuildpackIndex = detectedBuildpacks.findIndex(
-                (buildpack) => buildpack.buildpack === buildpackName
-              );
-              if (existingBuildpackIndex < 0) {
-                detectedBuildpacks.push(newBuildpack);
-              }
-            }
-          }
-        } else {
-          detectedBuildpacks = defaultBuilder.detected;
-          availableBuildpacks = defaultBuilder.others;
-          defaultStack = builders
-            .flatMap((builder) => builder.builders)
-            .find((stack) => {
-              return stack === DEFAULT_HEROKU_STACK;
-            });
-        }
-        setBuilders(builders);
-        setSelectedStack(defaultStack);
-
-        setStacks(defaultBuilder.builders);
-        setSelectedStack(defaultStack);
-        if (!Array.isArray(detectedBuildpacks)) {
-          setSelectedBuildpacks([]);
-        } else {
-          setSelectedBuildpacks(detectedBuildpacks);
-        }
-        if (!Array.isArray(availableBuildpacks)) {
-          setAvailableBuildpacks([]);
-        } else {
-          setAvailableBuildpacks(availableBuildpacks);
-        }
-      })
-      .catch((err) => {
-        console.error(err);
-      });
-  }, [currentProject, actionConfig]);
-
-  const builderOptions = useMemo(() => {
-    if (!Array.isArray(builders)) {
-      return;
-    }
-
-    return builders.map((builder) => ({
-      label: builder.name,
-      value: builder.name.toLowerCase(),
-    }));
-  }, [builders]);
-
-  const stackOptions = useMemo(() => {
-    if (!Array.isArray(builders)) {
-      return;
-    }
-
-    return builders.flatMap((builder) => {
-      return builder.builders.map((stack) => ({
-        label: `${builder.name} - ${stack}`,
-        value: stack.toLowerCase(),
-      }));
-    });
-  }, [builders]);
-  //   const builder = builders.find(
-  //     (b) => b.name.toLowerCase() === builderName.toLowerCase()
-  //   );
-  //   const detectedBuildpacks = builder.detected;
-  //   const availableBuildpacks = builder.others;
-  //   const defaultStack = builder.builders.find((stack) => {
-  //     return stack === DEFAULT_HEROKU_STACK || stack === DEFAULT_PAKETO_STACK;
-  //   });
-  //   setSelectedBuilder(builderName);
-  //   setBuilders(builders);
-  //   setSelectedBuilder(builderName.toLowerCase());
-
-  //   setStacks(builder.builders);
-  //   setSelectedStack(defaultStack);
-
-  //   if (!Array.isArray(detectedBuildpacks)) {
-  //     setSelectedBuildpacks([]);
-  //   } else {
-  //     setSelectedBuildpacks(detectedBuildpacks);
-  //   }
-  //   if (!Array.isArray(availableBuildpacks)) {
-  //     setAvailableBuildpacks([]);
-  //   } else {
-  //     setAvailableBuildpacks(availableBuildpacks);
-  //   }
-  // };
-
-  const renderBuildpacksList = (
-    buildpacks: Buildpack[],
-    action: "remove" | "add",
-    isLast: boolean = false
-  ) => {
-    return buildpacks?.map((buildpack, index) => {
-      const [languageName] = buildpack.name?.split("/").reverse();
-
-      const devicon = DeviconsNameList.find(
-        (devicon) => languageName.toLowerCase() === devicon.name
-      );
-
-      const icon = `devicon-${devicon?.name}-plain colored`;
-
-      let disableIcon = false;
-      if (!devicon) {
-        disableIcon = true;
-      }
-
-      return (
-        <StyledCard key={buildpack.name} marginBottom="5px">
-          <ContentContainer>
-            <Icon disableMarginRight={disableIcon} className={icon} />
-            <EventInformation>
-              <EventName>{buildpack?.name}</EventName>
-            </EventInformation>
-          </ContentContainer>
-          <ActionContainer>
-            {action === "add" && (
-              <ActionButton
-                onClick={() => handleAddBuildpack(buildpack.buildpack)}
-              >
-                <span className="material-icons-outlined">add</span>
-              </ActionButton>
-            )}
-            {action === "remove" && (
-              <ActionButton
-                onClick={() => handleRemoveBuildpack(buildpack.buildpack)}
-              >
-                <span className="material-icons">delete</span>
-              </ActionButton>
-            )}
-          </ActionContainer>
-        </StyledCard>
-      );
-    });
-  };
-
-  const handleRemoveBuildpack = (buildpackToRemove: string) => {
-    setSelectedBuildpacks((selBuildpacks) => {
-      const tmpSelectedBuildpacks = [...selBuildpacks];
-
-      const indexBuildpackToRemove = tmpSelectedBuildpacks.findIndex(
-        (buildpack) => buildpack.buildpack === buildpackToRemove
-      );
-      const buildpack = tmpSelectedBuildpacks[indexBuildpackToRemove];
-
-      setAvailableBuildpacks((availableBuildpacks) => [
-        ...availableBuildpacks,
-        buildpack,
-      ]);
-
-      tmpSelectedBuildpacks.splice(indexBuildpackToRemove, 1);
-
-      return [...tmpSelectedBuildpacks];
-    });
-  };
-
-  const handleAddBuildpack = (buildpackToAdd: string) => {
-    setAvailableBuildpacks((avBuildpacks) => {
-      const tmpAvailableBuildpacks = [...avBuildpacks];
-      const indexBuildpackToAdd = tmpAvailableBuildpacks.findIndex(
-        (buildpack) => buildpack.buildpack === buildpackToAdd
-      );
-      const buildpack = tmpAvailableBuildpacks[indexBuildpackToAdd];
-
-      setSelectedBuildpacks((selectedBuildpacks) => [
-        ...selectedBuildpacks,
-        buildpack,
-      ]);
-
-      tmpAvailableBuildpacks.splice(indexBuildpackToAdd, 1);
-      return [...tmpAvailableBuildpacks];
-    });
-  };
-
-  const handleAddCustomBuildpack = (buildpack: Buildpack) => {
-    setSelectedBuildpacks((selectedBuildpacks) => [
-      ...selectedBuildpacks,
-      buildpack,
-    ]);
-  };
-
-  if (hide) {
-    return null;
-  }
-
-  if (!stackOptions?.length || !builderOptions?.length) {
-    return <Loading />;
-  }
-
-  const sortedStackOptions = stackOptions.sort((a, b) => {
-    if (a.label < b.label) {
-      return -1;
-    }
-    if (a.label > b.label) {
-      return 1;
-    }
-    return 0;
-  });
-
-  return (
-    <BuildpackConfigurationContainer>
-      <>
-        <Select
-          value={selectedStack}
-          width="300px"
-          options={sortedStackOptions}
-          setValue={(option) => {
-            setSelectedStack(option);
-          }}
-          label="Builder and stack"
-        />
-        {!!selectedBuildpacks?.length && (
-          <Helper>
-            The following buildpacks were automatically detected. You can also
-            manually add/remove buildpacks.
-          </Helper>
-        )}
-        {!!selectedBuildpacks?.length && (
-          <>{renderBuildpacksList(selectedBuildpacks, "remove")}</>
-        )}
-        <Spacer y={1} />
-        <Button onClick={() => setIsModalOpen(true)}>
-          <I className="material-icons">add</I> Add buildpack
-        </Button>
-        {isModalOpen && (
-          <Modal closeModal={() => setIsModalOpen(false)}>
-            {renderModalContent()}
-          </Modal>
-        )}
-      </>
-    </BuildpackConfigurationContainer>
-  );
-};
-
-export const AddCustomBuildpackForm: React.FC<{
-  onAdd: (buildpack: Buildpack) => void;
-}> = ({ onAdd }) => {
-  const [buildpackUrl, setBuildpackUrl] = useState("");
-  const [error, setError] = useState(false);
-
-  const handleAddCustomBuildpack = () => {
-    const buildpack: Buildpack = {
-      buildpack: buildpackUrl,
-      name: buildpackUrl,
-      config: null,
-    };
-    setBuildpackUrl("");
-    onAdd(buildpack);
-  };
-
-  return (
-    <StyledCard marginBottom="0px">
-      <ContentContainer>
-        <EventInformation>
-          <BuildpackInputContainer>
-            GitHub or ZIP URL
-            <BuildpackUrlInput
-              placeholder="https://github.com/custom/buildpack"
-              type="input"
-              value={buildpackUrl}
-              isRequired
-              setValue={(newUrl) => {
-                setError(false);
-                setBuildpackUrl(newUrl as string);
-              }}
-            />
-            <ErrorText hasError={error}>Please enter a valid url</ErrorText>
-          </BuildpackInputContainer>
-        </EventInformation>
-      </ContentContainer>
-      <ActionContainer>
-        <ActionButton onClick={() => handleAddCustomBuildpack()}>
-          <span className="material-icons-outlined">add</span>
-        </ActionButton>
-      </ActionContainer>
-    </StyledCard>
-  );
-};
-
-const Shade = styled.div`
-  position: absolute;
-  top: -50px;
-  left: 0;
-  height: 50px;
-  width: 100%;
-  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
-`;
-
-const Footer = styled.div`
-  position: relative;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  padding: 0 25px;
-  border-bottom-left-radius: 10px;
-  border-bottom-right-radius: 10px;
-  background: ${({ theme }) => theme.fg};
-  margin-bottom: -30px;
-  padding-bottom: 30px;
-`;
-
-const I = styled.i`
-  color: white;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-  justify-content: center;
-`;
-
-const ErrorText = styled.span`
-  color: red;
-  margin-left: 10px;
-  display: ${(props: { hasError: boolean }) =>
-    props.hasError ? "inline-block" : "none"};
-`;
-
-const Scrollable = styled.div`
-  overflow-y: auto;
-  padding: 0 25px;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  max-height: calc(100vh - 300px);
-`;
-
-const fadeIn = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-const BuildpackUrlInput = styled(InputRow)`
-  width: auto;
-  min-width: 300px;
-  max-width: 600px;
-  margin: unset;
-  margin-left: 10px;
-  display: inline-block;
-`;
-
-const BuildpackConfigurationContainer = styled.div`
-  animation: ${fadeIn} 0.75s;
-`;
-
-const StyledCard = styled.div<{ marginBottom?: string }>`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  border: 1px solid #494b4f;
-  background: ${({ theme }) => theme.fg};
-  margin-bottom: ${(props) => props.marginBottom || "30px"};
-  border-radius: 8px;
-  padding: 14px;
-  overflow: hidden;
-  height: 60px;
-  font-size: 13px;
-  animation: ${fadeIn} 0.5s;
-`;
-
-const ContentContainer = styled.div`
-  display: flex;
-  height: 100%;
-  width: 100%;
-  align-items: center;
-`;
-
-const Icon = styled.span<{ disableMarginRight: boolean }>`
-  font-size: 20px;
-  margin-left: 10px;
-  ${(props) => {
-    if (!props.disableMarginRight) {
-      return "margin-right: 20px";
-    }
-  }}
-`;
-
-const EventInformation = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: space-around;
-  height: 100%;
-`;
-
-const EventName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-`;
-
-const BuildpackInputContainer = styled(EventName)`
-  padding-left: 15px;
-`;
-
-const ActionContainer = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  height: 100%;
-`;
-
-const ActionButton = styled.button`
-  position: relative;
-  border: none;
-  background: none;
-  color: white;
-  padding: 5px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 50%;
-  cursor: pointer;
-  color: #aaaabb;
-
-  :hover {
-    background: #ffffff11;
-    border: 1px solid #ffffff44;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-`;
-
-const SaveButton = withStyles({
-  root: {
-    backgroundColor: "#8590ff",
-    color: "white",
-    marginTop: "24px",
-    position: "absolute",
-    bottom: "16px",
-    right: "16px",
-  },
-})(MuiButton);
-
-const StyledModal = withStyles({
-  root: {
-    display: "flex",
-    alignItems: "center",
-    justifyContent: "center",
-  },
-})(MuiModal);
-const useStyles = makeStyles((theme) => ({
-  modal: {
-    display: "flex",
-    alignItems: "center",
-    justifyContent: "center",
-  },
-}));

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

@@ -1,678 +0,0 @@
-import React, { useState, useEffect, useContext, useCallback } 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 Button from "components/porter/Button";
-import api from "../../shared/api";
-import Error from "components/porter/Error";
-
-import { Context } from "../../shared/Context";
-import { ActionConfigType, BuildConfig, 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";
-import { render } from "react-dom";
-import Modal from "components/porter/Modal";
-import Input from "components/porter/Input";
-import Text from "components/porter/Text";
-import { set } from "lodash";
-import Link from "../porter/Link";
-
-interface AutoBuildpack {
-  name?: string;
-  valid: boolean;
-}
-
-type PropsType = {
-  actionConfig: ActionConfigType | null;
-  branch: string;
-  dockerfilePath?: string;
-  folderPath: string;
-  porterYaml?: string;
-  setActionConfig: (x: ActionConfigType) => void;
-  setDockerfilePath: (x: string) => void;
-  setFolderPath: (x: string) => void;
-  setBuildConfig: (x: any) => void;
-  setPorterYaml: (x: any) => void;
-  buildView: string;
-  setBuildView: (x: string) => void;
-  porterYamlPath: string;
-  setPorterYamlPath: (x: string) => void;
-};
-
-const DetectContentsList: React.FC<PropsType> = (props) => {
-  const [showModal, setShowModal] = useState(false);
-  const [loading, setLoading] = useState(true);
-  const [error, setError] = useState(false);
-  const [contents, setContents] = useState<FileType[]>([]);
-  const [currentDir, setCurrentDir] = useState("");
-  const [changedPorterYaml, setChangedPorterYaml] = useState(true);
-  const [displayInput, setDisplayInput] = useState(false);
-  const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
-
-  const [autoBuildpack, setAutoBuildpack] = useState<AutoBuildpack>({
-    valid: false,
-    name: "",
-  });
-  const [showingBuildContextPrompt, setShowingBuildContextPrompt] = useState(
-    "buildpacks"
-  );
-  const context = useContext(Context);
-  const fetchAndSetPorterYaml = useCallback(async (fileName: string) => {
-    try {
-      setButtonStatus("loading");
-      const response = await fetchPorterYamlContent(fileName);
-      props.setPorterYaml(atob(response.data));
-      setButtonStatus("success");
-    } catch (error) {
-      setButtonStatus(<Error message="Unable to detect porter.yaml" />);
-      console.error("Error fetching porter.yaml content:", error);
-    }
-  }, []);
-
-  const toggleModal = async () => {
-    if (!showModal) {
-      const porterYamlItem = contents.find((item: FileType) =>
-        item.path.includes(props.porterYamlPath + "porter.yaml")
-      );
-      if (porterYamlItem) {
-        fetchAndSetPorterYaml(props.porterYamlPath + "porter.yaml");
-        props.setPorterYamlPath("porter.yaml");
-        return;
-      }
-    }
-    setShowModal(!showModal);
-  };
-
-  useEffect(() => {
-    if (!loading) {
-      toggleModal();
-    }
-  }, [loading]);
-
-  useEffect(() => {
-    updateContents();
-  }, []);
-  useEffect(() => {
-    const dockerFileItem = contents.find((item: FileType) =>
-      item.path.includes("Dockerfile")
-    );
-
-    if (dockerFileItem) {
-      props.setDockerfilePath(dockerFileItem.path);
-      props.setBuildView("docker");
-    }
-  }, [contents]);
-
-  useEffect(() => {
-    detectBuildpacks().then(({ data }) => {
-      setAutoBuildpack(data);
-    });
-  }, [contents]);
-
-  const renderContentList = () => {
-    contents.map((item: FileType, i: number) => {
-      let splits = item.path.split("/");
-      let fileName = splits[splits.length - 1];
-      if (fileName.includes("Dockerfile")) {
-        return false;
-      }
-    });
-
-    return true;
-  };
-
-  const fetchContents = () => {
-    let { currentProject } = context;
-    const { actionConfig, branch } = props;
-
-    if (actionConfig.kind === "gitlab") {
-      return api
-        .getGitlabFolderContent(
-          "<token>",
-          {
-            repo_path: actionConfig.git_repo,
-            branch: branch,
-            dir: currentDir || "./",
-          },
-          {
-            project_id: currentProject.id,
-            integration_id: actionConfig.gitlab_integration_id,
-          }
-        )
-        .then((res) => {
-          const { data } = res;
-
-          return {
-            data: data.map((x: FileType) => ({
-              ...x,
-              type: x.type === "tree" ? "dir" : "file",
-            })),
-          };
-        });
-    }
-    return api.getBranchContents(
-      "<token>",
-      { dir: 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,
-      }
-    );
-  };
-
-  const fetchPorterYamlContent = async (porterYaml: string) => {
-    let { currentProject } = context;
-    let { actionConfig, branch } = props;
-    if (actionConfig.kind === "github") {
-      try {
-        const res = await api.getPorterYamlContents(
-          "<token>",
-          {
-            path: porterYaml,
-          },
-          {
-            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 res;
-      } catch (err) {
-        // console.log(err);
-      }
-    } else if (actionConfig.kind === "gitlab") {
-      try {
-        const res = await api.getGitlabPorterYamlContents(
-          "<token>",
-          {
-            repo_path: actionConfig.git_repo,
-            branch: branch,
-            path: porterYaml,
-          },
-          {
-            project_id: currentProject.id,
-            integration_id: actionConfig.gitlab_integration_id,
-          }
-        );
-        return res;
-      } catch (err) {
-        console.log(err);
-      }
-    }
-  };
-  const detectBuildpacks = () => {
-    let { currentProject } = context;
-    let { actionConfig, branch } = props;
-
-    if (actionConfig.kind === "github") {
-      return api.detectBuildpack(
-        "<token>",
-        {
-          dir: 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,
-        }
-      );
-    } else if (actionConfig.kind === "gitlab") {
-      return api.detectGitlabBuildpack(
-        "<token>",
-        {
-          repo_path: actionConfig.git_repo,
-          branch: branch,
-          dir: currentDir || ".",
-        },
-        {
-          project_id: currentProject.id,
-          integration_id: actionConfig.gitlab_integration_id,
-        }
-      );
-    }
-  };
-
-  const handleInputChange = (newValue: string) => {
-    props.setPorterYamlPath(newValue);
-    setChangedPorterYaml(newValue === "");
-    if (!displayInput && newValue !== "") {
-      setDisplayInput(true);
-    }
-  };
-  const handleUpdatePorterYamlPath = () => {
-    props.setPorterYamlPath(props.porterYamlPath);
-    fetchAndSetPorterYaml(props.porterYamlPath);
-    set;
-  };
-  const updateContents = async () => {
-    try {
-      const res = await fetchContents();
-      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);
-
-      setContents(contents);
-      setLoading(false);
-      setError(false);
-    } catch (err) {
-      console.log(err);
-      setLoading(false);
-      setError(true);
-    }
-
-    try {
-      const { data } = await detectBuildpacks();
-      setAutoBuildpack(data);
-    } catch (err) {
-      console.log(err);
-      setAutoBuildpack({
-        valid: false,
-      });
-    }
-  };
-  const updatePorterYamlPath = () => {
-    toggleModal();
-
-    fetchAndSetPorterYaml(props.porterYamlPath);
-  };
-  const ignoreModal = () => {
-    toggleModal();
-
-    props.setPorterYamlPath("");
-  };
-
-  const NoPorterYamlContent = () => (
-    <div>
-      <Text size={16}>No porter.yaml detected</Text>
-      <Spacer y={1} />
-      <span>
-        <Text color="helper">
-          We were unable to find <Code>porter.yaml</Code> in your root directory. We
-          recommend that you add a <Code>porter.yaml</Code> file to your root directory
-          or specify the path here.
-        </Text>
-        <Link
-          to="https://docs.porter.run/standard/deploying-applications/writing-porter-yaml"
-          target="_blank"
-          hasunderline
-        >
-          Using porter.yaml
-        </Link>
-      </span>
-    </div>
-  );
-  return (
-    <>
-      {showModal && (
-        <Modal closeModal={toggleModal}>
-          <NoPorterYamlContent />
-          <Spacer y={0.5} />
-          <Text color="helper">Porter.yaml path:</Text>
-          <Spacer y={0.5} />
-          <Input
-            disabled={false}
-            placeholder="ex: ./subdirectory/porter.yaml"
-            value={props.porterYamlPath}
-            width="100%"
-            setValue={props.setPorterYamlPath}
-          />
-          <Spacer y={1} />
-          <div style={{ display: "flex", justifyContent: "space-between" }}>
-            <Button
-              onClick={ignoreModal}
-              loadingText="Submitting..."
-              color="#ffffff11"
-              status={loading ? "loading" : undefined}
-            >
-              Ignore
-            </Button>
-            <Button
-              onClick={updatePorterYamlPath}
-              loadingText="Submitting..."
-              color="#616fee"
-              status={loading ? "loading" : undefined}
-            >
-              Update Path
-            </Button>
-          </div>
-        </Modal>
-      )}
-      {renderContentList() && (
-        <>
-          {props.porterYamlPath != "porter.yaml" &&
-            (displayInput || props.porterYamlPath) && (
-              <>
-                <Text color="helper">Porter.yaml path:</Text>
-                <Spacer y={0.5} />
-                <Input
-                  disabled={false}
-                  placeholder="ex: ./"
-                  value={props.porterYamlPath}
-                  width="100%"
-                  onValueChange={handleInputChange}
-                />
-                <Spacer y={0.5} />
-                <Button
-                  onClick={handleUpdatePorterYamlPath}
-                  loadingText="Submitting..."
-                  color={changedPorterYaml ? "#ffffff11" : "#616fee"}
-                  status={buttonStatus}
-                  disabled={changedPorterYaml}
-                >
-                  Update Path
-                </Button>
-                <Spacer y={1} />
-              </>
-            )}
-          <AdvancedBuildSettings
-            dockerfilePath={props.dockerfilePath}
-            setDockerfilePath={props.setDockerfilePath}
-            setBuildConfig={props.setBuildConfig}
-            autoBuildPack={autoBuildpack}
-            showSettings={false}
-            actionConfig={props.actionConfig}
-            branch={props.branch}
-            folderPath={props.folderPath}
-            buildView={props.buildView}
-            setBuildView={props.setBuildView}
-          />
-        </>
-      )}
-    </>
-  );
-};
-
-export default DetectContentsList;
-
-const Code = styled.span`
-  font-family: monospace;
-`;
-
-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;
-  }
-`;

+ 28 - 28
dashboard/src/components/repo-selector/RepoList.tsx

@@ -23,15 +23,15 @@ type Props = {
 
 type Provider =
   | {
-      provider: "github";
-      name: string;
-      installation_id: number;
-    }
+    provider: "github";
+    name: string;
+    installation_id: number;
+  }
   | {
-      provider: "gitlab";
-      instance_url: string;
-      integration_id: number;
-    };
+    provider: "gitlab";
+    instance_url: string;
+    integration_id: number;
+  };
 
 // Sort provider by name if it's github or instance url if it's gitlab
 const sortProviders = (providers: Provider[]) => {
@@ -111,7 +111,7 @@ const RepoList: React.FC<Props> = ({
 
       const repos = res.data.map((repo) => ({ ...repo, GHRepoID: repoId }));
       return repos;
-    } catch (error) {}
+    } catch (error) { }
   };
 
   const loadGitlabRepos = async (integrationId: number) => {
@@ -129,7 +129,7 @@ const RepoList: React.FC<Props> = ({
         GitIntegrationId: integrationId,
       }));
       return repos;
-    } catch (error) {}
+    } catch (error) { }
   };
 
   const loadRepos = (provider: any) => {
@@ -168,9 +168,9 @@ const RepoList: React.FC<Props> = ({
   // clear out actionConfig and SelectedRepository if new search is performed
   useEffect(() => {
     setActionConfig({
-      git_repo: null,
-      image_repo_uri: null,
-      git_branch: null,
+      git_repo: "",
+      image_repo_uri: "",
+      git_branch: "",
       git_repo_id: 0,
       kind: "github",
     });
@@ -243,20 +243,20 @@ const RepoList: React.FC<Props> = ({
     let results =
       searchFilter != null
         ? repos
-            .filter((repo: RepoType) => {
-              return repo.FullName.toLowerCase().includes(
-                searchFilter.toLowerCase()
-              );
-            })
-            .sort((a: RepoType, b: RepoType) => {
-              const aIndex = a.FullName.toLowerCase().indexOf(
-                searchFilter.toLowerCase()
-              );
-              const bIndex = b.FullName.toLowerCase().indexOf(
-                searchFilter.toLowerCase()
-              );
-              return aIndex - bIndex;
-            })
+          .filter((repo: RepoType) => {
+            return repo.FullName.toLowerCase().includes(
+              searchFilter.toLowerCase()
+            );
+          })
+          .sort((a: RepoType, b: RepoType) => {
+            const aIndex = a.FullName.toLowerCase().indexOf(
+              searchFilter.toLowerCase()
+            );
+            const bIndex = b.FullName.toLowerCase().indexOf(
+              searchFilter.toLowerCase()
+            );
+            return aIndex - bIndex;
+          })
         : repos.slice(0, 10);
 
     if (results.length == 0) {
@@ -366,7 +366,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#353a3e"};
+    props.disabled ? "" : "#353a3e"};
   }
 
   > i {

+ 150 - 0
dashboard/src/main/home/app-dashboard/build-settings/AddCustomBuildpackComponent.tsx

@@ -0,0 +1,150 @@
+import InputRow from "components/form-components/InputRow";
+import React, { useState } from "react";
+import styled, { keyframes } from "styled-components";
+import { Buildpack } from "./BuildpackStack";
+
+function isValidURL(url: string): boolean {
+  const pattern = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(:\d{2,5})?([\/\w.-]*)*\/?$/i;
+  return pattern.test(url);
+}
+
+const AddCustomBuildpackComponent: React.FC<{
+  onAdd: (buildpack: Buildpack) => void;
+}> = ({ onAdd }) => {
+  const [buildpackUrl, setBuildpackUrl] = useState("");
+  const [error, setError] = useState(false);
+
+  const handleAddCustomBuildpack = () => {
+    if (buildpackUrl === "" || !isValidURL(buildpackUrl)) {
+      setError(true);
+      return;
+    }
+    setBuildpackUrl("");
+    onAdd({
+      buildpack: buildpackUrl,
+      name: buildpackUrl,
+      config: {},
+    });
+  };
+
+  return (
+    <StyledCard marginBottom="0px">
+      <ContentContainer>
+        <EventInformation>
+          <BuildpackInputContainer>
+            GitHub or ZIP URL
+            <BuildpackUrlInput
+              placeholder="https://github.com/custom/buildpack"
+              type="input"
+              value={buildpackUrl}
+              isRequired
+              setValue={(newUrl) => {
+                setError(false);
+                setBuildpackUrl(newUrl as string);
+              }}
+            />
+            <ErrorText hasError={error}>Please enter a valid url</ErrorText>
+          </BuildpackInputContainer>
+        </EventInformation>
+      </ContentContainer>
+      <ActionContainer>
+        <ActionButton onClick={() => handleAddCustomBuildpack()}>
+          <span className="material-icons-outlined">add</span>
+        </ActionButton>
+      </ActionContainer>
+    </StyledCard>
+  );
+};
+
+export default AddCustomBuildpackComponent;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const StyledCard = styled.div<{ marginBottom?: string }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 1px solid #494b4f;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: ${(props) => props.marginBottom || "30px"};
+  border-radius: 8px;
+  padding: 14px;
+  overflow: hidden;
+  height: 60px;
+  font-size: 13px;
+  animation: ${fadeIn} 0.5s;
+`;
+
+const ContentContainer = styled.div`
+  display: flex;
+  height: 100%;
+  width: 100%;
+  align-items: center;
+`;
+
+const EventInformation = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  height: 100%;
+`;
+
+const BuildpackInputContainer = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  padding-left: 15px;
+`;
+
+const BuildpackUrlInput = styled(InputRow)`
+  width: auto;
+  min-width: 300px;
+  max-width: 600px;
+  margin: unset;
+  margin-left: 10px;
+  display: inline-block;
+`;
+
+const ErrorText = styled.span`
+  color: red;
+  margin-left: 10px;
+  display: ${(props: { hasError: boolean }) =>
+    props.hasError ? "inline-block" : "none"};
+`;
+
+const ActionContainer = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  height: 100%;
+`;
+
+const ActionButton = styled.button`
+  position: relative;
+  border: none;
+  background: none;
+  color: white;
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  cursor: pointer;
+  color: #aaaabb;
+
+  :hover {
+    background: #ffffff11;
+    border: 1px solid #ffffff44;
+  }
+
+  > span {
+    font-size: 20px;
+  }
+`;

+ 137 - 0
dashboard/src/main/home/app-dashboard/build-settings/AdvancedBuildSettings.tsx

@@ -0,0 +1,137 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import Input from "components/porter/Input";
+import AnimateHeight from "react-animate-height";
+import Select from "components/porter/Select";
+import { PorterApp } from "../types/porterApp";
+import BuildpackStack from "./BuildpackStack";
+import _ from "lodash";
+
+interface AdvancedBuildSettingsProps {
+  porterApp: PorterApp;
+  updatePorterApp: (attrs: Partial<PorterApp>) => void;
+  autoDetectBuildpacks: boolean;
+}
+
+const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = ({
+  porterApp,
+  updatePorterApp,
+  autoDetectBuildpacks,
+}) => {
+  const [showSettings, setShowSettings] = useState<boolean>(false);
+  const [buildView, setBuildView] = useState<string>(
+    !_.isEmpty(porterApp.dockerfile)
+      ? "docker" : "buildpacks"
+  );
+
+  return (
+    <>
+      <StyledAdvancedBuildSettings
+        showSettings={showSettings}
+        isCurrent={true}
+        onClick={() => {
+          setShowSettings(!showSettings);
+        }}
+      >
+        {buildView == "docker" ? (
+          <AdvancedBuildTitle>
+            <i className="material-icons dropdown">arrow_drop_down</i>
+            Configure Dockerfile settings
+          </AdvancedBuildTitle>
+        ) : (
+          <AdvancedBuildTitle>
+            <i className="material-icons dropdown">arrow_drop_down</i>
+            Configure buildpack settings
+          </AdvancedBuildTitle>
+        )}
+      </StyledAdvancedBuildSettings>
+
+      <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
+        <StyledSourceBox>
+          <Select
+            value={buildView}
+            width="300px"
+            options={[
+              { value: "docker", label: "Docker" },
+              { value: "buildpacks", label: "Buildpacks" },
+            ]}
+            setValue={(option) => setBuildView(option)}
+            label="Build method"
+          />
+          {buildView === "docker"
+            ?
+            <>
+              <Spacer y={0.5} />
+              <Text color="helper">Dockerfile path (absolute path)</Text>
+              <Spacer y={0.5} />
+              <Input
+                placeholder="ex: ./Dockerfile"
+                value={porterApp.dockerfile}
+                width="300px"
+                setValue={(val: string) => updatePorterApp({ dockerfile: val })}
+              />
+              <Spacer y={0.5} />
+            </>
+            : <BuildpackStack
+              porterApp={porterApp}
+              updatePorterApp={updatePorterApp}
+              autoDetectBuildpacks={autoDetectBuildpacks}
+            />}
+        </StyledSourceBox>
+      </AnimateHeight>
+    </>
+  );
+};
+
+export default AdvancedBuildSettings;
+
+const StyledAdvancedBuildSettings = styled.div`
+  color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")};
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+    color: white;
+  }
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-radius: 5px;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 10px;
+  cursor: pointer;
+  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
+  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
+
+  .dropdown {
+    margin-right: 8px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
+    props.showSettings ? "" : "rotate(-90deg)"};
+  }
+`;
+
+const AdvancedBuildTitle = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const StyledSourceBox = styled.div`
+  width: 100%;
+  color: #ffffff;
+  padding: 25px 35px 25px;
+  position: relative;
+  font-size: 13px;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid #494b4f;
+  border-top: 0px;
+  border-top-left-radius: 0px;
+  border-top-right-radius: 0px;
+`;

+ 200 - 0
dashboard/src/main/home/app-dashboard/build-settings/BranchSelector.tsx

@@ -0,0 +1,200 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+import branch_icon from "assets/branch.png";
+
+import Loading from "components/Loading";
+import SearchBar from "components/SearchBar";
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+
+type Props = {
+  setBranch: (x: string) => void;
+  currentBranch?: string;
+  repo_name: string;
+  git_repo_id: number;
+};
+
+const BranchSelector: React.FC<Props> = ({
+  setBranch,
+  currentBranch,
+  repo_name,
+  git_repo_id,
+}) => {
+  const sortBranches = (branches: string[]) => {
+    if (!currentBranch) return branches;
+    return [
+      currentBranch,
+      ...branches.filter((branch) => branch !== currentBranch),
+    ];
+  };
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(false);
+  const [branches, setBranches] = useState<string[]>([]);
+  const [searchFilter, setSearchFilter] = useState(null);
+
+  const { currentProject } = useContext(Context);
+
+  useEffect(() => {
+    api
+      .getBranches(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          git_repo_id,
+          kind: "github",
+          owner: repo_name.split("/")[0],
+          name: repo_name.split("/")[1],
+        }
+      )
+      .then((res) => {
+        setBranches(res.data);
+        setLoading(false);
+        setError(false);
+      })
+      .catch((err) => {
+        console.log(err);
+        setLoading(false);
+        setError(true);
+      });
+
+  }, [searchFilter]);
+
+  const renderBranchList = () => {
+    if (loading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    } else if (error || !branches) {
+      return <LoadingWrapper>Error loading branches</LoadingWrapper>;
+    }
+
+    let results =
+      searchFilter != null
+        ? branches
+          .filter((branch) => {
+            return branch.toLowerCase().includes(searchFilter.toLowerCase());
+          })
+          .sort((a: string, b: string) => {
+            const aIndex = a
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            const bIndex = b
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            return aIndex - bIndex;
+          })
+        : sortBranches(branches).slice(0, 10);
+
+    if (results.length == 0) {
+      return <LoadingWrapper>No matching Branches found.</LoadingWrapper>;
+    }
+    return results.map((branch: string, i: number) => {
+      return (
+        <BranchName
+          key={i}
+          lastItem={i === branches.length - 1}
+          onClick={() => setBranch(branch)}
+        >
+          <img src={branch_icon} alt={"branch icon"} />
+          {branch}
+        </BranchName>
+      );
+    });
+  };
+
+  return (
+    <>
+      <SearchBar
+        setSearchFilter={setSearchFilter}
+        disabled={error || loading}
+        prompt={"Search branches..."}
+      />
+      <BranchListWrapper>
+        <ExpandedWrapper>{renderBranchList()}</ExpandedWrapper>
+      </BranchListWrapper>
+    </>
+  );
+};
+
+export default BranchSelector;
+
+const BranchName = styled.div<{ lastItem: boolean; disabled?: boolean }>`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid
+    ${(props) => (props.lastItem ? "#00000000" : "#606166")};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: ${(props) => (props.disabled ? "default" : "pointer")};
+  background: #ffffff11;
+  :hover {
+    background: ${(props) => (props.disabled ? "#ffffff11" : "#ffffff22")};
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+  > div {
+    margin-left: auto;
+    display: flex;
+    align-items: center;
+
+    > span {
+      text-transform: capitalize;
+
+      :last-child {
+        margin-right: 15px;
+      }
+    }
+
+    > i {
+      margin-left: 10px;
+      margin-right: 15px;
+      font-size: 18px;
+      color: #03b503;
+    }
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  color: #ffffff44;
+`;
+
+const BranchListWrapper = styled.div`
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  overflow-y: auto;
+  position: relative;
+`;
+
+const ExpandedWrapper = styled.div`
+  width: 100%;
+  border-radius: 3px;
+  border: 0px solid #ffffff44;
+  max-height: 221px;
+  top: 40px;
+
+  > i {
+    font-size: 18px;
+    display: block;
+    position: absolute;
+    left: 10px;
+    top: 10px;
+  }
+`;

+ 189 - 0
dashboard/src/main/home/app-dashboard/build-settings/BuildSettingsTab.tsx

@@ -0,0 +1,189 @@
+import React, {
+  useContext,
+  useState,
+} from "react";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import { PorterAppOptions } from "shared/types";
+import { Context } from "shared/Context";
+
+import api from "shared/api";
+import { AxiosError } from "axios";
+import Button from "components/porter/Button";
+import Checkbox from "components/porter/Checkbox";
+import SharedBuildSettings from "./SharedBuildSettings";
+import { PorterApp } from "../types/porterApp";
+import _ from "lodash";
+
+type Props = {
+  porterApp: PorterApp;
+  setTempPorterApp: (app: PorterApp) => void;
+  updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
+  clearStatus: () => void;
+};
+
+const BuildSettingsTab: React.FC<Props> = ({
+  porterApp,
+  setTempPorterApp,
+  clearStatus,
+  updatePorterApp,
+}) => {
+  const { setCurrentError, currentCluster, currentProject } = useContext(Context);
+  const [redeployOnSave, setRedeployOnSave] = useState(true);
+  const [runningWorkflowURL, setRunningWorkflowURL] = useState("");
+
+  const [buttonStatus, setButtonStatus] = useState<
+    "loading" | "success" | string
+  >("");
+
+  const triggerWorkflow = async () => {
+    try {
+      if (currentProject == null || currentCluster == null) {
+        return;
+      }
+
+      const res = await api.reRunGHWorkflow(
+        "",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          git_installation_id: porterApp.git_repo_id,
+          owner: porterApp.repo_name?.split("/")[0],
+          name: porterApp.repo_name?.split("/")[1],
+          branch: porterApp.git_branch,
+          filename: "porter_stack_" + porterApp.name + ".yml",
+        }
+      );
+      if (res.data != null) {
+        window.open(res.data, "_blank", "noreferrer")
+      }
+    } catch (error) {
+      if (!error?.response) {
+        throw error;
+      }
+
+      let tmpError: AxiosError = error;
+
+      /**
+       * @smell
+       * Currently the expanded chart is clearing all the state when a chart update is triggered (saveEnvVariables).
+       * Temporary usage of setCurrentError until a context is applied to keep the state of the ReRunError during re renders.
+       */
+
+      if (tmpError.response.status === 400) {
+        // setReRunError({
+        //   title: "No previous run found",
+        //   description:
+        //     "There are no previous runs for this workflow, please trigger manually a run before changing the build settings.",
+        // });
+        setCurrentError(
+          "There are no previous runs for this workflow. Please manually trigger a run before changing build settings."
+        );
+        return;
+      }
+
+      if (tmpError.response.status === 409) {
+        // setReRunError({
+        //   title: "The workflow is still running",
+        //   description:
+        //     'If you want to make more changes, please choose the option "Save" until the workflow finishes.',
+        // });
+
+        if (typeof tmpError.response.data === "string") {
+          setRunningWorkflowURL(tmpError.response.data);
+        }
+        setCurrentError(
+          'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' +
+          tmpError.response.data
+        );
+        return;
+      }
+
+      if (tmpError.response.status === 404) {
+        let description = "No action file matching this deployment was found.";
+        if (typeof tmpError.response.data === "string") {
+          const filename = tmpError.response.data;
+          description = description.concat(
+            `Please check that the file "${filename}" exists in your repository.`
+          );
+        }
+        // setReRunError({
+        //   title: "The action doesn't seem to exist",
+        //   description,
+        // });
+
+        setCurrentError(description);
+        return;
+      }
+      throw error;
+    }
+  };
+
+  const saveConfig = async () => {
+    try {
+      await updatePorterApp({});
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  const handleSave = async () => {
+    setButtonStatus("loading");
+
+    try {
+      await saveConfig();
+      setButtonStatus("success");
+    } catch (error) {
+      setButtonStatus("Something went wrong");
+      console.log(error);
+    }
+  };
+
+  const handleSaveAndReDeploy = async () => {
+    setButtonStatus("loading");
+
+    try {
+      await saveConfig();
+      await triggerWorkflow();
+      setButtonStatus("success");
+      clearStatus();
+    } catch (error) {
+      setButtonStatus("Something went wrong");
+      console.log(error);
+    }
+  };
+  return (
+    <>
+      <SharedBuildSettings
+        porterApp={porterApp}
+        updatePorterApp={(attrs: Partial<PorterApp>) => setTempPorterApp(PorterApp.setAttributes(porterApp, attrs))}
+        setPorterYaml={() => { }}
+        autoDetectBuildpacks={false}
+        canChangeRepo={false}
+      />
+      <Spacer y={1} />
+      <Checkbox
+        checked={redeployOnSave}
+        toggleChecked={() => setRedeployOnSave(!redeployOnSave)}
+      >
+        <Text>Re-run build and deploy on save</Text>
+      </Checkbox>
+      <Spacer y={1} />
+      <Button
+        onClick={() => {
+          if (redeployOnSave) {
+            handleSaveAndReDeploy();
+          } else {
+            handleSave();
+          }
+        }}
+        status={buttonStatus}
+      >
+        Save build settings
+      </Button>
+    </>
+  );
+};
+
+export default BuildSettingsTab;

+ 166 - 0
dashboard/src/main/home/app-dashboard/build-settings/BuildpackCard.tsx

@@ -0,0 +1,166 @@
+import React from "react";
+import { Buildpack } from "./BuildpackStack";
+import { DeviconsNameList } from "assets/devicons-name-list";
+import styled, { keyframes } from "styled-components";
+import { Draggable } from "react-beautiful-dnd";
+
+interface Props {
+    buildpack: Buildpack;
+    action: 'add' | 'remove';
+    onClickFn: (buildpack: string) => void;
+    index: number;
+    draggable: boolean;
+}
+
+const BuildpackCard: React.FC<Props> = ({
+    buildpack,
+    action,
+    onClickFn,
+    index,
+    draggable,
+}) => {
+    const [languageName] = buildpack.name?.split("/").reverse();
+
+    const devicon = DeviconsNameList.find(
+        (devicon) => languageName.toLowerCase() === devicon.name
+    );
+
+    const icon = `devicon-${devicon?.name}-plain colored`;
+
+    return (
+        draggable ?
+            <Draggable
+                draggableId={buildpack.name}
+                index={index}
+                key={buildpack.name}
+            >
+                {(provided) => (
+                    <StyledCard
+                        marginBottom="5px"
+                        {...provided.draggableProps}
+                        {...provided.dragHandleProps}
+                        ref={provided.innerRef}
+                        key={buildpack.name}
+                    >
+                        <ContentContainer>
+                            <Icon disableMarginRight={devicon == null} className={icon} />
+                            <EventInformation>
+                                <EventName>{buildpack?.name}</EventName>
+                            </EventInformation>
+                        </ContentContainer>
+                        <ActionContainer>
+                            <ActionButton
+                                onClick={() => onClickFn(buildpack.buildpack)}
+                            >
+                                <span className="material-icons">{action === "remove" ? "delete" : "add"}</span>
+                            </ActionButton>
+
+                        </ActionContainer>
+                    </StyledCard>
+                )}
+            </Draggable>
+            :
+            <StyledCard marginBottom="5px" key={buildpack.name}>
+                <ContentContainer>
+                    <Icon disableMarginRight={devicon == null} className={icon} />
+                    <EventInformation>
+                        <EventName>{buildpack?.name}</EventName>
+                    </EventInformation>
+                </ContentContainer>
+                <ActionContainer>
+                    <ActionButton
+                        onClick={() => onClickFn(buildpack.buildpack)}
+                    >
+                        <span className="material-icons">{action === "remove" ? "delete" : "add"}</span>
+                    </ActionButton>
+
+                </ActionContainer>
+            </StyledCard>
+    );
+}
+
+export default BuildpackCard;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const StyledCard = styled.div<{ marginBottom?: string }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 1px solid #494b4f;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: ${(props) => props.marginBottom || "30px"};
+  border-radius: 8px;
+  padding: 14px;
+  overflow: hidden;
+  height: 60px;
+  font-size: 13px;
+  animation: ${fadeIn} 0.5s;
+`;
+
+const ContentContainer = styled.div`
+  display: flex;
+  height: 100%;
+  width: 100%;
+  align-items: center;
+`;
+
+const Icon = styled.span<{ disableMarginRight: boolean }>`
+  font-size: 20px;
+  margin-left: 10px;
+  ${(props) => {
+        if (!props.disableMarginRight) {
+            return "margin-right: 20px";
+        }
+    }}
+`;
+
+const EventInformation = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  height: 100%;
+`;
+
+const EventName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+`;
+
+const ActionContainer = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  height: 100%;
+`;
+
+const ActionButton = styled.button`
+  position: relative;
+  border: none;
+  background: none;
+  color: white;
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  cursor: pointer;
+  color: #aaaabb;
+
+  :hover {
+    background: #ffffff11;
+    border: 1px solid #ffffff44;
+  }
+
+  > span {
+    font-size: 20px;
+  }
+`;

+ 145 - 0
dashboard/src/main/home/app-dashboard/build-settings/BuildpackList.tsx

@@ -0,0 +1,145 @@
+import React from "react";
+import { PorterApp } from "../types/porterApp";
+import { Buildpack } from "./BuildpackStack";
+import BuildpackCard from "./BuildpackCard";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Loading from "components/Loading";
+import Error from "components/porter/Error";
+import { Droppable, DragDropContext } from "react-beautiful-dnd";
+
+interface Props {
+    porterApp: PorterApp,
+    updatePorterApp: (attrs: Partial<PorterApp>) => void,
+    selectedBuildpacks: Buildpack[],
+    availableBuildpacks: Buildpack[],
+    setSelectedBuildpacks: (buildpacks: Buildpack[]) => void,
+    setAvailableBuildpacks: (buildpacks: Buildpack[]) => void,
+    showAvailableBuildpacks: boolean,
+    isDetectingBuildpacks: boolean,
+    detectBuildpacksError: string,
+    droppableId: string,
+}
+const BuildpackList: React.FC<Props> = ({
+    porterApp,
+    updatePorterApp,
+    selectedBuildpacks,
+    availableBuildpacks,
+    setSelectedBuildpacks,
+    setAvailableBuildpacks,
+    showAvailableBuildpacks,
+    isDetectingBuildpacks,
+    detectBuildpacksError,
+    droppableId,
+}) => {
+    const handleRemoveBuildpack = (buildpackToRemove: string) => {
+        if (porterApp.buildpacks.includes(buildpackToRemove)) {
+            updatePorterApp({ buildpacks: porterApp.buildpacks.filter(bp => bp !== buildpackToRemove) });
+            const buildpack = selectedBuildpacks.find(bp => bp.buildpack === buildpackToRemove) as Buildpack;
+            if (buildpack != null) {
+                setAvailableBuildpacks([...availableBuildpacks, buildpack]);
+                setSelectedBuildpacks(selectedBuildpacks.filter(bp => bp.buildpack !== buildpackToRemove));
+            }
+        }
+    };
+
+    const handleAddBuildpack = (buildpackToAdd: string) => {
+        if (porterApp.buildpacks.find((bp) => bp === buildpackToAdd) == null) {
+            updatePorterApp({ buildpacks: [...porterApp.buildpacks, buildpackToAdd] });
+            const buildpack = availableBuildpacks.find((bp) => bp.buildpack === buildpackToAdd);
+            if (buildpack != null) {
+                setSelectedBuildpacks([...selectedBuildpacks, buildpack]);
+                setAvailableBuildpacks(availableBuildpacks.filter((bp) => bp.buildpack !== buildpackToAdd));
+            }
+        }
+    };
+
+    const onDragEnd = (result: any) => {
+        if (!result.destination) {
+            return;
+        }
+        const oldSelected = [...selectedBuildpacks];
+        const [removed] = oldSelected.splice(result.source.index, 1);
+        oldSelected.splice(result.destination.index, 0, removed);
+        setSelectedBuildpacks(oldSelected);
+        updatePorterApp({ buildpacks: oldSelected.map((bp) => bp.buildpack) });
+    };
+
+    const renderAvailableBuildpacks = () => {
+        if (isDetectingBuildpacks) {
+            return (
+                <Loading />
+            )
+        }
+
+        if (detectBuildpacksError) {
+            return (
+                <Error message={detectBuildpacksError} />
+            )
+        }
+
+        if (availableBuildpacks.length > 0) {
+            return (
+                <>
+                    <Spacer y={0.5} />
+                    <Text>Available buildpacks:</Text>
+                    <Spacer y={0.5} />
+                    {availableBuildpacks.map((buildpack, index) => {
+                        return (
+                            <BuildpackCard
+                                buildpack={buildpack}
+                                action={"add"}
+                                onClickFn={handleAddBuildpack}
+                                index={index}
+                                draggable={false}
+                            />
+                        )
+                    })
+                    }
+                </>
+            )
+        }
+
+        return (
+            <>
+                <Spacer y={0.5} />
+                <Text color="helper">No buildpacks detected. Click 'Detect buildpacks' below to scan your repository for available buildpacks.</Text>
+            </>
+        )
+    }
+
+    return (
+        <DragDropContext onDragEnd={onDragEnd}>
+            {showAvailableBuildpacks &&
+                <>
+                    <Spacer y={0.5} />
+                    <Text>Selected buildpacks:</Text>
+                    <Spacer y={0.5} />
+                </>
+            }
+            <Droppable droppableId={droppableId}>
+                {provided => (
+                    <div
+                        {...provided.droppableProps}
+                        ref={provided.innerRef}
+                    >
+                        {selectedBuildpacks?.map((buildpack, index) => (
+                            <BuildpackCard
+                                buildpack={buildpack}
+                                action={"remove"}
+                                onClickFn={handleRemoveBuildpack}
+                                index={index}
+                                draggable={true}
+                                key={index}
+                            />
+                        ))}
+                        {provided.placeholder}
+                    </div>
+                )}
+            </Droppable>
+            {showAvailableBuildpacks && renderAvailableBuildpacks()}
+        </DragDropContext>
+    );
+};
+
+export default BuildpackList;

+ 362 - 0
dashboard/src/main/home/app-dashboard/build-settings/BuildpackStack.tsx

@@ -0,0 +1,362 @@
+import { DeviconsNameList } from "assets/devicons-name-list";
+import Helper from "components/form-components/Helper";
+import Select from "components/porter/Select";
+import Loading from "components/Loading";
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import styled, { keyframes } from "styled-components";
+import Button from "components/porter/Button";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Error from "components/porter/Error";
+import { PorterApp } from "../types/porterApp";
+import AddCustomBuildpackComponent from "./AddCustomBuildpackComponent";
+import BuildpackList from "./BuildpackList";
+import Icon from "components/porter/Icon";
+import stars from "assets/stars-white.svg";
+
+const DEFAULT_BUILDER_NAME = "heroku";
+const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
+const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
+
+type BuildConfig = {
+  builder: string;
+  buildpacks: string[];
+  config: null | {
+    [key: string]: string;
+  };
+};
+
+export type Buildpack = {
+  name: string;
+  buildpack: string;
+  config: {
+    [key: string]: string;
+  };
+};
+
+type DetectedBuildpack = {
+  name: string;
+  builders: string[];
+  detected: Buildpack[];
+  others: Buildpack[];
+  buildConfig: BuildConfig;
+};
+
+const BUILDPACK_TO_NAME: { [key: string]: string } = {
+  "heroku/nodejs": "NodeJS",
+  "heroku/python": "Python",
+  "heroku/java": "Java",
+  "heroku/ruby": "Ruby",
+  "heroku/go": "Go",
+};
+
+const BuildpackStack: React.FC<{
+  porterApp: PorterApp;
+  updatePorterApp: (attrs: Partial<PorterApp>) => void;
+  autoDetectBuildpacks: boolean;
+}> = ({
+  porterApp,
+  updatePorterApp,
+  autoDetectBuildpacks,
+}) => {
+    const { currentProject } = useContext(Context);
+
+    const [builders, setBuilders] = useState<DetectedBuildpack[]>([]);
+    const [selectedStack, setSelectedStack] = useState<string>("");
+    const [isModalOpen, setIsModalOpen] = useState(false);
+    const [isDetectingBuildpacks, setIsDetectingBuildpacks] = useState(false);
+    const [error, setError] = useState<string>("");
+
+    const [selectedBuildpacks, setSelectedBuildpacks] = useState<Buildpack[]>([]);
+    const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>([]);
+    const renderModalContent = () => {
+      return (
+        <>
+          <Text size={16}>Buildpack Configuration</Text>
+          <Spacer y={1} />
+          <Scrollable>
+            <Select
+              value={selectedStack}
+              width="300px"
+              options={sortedStackOptions}
+              setValue={(option) => {
+                setSelectedStack(option);
+                updatePorterApp({ builder: option });
+              }}
+              label="Builder and stack"
+            />
+            <Spacer y={0.5} />
+            <BuildpackList
+              selectedBuildpacks={selectedBuildpacks}
+              setSelectedBuildpacks={setSelectedBuildpacks}
+              availableBuildpacks={availableBuildpacks}
+              setAvailableBuildpacks={setAvailableBuildpacks}
+              porterApp={porterApp}
+              updatePorterApp={updatePorterApp}
+              showAvailableBuildpacks={true}
+              isDetectingBuildpacks={isDetectingBuildpacks}
+              detectBuildpacksError={error}
+              droppableId={"modal"}
+            />
+            <Spacer y={0.5} />
+            <Text color="helper">
+              You may also add buildpacks by directly providing their GitHub links
+              or links to ZIP files that contain the buildpack source code.
+            </Text>
+            <Spacer y={1} />
+            <AddCustomBuildpackComponent onAdd={handleAddCustomBuildpack} />
+            <Spacer y={2} />
+          </Scrollable>
+          <Footer>
+            <Shade />
+            <FooterButtons>
+              <Button onClick={() => detectAndSetBuildPacks(true)}>
+                <Icon src={stars} height="15px" />
+                <Spacer inline x={0.5} />
+                Detect buildpacks
+              </Button>
+              <Button onClick={() => setIsModalOpen(false)} width={"75px"}>Save</Button>
+            </FooterButtons>
+          </Footer>
+        </>
+      );
+    };
+    const detectAndSetBuildPacks = async (detect: boolean) => {
+      try {
+        if (currentProject == null) {
+          return;
+        }
+
+        if (!detect) {
+          // in this case, we are not detecting buildpacks, so we just populate based on the DB
+          setBuilders([{
+            name: porterApp.builder.split("/")[0],
+            builders: [porterApp.builder],
+            detected: [],
+            others: [],
+            buildConfig: {} as BuildConfig,
+          }])
+          setSelectedStack(porterApp.builder);
+          setSelectedBuildpacks(porterApp.buildpacks?.map(bp => ({
+            name: BUILDPACK_TO_NAME[bp] ?? bp,
+            buildpack: bp,
+            config: {},
+          })) ?? []);
+          setAvailableBuildpacks([]);
+        } else {
+          if (isDetectingBuildpacks) {
+            return;
+          }
+          setIsDetectingBuildpacks(true);
+          const detectBuildPackRes = await api.detectBuildpack(
+            "<token>",
+            {
+              dir: porterApp.build_context || ".",
+            },
+            {
+              project_id: currentProject.id,
+              git_repo_id: porterApp.git_repo_id,
+              kind: "github",
+              owner: porterApp.repo_name.split("/")[0],
+              name: porterApp.repo_name.split("/")[1],
+              branch: porterApp.git_branch,
+            }
+          );
+
+          const builders = detectBuildPackRes.data as DetectedBuildpack[];
+          if (builders.length === 0) {
+            return;
+          }
+          setBuilders(builders);
+
+          const defaultBuilder = builders.find(
+            (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
+          ) ?? builders[0];
+
+          const allBuildpacks = defaultBuilder.others.concat(defaultBuilder.detected);
+
+          let detectedBuilder: string;
+          if (defaultBuilder.builders.length && defaultBuilder.builders.includes(DEFAULT_HEROKU_STACK)) {
+            setSelectedStack(DEFAULT_HEROKU_STACK);
+            detectedBuilder = DEFAULT_HEROKU_STACK;
+          } else {
+            setSelectedStack(defaultBuilder.builders[0]);
+            detectedBuilder = defaultBuilder.builders[0];
+          }
+
+          const newBuildpacks = defaultBuilder.detected.filter(bp => !porterApp.buildpacks.includes(bp.buildpack));
+          if (autoDetectBuildpacks) {
+            updatePorterApp({ builder: detectedBuilder, buildpacks: [...porterApp.buildpacks, ...newBuildpacks.map(bp => bp.buildpack)] });
+            setSelectedBuildpacks(defaultBuilder.detected);
+            setAvailableBuildpacks(defaultBuilder.others);
+            setError("");
+          } else {
+            setAvailableBuildpacks(allBuildpacks.filter(bp => !porterApp.buildpacks?.includes(bp.buildpack)));
+          }
+        }
+      } catch (err) {
+        if (autoDetectBuildpacks) {
+          updatePorterApp({ buildpacks: [] });
+          setSelectedBuildpacks([]);
+          setAvailableBuildpacks([]);
+          setError(`Unable to detect buildpacks at path: ${porterApp.build_context}. Please make sure your repo, branch, and application root path are all set correctly and attempt to detect again.`);
+        }
+      } finally {
+        setIsDetectingBuildpacks(false);
+      }
+    }
+
+    useEffect(() => {
+      detectAndSetBuildPacks(autoDetectBuildpacks);
+    }, [currentProject]);
+
+    const builderOptions = useMemo(() => {
+      if (!Array.isArray(builders)) {
+        return;
+      }
+
+      return builders.map((builder) => ({
+        label: builder.name,
+        value: builder.name.toLowerCase(),
+      }));
+    }, [builders]);
+
+    const stackOptions = useMemo(() => {
+      if (!Array.isArray(builders)) {
+        return;
+      }
+
+      return builders.flatMap((builder) => {
+        return builder.builders.map((stack) => ({
+          label: `${builder.name} - ${stack}`,
+          value: stack.toLowerCase(),
+        }));
+      });
+    }, [builders]);
+
+    const handleAddCustomBuildpack = (buildpack: Buildpack) => {
+      if (porterApp.buildpacks.find((bp) => bp === buildpack.buildpack) == null) {
+        updatePorterApp({ buildpacks: [...porterApp.buildpacks, buildpack.buildpack] });
+        setSelectedBuildpacks([...selectedBuildpacks, buildpack]);
+      }
+    };
+
+    if (!stackOptions?.length || !builderOptions?.length) {
+      return <Loading />;
+    }
+
+    const sortedStackOptions = stackOptions.sort((a, b) => {
+      if (a.label < b.label) {
+        return -1;
+      }
+      if (a.label > b.label) {
+        return 1;
+      }
+      return 0;
+    });
+
+    return (
+      <BuildpackConfigurationContainer>
+        {selectedBuildpacks.length > 0 && (
+          <>
+            <Helper>
+              The following buildpacks were automatically detected. You can also
+              manually add, remove, or re-order buildpacks here.
+            </Helper>
+            <BuildpackList
+              selectedBuildpacks={selectedBuildpacks}
+              setSelectedBuildpacks={setSelectedBuildpacks}
+              availableBuildpacks={availableBuildpacks}
+              setAvailableBuildpacks={setAvailableBuildpacks}
+              porterApp={porterApp}
+              updatePorterApp={updatePorterApp}
+              showAvailableBuildpacks={false}
+              isDetectingBuildpacks={isDetectingBuildpacks}
+              detectBuildpacksError={error}
+              droppableId={"non-modal"}
+            />
+          </>
+        )}
+        {autoDetectBuildpacks && error !== "" && (
+          <>
+            <Spacer y={1} />
+            <Error message={error} />
+          </>
+        )}
+        <Spacer y={1} />
+        <Button onClick={() => {
+          setIsModalOpen(true);
+          setError("");
+        }}>
+          <I className="material-icons">add</I> Add / detect buildpacks
+        </Button>
+        {isModalOpen && (
+          <Modal closeModal={() => setIsModalOpen(false)}>
+            {renderModalContent()}
+          </Modal>
+        )}
+      </BuildpackConfigurationContainer>
+    );
+  };
+
+export default BuildpackStack;
+
+
+const Shade = styled.div`
+  position: absolute;
+  top: -50px;
+  left: 0;
+  height: 50px;
+  width: 100%;
+  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
+`;
+
+const FooterButtons = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
+
+const Footer = styled.div`
+  position: relative;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  padding: 0 25px;
+  border-bottom-left-radius: 10px;
+  border-bottom-right-radius: 10px;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: -30px;
+  padding-bottom: 30px;
+`;
+
+const I = styled.i`
+  color: white;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-right: 5px;
+  justify-content: center;
+`;
+
+const Scrollable = styled.div`
+  overflow-y: auto;
+  padding: 0 25px;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  max-height: calc(100vh - 300px);
+`;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const BuildpackConfigurationContainer = styled.div`
+  animation: ${fadeIn} 0.75s;
+`;

+ 253 - 0
dashboard/src/main/home/app-dashboard/build-settings/DetectContentsList.tsx

@@ -0,0 +1,253 @@
+import React, { useState, useEffect, useContext, useCallback } from "react";
+import styled from "styled-components";
+import Button from "components/porter/Button";
+import api from "shared/api";
+import Error from "components/porter/Error";
+
+import { Context } from "shared/Context";
+import { FileType } from "shared/types";
+
+import Spacer from "components/porter/Spacer";
+import Modal from "components/porter/Modal";
+import Input from "components/porter/Input";
+import Text from "components/porter/Text";
+import Link from "components/porter/Link";
+import { PorterApp } from "../types/porterApp";
+
+type PropsType = {
+  setPorterYaml: (yaml: string, filename: string) => void;
+  porterApp: PorterApp;
+  updatePorterApp: (attrs: Partial<PorterApp>) => void;
+};
+
+const DetectContentsList: React.FC<PropsType> = ({
+  setPorterYaml,
+  porterApp,
+  updatePorterApp,
+}) => {
+  const [showModal, setShowModal] = useState(false);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(false);
+  const [contents, setContents] = useState<FileType[]>([]);
+  const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
+  const [possiblePorterYamlPath, setPossiblePorterYamlPath] = useState<string>("");
+
+  const { currentProject } = useContext(Context);
+  const fetchAndSetPorterYaml = async (fileName: string) => {
+    setButtonStatus("loading");
+    const response = await fetchPorterYamlContent(fileName);
+    if (response == null) {
+      setButtonStatus(<Error message="Unable to detect porter.yaml. Please check your path and try again, or continue without using porter.yaml." />);
+    } else {
+      setPorterYaml(atob(response.data), fileName);
+      setButtonStatus("success");
+    }
+    setShowModal(false);
+  };
+
+  useEffect(() => {
+    const fetchOnRender = async () => {
+      try {
+        const response = await fetchPorterYamlContent("./porter.yaml");
+        setPorterYaml(atob(response.data), "./porter.yaml");
+      } catch (error) {
+        setShowModal(true);
+      }
+    };
+    fetchOnRender();
+  }, []);
+
+  useEffect(() => {
+    updateContents();
+  }, []);
+
+  useEffect(() => {
+    const dockerFileItem = contents.find((item: FileType) =>
+      item.path.includes("Dockerfile")
+    );
+
+    if (dockerFileItem) {
+      updatePorterApp({ dockerfile: dockerFileItem.path });
+    }
+  }, [contents]);
+
+  const renderContentList = () => {
+    contents.map((item: FileType, i: number) => {
+      let splits = item.path.split("/");
+      let fileName = splits[splits.length - 1];
+      if (fileName.includes("Dockerfile")) {
+        return false;
+      }
+    });
+
+    return true;
+  };
+
+  const fetchContents = () => {
+    if (currentProject == null) {
+      return;
+    }
+
+    return api.getBranchContents(
+      "<token>",
+      { dir: porterApp.build_context || "./" },
+      {
+        project_id: currentProject.id,
+        git_repo_id: porterApp.git_repo_id,
+        kind: "github",
+        owner: porterApp.repo_name.split("/")[0],
+        name: porterApp.repo_name.split("/")[1],
+        branch: porterApp.git_branch,
+      }
+    );
+  };
+
+  const fetchPorterYamlContent = async (porterYamlPath: string) => {
+    try {
+      if (currentProject == null) {
+        return;
+      }
+      const res = await api.getPorterYamlContents(
+        "<token>",
+        {
+          path: porterYamlPath,
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: porterApp.git_repo_id,
+          kind: "github",
+          owner: porterApp.repo_name.split("/")[0],
+          name: porterApp.repo_name.split("/")[1],
+          branch: porterApp.git_branch,
+        }
+      );
+      return res;
+    } catch (err) {
+      // console.log(err);
+    }
+
+  };
+
+  const updateContents = async () => {
+    try {
+      const res = await fetchContents();
+      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);
+
+      setContents(contents);
+      setLoading(false);
+      setError(false);
+    } catch (err) {
+      console.log(err);
+      setLoading(false);
+      setError(true);
+    }
+  };
+
+  const NoPorterYamlContent = () => (
+    <div>
+      <Text size={16}>No <Code>porter.yaml</Code> detected</Text>
+      <Spacer y={0.5} />
+      <span>
+        <Text color="helper">
+          We were unable to find a <Code>porter.yaml</Code> file in your root directory. We
+          recommend that you add a <Code>porter.yaml</Code> file to your root directory
+          or specify the path here.
+        </Text>
+        <Spacer y={0.5} />
+        <Link
+          to="https://docs.porter.run/standard/deploying-applications/writing-porter-yaml"
+          target="_blank"
+          hasunderline
+        >
+          Using porter.yaml
+        </Link>
+      </span>
+    </div>
+  );
+  return (
+    <>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>
+          <NoPorterYamlContent />
+          <Spacer y={0.5} />
+          <Text color="helper">Path to <Code>porter.yaml</Code> from repository root:</Text>
+          <Spacer y={0.5} />
+          <Input
+            disabled={false}
+            placeholder="ex: ./subdirectory/porter.yaml"
+            value={possiblePorterYamlPath}
+            width="100%"
+            setValue={setPossiblePorterYamlPath}
+          />
+          <Spacer y={1} />
+          <div style={{ display: "flex", justifyContent: "space-between" }}>
+            <Button
+              onClick={() => {
+                setShowModal(false);
+                updatePorterApp({ porter_yaml_path: "" });
+              }}
+              loadingText="Submitting..."
+              color="#ffffff11"
+              status={loading ? "loading" : undefined}
+            >
+              Ignore
+            </Button>
+            <Button
+              onClick={() => fetchAndSetPorterYaml(possiblePorterYamlPath)}
+              loadingText="Submitting..."
+              color="#616fee"
+              status={loading ? "loading" : undefined}
+            >
+              Update path
+            </Button>
+          </div>
+        </Modal>
+      )}
+      {renderContentList() && (
+        <>
+          {possiblePorterYamlPath !== "" && (
+            <>
+              <Text color="helper">Porter.yaml path:</Text>
+              <Spacer y={0.5} />
+              <Input
+                disabled={false}
+                placeholder="ex: ./"
+                value={possiblePorterYamlPath}
+                width="100%"
+                onValueChange={setPossiblePorterYamlPath}
+              />
+              <Spacer y={1} />
+              <Button
+                onClick={() => fetchAndSetPorterYaml(possiblePorterYamlPath)}
+                loadingText="Submitting..."
+                status={buttonStatus}
+              >
+                Update Path
+              </Button>
+              <Spacer y={1} />
+            </>
+          )}
+
+        </>
+      )}
+    </>
+  );
+};
+
+export default DetectContentsList;
+
+const Code = styled.span`
+  font-family: monospace;
+`;

+ 146 - 0
dashboard/src/main/home/app-dashboard/build-settings/ProviderSelector.tsx

@@ -0,0 +1,146 @@
+import Loading from "components/Loading";
+import React, { useRef, useState } from "react";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
+import styled from "styled-components";
+
+interface Props {
+    values: any[];
+    currentValue: any;
+    onChange: (provider: any) => void;
+}
+const ProviderSelector: React.FC<Props> = ({
+    values,
+    currentValue,
+    onChange
+}) => {
+    const wrapperRef = useRef();
+    const [isOpen, setIsOpen] = useState(false);
+    const icon = `devicon-${currentValue?.provider}-plain colored`;
+    useOutsideAlerter(wrapperRef, () => {
+        setIsOpen(false);
+    });
+
+    if (!currentValue) {
+        return (
+            <ProviderSelectorStyles.Wrapper>
+                <Loading />
+            </ProviderSelectorStyles.Wrapper>
+        );
+    }
+
+    return (
+        <>
+            <ProviderSelectorStyles.Wrapper ref={wrapperRef} isOpen={isOpen}>
+                <ProviderSelectorStyles.Icon className={icon} />
+
+                <ProviderSelectorStyles.Button
+                    onClick={() => setIsOpen((prev) => !prev)}
+                >
+                    {currentValue?.name || currentValue?.instance_url}
+                </ProviderSelectorStyles.Button>
+                <i className="material-icons">arrow_drop_down</i>
+                {isOpen ? (
+                    <>
+                        <ProviderSelectorStyles.OptionWrapper>
+                            {values.map((provider, index) => {
+                                return (
+                                    <ProviderSelectorStyles.Option
+                                        onClick={() => {
+                                            setIsOpen(false);
+                                            onChange(provider);
+                                        }}
+                                        key={index}
+                                    >
+                                        <ProviderSelectorStyles.Icon
+                                            className={`devicon-${provider?.provider}-plain colored`}
+                                        />
+                                        <ProviderSelectorStyles.Text>
+                                            {provider?.name || provider?.instance_url}
+                                        </ProviderSelectorStyles.Text>
+                                    </ProviderSelectorStyles.Option>
+                                );
+                            })}
+                        </ProviderSelectorStyles.OptionWrapper>
+                    </>
+                ) : null}
+            </ProviderSelectorStyles.Wrapper>
+        </>
+    );
+};
+
+export default ProviderSelector;
+
+const ProviderSelectorStyles = {
+    Wrapper: styled.div<{ isOpen?: boolean }>`
+      position: relative;
+      margin-bottom: 10px;
+      height: 40px;
+      display: flex;
+      min-width: 50%;
+      cursor: pointer;
+      margin-right: 10px;
+      margin-left: 2px;
+      align-items: center;
+  
+      > i {
+        margin-left: -26px;
+        margin-right: 10px;
+        z-index: 0;
+        transform: ${(props) => (props.isOpen ? "rotate(180deg)" : "")};
+      }
+    `,
+    Button: styled.div`
+      height: 100%;
+      font-weight: bold;
+      font-size: 14px;
+      border-bottom: 0;
+      z-index: 999;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      padding: 6px 15px;
+      padding-left: 40px;
+      padding-right: 28px;
+      border-bottom: 2px solid #ffffff;
+      padding-top: 11px;
+    `,
+    OptionWrapper: styled.div`
+      top: 40px;
+      position: absolute;
+      background: #37393f;
+      border-radius: 3px;
+      max-height: 300px;
+      overflow-y: auto;
+      width: calc(100% - 4px);
+      box-shadow: 0 8px 20px 0px #00000088;
+      z-index: 999;
+    `,
+    Option: styled.div`
+      display: flex;
+      align-items: center;
+  
+      :hover {
+        background-color: #ffffff22;
+      }
+    `,
+    Icon: styled.span`
+      font-size: 20px;
+      filter: invert(1);
+      margin-left: 9px;
+      margin-right: -29px;
+      color: white;
+    `,
+    Text: styled.div`
+      font-weight: bold;
+      font-size: 14px;
+      margin-left: 40px;
+      height: 45px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      padding: 8px 10px;
+      width: 100%;
+      padding-top: 14px;
+      padding-left: 0;
+    `,
+};

+ 408 - 0
dashboard/src/main/home/app-dashboard/build-settings/RepositorySelector.tsx

@@ -0,0 +1,408 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+import github from "assets/github-white.png";
+
+import api from "shared/api";
+import { RepoType } from "shared/types";
+import { Context } from "shared/Context";
+
+import DynamicLink from "components/DynamicLink";
+import Loading from "components/Loading";
+import SearchBar from "components/SearchBar";
+import ProviderSelector from "./ProviderSelector";
+import { PorterApp } from "../types/porterApp";
+
+type Props = {
+  readOnly: boolean;
+  updatePorterApp: (attrs: Partial<PorterApp>) => void;
+  git_repo_name: string;
+};
+
+type Provider =
+  | {
+    provider: "github";
+    name: string;
+    installation_id: number;
+  }
+  | {
+    provider: "gitlab";
+    instance_url: string;
+    integration_id: number;
+  };
+
+const RepositorySelector: React.FC<Props> = ({
+  readOnly,
+  git_repo_name,
+  updatePorterApp,
+}) => {
+  const [providers, setProviders] = useState([]);
+  const [currentProvider, setCurrentProvider] = useState(null);
+  const [repos, setRepos] = useState<RepoType[]>([]);
+  const [repoLoading, setRepoLoading] = useState(true);
+  const [repoError, setRepoError] = useState(false);
+  const [searchFilter, setSearchFilter] = useState<string>("");
+  const [hasProviders, setHasProviders] = useState(true);
+  const { currentProject, setCurrentError } = useContext(Context);
+
+  // Sort provider by name if it's github or instance url if it's gitlab
+  const sortProviders = (providers: Provider[]) => {
+    const githubProviders = providers.filter(
+      (provider) => provider.provider === "github"
+    );
+
+    return githubProviders.sort((a, b) => {
+      if (a.provider === "github" && b.provider === "github") {
+        return a.name.localeCompare(b.name);
+      }
+    });
+  };
+
+  useEffect(() => {
+    let isSubscribed = true;
+    api
+      .getGitProviders("<token>", {}, { project_id: currentProject.id })
+      .then((res) => {
+        const data = res.data;
+        if (!isSubscribed) {
+          return;
+        }
+
+        if (!Array.isArray(data)) {
+          setHasProviders(false);
+          return;
+        }
+
+        const sortedProviders = sortProviders(data);
+        setProviders(sortedProviders);
+        setCurrentProvider(sortedProviders[0]);
+      })
+      .catch((err) => {
+        setHasProviders(false);
+        setCurrentError(err);
+      });
+
+    return () => {
+      isSubscribed = false;
+    };
+  }, []);
+
+  const loadGithubRepos = async (repoId: number) => {
+    try {
+      const res = await api.getGitRepoList<
+        { FullName: string; Kind: "github" }[]
+      >("<token>", {}, { project_id: currentProject.id, git_repo_id: repoId });
+
+      const repos = res.data.map((repo) => ({ ...repo, GHRepoID: repoId }));
+      return repos;
+    } catch (error) { }
+  };
+
+  const loadRepos = (provider: any) => {
+    return loadGithubRepos(provider.installation_id);
+  };
+
+  useEffect(() => {
+    let isSubscribed = true;
+    if (!currentProvider) {
+      return () => {
+        isSubscribed = false;
+      };
+    }
+
+    setRepoLoading(true);
+
+    loadRepos(currentProvider)
+      .then((repos) => {
+        if (isSubscribed) {
+          setRepos(repos);
+        }
+      })
+      .catch((err) => {
+        setRepos([]);
+        console.log(err);
+      })
+      .finally(() => {
+        setRepoLoading(false);
+      });
+  }, [currentProvider, searchFilter]);
+
+  // clear out actionConfig and SelectedRepository if new search is performed
+  useEffect(() => {
+    updatePorterApp({
+      repo_name: "",
+      git_repo_id: 0,
+      git_branch: "",
+      image_repo_uri: "",
+    });
+  }, [searchFilter]);
+
+  const setRepo = (x: RepoType) => {
+    updatePorterApp({
+      repo_name: x.FullName,
+      git_repo_id: x.GHRepoID,
+      git_branch: "",
+      image_repo_uri: "",
+    });
+  };
+
+  const renderRepoList = () => {
+    if (repoLoading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    } else if (repoError) {
+      return <LoadingWrapper>Error loading repos.</LoadingWrapper>;
+    } else if (!Array.isArray(repos) || repos.length === 0) {
+      return (
+        <LoadingWrapper>
+          No connected Github repos found. You can
+          <A
+            to={`${window.location.origin}/api/integrations/github-app/install`}
+          >
+            Install Porter in more repositories
+          </A>
+          or select another git provider.
+        </LoadingWrapper>
+      );
+    }
+
+    // show 10 most recently used repos if user hasn't searched anything yet
+    let results =
+      searchFilter != null
+        ? repos
+          .filter((repo: RepoType) => {
+            return repo.FullName.toLowerCase().includes(
+              searchFilter.toLowerCase()
+            );
+          })
+          .sort((a: RepoType, b: RepoType) => {
+            const aIndex = a.FullName.toLowerCase().indexOf(
+              searchFilter.toLowerCase()
+            );
+            const bIndex = b.FullName.toLowerCase().indexOf(
+              searchFilter.toLowerCase()
+            );
+            return aIndex - bIndex;
+          })
+        : repos.slice(0, 10);
+
+    if (results.length == 0) {
+      return <LoadingWrapper>No matching Github repos found.</LoadingWrapper>;
+    } else {
+      return results.map((repo: RepoType, i: number) => {
+        return (
+          <RepoName
+            key={i}
+            isSelected={repo.FullName === git_repo_name}
+            lastItem={i === repos.length - 1}
+            onClick={() => setRepo(repo)}
+            readOnly={readOnly}
+            disabled={false}
+          >
+            {repo.Kind === "github" ? (
+              <img src={github} alt={"github icon"} />
+            ) : (
+              <i className="devicon-gitlab-plain colored" />
+            )}
+            {repo.FullName}
+          </RepoName>
+        );
+      });
+    }
+  };
+
+  const renderExpanded = () => {
+    if (readOnly) {
+      return <ExpandedWrapperAlt>{renderRepoList()}</ExpandedWrapperAlt>;
+    } else {
+      return (
+        <>
+          <div style={{ display: "flex", marginBottom: "10px" }}>
+            <ProviderSelector
+              values={providers}
+              currentValue={currentProvider}
+              onChange={setCurrentProvider}
+            />
+            <SearchBar
+              setSearchFilter={setSearchFilter}
+              disabled={repoError || repoLoading}
+              prompt={"Search repos . . ."}
+              fullWidth
+            />
+          </div>
+          <RepoListWrapper>
+            <ExpandedWrapper>{renderRepoList()}</ExpandedWrapper>
+          </RepoListWrapper>
+        </>
+      );
+    }
+  };
+
+  if (!hasProviders) {
+    const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+    const encoded_redirect_uri = encodeURIComponent(url);
+
+    return (
+      <>
+        <ConnectToGithubButton
+          href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
+        >
+          <GitHubIcon src={github} /> Install the Porter GitHub app
+        </ConnectToGithubButton>
+      </>
+    );
+  }
+
+  return <>{renderExpanded()}</>;
+};
+
+export default RepositorySelector;
+
+const GitHubIcon = styled.img`
+  width: 20px;
+  filter: brightness(150%);
+  margin-right: 10px;
+`;
+
+const ConnectToGithubButton = styled.a`
+  width: 240px;
+  justify-content: center;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  color: white;
+  font-weight: 500;
+  padding: 10px;
+  overflow: hidden;
+  white-space: nowrap;
+  margin-top: 25px;
+  border: 1px solid #494b4f;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#2E3338"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "" : "#353a3e"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const RepoListWrapper = styled.div`
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  overflow-y: auto;
+`;
+
+type RepoNameProps = {
+  lastItem: boolean;
+  isSelected: boolean;
+  readOnly: boolean;
+  disabled: boolean;
+};
+
+const RepoName = styled.div<RepoNameProps>`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid
+    ${(props) => (props.lastItem ? "#00000000" : "#606166")};
+  color: ${(props) => (props.disabled ? "#ffffff88" : "#ffffff")};
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: ${(props) =>
+    props.readOnly || props.disabled ? "default" : "pointer"};
+  pointer-events: ${(props) =>
+    props.readOnly || props.disabled ? "none" : "auto"};
+
+  ${(props) => {
+    if (props.disabled) {
+      return "";
+    }
+
+    if (props.isSelected) {
+      return `background: #ffffff22;`;
+    }
+
+    return `background: #ffffff11;`;
+  }}
+
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img,
+  i {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+    font-size: 20px;
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  width: 100%;
+  border-radius: 3px;
+  border: 0px solid #ffffff44;
+  max-height: 221px;
+  top: 40px;
+
+  > i {
+    font-size: 18px;
+    display: block;
+    position: absolute;
+    left: 10px;
+    top: 10px;
+  }
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)`
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;
+
+const A = styled(DynamicLink)`
+  color: #8590ff;
+  text-decoration: underline;
+  margin-left: 5px;
+  margin-right: 5px;
+
+  cursor: pointer;
+`;

+ 184 - 0
dashboard/src/main/home/app-dashboard/build-settings/SharedBuildSettings.tsx

@@ -0,0 +1,184 @@
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import React from "react";
+import styled from "styled-components";
+import { PorterApp } from "../types/porterApp";
+import DetectContentsList from "./DetectContentsList";
+import RepositorySelector from "./RepositorySelector";
+import BranchSelector from "./BranchSelector";
+import AdvancedBuildSettings from "./AdvancedBuildSettings";
+
+type Props = {
+  setPorterYaml: (yaml: string, filename: string) => void;
+  updatePorterApp: (attrs: Partial<PorterApp>) => void;
+  porterApp: PorterApp;
+  autoDetectBuildpacks: boolean;
+  canChangeRepo: boolean;
+};
+
+const SharedBuildSettings: React.FC<Props> = ({
+  setPorterYaml,
+  updatePorterApp,
+  porterApp,
+  autoDetectBuildpacks,
+  canChangeRepo,
+}) => {
+  return (
+    <>
+      <Text size={16}>Build settings</Text>
+      <Spacer y={0.5} />
+      <Text color="helper">Specify your GitHub repository.</Text>
+      <Spacer y={0.5} />
+      {porterApp.repo_name === "" && (
+        <>
+          <ExpandedWrapper>
+            <RepositorySelector
+              readOnly={false}
+              updatePorterApp={updatePorterApp}
+              git_repo_name={porterApp.repo_name}
+            />
+          </ExpandedWrapper>
+          <DarkMatter antiHeight="-4px" />
+          <Spacer y={0.3} />
+        </>
+      )}
+      {porterApp.repo_name !== "" && (
+        <>
+          <Input
+            disabled={true}
+            label="GitHub repository:"
+            width="100%"
+            value={porterApp.repo_name}
+            setValue={() => { }}
+            placeholder=""
+          />
+          {canChangeRepo &&
+            <>
+              <BackButton
+                width="135px"
+                onClick={() => {
+                  updatePorterApp({
+                    repo_name: "",
+                    git_branch: "",
+                    dockerfile: "",
+                    build_context: "./",
+                    porter_yaml_path: "./porter.yaml",
+                  })
+                }}
+              >
+                <i className="material-icons">keyboard_backspace</i>
+                Select repo
+              </BackButton>
+              <Spacer y={0.5} />
+            </>
+          }
+          <Spacer y={0.5} />
+          <Text color="helper">Specify your GitHub branch.</Text>
+          <Spacer y={0.5} />
+          {porterApp.git_branch === "" && (
+            <>
+              <ExpandedWrapper>
+                <BranchSelector
+                  setBranch={(branch: string) => updatePorterApp({ git_branch: branch })}
+                  repo_name={porterApp.repo_name}
+                  git_repo_id={porterApp.git_repo_id}
+                />
+              </ExpandedWrapper>
+            </>
+          )}
+          {porterApp.git_branch !== "" && (
+            <>
+              <Input
+                disabled={true}
+                label="GitHub branch:"
+                type="text"
+                width="100%"
+                value={porterApp.git_branch}
+                setValue={() => { }}
+                placeholder=""
+              />
+              <BackButton
+                width="145px"
+                onClick={() => {
+                  updatePorterApp({
+                    git_branch: "",
+                    dockerfile: "",
+                    build_context: "./",
+                    porter_yaml_path: "./porter.yaml",
+                  })
+                }}
+              >
+                <i className="material-icons">keyboard_backspace</i>
+                Select branch
+              </BackButton>
+              <Spacer y={1} />
+              <Text color="helper">Specify your application root path.</Text>
+              <Spacer y={0.5} />
+              <Input
+                placeholder="ex: ./"
+                value={porterApp.build_context}
+                width="100%"
+                setValue={(val: string) => updatePorterApp({ build_context: val })}
+              />
+              <Spacer y={1} />
+              <DetectContentsList
+                setPorterYaml={setPorterYaml}
+                porterApp={porterApp}
+                updatePorterApp={updatePorterApp}
+              />
+              <AdvancedBuildSettings
+                porterApp={porterApp}
+                updatePorterApp={updatePorterApp}
+                autoDetectBuildpacks={autoDetectBuildpacks}
+              />
+            </>
+          )}
+        </>
+      )}
+    </>
+  );
+};
+
+export default SharedBuildSettings;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  max-height: 275px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 22px;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  margin-bottom: -7px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+

+ 0 - 427
dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx

@@ -1,427 +0,0 @@
-import AnimateHeight from "react-animate-height";
-import React, {
-  Component,
-  Dispatch,
-  useContext,
-  useEffect,
-  useMemo,
-  useRef,
-  useState,
-} from "react";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import Input from "components/porter/Input";
-import AdvancedBuildSettings from "../new-app-flow/AdvancedBuildSettings";
-import styled from "styled-components";
-import { SourceType } from "../new-app-flow/SourceSelector";
-import ActionConfEditorStack from "components/repo-selector/ActionConfEditorStack";
-import {
-  ActionConfigType,
-  BuildConfig,
-  FullActionConfigType,
-  GithubActionConfigType,
-  PorterAppOptions,
-} from "shared/types";
-import { RouteComponentProps } from "react-router";
-import { Context } from "shared/Context";
-import ActionConfBranchSelector from "components/repo-selector/ActionConfBranchSelector";
-
-import { BuildpackStack } from "components/repo-selector/BuildpackStack";
-import api from "shared/api";
-import { AxiosError } from "axios";
-import InputRow from "components/form-components/InputRow";
-import Loading from "components/Loading";
-import Button from "components/porter/Button";
-import Container from "components/porter/Container";
-import Checkbox from "components/porter/Checkbox";
-type Props = {
-  appData: any;
-  setAppData: Dispatch<any>;
-  onTabSwitch: () => void;
-  updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
-  clearStatus: () => void;
-};
-interface AutoBuildpack {
-  name?: string;
-  valid: boolean;
-}
-
-const BuildSettingsTabStack: React.FC<Props> = ({
-  appData,
-  setAppData,
-  onTabSwitch,
-  clearStatus,
-  updatePorterApp,
-}) => {
-  const { setCurrentError } = useContext(Context);
-  const [updated, setUpdated] = useState(null);
-  const [branch, setBranch] = useState(appData.app.git_branch);
-  const [showSettings, setShowSettings] = useState(false);
-  const [dockerfilePath, setDockerfilePath] = useState(appData.app.dockerfile);
-  const [buildView, setBuildView] = useState<string>(
-    appData.app.dockerfile ? "docker" : "buildpacks"
-  );
-
-  const [folderPath, setFolderPath] = useState(appData.app.build_context);
-  const defaultActionConfig: ActionConfigType = {
-    git_repo: appData.app.repo_name,
-    image_repo_uri: appData.chart.image_repo_uri,
-    git_branch: appData.app.git_branch,
-    git_repo_id: appData.app.git_repo_id,
-    kind: "github",
-  };
-  const defaultBuildConfig: BuildConfig = {
-    builder: appData.app.builder
-      ? appData.app.builder
-      : "paketobuildpacks/builder:full",
-    buildpacks: appData.app.build_packs
-      ? appData.app.build_packs.split(",")
-      : [],
-    config: appData.chart.config,
-  };
-  const [buildConfig, setBuildConfig] = useState<BuildConfig>({
-    ...defaultBuildConfig,
-  });
-  const [redeployOnSave, setRedeployOnSave] = useState(true);
-  const [runningWorkflowURL, setRunningWorkflowURL] = useState("");
-  const [autoBuildpack, setAutoBuildpack] = useState<AutoBuildpack>({
-    valid: false,
-    name: "",
-  });
-
-  const [actionConfig, setActionConfig] = useState<ActionConfigType>({
-    ...defaultActionConfig,
-  });
-  const [buttonStatus, setButtonStatus] = useState<
-    "loading" | "success" | string
-  >("");
-  const [imageUrl, setImageUrl] = useState(appData.chart.image_uri);
-
-  const triggerWorkflow = async () => {
-    try {
-      const res = await api.reRunGHWorkflow(
-        "",
-        {},
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          git_installation_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          branch: branch,
-          filename: "porter_stack_" + appData.chart.name + ".yml",
-        }
-      );
-      if (res.data != null) {
-        window.open(res.data, "_blank", "noreferrer")
-      }
-    } catch (error) {
-      if (!error?.response) {
-        throw error;
-      }
-
-      let tmpError: AxiosError = error;
-
-      /**
-       * @smell
-       * Currently the expanded chart is clearing all the state when a chart update is triggered (saveEnvVariables).
-       * Temporary usage of setCurrentError until a context is applied to keep the state of the ReRunError during re renders.
-       */
-
-      if (tmpError.response.status === 400) {
-        // setReRunError({
-        //   title: "No previous run found",
-        //   description:
-        //     "There are no previous runs for this workflow, please trigger manually a run before changing the build settings.",
-        // });
-        setCurrentError(
-          "There are no previous runs for this workflow. Please manually trigger a run before changing build settings."
-        );
-        return;
-      }
-
-      if (tmpError.response.status === 409) {
-        // setReRunError({
-        //   title: "The workflow is still running",
-        //   description:
-        //     'If you want to make more changes, please choose the option "Save" until the workflow finishes.',
-        // });
-
-        if (typeof tmpError.response.data === "string") {
-          setRunningWorkflowURL(tmpError.response.data);
-        }
-        setCurrentError(
-          'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' +
-          tmpError.response.data
-        );
-        return;
-      }
-
-      if (tmpError.response.status === 404) {
-        let description = "No action file matching this deployment was found.";
-        if (typeof tmpError.response.data === "string") {
-          const filename = tmpError.response.data;
-          description = description.concat(
-            `Please check that the file "${filename}" exists in your repository.`
-          );
-        }
-        // setReRunError({
-        //   title: "The action doesn't seem to exist",
-        //   description,
-        // });
-
-        setCurrentError(description);
-        return;
-      }
-      throw error;
-    }
-  };
-  const saveConfig = async () => {
-    try {
-      await updatePorterApp({
-        repo_name: appData.app.repo_name,
-        git_branch: branch,
-        build_context: folderPath,
-        builder: buildView === "buildpacks"
-          ? buildConfig.builder
-          : "null",
-        buildpacks:
-          buildView === "buildpacks"
-            ? buildConfig?.buildpacks?.join(",")
-            : "null",
-        dockerfile: buildView === "buildpacks" ? "null" : dockerfilePath,
-        image_repo_uri: appData.chart.image_repo_uri,
-      });
-      onTabSwitch();
-    } catch (err) {
-      throw err;
-    }
-  };
-  const handleSave = async () => {
-    setButtonStatus("loading");
-
-    try {
-      await saveConfig();
-      setAppData(appData);
-
-      onTabSwitch();
-      setButtonStatus("success");
-    } catch (error) {
-      setButtonStatus("Something went wrong");
-      console.log(error);
-    }
-  };
-  const handleSaveAndReDeploy = async () => {
-    setButtonStatus("loading");
-
-    try {
-      await saveConfig();
-      setAppData(appData);
-
-      await triggerWorkflow();
-
-      onTabSwitch();
-      setButtonStatus("success");
-      clearStatus();
-    } catch (error) {
-      setButtonStatus("Something went wrong");
-      console.log(error);
-    }
-  };
-  return (
-    <>
-      <Text size={16}>Build settings</Text>
-      <Spacer y={0.5} />
-      <Input
-        disabled={true}
-        label="GitHub repository:"
-        width="100%"
-        value={actionConfig?.git_repo}
-        setValue={() => { }}
-        placeholder=""
-      />
-      <Spacer y={0.5} />
-      {/* <DarkMatter antiHeight="-1px" /> */}
-      {actionConfig.git_repo && (
-        <>
-          <ActionConfBranchSelector
-            actionConfig={actionConfig}
-            branch={branch}
-            setActionConfig={(actionConfig: ActionConfigType) => {
-              setActionConfig((currentActionConfig: ActionConfigType) => ({
-                ...currentActionConfig,
-                ...actionConfig,
-              }));
-              setImageUrl(actionConfig.image_repo_uri);
-            }}
-            setBranch={setBranch}
-            setDockerfilePath={setDockerfilePath}
-            setFolderPath={setFolderPath}
-            setBuildView={setBuildView}
-          />
-        </>
-      )}
-      {actionConfig.git_repo && branch && (
-        <>
-          <Spacer y={1} />
-          <Text color="helper">Application root path:</Text>
-          <Spacer y={0.5} />
-          <Input
-            disabled={!branch ? true : false}
-            placeholder="ex: ./"
-            value={folderPath}
-            width="100%"
-            setValue={setFolderPath}
-          />
-        </>
-      )}
-      <AdvancedBuildSettings
-        dockerfilePath={dockerfilePath}
-        setDockerfilePath={setDockerfilePath}
-        setBuildConfig={setBuildConfig}
-        autoBuildPack={autoBuildpack}
-        showSettings={false}
-        buildView={buildView}
-        setBuildView={setBuildView}
-        actionConfig={actionConfig}
-        branch={branch}
-        folderPath={folderPath}
-        currentBuildConfig={buildConfig}
-      />
-      <Spacer y={1} />
-      <Checkbox
-        checked={redeployOnSave}
-        toggleChecked={() => setRedeployOnSave(!redeployOnSave)}
-      >
-        <Text>Re-run build and deploy on save</Text>
-      </Checkbox>
-      <Spacer y={1} />
-      <Button
-        onClick={() => {
-          if (redeployOnSave) {
-            handleSaveAndReDeploy();
-          } else {
-            handleSave();
-          }
-        }}
-        status={buttonStatus}
-      >
-        Save build settings
-      </Button>
-    </>
-  );
-};
-
-export default BuildSettingsTabStack;
-
-const SourceSettingsContainer = styled.div``;
-
-const DarkMatter = styled.div<{ antiHeight?: string }>`
-  width: 100%;
-  margin-top: ${(props) => props.antiHeight || "-15px"};
-`;
-
-const AdvancedBuildTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledAdvancedBuildSettings = styled.div`
-  color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")};
-  background: #26292e;
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-top: 15px;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
-  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
-
-  .dropdown {
-    margin-right: 8px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-    props.showSettings ? "" : "rotate(-90deg)"};
-  }
-`;
-const StyledSourceBox = styled.div`
-  width: 100%;
-  color: #ffffff;
-  padding: 14px 35px 20px;
-  position: relative;
-  font-size: 13px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  border-top: 0px;
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
-`;
-
-const StyledButtonWrapper = styled.div`
-  display: flex;
-  gap: 10px;
-  align-items: center;
-`;
-
-const StyledButton = styled.button`
-  background: #3a48ca;
-  border: 1px solid #494b4f;
-  color: #ffffffff;
-  cursor: pointer;
-  font-size: 13px;
-  padding: 8px 12px;
-  position: relative;
-  border-radius: 5px;
-  margin-bottom: 35px;
-  position: relative;
-  text-align: center;
-  transition: border 0.3s, color 0.3s;
-
-  &:hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-
-  &::after {
-    content: attr(data-description);
-    background-color: #333;
-    border-radius: 4px;
-    bottom: calc(100% + 8px);
-    color: #fff;
-    font-size: 12px;
-    opacity: 0;
-    padding: 8px;
-    position: absolute;
-    left: 0;
-    top: 100%;
-    transform: translateY(0);
-    white-space: nowrap;
-    pointer-events: none;
-  }
-
-  &:hover::after {
-    opacity: 1;
-    bottom: auto;
-    top: 120%;
-  }
-`;
-
-const StyledLoadingDial = styled(Loading)`
-  position: absolute;
-  right: -45px;
-  top: 50%;
-  transform: translateY(-50%);
-`;

+ 38 - 41
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -2,7 +2,6 @@ import React, { useEffect, useState, useContext, useCallback } from "react";
 import { RouteComponentProps, withRouter } from "react-router";
 import styled from "styled-components";
 import yaml from "js-yaml";
-import { z } from "zod";
 
 import notFound from "assets/not-found.png";
 import web from "assets/web.png";
@@ -11,14 +10,11 @@ import github from "assets/github-white.png";
 import pr_icon from "assets/pull_request_icon.svg";
 import loadingImg from "assets/loading.gif";
 import refresh from "assets/refresh.png";
-import deploy from "assets/deploy.png";
 import save from "assets/save-01.svg";
-import danger from "assets/danger.svg";
 
 import api from "shared/api";
 import JSZip from "jszip";
 import { Context } from "shared/Context";
-import useAuth from "shared/auth/useAuth";
 import Error from "components/porter/Error";
 
 import Banner from "components/porter/Banner";
@@ -30,18 +26,16 @@ import Link from "components/porter/Link";
 import Back from "components/porter/Back";
 import TabSelector from "components/TabSelector";
 import Icon from "components/porter/Icon";
-import { ChartType, PorterAppOptions, ResourceType } from "shared/types";
+import { ChartType, PorterAppOptions } from "shared/types";
 import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
-import BuildSettingsTabStack from "./BuildSettingsTabStack";
+import BuildSettingsTab from "../build-settings/BuildSettingsTab";
 import Button from "components/porter/Button";
 import Services from "../new-app-flow/Services";
-import { ReleaseService, Service } from "../new-app-flow/serviceTypes";
+import { Service } from "../new-app-flow/serviceTypes";
 import ConfirmOverlay from "components/porter/ConfirmOverlay";
 import Fieldset from "components/porter/Fieldset";
 import { PorterJson, createFinalPorterYaml } from "../new-app-flow/schema";
-import EnvGroupArray, {
-  KeyValueType,
-} from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import { PorterYamlSchema } from "../new-app-flow/schema";
 import { EnvVariablesTab } from "./EnvVariablesTab";
 import GHABanner from "./GHABanner";
@@ -53,10 +47,10 @@ import StatusSectionFC from "./status/StatusSection";
 import ExpandedJob from "./expanded-job/ExpandedJob";
 import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
 import Anser, { AnserJsonEntry } from "anser";
-import GHALogsModal from "./status/GHALogsModal";
 import _ from "lodash";
 import AnimateHeight from "react-animate-height";
 import EventsTab from "./EventsTab";
+import { PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {};
 
@@ -111,7 +105,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
   const [subdomain, setSubdomain] = useState<string>("");
 
+  const [porterApp, setPorterApp] = useState<PorterApp>();
+  // this is the version of the porterApp that is being edited. on save, we set the real porter app to be this version
+  const [tempPorterApp, setTempPorterApp] = useState<PorterApp>();
 
+  // this method fetches and reconstructs the porter yaml as well as the DB info (stored in PorterApp)
   const getPorterApp = async () => {
     setBannerLoading(true);
     const { appName } = props.match.params as any;
@@ -172,6 +170,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
 
       setPorterJson(porterJson);
       setAppData(newAppData);
+      // annoying that we have to parse buildpacks like this but alas
+      const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") };
+      setPorterApp(parsedPorterApp);
+      setTempPorterApp(parsedPorterApp);
+
       const [newServices, newEnvVars] = updateServicesAndEnvVariables(
         resChartData?.data,
         releaseChartData?.data,
@@ -279,7 +282,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         appData != null &&
         currentCluster != null &&
         currentProject != null &&
-        appData.app != null
+        appData.app != null &&
+        tempPorterApp != null
       ) {
         const finalPorterYaml = createFinalPorterYaml(
           services,
@@ -294,6 +298,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           "<token>",
           {
             porter_yaml: base64Encoded,
+            repo_name: tempPorterApp.repo_name,
+            git_branch: tempPorterApp.git_branch,
+            build_context: tempPorterApp.build_context,
+            builder: !_.isEmpty(tempPorterApp.dockerfile) ? "null" : tempPorterApp.builder,
+            buildpacks: !_.isEmpty(tempPorterApp.dockerfile) ? "null" : tempPorterApp.buildpacks.join(","),
+            dockerfile: tempPorterApp.dockerfile,
             ...options,
             override_release: true,
           },
@@ -304,6 +314,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           }
         );
         setPorterYaml(finalPorterYaml);
+        setPorterApp(tempPorterApp);
         setButtonStatus("success");
         setShowUnsavedChangesBanner(false);
       } else {
@@ -327,6 +338,15 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     });
   }, [appData]);
 
+  useEffect(() => {
+    if (!_.isEqual(_.omitBy(porterApp, _.isEmpty), _.omitBy(tempPorterApp, _.isEmpty))) {
+      setButtonStatus("");
+      setShowUnsavedChangesBanner(true);
+    } else {
+      setShowUnsavedChangesBanner(false);
+    }
+  }, [tempPorterApp, porterApp]);
+
   const getBuildLogs = async () => {
     try {
       const res = await api.getGHWorkflowLogs(
@@ -641,12 +661,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       appData.app.builder != null && appData.app.builder.includes("heroku")
     );
     if (!_.isEqual(porterYaml, newPorterYaml)) {
+      setButtonStatus("");
       setShowUnsavedChangesBanner(true);
     } else {
       setShowUnsavedChangesBanner(false);
     }
-    // console.log("old porter yaml", porterYaml);
-    // console.log("new porter yaml", newPorterYaml);
   };
 
   const renderTabContents = () => {
@@ -720,12 +739,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         );
       case "build-settings":
         return (
-          <BuildSettingsTabStack
-            appData={appData}
-            setAppData={setAppData}
-            onTabSwitch={getPorterApp}
+          <BuildSettingsTab
+            porterApp={tempPorterApp}
+            setTempPorterApp={(attrs: Partial<PorterApp>) => setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs))}
             clearStatus={() => setButtonStatus("")}
             updatePorterApp={updatePorterApp}
+            setShowUnsavedChangesBanner={setShowUnsavedChangesBanner}
           />
         );
       case "settings":
@@ -840,7 +859,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         <StyledExpandedApp>
           <Back to="/apps" />
           <Container row>
-            {renderIcon(appData.app?.build_packs)}
+            {renderIcon(appData.app?.buildpacks)}
             <Spacer inline x={1} />
             <Text size={21}>{appData.app.name}</Text>
             {appData.app.repo_name && (
@@ -1083,28 +1102,6 @@ const RefreshButton = styled.div`
   }
 `;
 
-const LogsButton = styled.div`
-  color: white;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  :hover {
-    color: red;
-    > img {
-      opacity: 1;
-    }
-  }
-
-  > img {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 5px;
-    margin-right: 10px;
-    opacity: 0.8;
-  }
-`;
-
 const Spinner = styled.img`
   width: 15px;
   height: 15px;

+ 0 - 159
dashboard/src/main/home/app-dashboard/expanded-app/SharedBuildSettings.tsx

@@ -1,159 +0,0 @@
-import Input from "components/porter/Input";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import ActionConfBranchSelector from "components/repo-selector/ActionConfBranchSelector";
-import ActionConfEditorStack from "components/repo-selector/ActionConfEditorStack";
-import DetectContentsList from "components/repo-selector/DetectContentsList";
-import React, { useEffect, useState } from "react";
-import AnimateHeight from "react-animate-height";
-import { ActionConfigType, BuildConfig } from "shared/types";
-import styled from "styled-components";
-
-type Props = {
-  actionConfig: ActionConfigType;
-  setActionConfig: (
-    x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
-  ) => void;
-  branch: string;
-  setBranch: (x: string) => void;
-  dockerfilePath: string | null;
-  setDockerfilePath: (x: string) => void;
-  folderPath: string;
-  setFolderPath: (x: string) => void;
-  setBuildConfig: (x: any) => void;
-  porterYaml: string;
-  setPorterYaml: (x: any) => void;
-  imageUrl: string;
-  setImageUrl: (x: string) => void;
-  buildView: string;
-  setBuildView: (x: string) => void;
-  porterYamlPath: string;
-  setPorterYamlPath: (x: string) => void;
-};
-
-const SharedBuildSettings: React.FC<Props> = ({
-  actionConfig,
-  setActionConfig,
-  branch,
-  setBranch,
-  dockerfilePath,
-  setDockerfilePath,
-  folderPath,
-  setFolderPath,
-  setBuildConfig,
-  porterYaml,
-  setPorterYaml,
-  imageUrl,
-  setImageUrl,
-  buildView,
-  setBuildView,
-  porterYamlPath,
-  setPorterYamlPath,
-}) => {
-  const [isExpanded, setIsExpanded] = useState(false);
-
-  return (
-    <>
-      <Text size={16}>Build settings</Text>
-      <Spacer y={0.5} />
-      <Text color="helper">Specify your GitHub repository.</Text>
-      <Spacer y={0.5} />
-      <ActionConfEditorStack
-        actionConfig={actionConfig}
-        setActionConfig={(actionConfig: ActionConfigType) => {
-          setActionConfig((currentActionConfig: ActionConfigType) => ({
-            ...currentActionConfig,
-            ...actionConfig,
-          }));
-          setImageUrl(actionConfig.image_repo_uri);
-        }}
-        setBranch={setBranch}
-        setDockerfilePath={setDockerfilePath}
-        setFolderPath={setFolderPath}
-        setBuildView={setBuildView}
-        setPorterYamlPath={setPorterYamlPath}
-      />
-      <DarkMatter antiHeight="-4px" />
-      <Spacer y={0.3} />
-      {actionConfig.git_repo && (
-        <>
-          <Spacer y={0.5} />
-          <Text color="helper">Specify your GitHub branch.</Text>
-          <Spacer y={0.5} />
-          <ActionConfBranchSelector
-            actionConfig={actionConfig}
-            branch={branch}
-            setActionConfig={(actionConfig: ActionConfigType) => {
-              setActionConfig((currentActionConfig: ActionConfigType) => ({
-                ...currentActionConfig,
-                ...actionConfig,
-              }));
-              setImageUrl(actionConfig.image_repo_uri);
-            }}
-            setBranch={setBranch}
-            setDockerfilePath={setDockerfilePath}
-            setFolderPath={setFolderPath}
-            setBuildView={setBuildView}
-            setPorterYamlPath={setPorterYamlPath}
-          />
-        </>
-      )}
-      <Spacer y={0.3} />
-      {actionConfig.git_repo && branch && (
-        <>
-          <Spacer y={1} />
-          <Text color="helper">Specify your application root path.</Text>
-          <Spacer y={0.5} />
-          <Input
-            disabled={!branch ? true : false}
-            placeholder="ex: ./"
-            value={folderPath}
-            width="100%"
-            setValue={setFolderPath}
-          />
-          <Spacer y={1} />
-
-          <DetectContentsList
-            actionConfig={actionConfig}
-            branch={branch}
-            dockerfilePath={dockerfilePath}
-            folderPath={folderPath}
-            setActionConfig={setActionConfig}
-            setDockerfilePath={setDockerfilePath}
-            setFolderPath={setFolderPath}
-            setBuildConfig={setBuildConfig}
-            porterYaml={porterYaml}
-            setPorterYaml={setPorterYaml}
-            buildView={buildView}
-            setBuildView={setBuildView}
-            porterYamlPath={porterYamlPath}
-            setPorterYamlPath={setPorterYamlPath}
-          />
-        </>
-      )}
-    </>
-  );
-};
-
-export default SharedBuildSettings;
-
-const SourceSettingsContainer = styled.div``;
-
-const DarkMatter = styled.div<{ antiHeight?: string }>`
-  width: 100%;
-  margin-top: ${(props) => props.antiHeight || "-15px"};
-`;
-
-const Subtitle = styled.div`
-  padding: 11px 0px 16px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #aaaabb;
-  line-height: 1.6em;
-`;
-
-const Required = styled.div`
-  margin-left: 8px;
-  color: #fc4976;
-  display: inline-block;
-`;

+ 0 - 284
dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx

@@ -1,284 +0,0 @@
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import Input from "components/porter/Input";
-import Toggle from "components/porter/Toggle";
-import AnimateHeight from "react-animate-height";
-import { DeviconsNameList } from "assets/devicons-name-list";
-import { BuildpackStack } from "components/repo-selector/BuildpackStack";
-import { ActionConfigType, BuildConfig } from "shared/types";
-import SelectRow from "components/form-components/SelectRow";
-import Select from "components/porter/Select";
-
-interface AutoBuildpack {
-  name?: string;
-  valid: boolean;
-}
-
-interface AdvancedBuildSettingsProps {
-  autoBuildPack?: AutoBuildpack;
-  buildView: string;
-  showSettings: boolean;
-  actionConfig: ActionConfigType | null;
-  branch: string;
-  folderPath: string;
-  dockerfilePath?: string;
-  setDockerfilePath: (x: string) => void;
-  setBuildConfig?: (x: any) => void;
-  currentBuildConfig?: BuildConfig;
-  setBuildView: (x: string) => void;
-}
-
-type Buildpack = {
-  name: string;
-  buildpack: string;
-  config?: {
-    [key: string]: string;
-  };
-};
-
-const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
-  const [showSettings, setShowSettings] = useState<boolean>(props.showSettings);
-  const buildView = props.setBuildView(props.buildView || "buildpacks");
-
-  useEffect(() => { }, [props.buildView]);
-  const createDockerView = () => {
-    // props.setBuildConfig({});
-    return (
-      <>
-        <Text color="helper">Dockerfile path (absolute path)</Text>
-        <Spacer y={0.5} />
-        <Input
-          placeholder="ex: ./Dockerfile"
-          value={props.dockerfilePath}
-          width="300px"
-          setValue={props.setDockerfilePath}
-        />
-        <Spacer y={0.5} />
-      </>
-    );
-  };
-
-  const createBuildpackView = () => {
-    return (
-      <>
-        <BuildpackStack
-          actionConfig={props.actionConfig}
-          branch={props.branch}
-          folderPath={props.folderPath}
-          onChange={(config) => {
-            props.setBuildConfig(config);
-          }}
-          hide={false}
-          currentBuildConfig={props.currentBuildConfig}
-          setBuildConfig={props.setBuildConfig}
-        />
-      </>
-    );
-  };
-
-  return (
-    <>
-      <StyledAdvancedBuildSettings
-        showSettings={showSettings}
-        isCurrent={true}
-        onClick={() => {
-          setShowSettings(!showSettings);
-        }}
-      >
-        {props.buildView == "docker" ? (
-          <AdvancedBuildTitle>
-            <i className="material-icons dropdown">arrow_drop_down</i>
-            Configure Dockerfile settings
-          </AdvancedBuildTitle>
-        ) : (
-          <AdvancedBuildTitle>
-            <i className="material-icons dropdown">arrow_drop_down</i>
-            Configure buildpack settings
-          </AdvancedBuildTitle>
-        )}
-      </StyledAdvancedBuildSettings>
-
-      <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
-        <StyledSourceBox>
-          <Select
-            value={props.buildView}
-            width="300px"
-            options={[
-              { value: "docker", label: "Docker" },
-              { value: "buildpacks", label: "Buildpacks" },
-            ]}
-            setValue={(option) => props.setBuildView(option)}
-            label="Build method"
-          />
-          <Spacer y={1} />
-          {props.buildView === "docker"
-            ? createDockerView()
-            : createBuildpackView()}
-        </StyledSourceBox>
-      </AnimateHeight>
-    </>
-  );
-};
-
-export default AdvancedBuildSettings;
-
-const StyledAdvancedBuildSettings = styled.div`
-  color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")};
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-top: 15px;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
-  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
-
-  .dropdown {
-    margin-right: 8px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-    props.showSettings ? "" : "rotate(-90deg)"};
-  }
-`;
-
-const AdvancedBuildTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSourceBox = styled.div`
-  width: 100%;
-  color: #ffffff;
-  padding: 25px 35px 25px;
-  position: relative;
-  font-size: 13px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  border-top: 0px;
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
-`;
-
-const ToggleWrapper = styled.div`
-  display: flex;
-  justify-content: center;
-  width: 100%;
-`;
-
-const StyledCard = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  border: 1px solid #ffffff00;
-  background: #ffffff08;
-  margin-bottom: 5px;
-  border-radius: 8px;
-  padding: 14px;
-  overflow: hidden;
-  height: 60px;
-  font-size: 13px;
-`;
-
-const ContentContainer = styled.div`
-  display: flex;
-  height: 100%;
-  width: 100%;
-  align-items: center;
-`;
-const Icon = styled.span<{ disableMarginRight: boolean }>`
-  font-size: 20px;
-  margin-left: 10px;
-  ${(props) => {
-    if (!props.disableMarginRight) {
-      return "margin-right: 20px";
-    }
-  }}
-`;
-
-const EventInformation = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: space-around;
-  height: 100%;
-`;
-
-const EventName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-`;
-
-const ActionContainer = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  height: 100%;
-`;
-
-const ActionButton = styled.button`
-  position: relative;
-  border: none;
-  background: none;
-  color: white;
-  padding: 5px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 50%;
-  cursor: pointer;
-  color: #aaaabb;
-
-  :hover {
-    background: #ffffff11;
-    border: 1px solid #ffffff44;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-`;
-const SelectWrapper = styled.div`
-  display: flex;
-  justify-content: center;
-  width: 100%;
-  align-items: center;
-`;
-
-const SelectLabel = styled.label`
-  color: #ffffff;
-  font-size: 13px;
-  margin-right: 8px;
-`;
-
-const StyledSelect = styled.select`
-  background-color: #26292e;
-  border: 1px solid #494b4f;
-  border-radius: 5px;
-  color: #aaaabb;
-  cursor: pointer;
-  font-size: 13px;
-  height: 30px;
-  outline: none;
-  padding: 0 8px;
-  width: 150px;
-
-  &:hover {
-    border: 1px solid #7a7b80;
-    color: #ffffff;
-  }
-`;

+ 0 - 9
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -6,18 +6,14 @@ import Modal from "components/porter/Modal";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import ExpandableSection from "components/porter/ExpandableSection";
-import Fieldset from "components/porter/Fieldset";
 import Button from "components/porter/Button";
 import Select from "components/porter/Select";
 import api from "shared/api";
 import { getGithubAction } from "./utils";
-import AceEditor from "react-ace";
 import YamlEditor from "components/YamlEditor";
 import Error from "components/porter/Error";
-import Container from "components/porter/Container";
 import Checkbox from "components/porter/Checkbox";
 
-
 type Props = RouteComponentProps & {
   closeModal: () => void;
   githubAppInstallationID?: number;
@@ -181,11 +177,6 @@ const GithubActionModal: React.FC<Props> = ({
 
 export default withRouter(GithubActionModal);
 
-const Tab = styled.span`
-  margin-left: 20px;
-  height: 1px;
-`;
-
 const ModalHeader = styled.div`
   font-weight: 600;
   font-size: 16px;

+ 3 - 21
dashboard/src/main/home/app-dashboard/new-app-flow/GithubConnectModal.tsx

@@ -53,7 +53,7 @@ const GithubConnectModal: React.FC<Props> = ({
     if (accessError) {
       return (
         <>
-          <Text color="helper">To deploy from GitHub, authorize Porter to view your repos.</Text> 
+          <Text color="helper">To deploy from GitHub, authorize Porter to view your repos.</Text>
           <ListWrapper>
             <Helper>
               No connected repos found.
@@ -150,20 +150,6 @@ const GitIcon = styled.img`
   filter: brightness(120%);
 `;
 
-const Tab = styled.span`
-  margin-left: 20px;
-  height: 1px;
-`;
-
-const ModalHeader = styled.div`
-  font-weight: 600;
-  font-size: 16px;
-  font-family: monospace;
-  height: 40px;
-  display: flex;
-  align-items: center;
-`;
-
 const ListWrapper = styled.div`
   width: 100%;
   height: 240px;
@@ -206,7 +192,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#353a3e"};
+    props.disabled ? "" : "#353a3e"};
   }
 
   > i {
@@ -223,7 +209,7 @@ const ConnectToGithubButton = styled.a`
   }
   &:hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#353a3e"};
+    props.disabled ? "" : "#353a3e"};
   }
 
   &:not([disabled]) {
@@ -236,7 +222,3 @@ const GitHubIcon = styled.img`
   filter: brightness(150%);
   margin-right: 10px;
 `;
-const ButtonWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-`;

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

@@ -22,23 +22,15 @@ import SourceSettings from "./SourceSettings";
 import Services from "./Services";
 import EnvGroupArray, { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import GithubActionModal from "./GithubActionModal";
-import { ActionConfigType } from "shared/types";
 import Error from "components/porter/Error";
 import { PorterJson, PorterYamlSchema, createFinalPorterYaml } from "./schema";
 import { Service } from "./serviceTypes";
 import GithubConnectModal from "./GithubConnectModal";
 import Link from "components/porter/Link";
+import { PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {};
 
-const defaultActionConfig: ActionConfigType = {
-  git_repo: "",
-  image_repo_uri: "",
-  git_branch: "",
-  git_repo_id: 0,
-  kind: "github",
-};
-
 interface FormState {
   applicationName: string;
   selectedSourceType: SourceType | undefined;
@@ -70,21 +62,15 @@ interface GithubAppAccessData {
   username?: string;
   accounts?: string[];
 }
-type Provider =
-  | {
-    provider: "github";
-    name: string;
-    installation_id: number;
-  }
-  | {
-    provider: "gitlab";
-    instance_url: string;
-    integration_id: number;
-  };
+
+interface PorterJsonWithPath {
+  porterYamlPath: string;
+  porterJson: PorterJson;
+}
+
 const NewAppFlow: React.FC<Props> = ({ ...props }) => {
-  const [porterYamlPath, setPorterYamlPath] = useState("");
+  const [porterApp, setPorterApp] = useState<PorterApp>(PorterApp.empty());
 
-  const [imageUrl, setImageUrl] = useState("");
   const [imageTag, setImageTag] = useState("latest");
   const { currentCluster, currentProject } = useContext(Context);
   const [deploying, setDeploying] = useState<boolean>(false);
@@ -92,13 +78,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [currentStep, setCurrentStep] = useState<number>(0);
   const [existingStep, setExistingStep] = useState<number>(0);
   const [formState, setFormState] = useState<FormState>(INITIAL_STATE);
-  const [actionConfig, setActionConfig] = useState<ActionConfigType>(defaultActionConfig);
-  const [buildView, setBuildView] = useState<string>("buildpacks");
-  const [branch, setBranch] = useState("");
-  const [dockerfilePath, setDockerfilePath] = useState("./Dockerfile");
-  const [procfilePath, setProcfilePath] = useState(null);
-  const [folderPath, setFolderPath] = useState("./");
-  const [buildConfig, setBuildConfig] = useState({});
   const [porterYaml, setPorterYaml] = useState("");
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
   const [showGithubConnectModal, setShowGithubConnectModal] = useState<boolean>(
@@ -112,11 +91,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [accessLoading, setAccessLoading] = useState(true);
   const [accessError, setAccessError] = useState(false);
   const [accessData, setAccessData] = useState<GithubAppAccessData>({});
-  const [providers, setProviders] = useState([]);
-  const [currentProvider, setCurrentProvider] = useState(null);
   const [hasProviders, setHasProviders] = useState(true);
 
-  const [porterJson, setPorterJson] = useState<PorterJson | undefined>(undefined);
+  const [porterJsonWithPath, setPorterJsonWithPath] = useState<PorterJsonWithPath | undefined>(undefined);
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
   const handleSetAccessData = (data: GithubAppAccessData) => {
     setAccessData(data);
@@ -143,7 +120,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         "<token>",
         {
           step,
-          stack_name: formState.applicationName,
+          stack_name: porterApp.name,
         },
         {
           cluster_id: currentCluster.id,
@@ -155,13 +132,13 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     }
   };
 
-  const validatePorterYaml = (yamlString: string) => {
+  const validateAndSetPorterYaml = (yamlString: string, filename: string) => {
     let parsedYaml;
     try {
       parsedYaml = yaml.load(yamlString);
       const parsedData = PorterYamlSchema.parse(parsedYaml);
       const porterYamlToJson = parsedData as PorterJson;
-      setPorterJson(porterYamlToJson);
+      setPorterJsonWithPath({ porterJson: porterYamlToJson, porterYamlPath: filename });
       const newServices = [];
       const existingServices = formState.serviceList.map((s) => s.name);
       for (const [name, app] of Object.entries(porterYamlToJson.apps)) {
@@ -179,13 +156,13 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         newServices.push(Service.default("pre-deploy", "release", porterYamlToJson));
       }
       const newServiceList = [...formState.serviceList, ...newServices];
+      if (Validators.serviceList(newServiceList)) {
+        setCurrentStep(Math.max(currentStep, 5));
+      }
       setFormState({
         ...formState,
         serviceList: newServiceList,
       });
-      if (Validators.serviceList(newServiceList)) {
-        setCurrentStep(Math.max(currentStep, 5));
-      }
       if (
         porterYamlToJson &&
         porterYamlToJson.apps &&
@@ -194,7 +171,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         setDetected({
           detected: true,
           message: `Detected ${Object.keys(porterYamlToJson.apps).length
-            } services from porter.yaml`,
+            } service${Object.keys(porterYamlToJson.apps).length === 1 ? "" : "s"} from porter.yaml`,
         });
       } else {
         setDetected({
@@ -207,31 +184,21 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       console.log("Error converting porter yaml file to input: " + error);
     }
   };
-  const sortProviders = (providers: Provider[]) => {
-    const githubProviders = providers.filter(
-      (provider) => provider.provider === "github"
-    );
-
-    const gitlabProviders = providers.filter(
-      (provider) => provider.provider === "gitlab"
-    );
 
-    const githubSortedProviders = githubProviders.sort((a, b) => {
-      if (a.provider === "github" && b.provider === "github") {
-        return a.name.localeCompare(b.name);
-      }
-    });
+  // this advances the step in the case that a user chooses a repo that doesn't have a porter.yaml
+  useEffect(() => {
+    if (porterApp.git_branch !== "") {
+      setCurrentStep(Math.max(currentStep, 2));
+    }
+  }, [porterApp.git_branch]);
 
-    const gitlabSortedProviders = gitlabProviders.sort((a, b) => {
-      if (a.provider === "gitlab" && b.provider === "gitlab") {
-        return a.instance_url.localeCompare(b.instance_url);
-      }
-    });
-    return [...gitlabSortedProviders, ...githubSortedProviders];
-  };
   useEffect(() => {
     let isSubscribed = true;
 
+    if (currentProject == null) {
+      return;
+    }
+
     api
       .getGitProviders("<token>", {}, { project_id: currentProject?.id })
       .then((res) => {
@@ -244,10 +211,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           setHasProviders(false);
           return;
         }
-
-        const sortedProviders = sortProviders(data);
-        setProviders(sortedProviders);
-        setCurrentProvider(sortedProviders[0]);
       })
       .catch((err) => {
         setHasProviders(false);
@@ -263,8 +226,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     return regex.test(name);
   };
   const handleAppNameChange = (name: string) => {
-    setCurrentStep(currentStep);
-    setFormState({ ...formState, applicationName: name });
+    setPorterApp(PorterApp.setAttribute(porterApp, "name", name));
     if (isAppNameValid(name) && Validators.applicationName(name)) {
       setCurrentStep(Math.max(Math.max(currentStep, 1), existingStep));
     } else {
@@ -280,9 +242,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
 
   const shouldHighlightAppNameInput = () => {
     return (
-      formState.applicationName !== "" &&
-      (!isAppNameValid(formState.applicationName) ||
-        formState.applicationName.length > 61)
+      porterApp.name !== "" &&
+      (!isAppNameValid(porterApp.name) ||
+        porterApp.name.length > 61)
     );
   };
 
@@ -292,7 +254,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       setDeploymentError(undefined);
 
       // log analytics event that we started form submission
-      await updateStackStep("stack-launch-complete");
+      updateStackStep("stack-launch-complete");
 
       if (currentProject?.id == null || currentCluster?.id == null) {
         throw "Project or cluster not found";
@@ -302,10 +264,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       const finalPorterYaml = createFinalPorterYaml(
         formState.serviceList,
         formState.envVariables,
-        porterJson,
+        porterJsonWithPath?.porterJson,
         // if we are using a heroku buildpack, inject a PORT env variable
-        (buildConfig as any)?.builder != null &&
-        (buildConfig as any)?.builder.includes("heroku")
+        porterApp.builder.includes("heroku")
       );
 
       const yamlString = yaml.dump(finalPorterYaml);
@@ -314,9 +275,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         repository: "",
         tag: "",
       };
-      if (imageUrl && imageTag) {
+      if (porterApp.image_repo_uri && imageTag) {
         imageInfo = {
-          repository: imageUrl,
+          repository: porterApp.image_repo_uri,
           tag: imageTag,
         };
       }
@@ -324,39 +285,36 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       await api.createPorterApp(
         "<token>",
         {
-          repo_name: actionConfig.git_repo,
-          git_branch: branch,
-          git_repo_id: actionConfig?.git_repo_id,
-          build_context: folderPath,
-          builder:
-            buildView === "buildpacks" ? (buildConfig as any)?.builder : "",
-          buildpacks:
-            buildView === "buildpacks"
-              ? (buildConfig as any)?.buildpacks?.join(",") ?? ""
-              : "",
-          dockerfile: buildView === "docker" ? dockerfilePath : "",
-          image_repo_uri: imageUrl,
+          repo_name: porterApp.repo_name,
+          git_branch: porterApp.git_branch,
+          git_repo_id: porterApp.git_repo_id,
+          build_context: porterApp.build_context,
+          builder: !_.isEmpty(porterApp.dockerfile) ? "" : porterApp.builder,
+          buildpacks: !_.isEmpty(porterApp.dockerfile) ? "" : porterApp.buildpacks.join(","),
+          dockerfile: porterApp.dockerfile,
+          image_repo_uri: porterApp.image_repo_uri,
           porter_yaml: base64Encoded,
           override_release: true,
           image_info: imageInfo,
-          porter_yaml_path: porterYamlPath,
+          // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/
+          porter_yaml_path: porterJsonWithPath?.porterYamlPath,
         },
         {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
-          stack_name: formState.applicationName,
+          stack_name: porterApp.name,
         }
       );
 
-      if (!actionConfig?.git_repo) {
-        props.history.push(`/apps/${formState.applicationName}`);
+      if (porterApp.repo_name === "") {
+        props.history.push(`/apps/${porterApp.name}`);
       }
 
       // log analytics event that we successfully deployed
-      await updateStackStep("stack-launch-success");
+      updateStackStep("stack-launch-success");
 
       return true;
-    } catch (err) {
+    } catch (err: any) {
       // TODO: better error handling
       console.log(err);
       const errMessage =
@@ -370,14 +328,10 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       setDeploying(false);
     }
   };
+
   useEffect(() => {
     setFormState({ ...formState, serviceList: [] });
-  }, [actionConfig, branch]);
-  useEffect(() => {
-    if (imageUrl || dockerfilePath || folderPath) {
-      setCurrentStep(Math.max(currentStep, 2));
-    }
-  }, [imageUrl, buildConfig, dockerfilePath, setCurrentStep, currentStep]);
+  }, [porterApp.git_branch]);
 
   return (
     <CenterWrapper>
@@ -415,11 +369,11 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 <Spacer y={0.5} />
                 <Input
                   placeholder="ex: academic-sophon"
-                  value={formState.applicationName}
+                  value={porterApp.name}
                   width="300px"
                   error={
                     shouldHighlightAppNameInput() &&
-                    (formState.applicationName.length > 30
+                    (porterApp.name.length > 30
                       ? "Maximum 30 characters allowed."
                       : 'Lowercase letters, numbers, and "-" only.')
                   }
@@ -453,34 +407,18 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 />
                 <SourceSettings
                   source={formState.selectedSourceType}
-                  imageUrl={imageUrl}
-                  setImageUrl={(x) => {
-                    setImageUrl(x);
-                    setCurrentStep(Math.max(currentStep, 1));
+                  setPorterYaml={(newYaml: string, filename: string) => {
+                    validateAndSetPorterYaml(newYaml, filename);
+                  }}
+                  porterApp={porterApp}
+                  setPorterApp={setPorterApp}
+                  imageUrl={porterApp.image_repo_uri}
+                  setImageUrl={(url: string) => {
+                    setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", url));
+                    setCurrentStep(Math.max(currentStep, 2));
                   }}
                   imageTag={imageTag}
                   setImageTag={setImageTag}
-                  actionConfig={actionConfig}
-                  setActionConfig={setActionConfig}
-                  branch={branch}
-                  setBranch={setBranch}
-                  dockerfilePath={dockerfilePath}
-                  setDockerfilePath={setDockerfilePath}
-                  folderPath={folderPath}
-                  setFolderPath={setFolderPath}
-                  procfilePath={procfilePath}
-                  setProcfilePath={setProcfilePath}
-                  setBuildConfig={setBuildConfig}
-                  porterYaml={porterYaml}
-                  setPorterYaml={(newYaml: string) => {
-                    validatePorterYaml(newYaml);
-                  }}
-                  buildView={buildView}
-                  setBuildView={setBuildView}
-                  setCurrentStep={setCurrentStep}
-                  currentStep={currentStep}
-                  porterYamlPath={porterYamlPath}
-                  setPorterYamlPath={setPorterYamlPath}
                 />
               </>,
               <>
@@ -548,13 +486,12 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   defaultExpanded={true}
                   limitOne={true}
                   addNewText={"Add a new pre-deploy job"}
-                  prePopulateService={Service.default("pre-deploy", "release", porterJson)}
+                  prePopulateService={Service.default("pre-deploy", "release", porterJsonWithPath?.porterJson)}
                 />
               </>,
               <Button
                 onClick={() => {
-                  if (imageUrl) {
-                    console.log(porterYaml);
+                  if (porterApp.image_repo_uri) {
                     deployPorterApp();
                   } else {
                     setDeploymentError(undefined);
@@ -578,19 +515,19 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           <Spacer y={3} />
         </StyledConfigureTemplate>
       </Div>
-      {showGHAModal && (
+      {showGHAModal && currentCluster != null && currentProject != null && (
         <GithubActionModal
           closeModal={() => setShowGHAModal(false)}
-          githubAppInstallationID={actionConfig.git_repo_id}
-          githubRepoOwner={actionConfig.git_repo.split("/")[0]}
-          githubRepoName={actionConfig.git_repo.split("/")[1]}
-          branch={branch}
-          stackName={formState.applicationName}
+          githubAppInstallationID={porterApp.git_repo_id}
+          githubRepoOwner={porterApp.repo_name.split("/")[0]}
+          githubRepoName={porterApp.repo_name.split("/")[1]}
+          branch={porterApp.git_branch}
+          stackName={porterApp.name}
           projectId={currentProject.id}
           clusterId={currentCluster.id}
           deployPorterApp={deployPorterApp}
           deploymentError={deploymentError}
-          porterYamlPath={porterYamlPath}
+          porterYamlPath={porterJsonWithPath?.porterYamlPath}
         />
       )}
     </CenterWrapper>
@@ -662,75 +599,4 @@ const StyledConfigureTemplate = styled.div`
   height: 100%;
 `;
 
-const ExpandedWrapper = styled.div`
-  margin-top: 10px;
-  width: 100%;
-  border-radius: 3px;
-  border: 1px solid #ffffff44;
-  max-height: 275px;
-`;
-const ListWrapper = styled.div`
-  width: 100%;
-  height: 240px;
-  background: #ffffff11;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border-radius: 5px;
-  margin-top: 20px;
-  padding: 40px;
-`;
-const A = styled.a`
-  color: #8590ff;
-  text-decoration: underline;
-  margin-left: 5px;
-  cursor: pointer;
-`;
 
-const ConnectToGithubButton = styled.a`
-  width: 180px;
-  justify-content: center;
-  border-radius: 5px;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  color: white;
-  font-weight: 500;
-  padding: 10px;
-  overflow: hidden;
-  white-space: nowrap;
-  margin-top: 25px;
-  border: 1px solid #494b4f;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#2E3338"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#353a3e"};
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const GitHubIcon = styled.img`
-  width: 20px;
-  filter: brightness(150%);
-  margin-right: 10px;
-`;

+ 44 - 161
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -1,21 +1,14 @@
 import AnimateHeight from "react-animate-height";
-import React, { Component, useEffect } from "react";
-import Text from "components/porter/Text";
+import React from "react";
 import Spacer from "components/porter/Spacer";
-import Input from "components/porter/Input";
-import AdvancedBuildSettings from "./AdvancedBuildSettings";
 import styled from "styled-components";
 import { SourceType } from "./SourceSelector";
-import ActionConfEditorStack from "components/repo-selector/ActionConfEditorStack";
-import { ActionConfigType, BuildConfig } from "shared/types";
 import { RouteComponentProps, withRouter } from "react-router";
-import { Context } from "shared/Context";
-import ActionConfBranchSelector from "components/repo-selector/ActionConfBranchSelector";
-import DetectContentsList from "components/repo-selector/DetectContentsList";
 import { pushFiltered } from "shared/routing";
 import ImageSelector from "components/image-selector/ImageSelector";
-import SharedBuildSettings from "../expanded-app/SharedBuildSettings";
+import SharedBuildSettings from "../build-settings/SharedBuildSettings";
 import Link from "components/porter/Link";
+import { PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {
   source: SourceType | undefined;
@@ -23,27 +16,9 @@ type Props = RouteComponentProps & {
   setImageUrl: (x: string) => void;
   imageTag: string;
   setImageTag: (x: string) => void;
-  actionConfig: ActionConfigType;
-  setActionConfig: (
-    x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
-  ) => void;
-  branch: string;
-  setBranch: (x: string) => void;
-  dockerfilePath: string | null;
-  setDockerfilePath: (x: string) => void;
-  procfilePath: string | null;
-  setProcfilePath: (x: string) => void;
-  folderPath: string | null;
-  setFolderPath: (x: string) => void;
-  setBuildConfig: (x: any) => void;
-  porterYaml: string;
-  setPorterYaml: (x: any) => void;
-  buildView: string;
-  setBuildView: (x: string) => void;
-  setCurrentStep: (x: number) => void;
-  currentStep: number;
-  porterYamlPath: string;
-  setPorterYamlPath: (x: string) => void;
+  setPorterYaml: (yaml: string, filename: string) => void;
+  porterApp: PorterApp;
+  setPorterApp: (x: PorterApp) => void;
 };
 
 const SourceSettings: React.FC<Props> = ({
@@ -52,112 +27,50 @@ const SourceSettings: React.FC<Props> = ({
   setImageUrl,
   imageTag,
   setImageTag,
-  actionConfig,
-  setActionConfig,
-  branch,
-  setBranch,
-  dockerfilePath,
-  setDockerfilePath,
-  folderPath,
-  setFolderPath,
-  setBuildConfig,
-  porterYaml,
   setPorterYaml,
-  buildView,
-  setBuildView,
-  setCurrentStep,
-  currentStep,
-  setPorterYamlPath,
-  porterYamlPath,
-  ...props
+  porterApp,
+  setPorterApp,
+  location,
+  history,
 }) => {
-  const renderDockerSettings = () => {
-    setFolderPath("");
-    setDockerfilePath("");
-    setBuildView("buildpacks");
-    setPorterYamlPath("");
-    setBranch("");
-    return (
-      <>
-        {/* /* <Text size={16}>Registry settings</Text>
-        <Spacer y={0.5} />
-        <Text color="helper">
-          Specify the complete registry URL for your Docker image:
-        </Text>
-        <Spacer height="20px" />
-        <Input
-          placeholder="ex: nginx"
-          value={imageUrl}
-          width="300px"
-          setValue={setImageUrl}
-        /> */}
-
-        <StyledSourceBox>
-          {/* <CloseButton
-            onClick={() => {
-              setSourceType("");
-              setImageUrl("");
-              setImageTag("");
-            }}
-          >
-            <i className="material-icons">close</i>
-          </CloseButton> */}
-          <Subtitle>
-            Specify the container image you would like to connect to this
-            template.
-            <Spacer inline width="5px" />
-            <Link
-              hasunderline
-              onClick={() =>
-                pushFiltered(props, "/integrations/registry", ["project_id"])
-              }
-            >
-              Manage Docker registries
-            </Link>
-          </Subtitle>
-          <DarkMatter antiHeight="-4px" />
-          <ImageSelector
-            selectedTag={imageTag}
-            selectedImageUrl={imageUrl}
-            setSelectedImageUrl={setImageUrl}
-            setSelectedTag={setImageTag}
-            forceExpanded={true}
-          />
-          <br />
-        </StyledSourceBox>
-      </>
-    );
-  };
-
   return (
     <SourceSettingsContainer>
-      {source && <Spacer y={1} />}
       <AnimateHeight height={source ? "auto" : 0}>
-        <div>
-          {source === "github" ? (
-            <SharedBuildSettings
-              actionConfig={actionConfig}
-              branch={branch}
-              dockerfilePath={dockerfilePath}
-              folderPath={folderPath}
-              setActionConfig={setActionConfig}
-              setDockerfilePath={setDockerfilePath}
-              setFolderPath={setFolderPath}
-              setBuildConfig={setBuildConfig}
-              porterYaml={porterYaml}
-              setPorterYaml={setPorterYaml}
-              setBranch={setBranch}
-              imageUrl={imageUrl}
-              setImageUrl={setImageUrl}
-              buildView={buildView}
-              setBuildView={setBuildView}
-              porterYamlPath={porterYamlPath}
-              setPorterYamlPath={setPorterYamlPath}
+        <Spacer y={1} />
+        {source === "github" ? (
+          <SharedBuildSettings
+            setPorterYaml={setPorterYaml}
+            porterApp={porterApp}
+            updatePorterApp={(attrs: Partial<PorterApp>) => setPorterApp(PorterApp.setAttributes(porterApp, attrs))}
+            autoDetectBuildpacks={true}
+            canChangeRepo={true}
+          />
+        ) : (
+          <StyledSourceBox>
+            <Subtitle>
+              Specify the container image you would like to connect to this
+              template.
+              <Spacer inline width="5px" />
+              <Link
+                hasunderline
+                onClick={() =>
+                  pushFiltered({ location, history }, "/integrations/registry", ["project_id"])
+                }
+              >
+                Manage Docker registries
+              </Link>
+            </Subtitle>
+            <DarkMatter antiHeight="-4px" />
+            <ImageSelector
+              selectedTag={imageTag}
+              selectedImageUrl={imageUrl}
+              setSelectedImageUrl={setImageUrl}
+              setSelectedTag={setImageTag}
+              forceExpanded={true}
             />
-          ) : (
-            renderDockerSettings()
-          )}
-        </div>
+            <br />
+          </StyledSourceBox>)
+        }
       </AnimateHeight>
     </SourceSettingsContainer>
   );
@@ -180,36 +93,6 @@ 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 Highlight = styled.a`
-  color: #8590ff;
-  text-decoration: none;
-  margin-left: 5px;
-  cursor: pointer;
-  display: inline;
-`;
-
 const StyledSourceBox = styled.div`
   width: 100%;
   color: #ffffff;

+ 1 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -1,5 +1,5 @@
 import Input from "components/porter/Input";
-import React, { useEffect, useRef } from "react";
+import React from "react";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
@@ -11,7 +11,6 @@ interface Props {
   service: WebService;
   editService: (service: WebService) => void;
   setHeight: (height: Height) => void;
-  hasFooter?: boolean;
 }
 
 const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 373;
@@ -23,7 +22,6 @@ const WebTabs: React.FC<Props> = ({
   service,
   editService,
   setHeight,
-  hasFooter,
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>("main");
 

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

@@ -2,6 +2,7 @@ import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArr
 import * as z from "zod";
 import { JobService, ReleaseService, Service, WebService, WorkerService } from "./serviceTypes";
 import { overrideObjectValues } from "./utils";
+import _ from "lodash";
 
 const appConfigSchema = z.object({
     run: z.string().min(1),
@@ -59,7 +60,7 @@ export const createFinalPorterYaml = (
 
     const release = services.find(Service.isRelease);
 
-    return release != null && release.startCommand.value.trim() != "" ? {
+    return release != null && !_.isEmpty(release.startCommand.value) ? {
         version: "v1stack",
         env,
         apps,

+ 41 - 0
dashboard/src/main/home/app-dashboard/types/porterApp.ts

@@ -0,0 +1,41 @@
+export interface PorterApp {
+    name: string;
+    git_branch: string;
+    git_repo_id: number;
+    repo_name: string;
+    build_context: string;
+    builder: string;
+    buildpacks: string[];
+    dockerfile: string;
+    image_repo_uri: string;
+    porter_yaml_path: string;
+}
+
+export const PorterApp = {
+    empty: (): PorterApp => ({
+        name: "",
+        git_branch: "",
+        git_repo_id: 0,
+        repo_name: "",
+        build_context: "./",
+        builder: "",
+        buildpacks: [],
+        dockerfile: "",
+        image_repo_uri: "",
+        porter_yaml_path: "",
+    }),
+
+    setAttribute: <K extends keyof PorterApp>(
+        app: PorterApp,
+        key: K,
+        value: PorterApp[K]
+    ): PorterApp => ({
+        ...app,
+        [key]: value,
+    }),
+
+    setAttributes: (app: PorterApp, values: Partial<PorterApp>): PorterApp => ({
+        ...app,
+        ...values,
+    }),
+}

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -16,10 +16,10 @@ import CheckboxRow from "components/form-components/CheckboxRow";
 import BranchFilterSelector from "./components/BranchFilterSelector";
 import Helper from "components/form-components/Helper";
 import NamespaceLabels, { KeyValueType } from "./components/NamespaceLabels";
-import ActionConfEditorStack from "components/repo-selector/ActionConfEditorStack";
 import AnimateHeight from "react-animate-height";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
+import ConnectNewRepoActionConfEditor from "./ConnectNewRepoActionConfEditor";
 
 const ConnectNewRepo: React.FC = () => {
   const { currentProject, currentCluster, setCurrentError } = useContext(
@@ -77,7 +77,7 @@ const ConnectNewRepo: React.FC = () => {
         });
         setFilteredRepos(newFilteredRepos || []);
       })
-      .catch(() => {});
+      .catch(() => { });
   }, []);
 
   useEffect(() => {
@@ -217,7 +217,7 @@ const ConnectNewRepo: React.FC = () => {
         readOnly={false}
         filteredRepos={filteredRepos}
       /> */}
-      <ActionConfEditorStack
+      <ConnectNewRepoActionConfEditor
         actionConfig={actionConfig}
         setActionConfig={(actionConfig: ActionConfigType) => {
           setActionConfig((currentActionConfig: ActionConfigType) => ({
@@ -509,7 +509,7 @@ const StyledAdvancedBuildSettings = styled.div`
     cursor: pointer;
     border-radius: 20px;
     transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-      props.showSettings ? "" : "rotate(-90deg)"};
+    props.showSettings ? "" : "rotate(-90deg)"};
   }
 `;
 const AdvancedBuildTitle = styled.div`

+ 116 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx

@@ -0,0 +1,116 @@
+import React from "react";
+import styled from "styled-components";
+
+import { ActionConfigType } from "shared/types";
+
+import Input from "components/porter/Input";
+import RepoList from "components/repo-selector/RepoList";
+
+type Props = {
+    actionConfig: ActionConfigType | null;
+    setActionConfig: (x: ActionConfigType) => void;
+    setBranch?: (x: string) => void;
+    setDockerfilePath?: (x: string) => void;
+    setFolderPath?: (x: string) => void;
+    setBuildView?: (x: string) => void;
+    setPorterYamlPath?: (x: string) => void;
+};
+
+const defaultActionConfig: ActionConfigType = {
+    git_repo: null,
+    image_repo_uri: null,
+    git_branch: null,
+    git_repo_id: 0,
+    kind: "github",
+};
+
+const ConnectNewRepoActionConfEditor: React.FC<Props> = ({
+    actionConfig,
+    setBranch,
+    setActionConfig,
+    setFolderPath,
+    setDockerfilePath,
+    setBuildView,
+    setPorterYamlPath,
+}) => {
+    if (!actionConfig.git_repo) {
+        return (
+            <ExpandedWrapperAlt>
+                <RepoList
+                    actionConfig={actionConfig}
+                    setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
+                    readOnly={false}
+                />
+            </ExpandedWrapperAlt>
+        );
+    } else {
+        return (
+            <>
+                <Input
+                    disabled={true}
+                    label="GitHub repository:"
+                    width="100%"
+                    value={actionConfig?.git_repo}
+                    setValue={() => { }}
+                    placeholder=""
+                />
+                <BackButton
+                    width="135px"
+                    onClick={() => {
+                        setActionConfig({ ...defaultActionConfig });
+                        setBranch ? setBranch("") : null;
+                        setFolderPath ? setFolderPath("") : null;
+                        setDockerfilePath ? setDockerfilePath("") : null;
+                        setBuildView ? setBuildView("buildpacks") : null;
+                        setPorterYamlPath("");
+                    }}
+                >
+                    <i className="material-icons">keyboard_backspace</i>
+                    Select repo
+                </BackButton>
+            </>
+        );
+    }
+};
+
+export default ConnectNewRepoActionConfEditor;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)`
+  border: 0;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 22px;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  margin-bottom: -7px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;