Переглянути джерело

auto setup preview from stack

Ian Edwards 2 роки тому
батько
коміт
cfd594fc2c

+ 19 - 0
dashboard/package-lock.json

@@ -128,6 +128,7 @@
         "style-loader": "^2.0.0",
         "terser-webpack-plugin": "^4.2.3",
         "ts-loader": "^8.0.4",
+        "type-fest": "^3.12.0",
         "typescript": "^4.1.2",
         "webpack": "^4.44.2",
         "webpack-bundle-analyzer": "^4.4.2",
@@ -13030,6 +13031,18 @@
       "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==",
       "dev": true
     },
+    "node_modules/type-fest": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.12.0.tgz",
+      "integrity": "sha512-qj9wWsnFvVEMUDbESiilKeXeHL7FwwiFcogfhfyjmvT968RXSvnl23f1JOClTHYItsi7o501C/7qVllscUP3oA==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -25300,6 +25313,12 @@
       "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==",
       "dev": true
     },
+    "type-fest": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.12.0.tgz",
+      "integrity": "sha512-qj9wWsnFvVEMUDbESiilKeXeHL7FwwiFcogfhfyjmvT968RXSvnl23f1JOClTHYItsi7o501C/7qVllscUP3oA==",
+      "dev": true
+    },
     "type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

+ 1 - 0
dashboard/package.json

@@ -129,6 +129,7 @@
     "style-loader": "^2.0.0",
     "terser-webpack-plugin": "^4.2.3",
     "ts-loader": "^8.0.4",
+    "type-fest": "^3.12.0",
     "typescript": "^4.1.2",
     "webpack": "^4.44.2",
     "webpack-bundle-analyzer": "^4.4.2",

+ 5 - 4
dashboard/src/components/SearchBar.tsx

@@ -16,16 +16,17 @@ const SearchBar: React.FC<Props> = ({
   fullWidth,
 }) => {
   const [searchInput, setSearchInput] = useState("");
-  const inputRef = useRef(null);
+  const inputRef = useRef<HTMLDivElement | null>(null);
 
   // hack for deferring the focus call to the next tick of the event loop, giving the browser enough time to render the input element before setting focus on it
   useEffect(() => {
     setTimeout(() => {
-      inputRef.current.focus();
+      if (inputRef.current) {
+        inputRef.current.focus();
+      }
     }, 0);
   }, []);
 
-
   return (
     <SearchRowWrapper fullWidth={fullWidth}>
       <SearchBarWrapper>
@@ -84,7 +85,7 @@ const ButtonWrapper = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#505edddd"};
+      props.disabled ? "" : "#505edddd"};
   }
   height: 40px;
   display: flex;

+ 40 - 25
dashboard/src/components/repo-selector/RepoList.tsx

