Explorar el Código

chore: minor fixes

- fix preview deletion, creation
- fix last workflow run
Soham Parekh hace 3 años
padre
commit
55c778837c

+ 3 - 3
dashboard/package-lock.json

@@ -1549,9 +1549,9 @@
       }
     },
     "@tanstack/react-query-devtools": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.13.0.tgz",
-      "integrity": "sha512-Gv33auVlcUMrnj5qXJuhhTJtBs8bKp7v3TFRysnS+VlLOmdj9gwl5bc0e0/URL7m+PQoR1Rr55yzQ5bmZcWm1g==",
+      "version": "4.13.5",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.13.5.tgz",
+      "integrity": "sha512-ItXysFt7a2WJcodeoZ52/NFoMeWUgwzSaKL3NXhHvdyDOFsX945/AN/+Q9WgXRDG+YgqDJm6f/ozYIfFaORoZQ==",
       "requires": {
         "@tanstack/match-sorter-utils": "^8.1.1",
         "superjson": "^1.10.0",

+ 1 - 1
dashboard/package.json

@@ -10,7 +10,7 @@
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
     "@tanstack/react-query": "^4.13.0",
-    "@tanstack/react-query-devtools": "^4.13.0",
+    "@tanstack/react-query-devtools": "^4.13.5",
     "@visx/axis": "^1.6.1",
     "@visx/curve": "^1.0.0",
     "@visx/event": "^1.3.0",

+ 1 - 1
dashboard/src/components/Banner.tsx

@@ -38,7 +38,7 @@ const StyledBanner = styled.div<{ color?: string }>`
   display: flex;
   border: 1px solid ${(props) => props.color || "#ffffff00"};
   border-radius: 8px;
-  padding-left: 14px;
+  padding-inline: 14px;
   color: ${(props) => props.color || "#ffffff"};
   align-items: center;
   background: #ffffff11;

+ 9 - 15
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -13,7 +13,6 @@ import ChartList from "../../chart/ChartList";
 import github from "assets/github-white.png";
 import { integrationList } from "shared/common";
 import { capitalize } from "shared/string_utils";
-import leftArrow from "assets/left-arrow.svg";
 import Banner from "components/Banner";
 import Modal from "main/home/modals/Modal";
 import { validatePorterYAML } from "../utils";
@@ -129,10 +128,15 @@ const DeploymentDetail = () => {
       </BreadcrumbRow>
       <StyledExpandedChart>
         <HeaderWrapper>
-          <Title 
-            icon={pr_icon} 
+          <Title
+            icon={pr_icon}
             iconWidth="25px"
-            onClick={() => window.open(`https://github.com/${repository}/pull/${prDeployment.pull_request_id}`, '_blank')}
+            onClick={() =>
+              window.open(
+                `https://github.com/${repository}/pull/${prDeployment.pull_request_id}`,
+                "_blank"
+              )
+            }
           >
             {prDeployment.gh_pr_name}
           </Title>
@@ -168,19 +172,9 @@ const DeploymentDetail = () => {
               {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
             </DeploymentImageContainer>
             <Dot>•</Dot>
-            <GHALink
-              to={`https://github.com/${repository}/pull/${prDeployment.pull_request_id}`}
-              target="_blank"
-            >
-              <img src={github} /> View last workflow run
-              <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
+                <img src={github} /> View last workflow run
                 <i className="material-icons">open_in_new</i>
               </GHALink>
             ) : null}

+ 11 - 114
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx

@@ -26,6 +26,7 @@ const AvailableStatusFilters = ["all", "created", "failed", "not_deployed"];
 
 type AvailableStatusFiltersType = typeof AvailableStatusFilters[number];
 
+
 const HARD_CODED_DEPLOYMENTS: PRDeployment[] = [
   {
     id: 1,
@@ -71,9 +72,9 @@ const DeploymentList = () => {
   const [sortOrder, setSortOrder] = useState("Newest");
   const [isLoading, setIsLoading] = useState(true);
   const [hasError, setHasError] = useState(false);
-  const [deploymentList, setDeploymentList] = useState<PRDeployment[]>(
-    HARD_CODED_DEPLOYMENTS
-  );
+  const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([
+    ...HARD_CODED_DEPLOYMENTS
+  ]);
   const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
   const [searchValue, setSearchValue] = useState("");
   const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
@@ -87,7 +88,9 @@ const DeploymentList = () => {
     setStatusSelectorVal,
   ] = useState<AvailableStatusFiltersType>("all");
 
-  const { currentProject, currentCluster } = useContext(Context);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
   const { getQueryParam, pushQueryParams } = useRouting();
   const location = useLocation();
   const history = useHistory();
@@ -169,9 +172,7 @@ const DeploymentList = () => {
           }
 
           setPorterYAMLErrors(porterYAMLErrors);
-          setDeploymentList(
-            deploymentList.deployments || HARD_CODED_DEPLOYMENTS
-          );
+          setDeploymentList(deploymentList.deployments ?? HARD_CODED_DEPLOYMENTS);
           setPullRequests(deploymentList.pull_requests || []);
 
           setNewCommentsDisabled(
@@ -181,8 +182,9 @@ const DeploymentList = () => {
           setIsLoading(false);
         }
       )
-      .catch(() => {
-        setDeploymentList(HARD_CODED_DEPLOYMENTS);
+      .catch((err) => {
+        setDeploymentList([]);
+        setCurrentError(err);
       });
 
     return () => {
@@ -207,19 +209,6 @@ const DeploymentList = () => {
     setIsLoading(false);
   };
 
-  const handlePreviewEnvironmentManualCreation = (pullRequest: PullRequest) => {
-    setPullRequests((prev) => {
-      return prev.filter((pr) => {
-        return (
-          pr.pr_title === pullRequest.pr_title &&
-          `${pr.repo_owner}/${pr.repo_name}` ===
-            `${pullRequest.repo_owner}/${pullRequest.repo_name}`
-        );
-      });
-    });
-    handleRefresh();
-  };
-
   const searchFilter = (value: string | number) => {
     const val = String(value);
 
@@ -289,16 +278,6 @@ const DeploymentList = () => {
 
     return (
       <>
-        {/* Deprecated -> New Preview Env button */}
-        {/* {filteredPullRequests.map((pr) => {
-          return (
-            <PullRequestCard
-              key={pr.pr_title}
-              pullRequest={pr}
-              onCreation={handlePreviewEnvironmentManualCreation}
-            />
-          );
-        })} */}
         {filteredDeployments.map((d: any) => {
           return (
             <DeploymentCard
@@ -394,44 +373,6 @@ const DeploymentList = () => {
           </LinkButton>
         </Banner>
       ) : null}
-      {/* <Flex>
-        <ActionsWrapper>
-          <StyledStatusSelector>
-            <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}
-              options={[
-                {
-                  value: "active",
-                  label: "Active",
-                },
-                {
-                  value: "inactive",
-                  label: "Inactive",
-                },
-              ]}
-              dropdownLabel="Status"
-              width="150px"
-              dropdownWidth="230px"
-              closeOverlay={true}
-            />
-            <EnvironmentSettings environmentId={environment_id} />
-          </StyledStatusSelector>
-        </ActionsWrapper>
-      </Flex> */}
       <FlexRow>
         <Flex>
           <SearchRowWrapper>
@@ -584,41 +525,6 @@ const Flex = styled.div`
   align-items: center;
 `;
 
-const Div = styled.div`
-  margin-bottom: -7px;
-`;
-
-const FlexWrap = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const BackButton = styled(DynamicLink)`
-  cursor: pointer;
-  font-size: 24px;
-  color: #969fbbaa;
-  padding: 3px;
-  border-radius: 100px;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Icon = styled.img`
-  width: 25px;
-  height: 25px;
-  margin-right: 6px;
-`;
-
-const Title = styled.div`
-  font-size: 20px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  margin-left: 10px;
-  border-radius: 2px;
-  color: #ffffff;
-`;
-
 const RefreshButton = styled.button`
   display: flex;
   align-items: center;
@@ -652,15 +558,6 @@ const EventsGrid = styled.div`
   grid-template-columns: 1;
 `;
 
-const StyledStatusSelector = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-  :not(:first-child) {
-    margin-left: 15px;
-  }
-`;
-
 const SearchInput = styled.input`
   outline: none;
   border: none;

+ 0 - 33
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx

@@ -2,12 +2,10 @@ import React, { useState, useContext } from "react";
 import styled from "styled-components";
 import pr_icon from "assets/pull_request_icon.svg";
 import { PullRequest } from "../types";
-import { integrationList } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { ActionButton } from "../components/ActionButton";
 import Loading from "components/Loading";
-import DynamicLink from "components/DynamicLink";
 import RecreateWorkflowFilesModal from "../components/RecreateWorkflowFilesModal";
 import { EllipsisTextWrapper, RepoLink } from "../components/styled";
 
@@ -193,37 +191,6 @@ const StatusDot = styled.div`
   margin-left: 3px;
 `;
 
-const DeploymentImageContainer = styled.div`
-  height: 20px;
-  font-size: 13px;
-  position: relative;
-  display: flex;
-  margin-left: 15px;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff66;
-  padding-left: 5px;
-`;
-
-const Icon = styled.img`
-  width: 100%;
-`;
-
-const DeploymentTypeIcon = styled(Icon)`
-  width: 20px;
-  margin-right: 10px;
-`;
-
-const RepositoryName = styled.div`
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  max-width: 390px;
-  position: relative;
-  margin-right: 3px;
-`;
-
 const Tooltip = styled.div`
   position: absolute;
   left: 14px;

+ 59 - 206
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx

@@ -1,52 +1,24 @@
 import DynamicLink from "components/DynamicLink";
 import Loading from "components/Loading";
 import React, { useContext, useEffect, useState } from "react";
-import api from "shared/api";
 import styled from "styled-components";
-import { CellProps } from "react-table";
 import { Context } from "shared/Context";
 import { useParams } from "react-router";
-import { PRDeployment, PullRequest } from "../types";
+import { PullRequest } from "../types";
 import DashboardHeader from "../../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
 import Helper from "components/form-components/Helper";
-import Table from "components/Table";
 import pr_icon from "assets/pull_request_icon.svg";
+import api from "shared/api";
 import { EllipsisTextWrapper, RepoLink } from "../components/styled";
 import { useQuery, useQueryClient } from "@tanstack/react-query";
 import { getPRDeploymentList, validatePorterYAML } from "../utils";
 import Banner from "components/Banner";
 import Modal from "main/home/modals/Modal";
-
-const dummyData: any = [
-  {
-    pr_title: "pr_title1",
-    pr_number: 1,
-    repo_owner: "repo_owner",
-    repo_name: "repo_name",
-    branch_from: "test1",
-    branch_into: "test",
-  },
-  {
-    pr_title: "pr_title2",
-    pr_number: 2,
-    repo_owner: "repo_owner",
-    repo_name: "repo_name",
-    branch_from: "test2",
-    branch_into: "test",
-  },
-  {
-    pr_title: "pr_title3",
-    pr_number: 3,
-    repo_owner: "repo_owner",
-    repo_name: "repo_name",
-    branch_from: "test3",
-    branch_into: "test",
-  },
-];
+import { useRouting } from "shared/routing";
 
 const CreateEnvironment: React.FC = () => {
-  // TODO Soham: Replace any
+  const router = useRouting();
   const queryClient = useQueryClient();
   const [modalContent, setModalContent] = useState<React.ReactNode>();
   const { currentProject, currentCluster, setCurrentError } = useContext(
@@ -74,12 +46,9 @@ const CreateEnvironment: React.FC = () => {
       } catch (err) {
         setCurrentError(err);
       }
-
-      // TODO Soham: Replace with actual data
-      return dummyData; // [];
     }
   );
-  
+
   const [selectedPR, setSelectedPR] = useState<PullRequest>();
   const [loading, setLoading] = useState(false);
   const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
@@ -101,28 +70,20 @@ const CreateEnvironment: React.FC = () => {
     setLoading(false);
   };
 
-  const columns = React.useMemo(
-    () => [
-      {
-        Header: "Monitors",
-        columns: [
-          {
-            Header: "Open pull requests",
-            accessor: "name",
-            width: 140,
-            Cell: ({
-              row: { original: pullRequest },
-            }: CellProps<PullRequest>) => {
-              return (
-                <h1>asdf</h1>
-              );
-            },
-          },
-        ],
-      },
-    ],
-    [pullRequests]
-  );
+  const handleCreatePreviewDeployment = async () => {
+    try {
+      await api.createPreviewEnvironmentDeployment("<token>", selectedPR, {
+        cluster_id: currentCluster?.id,
+        project_id: currentProject?.id,
+      });
+
+      router.push(
+        `/preview-environments/deployments/${environment_id}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all`
+      );
+    } catch (err) {
+      setCurrentError(err);
+    }
+  };
 
   return (
     <>
@@ -151,42 +112,42 @@ const CreateEnvironment: React.FC = () => {
       </Helper>
       <Br height="10px" />
       <PullRequestList>
-        {
-          pullRequests.map((pullRequest: any, i: number) => {
-            return (
-              <PullRequestRow
-                onClick={() => {
-                  handlePRRowItemClick(pullRequest);
-                }}
-                isLast={i === dummyData.length - 1}
-                isSelected={pullRequest === selectedPR}
-              >
-                <PRName>
-                  <PRIcon src={pr_icon} alt="pull request icon" />
-                  <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
-                    {pullRequest.pr_title}
-                  </EllipsisTextWrapper>
-                </PRName>
-
-                <Flex>
-                  <DeploymentImageContainer>
-                    <InfoWrapper>
-                      <LastDeployed>#{pullRequest.pr_number} last updated xyz</LastDeployed>
-                    </InfoWrapper>
-                    <SepDot>•</SepDot>
-                    <MergeInfoWrapper>
-                      <MergeInfo>
-                        {pullRequest.branch_from}
-                        <i className="material-icons">arrow_forward</i>
-                        {pullRequest.branch_into}
-                      </MergeInfo>
-                    </MergeInfoWrapper>
-                  </DeploymentImageContainer>
-                </Flex>
-              </PullRequestRow>
-            )
-          })
-        }
+        {(pullRequests ?? []).map((pullRequest: PullRequest, i: number) => {
+          return (
+            <PullRequestRow
+              onClick={() => {
+                handlePRRowItemClick(pullRequest);
+              }}
+              isLast={i === pullRequests.length - 1}
+              isSelected={pullRequest === selectedPR}
+            >
+              <PRName>
+                <PRIcon src={pr_icon} alt="pull request icon" />
+                <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
+                  {pullRequest.pr_title}
+                </EllipsisTextWrapper>
+              </PRName>
+
+              <Flex>
+                <DeploymentImageContainer>
+                  <InfoWrapper>
+                    <LastDeployed>
+                      #{pullRequest.pr_number} last updated xyz
+                    </LastDeployed>
+                  </InfoWrapper>
+                  <SepDot>•</SepDot>
+                  <MergeInfoWrapper>
+                    <MergeInfo>
+                      {pullRequest.branch_from}
+                      <i className="material-icons">arrow_forward</i>
+                      {pullRequest.branch_into}
+                    </MergeInfo>
+                  </MergeInfoWrapper>
+                </DeploymentImageContainer>
+              </Flex>
+            </PullRequestRow>
+          );
+        })}
       </PullRequestList>
       {modalContent ? (
         <Modal onRequestClose={() => setModalContent(null)} height="auto">
@@ -220,6 +181,7 @@ const CreateEnvironment: React.FC = () => {
         </ValidationErrorBannerWrapper>
       ) : null}
       <SubmitButton
+        onClick={handleCreatePreviewDeployment}
         disabled={loading || !selectedPR || porterYAMLErrors.length > 0}
       >
         Create preview deployment
@@ -230,24 +192,18 @@ const CreateEnvironment: React.FC = () => {
 
 export default CreateEnvironment;
 
-const PRNumber = styled.div`
-  color: #aaaabb;
-  font-weight: 400;
-  margin-left: 7px;
-`;
-
 const PullRequestList = styled.div`
   border: 1px solid #494b4f;
   border-radius: 5px;
   overflow: hidden;
 `;
 
-const PullRequestRow = styled.div<{ isLast?: boolean, isSelected?: boolean }>`
+const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
   width: 100%;
   padding: 15px;
   cursor: pointer;
-  background: ${props => props.isSelected ? "#ffffff11" : "#26292e"};
-  border-bottom: ${props => props.isLast ? "" : "1px solid #494b4f"};
+  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
   :hover {
     background: #ffffff11;
   }
@@ -257,10 +213,6 @@ const Code = styled.span`
   font-family: monospace; ;
 `;
 
-const Spacer = styled.div`
-  width: 5px;
-`;
-
 const SepDot = styled.div`
   color: #aaaabb66;
 `;
@@ -391,48 +343,11 @@ const DarkMatter = styled.div`
   margin-top: -15px;
 `;
 
-const DeleteButton = styled.div`
-  height: 30px;
-  font-size: 13px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  color: white;
-  display: flex;
-  width: 210px;
-  align-items: center;
-  padding: 0 15px;
-  margin-top: 20px;
-  text-align: left;
-  border-radius: 5px;
-  cursor: pointer;
-  user-select: none;
-  :focus {
-    outline: 0;
-  }
-  :hover {
-    filter: brightness(120%);
-  }
-  background: #b91133;
-  border: none;
-  :hover {
-    filter: brightness(120%);
-  }
-`;
-
 const Br = styled.div<{ height: string }>`
   width: 100%;
   height: ${(props) => props.height || "2px"};
 `;
 
-const StyledPlaceholder = styled.div`
-  width: 100%;
-  padding: 30px;
-  font-size: 13px;
-  border-radius: 5px;
-  background: #26292e;
-  border: 1px solid #494b4f;
-`;
-
 const Slash = styled.div`
   margin: 0 4px;
   color: #aaaabb88;
@@ -477,68 +392,6 @@ const Breadcrumb = styled(DynamicLink)`
   }
 `;
 
-const Relative = styled.div`
-  position: relative;
-`;
-
-const EnvironmentsGrid = styled.div`
-  padding-bottom: 150px;
-  display: grid;
-  grid-row-gap: 15px;
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  margin-left: auto;
-  justify-content: space-between;
-  align-items: center;
-  margin: 35px 0 30px;
-  padding-left: 0px;
-`;
-
-const Button = styled(DynamicLink)`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
-  color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  margin-right: 10px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-
-  > 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 ValidationErrorBannerWrapper = styled.div`
   margin-block: 20px;
 `;

+ 132 - 88
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx

@@ -1,6 +1,6 @@
 import DynamicLink from "components/DynamicLink";
 import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import { useParams } from "react-router";
@@ -14,17 +14,15 @@ import SaveButton from "components/SaveButton";
 import _ from "lodash";
 import { Context } from "shared/Context";
 import PageNotFound from "components/PageNotFound";
+import Banner from "components/Banner";
+import InputRow from "components/form-components/InputRow";
+import Modal from "main/home/modals/Modal";
+import { useRouting } from "shared/routing";
 
-/**
- * 
- * TODO Soham:
- * 
- * - Handle errors when fetching environments
- * - Handle errors when the environment is not found
- * - Handle errors on saving and deleting the environment
- */
-const EnvironmentSettings: React.FC = () => {
-  const [error, setError] = useState("");
+const EnvironmentSettings = () => {
+  const router = useRouting();
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [deleteConfirmationPrompt, setDeleteConfirmationPrompt] = useState("");
   const { currentProject, currentCluster, setCurrentError } = useContext(
     Context
   );
@@ -60,7 +58,7 @@ const EnvironmentSettings: React.FC = () => {
       );
 
       setEnvironment(environment);
-      setNewCommentsDisabled(environment.disable_new_comments);
+      setNewCommentsDisabled(environment.new_comments_disabled);
       setDeploymentMode(environment.mode);
     };
 
@@ -95,8 +93,56 @@ const EnvironmentSettings: React.FC = () => {
     setSaveStatus("");
   };
 
+  const closeDeleteConfirmationModal = () => {
+    setShowDeleteModal(false);
+    setDeleteConfirmationPrompt("");
+  };
+
+  const canDelete = useMemo(() => {
+    return deleteConfirmationPrompt === `${repoOwner}/${repoName}`;
+  }, [deleteConfirmationPrompt]);
+
+  const handleDelete = async () => {
+    if (!canDelete) {
+      return;
+    }
+
+    try {
+      await api.deleteEnvironment(
+        "<token>",
+        {
+          name: environment?.name,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          git_installation_id: environment?.git_installation_id,
+          git_repo_owner: repoOwner,
+          git_repo_name: repoName,
+        }
+      );
+
+      closeDeleteConfirmationModal();
+      router.push(`/preview-environments`);
+    } catch (err) {
+      setCurrentError(JSON.stringify(err));
+      closeDeleteConfirmationModal();
+    }
+  };
+
   return (
     <>
+      {showDeleteModal ? (
+        <DeletePreviewEnvironmentModal
+          repoOwner={repoOwner}
+          repoName={repoName}
+          onClose={closeDeleteConfirmationModal}
+          prompt={deleteConfirmationPrompt}
+          setPrompt={setDeleteConfirmationPrompt}
+          onDelete={handleDelete}
+          disabled={!canDelete}
+        />
+      ) : null}
       <BreadcrumbRow>
         <Breadcrumb to={`/preview-environments/deployments/settings`}>
           <ArrowIcon src={PullRequestIcon} />
@@ -156,7 +202,12 @@ const EnvironmentSettings: React.FC = () => {
           Delete the Porter preview environment integration for this repo. All
           preview deployments will also be destroyed.
         </Helper>
-        <DeleteButton disabled={saveStatus === "loading"} onClick={_.noop}>
+        <DeleteButton
+          disabled={saveStatus === "loading"}
+          onClick={() => {
+            setShowDeleteModal(true);
+          }}
+        >
           Delete preview environment
         </DeleteButton>
       </StyledPlaceholder>
@@ -164,37 +215,92 @@ const EnvironmentSettings: React.FC = () => {
   );
 };
 
+interface DeletePreviewEnvironmentModalProps {
+  repoName: string;
+  repoOwner: string;
+  prompt: string;
+  setPrompt: (prompt: string) => void;
+  onDelete: () => void;
+  onClose: () => void;
+  disabled: boolean;
+}
+
+const DeletePreviewEnvironmentModal = (
+  props: DeletePreviewEnvironmentModalProps
+) => {
+  return (
+    <Modal
+      height="fit-content"
+      title={`Remove Preview Envs for ${props.repoOwner}/${props.repoName}`}
+      onRequestClose={props.onClose}
+    >
+      <DeletePreviewEnvironmentModalContentsWrapper>
+        <Banner type="warning">
+          All Preview Environment deployments associated with this repo will be
+          deleted.
+        </Banner>
+        <InputRow
+          type="text"
+          label={`Enter ${props.repoOwner}/${props.repoName} to delete Preview Environments:`}
+          value={props.prompt}
+          placeholder={`${props.repoOwner}/${props.repoName}`}
+          setValue={(x: string) => props.setPrompt(x)}
+          width={"500px"}
+        />
+        <Flex justifyContent="center" alignItems="center">
+          <DeleteButton
+            onClick={() => props.onDelete()}
+            disabled={props.disabled}
+          >
+            Delete
+          </DeleteButton>
+        </Flex>
+      </DeletePreviewEnvironmentModalContentsWrapper>
+    </Modal>
+  );
+};
+
 export default EnvironmentSettings;
 
+const DeletePreviewEnvironmentModalContentsWrapper = styled.div`
+  margin-block-start: 25px;
+`;
+
 const SavePreviewEnvironmentSettings = styled(SaveButton)`
   margin-top: 30px;
 `;
 
-const DeleteButton = styled.button`
-  height: 30px;
+const Flex = styled.div<{
+  justifyContent?: string;
+  alignItems?: string;
+}>`
+  display: flex;
+  align-items: ${({ alignItems }) => alignItems || "flex-start"};
+  justify-content: ${({ justifyContent }) => justifyContent || "flex-start"};
+`;
+
+const DeleteButton = styled.button<{ disabled?: boolean }>`
   font-size: 13px;
   font-weight: 500;
   font-family: "Work Sans", sans-serif;
   color: white;
   display: flex;
-  width: 210px;
   align-items: center;
-  padding: 0 15px;
+  padding: 10px 15px;
   margin-top: 20px;
   text-align: left;
   border-radius: 5px;
-  cursor: pointer;
   user-select: none;
-  :focus {
-    outline: 0;
-  }
-  :hover {
-    filter: brightness(120%);
-  }
   background: #b91133;
   border: none;
-  :hover {
-    filter: brightness(120%);
+  cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
+  filter: ${({ disabled }) => (disabled ? "brightness(0.8)" : "none")};
+
+  &:focus {
+    outline: 0;
+  }
+  &:hover {
+    filter: ${({ disabled }) => (disabled ? "brightness(0.8)" : "none")};
   }
 `;
 
@@ -255,65 +361,3 @@ const Breadcrumb = styled(DynamicLink)`
     background: #ffffff11;
   }
 `;
-
-const Relative = styled.div`
-  position: relative;
-`;
-
-const EnvironmentsGrid = styled.div`
-  padding-bottom: 150px;
-  display: grid;
-  grid-row-gap: 15px;
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  margin-left: auto;
-  justify-content: space-between;
-  align-items: center;
-  margin: 35px 0 30px;
-  padding-left: 0px;
-`;
-
-const Button = styled(DynamicLink)`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
-  color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  margin-right: 10px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-
-  > 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;
-  }
-`;

+ 1 - 34
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx

@@ -10,45 +10,12 @@ import { Environment } from "../types";
 import EnvironmentCard from "./EnvironmentCard";
 import Placeholder from "components/Placeholder";
 
-const HARD_CODED_ENVS: Environment[] = [
-  {
-    id: 12,
-    project_id: 1234,
-    cluster_id: 4321,
-    git_installation_id: 55,
-    name: "asdf",
-    git_repo_owner: "owned",
-    git_repo_name: "this-is-a-repo",
-    last_deployment_status: "failed",
-    deployment_count: 12,
-    mode: "manual",
-    git_repo_branches: [],
-    disable_new_comments: true,
-  },
-  {
-    id: 13,
-    project_id: 1234,
-    cluster_id: 4321,
-    git_installation_id: 55,
-    name: "asdf",
-    git_repo_owner: "owned",
-    git_repo_name: "this-is-a-repo",
-    last_deployment_status: "failed",
-    deployment_count: 12,
-    mode: "manual",
-    git_repo_branches: [],
-    disable_new_comments: true,
-  },
-];
-
 const EnvironmentsList = () => {
   const { currentCluster, currentProject } = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
   const [buttonIsReady, setButtonIsReady] = useState(false);
 
-  const [environments, setEnvironments] = useState<Environment[]>(
-    HARD_CODED_ENVS
-  );
+  const [environments, setEnvironments] = useState<Environment[]>([]);
 
   const removeEnvironmentFromList = (deletedEnv: Environment) => {
     setEnvironments((prev) => {

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

@@ -40,7 +40,7 @@ export type Environment = {
   git_repo_owner: string;
   git_repo_name: string;
   git_repo_branches: string[];
-  disable_new_comments: boolean;
+  new_comments_disabled: boolean;
   last_deployment_status: DeploymentStatusUnion;
   deployment_count: number;
   mode: EnvironmentDeploymentMode;

+ 3 - 0
dashboard/src/shared/routing.tsx

@@ -91,6 +91,9 @@ export const useRouting = () => {
   const history = useHistory();
 
   return {
+    push(path: string, state?: any) {
+      history.push(path, state);
+    },
     pushQueryParams: (
       params: { [key: string]: unknown },
       removedParams?: string[]