瀏覽代碼

Merge pull request #1921 from porter-dev/nico/implement-search-and-repo-filter-on-preview-environments

[Improvement] Implement search input on deployment list and redesigned routing for preview environments
abelanger5 4 年之前
父節點
當前提交
2c3d197d15

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

@@ -9,6 +9,7 @@ export const OptionsDropdown: React.FC<{
 
   const handleClick = (e: any) => {
     e.stopPropagation();
+    e.preventDefault();
     setIsOpen(!isOpen);
   };
 

+ 12 - 5
dashboard/src/main/CurrentError.tsx

@@ -5,7 +5,7 @@ import close from "assets/close.png";
 import { Context } from "shared/Context";
 
 type PropsType = {
-  currentError: string;
+  currentError: any;
 };
 
 type StateType = {};
@@ -26,11 +26,18 @@ export default class CurrentError extends Component<PropsType, StateType> {
   }
 
   render() {
-    let currentError = this.props.currentError;
-    if (!React.isValidElement(this.props.currentError)) {
-      currentError = String(this.props.currentError);
+    if (!this.props.currentError) {
+      return null;
     }
-    if (this.props.currentError) {
+
+    // Check if it's an error from the API then retrieve the error message that we get from the API
+    let currentError =
+      this.props.currentError?.response?.data?.error || this.props.currentError;
+    if (!React.isValidElement(currentError)) {
+      currentError = String(currentError);
+    }
+
+    if (currentError) {
       if (!this.state.expanded) {
         return (
           <StyledCurrentError>

+ 3 - 3
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -285,6 +285,9 @@ class ClusterDashboard extends Component<PropsType, StateType> {
     let { setSidebar } = this.props;
     return (
       <Switch>
+        <Route path={"/preview-environments"}>
+          <LazyPreviewEnvironmentsRoutes />
+        </Route>
         <Route path="/:baseRoute/:clusterName+/:namespace/:chartName">
           <ExpandedChartWrapper
             setSidebar={setSidebar}
@@ -329,9 +332,6 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         >
           <EnvGroupDashboard currentCluster={this.props.currentCluster} />
         </GuardedRoute>
-        <Route path={"/preview-environments"}>
-          <LazyPreviewEnvironmentsRoutes />
-        </Route>
         <Route path={"/databases"}>
           <LazyDatabasesRoutes />
         </Route>

+ 29 - 63
dashboard/src/main/home/cluster-dashboard/preview-environments/PreviewEnvironmentsHome.tsx

@@ -1,5 +1,5 @@
 import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useCallback, useContext, useEffect, useState } from "react";
 import { useHistory, useLocation } from "react-router";
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -11,10 +11,7 @@ import PullRequestIcon from "assets/pull_request_icon.svg";
 import DeploymentList from "./deployments/DeploymentList";
 import EnvironmentsList from "./environments/EnvironmentsList";
 import { environments } from "./mocks";
-
-const AvailableTabs = ["repositories", "pull_requests"];
-
-type TabEnum = typeof AvailableTabs[number];
+import { PreviewEnvironmentsHeader } from "./components/PreviewEnvironmentsHeader";
 
 const PreviewEnvironmentsHome = () => {
   const { currentCluster, currentProject } = useContext(Context);
@@ -22,11 +19,10 @@ const PreviewEnvironmentsHome = () => {
   const [hasGHAccountsLinked, setHasGHAccountsLinked] = useState(false);
   const [hasEnvironments, setHasEnvironments] = useState(false);
   const [isLoading, setIsLoading] = useState(true);
-  const [hasError, setHasError] = useState(false);
   const [environments, setEnvironments] = useState([]);
   const [selectedRepo, setSelectedRepo] = useState("");
 
-  const { getQueryParam, pushQueryParams } = useRouting();
+  const { getQueryParam } = useRouting();
   const location = useLocation();
   const history = useHistory();
 
@@ -107,21 +103,21 @@ const PreviewEnvironmentsHome = () => {
     setSelectedRepo(current_repo);
   }, [location.search, history]);
 
-  const renderMain = () => {
-    if (isLoading) {
-      return (
+  if (isLoading) {
+    return (
+      <>
+        <PreviewEnvironmentsHeader />
         <Placeholder>
           <Loading />
         </Placeholder>
-      );
-    }
-  
-    if (hasError) {
-      return <Placeholder>Something went wrong, please try again</Placeholder>;
-    }
-  
-    if (!hasGHAccountsLinked) {
-      return (
+      </>
+    );
+  }
+
+  if (!hasGHAccountsLinked) {
+    return (
+      <>
+        <PreviewEnvironmentsHeader />
         <Placeholder>
           <Title>There are no repositories linked</Title>
           <Subtitle>
@@ -130,11 +126,15 @@ const PreviewEnvironmentsHome = () => {
           </Subtitle>
           <ButtonEnablePREnvironments />
         </Placeholder>
-      );
-    }
-  
-    if (!hasEnvironments) {
-      return (
+      </>
+    );
+  }
+
+  if (!hasEnvironments) {
+    return (
+      <>
+        <PreviewEnvironmentsHeader />
+
         <Placeholder>
           <Title>Preview environments are not enabled on this cluster</Title>
           <Subtitle>
@@ -143,57 +143,23 @@ const PreviewEnvironmentsHome = () => {
           </Subtitle>
           <ButtonEnablePREnvironments />
         </Placeholder>
-      );
-    }
-
-    if (!selectedRepo) {
-      return (
-        <EnvironmentsList
-          environments={environments}
-          setEnvironments={setEnvironments}
-        />
-      );
-    }
-
-    return (
-      <DeploymentList
-        // selectedRepo={selectedRepo}
-        environments={environments}
-      />
+      </>
     );
   }
 
   return (
     <>
-      <DashboardHeader
-        image={PullRequestIcon}
-        title="Preview Environments"
-        description="Create full-stack preview environments for your pull requests."
+      <PreviewEnvironmentsHeader />
+      <EnvironmentsList
+        environments={environments}
+        setEnvironments={setEnvironments}
       />
-      {renderMain()}
     </>
   );
 };
 
-/*
-<DeploymentList environments={environments} />
-*/
 export default PreviewEnvironmentsHome;
 
-const mockRequest = () =>
-  new Promise((res) => {
-    setTimeout(() => {
-      res({ data: environments });
-    }, 1000);
-  });
-
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
-  margin: 10px 0px 35px;
-`;
-
 const Placeholder = styled.div`
   padding: 30px;
   margin-top: 35px;

+ 7 - 66
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx

@@ -1,73 +1,14 @@
 import React from "react";
-import TitleSection from "components/TitleSection";
 import styled from "styled-components";
+import DashboardHeader from "../../DashboardHeader";
+import PullRequestIcon from "assets/pull_request_icon.svg";
 
 export const PreviewEnvironmentsHeader = () => (
   <>
-    <TitleSection>
-      <DashboardIcon>
-        <i className="material-icons">device_hub</i>
-      </DashboardIcon>
-      Preview environments
-    </TitleSection>
-    <InfoSection>
-      <TopRow>
-        <InfoLabel>
-          <i className="material-icons">info</i> Info
-        </InfoLabel>
-      </TopRow>
-      <Description>
-        Create preview environments for your pull requests
-      </Description>
-    </InfoSection>
+    <DashboardHeader
+      image={PullRequestIcon}
+      title="Preview Environments"
+      description="Create full-stack preview environments for your pull requests."
+    />
   </>
 );
-
-const DashboardIcon = styled.div`
-  height: 45px;
-  min-width: 45px;
-  width: 45px;
-  border-radius: 5px;
-  margin-right: 17px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #676c7c;
-  border: 2px solid #8e94aa;
-  > i {
-    font-size: 22px;
-  }
-`;
-
-const TopRow = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const Description = styled.div`
-  color: #aaaabb;
-  margin-top: 13px;
-  margin-left: 2px;
-  font-size: 13px;
-`;
-
-const InfoLabel = styled.div`
-  width: 72px;
-  height: 20px;
-  display: flex;
-  align-items: center;
-  color: #7a838f;
-  font-size: 13px;
-  > i {
-    color: #8b949f;
-    font-size: 18px;
-    margin-right: 5px;
-  }
-`;
-
-const InfoSection = styled.div`
-  margin-top: 36px;
-  font-family: "Work Sans", sans-serif;
-  margin-left: 0px;
-  margin-bottom: 35px;
-`;

+ 21 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -18,6 +18,7 @@ const DeploymentDetail = () => {
   const { params } = useRouteMatch<{ namespace: string }>();
   const context = useContext(Context);
   const [prDeployment, setPRDeployment] = useState<PRDeployment>(null);
+  const [environmentId, setEnvironmentId] = useState("");
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
 
   const { currentProject, currentCluster } = useContext(Context);
@@ -28,6 +29,7 @@ const DeploymentDetail = () => {
   useEffect(() => {
     let isSubscribed = true;
     let environment_id = parseInt(searchParams.get("environment_id"));
+    setEnvironmentId(searchParams.get("environment_id"));
     api
       .getPRDeploymentByCluster(
         "<token>",
@@ -64,7 +66,9 @@ const DeploymentDetail = () => {
   return (
     <StyledExpandedChart>
       <HeaderWrapper>
-        <BackButton to={`/preview-environments?repository=${repository}`}>
+        <BackButton
+          to={`/preview-environments/deployments/${environmentId}/${repository}`}
+        >
           <BackButtonImg src={backArrow} />
         </BackButton>
         <Title icon={pr_icon} iconWidth="25px">
@@ -109,6 +113,15 @@ const DeploymentDetail = () => {
             <img src={github} /> GitHub PR
             <i className="material-icons">open_in_new</i>
           </GHALink>
+          {prDeployment.last_workflow_run_url ? (
+            <GHALink to={prDeployment.last_workflow_run_url} target="_blank">
+              <span className="material-icons-outlined">
+                play_circle_outline
+              </span>
+              Last workflow run
+              <i className="material-icons">open_in_new</i>
+            </GHALink>
+          ) : null}
         </Flex>
         <LinkToActionsWrapper></LinkToActionsWrapper>
       </HeaderWrapper>
@@ -161,6 +174,13 @@ const GHALink = styled(DynamicLink)`
     }
   }
 
+  > span {
+    font-size: 17px;
+    margin-right: 9px;
+    margin-left: 5px;
+    text-decoration: none;
+  }
+
   > i {
     margin-left: 7px;
     font-size: 17px;

+ 108 - 113
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx

@@ -10,9 +10,12 @@ import _ from "lodash";
 import DeploymentCard from "./DeploymentCard";
 import { Environment, PRDeployment, PullRequest } from "../types";
 import { useRouting } from "shared/routing";
-import { useHistory, useLocation } from "react-router";
+import { useHistory, useLocation, useParams } from "react-router";
 import { deployments, pull_requests } from "../mocks";
 import PullRequestCard from "./PullRequestCard";
+import DynamicLink from "components/DynamicLink";
+import { PreviewEnvironmentsHeader } from "../components/PreviewEnvironmentsHeader";
+import SearchBar from "components/SearchBar";
 
 const AvailableStatusFilters = [
   "all",
@@ -25,27 +28,36 @@ const AvailableStatusFilters = [
 
 type AvailableStatusFiltersType = typeof AvailableStatusFilters[number];
 
-const DeploymentList = ({ environments }: { environments: Environment[] }) => {
+const DeploymentList = () => {
   const [isLoading, setIsLoading] = useState(true);
   const [hasError, setHasError] = useState(false);
   const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([]);
   const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
+  const [searchValue, setSearchValue] = useState("");
 
   const [
     statusSelectorVal,
     setStatusSelectorVal,
   ] = useState<AvailableStatusFiltersType>("active");
-  const [selectedRepo, setSelectedRepo] = useState("");
 
   const { currentProject, currentCluster } = useContext(Context);
   const { getQueryParam, pushQueryParams } = useRouting();
   const location = useLocation();
   const history = useHistory();
+  const { environment_id, repo_name, repo_owner } = useParams<{
+    environment_id: string;
+    repo_name: string;
+    repo_owner: string;
+  }>();
+
+  const selectedRepo = `${repo_owner}/${repo_name}`;
 
   const getPRDeploymentList = () => {
     return api.getPRDeploymentList(
       "<token>",
-      {},
+      {
+        environment_id: Number(environment_id),
+      },
       {
         project_id: currentProject.id,
         cluster_id: currentCluster.id,
@@ -54,18 +66,6 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
     // return mockRequest();
   };
 
-  useEffect(() => {
-    const selected_repo = getQueryParam("repository");
-
-    const repo = environments.find(
-      (env) => `${env.git_repo_owner}/${env.git_repo_name}` === selected_repo
-    );
-
-    if (repo && true) {
-      setSelectedRepo(`${repo.git_repo_owner}/${repo.git_repo_name}`);
-    }
-  }, [location.search, history]);
-
   useEffect(() => {
     const status_filter = getQueryParam("status_filter");
 
@@ -105,20 +105,20 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
     return () => {
       isSubscribed = false;
     };
-  }, [currentCluster, currentProject, statusSelectorVal]);
+  }, [currentCluster, currentProject]);
 
-  const handleRefresh = () => {
+  const handleRefresh = async () => {
     setIsLoading(true);
-    getPRDeploymentList()
-      .then(({ data }) => {
-        setDeploymentList(data.deployments || []);
-        setPullRequests(data.pull_requests || []);
-      })
-      .catch((err) => {
-        setHasError(true);
-        console.error(err);
-      })
-      .finally(() => setIsLoading(false));
+    try {
+      const { data } = await getPRDeploymentList();
+      setDeploymentList(data.deployments || []);
+      setPullRequests(data.pull_requests || []);
+    } catch (error) {
+      setHasError(true);
+      console.error(error);
+    } finally {
+      setIsLoading(false);
+    }
   };
 
   const handlePreviewEnvironmentManualCreation = (pullRequest: PullRequest) => {
@@ -134,56 +134,46 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
     handleRefresh();
   };
 
-  const filteredDeployments = useMemo(() => {
-    if (statusSelectorVal === "not_deployed") {
-      return [];
-    }
-
-    if (statusSelectorVal === "all" && selectedRepo === "all") {
-      return deploymentList;
-    }
+  const searchFilter = (value: string | number) => {
+    const val = String(value);
 
-    let tmpDeploymentList = [...deploymentList];
-
-    if (selectedRepo !== "all") {
-      tmpDeploymentList = tmpDeploymentList.filter((deployment) => {
-        return (
-          `${deployment.gh_repo_owner}/${deployment.gh_repo_name}` ===
-          selectedRepo
-        );
-      });
-    }
+    return val.toLowerCase().includes(searchValue.toLowerCase());
+  };
 
+  const filteredDeployments = useMemo(() => {
     // Only filter out inactive when status filter is "active"
     if (statusSelectorVal === "active") {
-      tmpDeploymentList = tmpDeploymentList.filter((d) => {
-        return d.status !== "inactive";
-      });
-    } else if (statusSelectorVal === "inactive") {
-      tmpDeploymentList = tmpDeploymentList.filter((d) => {
-        return d.status === "inactive";
-      });
+      return deploymentList
+        .filter((d) => {
+          return d.status !== "inactive";
+        })
+        .filter((d) => {
+          return Boolean(Object.values(d).find(searchFilter));
+        });
     }
 
-    return tmpDeploymentList;
-  }, [selectedRepo, statusSelectorVal, deploymentList]);
+    if (statusSelectorVal === "inactive") {
+      return deploymentList
+        .filter((d) => {
+          return d.status === "inactive";
+        })
+        .filter((d) => {
+          return Boolean(Object.values(d).find(searchFilter));
+        });
+    }
+
+    return deploymentList;
+  }, [statusSelectorVal, deploymentList, searchValue]);
 
   const filteredPullRequests = useMemo(() => {
-    if (
-      statusSelectorVal !== "not_deployed" &&
-      statusSelectorVal !== "inactive"
-    ) {
+    if (statusSelectorVal !== "inactive") {
       return [];
     }
 
-    if (selectedRepo === "inactive") {
-      return pullRequests;
-    }
-
     return pullRequests.filter((pr) => {
-      return `${pr.repo_owner}/${pr.repo_name}` === selectedRepo;
+      return Boolean(Object.values(pr).find(searchFilter));
     });
-  }, [selectedRepo, pullRequests]);
+  }, [pullRequests, statusSelectorVal, searchValue]);
 
   const renderDeploymentList = () => {
     if (isLoading) {
@@ -237,28 +227,18 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
   };
 
   const handleStatusFilterChange = (value: string) => {
-    setIsLoading(true);
     pushQueryParams({ status_filter: value });
     setStatusSelectorVal(value);
   };
 
-  const renderMain = () => {
-    return (
-      <Container>
-        <EventsGrid>{renderDeploymentList()}</EventsGrid>
-      </Container>
-    );
-  };
-
   return (
     <>
+      <PreviewEnvironmentsHeader />
       <Flex>
-        <i
-          className="material-icons"
-          onClick={() => pushQueryParams({}, ["status_filter", "repository"])}
-        >
+        <BackButton to={"/preview-environments"} className="material-icons">
           keyboard_backspace
-        </i>
+        </BackButton>
+
         <Icon
           src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"
           alt="git repository icon"
@@ -270,6 +250,16 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
             <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
               <i className="material-icons">refresh</i>
             </RefreshButton>
+            <SearchRow>
+              <i className="material-icons">search</i>
+              <SearchInput
+                value={searchValue}
+                onChange={(e: any) => {
+                  setSearchValue(e.target.value);
+                }}
+                placeholder="Search"
+              />
+            </SearchRow>
             <Selector
               activeValue={statusSelectorVal}
               setActiveValue={handleStatusFilterChange}
@@ -291,7 +281,9 @@ const DeploymentList = ({ environments }: { environments: Environment[] }) => {
           </StyledStatusSelector>
         </ActionsWrapper>
       </Flex>
-      {renderMain()}
+      <Container>
+        <EventsGrid>{renderDeploymentList()}</EventsGrid>
+      </Container>
     </>
   );
 };
@@ -312,16 +304,16 @@ const mockRequest = () =>
 const Flex = styled.div`
   display: flex;
   align-items: center;
+`;
 
-  > i {
-    cursor: pointer;
-    font-size: 24px;
-    color: #969fbbaa;
-    padding: 3px;
-    border-radius: 100px;
-    :hover {
-      background: #ffffff11;
-    }
+const BackButton = styled(DynamicLink)`
+  cursor: pointer;
+  font-size: 24px;
+  color: #969fbbaa;
+  padding: 3px;
+  border-radius: 100px;
+  :hover {
+    background: #ffffff11;
   }
 `;
 
@@ -393,15 +385,6 @@ const Container = styled.div`
   padding-bottom: 120px;
 `;
 
-const ControlRow = styled.div`
-  display: flex;
-  margin-left: auto;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 35px;
-  padding-left: 0px;
-`;
-
 const EventsGrid = styled.div`
   display: grid;
   grid-row-gap: 20px;
@@ -417,25 +400,37 @@ const StyledStatusSelector = styled.div`
   }
 `;
 
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-  width: 50%;
-`;
-
-const Subheader = styled.div`
-  width: 50%;
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  padding: 0;
+  height: 20px;
 `;
 
-const Label = styled.div`
+const SearchRow = styled.div`
   display: flex;
+  width: 100%;
+  font-size: 13px;
+  color: #ffffff55;
+  border-radius: 4px;
+  user-select: none;
   align-items: center;
-  margin-right: 12px;
+  padding: 10px 0px;
+  min-width: 300px;
+  max-width: min-content;
+  max-height: 35px;
+  background: #ffffff11;
+  margin-right: 15px;
 
-  > i {
-    margin-right: 8px;
-    font-size: 18px;
+  i {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+    font-size: 20px;
   }
 `;

+ 6 - 16
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx

@@ -1,14 +1,8 @@
-import React, {
-  FormEvent,
-  FormEventHandler,
-  useContext,
-  useState,
-} from "react";
+import React, { useContext, useState } from "react";
 import { capitalize } from "shared/string_utils";
 import styled from "styled-components";
 import { Environment } from "../types";
 import Options from "components/OptionsDropdown";
-import { useRouting } from "shared/routing";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import Modal from "main/home/modals/Modal";
@@ -25,7 +19,6 @@ const EnvironmentCard = ({ environment, onDelete }: Props) => {
   const { currentCluster, currentProject, setCurrentError } = useContext(
     Context
   );
-  const { pushFiltered } = useRouting();
 
   const [showDeleteModal, setShowDeleteModal] = useState(false);
   const [deleteConfirmationRepoName, setDeleteConfirmationRepoName] = useState(
@@ -42,12 +35,6 @@ const EnvironmentCard = ({ environment, onDelete }: Props) => {
     last_deployment_status,
   } = environment;
 
-  const showOpenPrs = () => {
-    pushFiltered("/preview-environments", [], {
-      repository: `${git_repo_owner}/${git_repo_name}`,
-    });
-  };
-
   const handleDelete = () => {
     if (!canDelete()) {
       return;
@@ -116,7 +103,9 @@ const EnvironmentCard = ({ environment, onDelete }: Props) => {
           </ActionWrapper>
         </Modal>
       ) : null}
-      <EnvironmentCardWrapper onClick={showOpenPrs}>
+      <EnvironmentCardWrapper
+        to={`/preview-environments/deployments/${id}/${git_repo_owner}/${git_repo_name}`}
+      >
         <DataContainer>
           <RepoName>
             <Icon
@@ -176,8 +165,9 @@ const OptionWrapper = styled.div`
   justify-content: center;
 `;
 
-const EnvironmentCardWrapper = styled.div`
+const EnvironmentCardWrapper = styled(DynamicLink)`
   display: flex;
+  color: #ffffff;
   background: #2b2e3699;
   justify-content: space-between;
   border-radius: 5px;

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

@@ -3,10 +3,11 @@ import { Redirect, Route, Switch, useRouteMatch } from "react-router";
 import { Context } from "shared/Context";
 import ConnectNewRepo from "./ConnectNewRepo";
 import DeploymentDetail from "./deployments/DeploymentDetail";
+import DeploymentList from "./deployments/DeploymentList";
 import PreviewEnvironmentsHome from "./PreviewEnvironmentsHome";
 
 export const Routes = () => {
-  const { url } = useRouteMatch();
+  const { path } = useRouteMatch();
   const { currentProject } = useContext(Context);
 
   if (!currentProject?.preview_envs_enabled) {
@@ -16,13 +17,18 @@ export const Routes = () => {
   return (
     <>
       <Switch>
-        <Route path={`${url}/connect-repo`}>
+        <Route path={`${path}/connect-repo`}>
           <ConnectNewRepo />
         </Route>
-        <Route path={`${url}/details/:namespace?`}>
+        <Route path={`${path}/details/:namespace?`}>
           <DeploymentDetail />
         </Route>
-        <Route path={`${url}/:selected_tab?`}>
+        <Route
+          path={`${path}/deployments/:environment_id/:repo_owner/:repo_name`}
+        >
+          <DeploymentList />
+        </Route>
+        <Route path={`${path}/`}>
           <PreviewEnvironmentsHome />
         </Route>
       </Switch>

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts

@@ -13,6 +13,7 @@ export type PRDeployment = {
   gh_commit_sha: string;
   gh_pr_branch_from?: string;
   gh_pr_branch_into?: string;
+  last_workflow_run_url: string;
 };
 
 export type Environment = {

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

@@ -313,7 +313,7 @@ const updateNotificationConfig = baseApi<
 
 const getPRDeploymentList = baseApi<
   {
-    status?: string[];
+    environment_id?: number;
   },
   {
     cluster_id: number;
@@ -373,7 +373,14 @@ const deletePRDeployment = baseApi<
     pr_number: number;
   }
 >("DELETE", (pathParams) => {
-  const { cluster_id, project_id, environment_id, repo_owner, repo_name, pr_number } = pathParams;
+  const {
+    cluster_id,
+    project_id,
+    environment_id,
+    repo_owner,
+    repo_name,
+    pr_number,
+  } = pathParams;
   return `/api/projects/${project_id}/clusters/${cluster_id}/deployments/${environment_id}/${repo_owner}/${repo_name}/${pr_number}`;
 });