@@ -19,19 +19,20 @@ type Props = {
   userId?: number;
   readOnly: boolean;
   filteredRepos?: string[];
+  defaultRepo?: string;
 };
 
 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[]) => {
@@ -63,6 +64,7 @@ const RepoList: React.FC<Props> = ({
   userId,
   readOnly,
   filteredRepos,
+  defaultRepo,
 }) => {
   const [providers, setProviders] = useState([]);
   const [currentProvider, setCurrentProvider] = useState(null);
@@ -111,7 +113,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 +131,7 @@ const RepoList: React.FC<Props> = ({
         GitIntegrationId: integrationId,
       }));
       return repos;
-    } catch (error) { }
+    } catch (error) {}
   };
 
   const loadRepos = (provider: any) => {
@@ -177,6 +179,19 @@ const RepoList: React.FC<Props> = ({
     setSelectedRepo(null);
   }, [searchFilter]);
 
+  useEffect(() => {
+    if (repos.length && !selectedRepo && defaultRepo) {
+      console.log('all repos', repos)
+      const repo = repos.find(
+        (repo) => repo.Kind === "github" && repo.FullName === defaultRepo
+      );
+      console.log('repo', repo)
+      if (repo) {
+        setRepo(repo);
+      }
+    }
+  }, [repos.length]);
+
   const setRepo = (x: RepoType) => {
     let repoConfig: any;
     if (x.Kind === "gitlab") {
@@ -243,20 +258,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 +381,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#353a3e"};
+      props.disabled ? "" : "#353a3e"};
   }
 
   > i {

+ 80 - 47
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -49,6 +49,9 @@ import Anser, { AnserJsonEntry } from "anser";
 import _ from "lodash";
 import AnimateHeight from "react-animate-height";
 import { BuildMethod, PorterApp } from "../types/porterApp";
+import { useRouting } from "shared/routing";
+import { IterableElement } from "type-fest";
+import { PreviewEnvSettings } from "./PreviewEnvSettings";
 
 type Props = RouteComponentProps & {};
 
@@ -71,19 +74,18 @@ const validTabs = [
   "settings",
 ] as const;
 const DEFAULT_TAB = "activity";
-type ValidTab = typeof validTabs[number];
+type ValidTab = IterableElement<typeof validTabs>;
 interface Params {
   eventId?: string;
   tab?: ValidTab;
 }
 
 const ExpandedApp: React.FC<Props> = ({ ...props }) => {
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentError,
-    featurePreview,
-  } = useContext(Context);
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+  const router = useRouting();
+
   const [isLoading, setIsLoading] = useState(true);
   const [deleting, setDeleting] = useState(false);
   const [appData, setAppData] = useState(null);
@@ -103,10 +105,15 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [showDeleteOverlay, setShowDeleteOverlay] = useState<boolean>(false);
 
   // this is what we read from their porter.yaml in github
-  const [porterJson, setPorterJson] = useState<PorterJson | undefined>(undefined);
+  const [porterJson, setPorterJson] = useState<PorterJson | undefined>(
+    undefined
+  );
   // this is what we use to update the release. the above is a subset of this
   const [porterYaml, setPorterYaml] = useState<PorterJson>({} as PorterJson);
-  const [showUnsavedChangesBanner, setShowUnsavedChangesBanner] = useState<boolean>(false);
+  const [
+    showUnsavedChangesBanner,
+    setShowUnsavedChangesBanner,
+  ] = useState<boolean>(false);
 
   const [expandedJob, setExpandedJob] = useState(null);
   const [logs, setLogs] = useState<Log[]>([]);
@@ -122,7 +129,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [buildView, setBuildView] = useState<BuildMethod>("docker");
 
   const { eventId, tab } = useParams<Params>();
-  const selectedTab: ValidTab = tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB;
+  const selectedTab: ValidTab =
+    tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB;
 
   useEffect(() => {
     setBannerLoading(true);
@@ -132,7 +140,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   }, [appData]);
 
   useEffect(() => {
-    if (!_.isEqual(_.omitBy(porterApp, _.isEmpty), _.omitBy(tempPorterApp, _.isEmpty))) {
+    if (
+      !_.isEqual(
+        _.omitBy(porterApp, _.isEmpty),
+        _.omitBy(tempPorterApp, _.isEmpty)
+      )
+    ) {
       setButtonStatus("");
       setShowUnsavedChangesBanner(true);
     } else {
@@ -211,22 +224,28 @@ 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(",") ?? [] };
+      const parsedPorterApp = {
+        ...resPorterApp?.data,
+        buildpacks: newAppData.app.buildpacks?.split(",") ?? [],
+      };
       setPorterApp(parsedPorterApp);
       setTempPorterApp(parsedPorterApp);
-      setBuildView(!_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks")
+      setBuildView(
+        !_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks"
+      );
 
       const [newServices, newEnvVars] = updateServicesAndEnvVariables(
         resChartData?.data,
         preDeployChartData?.data,
-        porterJson,
+        porterJson
       );
       const finalPorterYaml = createFinalPorterYaml(
         newServices,
         newEnvVars,
         porterJson,
         // if we are using a heroku buildpack, inject a PORT env variable
-        newAppData.app.builder != null && newAppData.app.builder.includes("heroku")
+        newAppData.app.builder != null &&
+          newAppData.app.builder.includes("heroku")
       );
       setPorterYaml(finalPorterYaml);
 
@@ -345,7 +364,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           git_branch: tempPorterApp.git_branch,
           buildpacks: "",
           ...options,
-        }
+        };
         if (buildView === "docker") {
           updatedPorterApp.dockerfile = tempPorterApp.dockerfile;
           updatedPorterApp.builder = "null";
@@ -356,15 +375,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           updatedPorterApp.dockerfile = "null";
         }
 
-        await api.createPorterApp(
-          "<token>",
-          updatedPorterApp,
-          {
-            cluster_id: currentCluster.id,
-            project_id: currentProject.id,
-            stack_name: appData.app.name,
-          }
-        );
+        await api.createPorterApp("<token>", updatedPorterApp, {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+          stack_name: appData.app.name,
+        });
         setPorterYaml(finalPorterYaml);
         setPorterApp(tempPorterApp);
         setButtonStatus("success");
@@ -505,7 +520,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const updateServicesAndEnvVariables = (
     currentChart?: ChartType,
     releaseChart?: ChartType,
-    porterJson?: PorterJson,
+    porterJson?: PorterJson
   ): [Service[], KeyValueType[]] => {
     // handle normal chart
     const helmValues = currentChart?.config;
@@ -532,7 +547,10 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
 
     // handle release chart
     if (releaseChart?.config || porterJson?.release) {
-      const release = Service.deserializeRelease(releaseChart?.config, porterJson);
+      const release = Service.deserializeRelease(
+        releaseChart?.config,
+        porterJson
+      );
       newServices.push(release);
     }
 
@@ -586,15 +604,19 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     if (buttonStatus !== "") {
                       setButtonStatus("");
                     }
-                    const nonRelease = services.filter(Service.isNonRelease)
-                    const newServices = [...nonRelease, ...release]
-                    setServices(newServices)
-                    onAppUpdate(newServices, envVars)
+                    const nonRelease = services.filter(Service.isNonRelease);
+                    const newServices = [...nonRelease, ...release];
+                    setServices(newServices);
+                    onAppUpdate(newServices, envVars);
                   }}
                   chart={appData.releaseChart}
                   services={services.filter(Service.isRelease)}
                   limitOne={true}
-                  prePopulateService={Service.default("pre-deploy", "release", porterJson)}
+                  prePopulateService={Service.default(
+                    "pre-deploy",
+                    "release",
+                    porterJson
+                  )}
                   addNewText={"Add a new pre-deploy job"}
                   defaultExpanded={false}
                 />
@@ -619,8 +641,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                 if (buttonStatus !== "") {
                   setButtonStatus("");
                 }
-                const release = services.filter(Service.isRelease)
-                const newServices = [...svcs, ...release]
+                const release = services.filter(Service.isRelease);
+                const newServices = [...svcs, ...release];
                 setServices(newServices);
                 onAppUpdate(newServices, envVars);
               }}
@@ -644,7 +666,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         return (
           <BuildSettingsTab
             porterApp={tempPorterApp}
-            setTempPorterApp={(attrs: Partial<PorterApp>) => setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs))}
+            setTempPorterApp={(attrs: Partial<PorterApp>) =>
+              setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs))
+            }
             clearStatus={() => setButtonStatus("")}
             updatePorterApp={updatePorterApp}
             setShowUnsavedChangesBanner={setShowUnsavedChangesBanner}
@@ -655,8 +679,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       case "settings":
         return (
           <>
+            {currentProject?.preview_envs_enabled && (
+              <PreviewEnvSettings
+                appName={appData?.app.name}
+                repoName={appData?.app.repo_name}
+              />
+            )}
             <Text size={16}>Delete "{appData.app.name}"</Text>
-            <Spacer y={1} />
+            <Spacer y={0.25} />
             <Text color="helper">
               Delete this application and all of its resources.
             </Text>
@@ -683,7 +713,10 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             envVars={envVars}
             setEnvVars={(envVars: KeyValueType[]) => {
               setEnvVars(envVars);
-              onAppUpdate(services, envVars.filter((e) => e.key !== "" || e.value !== ""));
+              onAppUpdate(
+                services,
+                envVars.filter((e) => e.key !== "" || e.value !== "")
+              );
             }}
             status={buttonStatus}
             updatePorterApp={updatePorterApp}
@@ -691,12 +724,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           />
         );
       default:
-        return <ActivityFeed
-          chart={appData.chart}
-          stackName={appData?.app?.name}
-          appData={appData}
-          eventId={eventId}
-        />;
+        return (
+          <ActivityFeed
+            chart={appData.chart}
+            stackName={appData?.app?.name}
+            appData={appData}
+            eventId={eventId}
+          />
+        );
     }
   };
 
@@ -827,9 +862,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   <Banner
                     suffix={
                       <>
-                        <RefreshButton
-                          onClick={() => window.location.reload()}
-                        >
+                        <RefreshButton onClick={() => window.location.reload()}>
                           <img src={refresh} />
                           Refresh
                         </RefreshButton>
@@ -862,7 +895,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     shouldUpdate={
                       appData.chart.latest_version &&
                       appData.chart.latest_version !==
-                      appData.chart.chart.metadata.version
+                        appData.chart.chart.metadata.version
                     }
                     latestVersion={appData.chart.latest_version}
                   />

+ 131 - 0
dashboard/src/main/home/app-dashboard/expanded-app/PreviewEnvSettings.tsx

@@ -0,0 +1,131 @@
+import { useQuery } from "@tanstack/react-query";
+import Button from "components/porter/Button";
+import Banner from "components/porter/Banner";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import React, { useEffect, useMemo, useState } from "react";
+import { useContext } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { useRouting } from "shared/routing";
+import styled from "styled-components";
+import { z } from "zod";
+import loadingSrc from "assets/loading.gif";
+import { Environment } from "main/home/cluster-dashboard/preview-environments/types";
+
+type Props = {
+  appName: string;
+  repoName: string;
+};
+
+export const PreviewEnvSettings: React.FC<Props> = ({ appName, repoName }) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const { pushFiltered, push } = useRouting();
+
+  const { data: environments, status } = useQuery<Environment[]>(
+    ["listEnvironments", currentProject?.id, currentCluster?.id],
+    async () => {
+      const inputSchema = z.object({
+        project_id: z.number(),
+        cluster_id: z.number(),
+      });
+
+      const input = await inputSchema.parseAsync({
+        project_id: currentProject?.id,
+        cluster_id: currentCluster?.id,
+      });
+
+      const { data } = await api.listEnvironments("<token>", {}, input);
+      return data;
+    }
+  );
+
+  console.log("repoName", repoName);
+
+  const [
+    showPreviewDisabledBanner,
+    setShowPreviewDisabledBanner,
+  ] = useState<boolean>(false);
+
+  const alreadyEnabled = useMemo(
+    () =>
+      environments?.some(
+        (e) => `${e.git_repo_owner}/${e.git_repo_name}` === repoName
+      ),
+    [environments, repoName]
+  );
+
+  useEffect(() => {
+    if (showPreviewDisabledBanner) {
+      setTimeout(() => {
+        setShowPreviewDisabledBanner(false);
+      }, 5000);
+    }
+  }, [showPreviewDisabledBanner]);
+
+  return (
+    <>
+      {showPreviewDisabledBanner && (
+        <Banner type="error">
+          Preview Envs are disabled for this cluster.
+          <StyledLink
+            onClick={() => {
+              pushFiltered("/cluster-dashboard", ["project_id"], {
+                cluster: currentCluster?.name,
+                project_id: currentProject?.id,
+                selected_tab: "settings",
+              });
+            }}
+          >
+            Enable on Cluster
+          </StyledLink>
+        </Banner>
+      )}
+      {status !== "error" && (
+        <>
+          <Text size={16}>Enable Preview Environments</Text>
+          <Spacer y={0.25} />
+          <Text color="helper">Setup Preview Environments for "{appName}"</Text>
+          <Spacer y={1} />
+          <Button
+            onClick={() => {
+              if (!currentCluster?.preview_envs_enabled) {
+                setShowPreviewDisabledBanner(true);
+                return;
+              }
+              push(
+                `/preview-environments/connect-repo?selected_repo=${repoName}`
+              );
+            }}
+            disabled={status === "loading" || alreadyEnabled}
+          >
+            {status === "loading" ? (
+              <Spinner src={loadingSrc} />
+            ) : alreadyEnabled ? (
+              "Enabled!"
+            ) : (
+              "Enable"
+            )}
+          </Button>
+        </>
+      )}
+      <Spacer y={1} />
+    </>
+  );
+};
+
+const StyledLink = styled.div`
+  cursor: pointer;
+  :hover {
+    font-weight: 500;
+  }
+  text-decoration: underline;
+  margin-left: 7px;
+`;
+
+const Spinner = styled.img`
+  width: 15px;
+  height: 15px;
+  margin-right: 12px;
+  margin-bottom: -2px;
+`;

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -97,7 +97,6 @@ export const Dashboard: React.FunctionComponent = () => {
   };
 
   useEffect(() => {
-    ``;
     if (
       context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
       !tabOptions.find((tab) => tab.value === "nodes")
@@ -162,7 +161,7 @@ export const Dashboard: React.FunctionComponent = () => {
         setIngressIp(ingress_ip);
         setIngressError(ingress_error);
       }
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
@@ -285,8 +284,9 @@ export const Dashboard: React.FunctionComponent = () => {
             </EditIconStyle>
           </Flex>
         }
-        description={`Cluster settings and status for ${context.currentCluster.vanity_name || context.currentCluster.name
-          }.`}
+        description={`Cluster settings and status for ${
+          context.currentCluster.vanity_name || context.currentCluster.name
+        }.`}
         disableLineBreak
         capitalize={false}
       />

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

@@ -4,7 +4,7 @@ import RepoList from "components/repo-selector/RepoList";
 import SaveButton from "components/SaveButton";
 import DocsHelper from "components/DocsHelper";
 import { ActionConfigType, GithubActionConfigType } from "shared/types";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -22,11 +22,14 @@ import Spacer from "components/porter/Spacer";
 import ConnectNewRepoActionConfEditor from "./ConnectNewRepoActionConfEditor";
 import VerticalSteps from "components/porter/VerticalSteps";
 import Back from "components/porter/Back";
+import { useParams } from "react-router";
 
 const ConnectNewRepo: React.FC = () => {
+  const { getQueryParam } = useRouting();
   const { currentProject, currentCluster, setCurrentError } = useContext(
     Context
   );
+
   const [repo, setRepo] = useState(null);
   const [enableAutomaticDeployments, setEnableAutomaticDeployments] = useState(
     false
@@ -52,6 +55,9 @@ const ConnectNewRepo: React.FC = () => {
   const [deployBranches, setDeployBranches] = useState<string[]>([]);
   const [availableBranches, setAvailableBranches] = useState<string[]>([]);
   const [isLoadingBranches, setIsLoadingBranches] = useState(false);
+  const [preselectedRepo, setPreselectedRepo] = useState<string | undefined>(
+    undefined
+  );
 
   // Disable new comments data
   const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false);
@@ -120,6 +126,13 @@ const ConnectNewRepo: React.FC = () => {
       });
   }, [actionConfig]);
 
+  useEffect(() => {
+    const repo = getQueryParam("selected_repo");
+    if (repo) {
+      setPreselectedRepo(repo);
+    }
+  }, [preselectedRepo]);
+
   const addRepo = () => {
     let [owner, repoName] = actionConfig.git_repo.split("/");
     const labels: Record<string, string> = {};
@@ -195,6 +208,7 @@ const ConnectNewRepo: React.FC = () => {
               <>
                 <Text size={16}>Choose a repository</Text>
                 <ConnectNewRepoActionConfEditor
+                  defaultRepo={preselectedRepo}
                   actionConfig={actionConfig}
                   setActionConfig={(actionConfig: ActionConfigType) => {
                     setActionConfig(

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

@@ -14,6 +14,7 @@ type Props = {
   setFolderPath?: (x: string) => void;
   setBuildView?: (x: string) => void;
   setPorterYamlPath?: (x: string) => void;
+  defaultRepo?: string
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -32,11 +33,13 @@ const ConnectNewRepoActionConfEditor: React.FC<Props> = ({
   setDockerfilePath,
   setBuildView,
   setPorterYamlPath,
+  defaultRepo
 }) => {
   if (!actionConfig.git_repo) {
     return (
       <ExpandedWrapperAlt>
         <RepoList
+          defaultRepo={defaultRepo}
           actionConfig={actionConfig}
           setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
           readOnly={false}