Quellcode durchsuchen

deletions part 3 (#4676)

Feroze Mohideen vor 2 Jahren
Ursprung
Commit
d8a6e4812f
49 geänderte Dateien mit 0 neuen und 12376 gelöschten Zeilen
  1. 0 106
      dashboard/src/legacy/components/porter/ConfirmOverlay.tsx
  2. 0 117
      dashboard/src/legacy/main/auth/InfoPanel.tsx
  3. 0 239
      dashboard/src/legacy/main/auth/Login.tsx
  4. 0 157
      dashboard/src/legacy/main/auth/LoginWrapper.tsx
  5. 0 600
      dashboard/src/legacy/main/auth/Register.tsx
  6. 0 426
      dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx
  7. 0 373
      dashboard/src/legacy/main/auth/ResetPasswordInit.tsx
  8. 0 300
      dashboard/src/legacy/main/auth/SetInfo.tsx
  9. 0 241
      dashboard/src/legacy/main/auth/VerifyEmail.tsx
  10. 0 185
      dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx
  11. 0 195
      dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx
  12. 0 170
      dashboard/src/legacy/main/home/modals/UpgradeChartModal.tsx
  13. 0 42
      dashboard/src/legacy/main/home/onboarding/constants.ts
  14. 0 77
      dashboard/src/legacy/main/home/project-settings/Bars.tsx
  15. 0 35
      dashboard/src/legacy/main/home/project-settings/ReferralsPage.tsx
  16. 0 30
      dashboard/src/legacy/shared/error_handling/MainWrapperErrorBoundary.tsx
  17. 0 18
      dashboard/src/legacy/shared/error_handling/sentry/setup.ts
  18. 0 37
      dashboard/src/legacy/shared/error_handling/window_error_handling.ts
  19. 0 34
      dashboard/src/legacy/shared/hooks/useEffectDebugger.ts
  20. 0 24
      dashboard/src/legacy/shared/icons/MoonBase.tsx
  21. 0 185
      dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx
  22. 0 57
      dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx
  23. 0 168
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  24. 0 83
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  25. 0 70
      dashboard/src/main/home/cluster-dashboard/TagFilter.tsx
  26. 0 198
      dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx
  27. 0 403
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  28. 0 506
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  29. 0 541
      dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx
  30. 0 473
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx
  31. 0 151
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettingsModal.tsx
  32. 0 313
      dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx
  33. 0 618
      dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx
  34. 0 316
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  35. 0 198
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  36. 0 171
      dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx
  37. 0 26
      dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx
  38. 0 463
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  39. 0 217
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx
  40. 0 275
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  41. 0 158
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx
  42. 0 1303
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  43. 0 211
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx
  44. 0 157
      dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx
  45. 0 104
      dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx
  46. 0 1331
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  47. 0 160
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx
  48. 0 78
      dashboard/src/main/home/project-settings/Bars.tsx
  49. 0 36
      dashboard/src/main/home/project-settings/ReferralsPage.tsx

+ 0 - 106
dashboard/src/legacy/components/porter/ConfirmOverlay.tsx

@@ -1,106 +0,0 @@
-import React from "react";
-import Loading from "legacy/components/Loading";
-import { createPortal } from "react-dom";
-import styled from "styled-components";
-
-type Props = {
-  message: string;
-  onYes: React.MouseEventHandler;
-  onNo: React.MouseEventHandler;
-  loading?: boolean;
-};
-
-const ConfirmOverlay: React.FC<Props> = ({ message, onYes, onNo, loading }) => {
-  return (
-    <>
-      {createPortal(
-        <StyledConfirmOverlay>
-          {loading ? (
-            <Loading />
-          ) : (
-            <>
-              {message}
-              <ButtonRow>
-                <ConfirmButton onClick={onYes}>Yes</ConfirmButton>
-                <ConfirmButton onClick={onNo}>No</ConfirmButton>
-              </ButtonRow>
-            </>
-          )}
-        </StyledConfirmOverlay>,
-        document.body
-      )}
-    </>
-  );
-};
-
-export default ConfirmOverlay;
-
-const StyledConfirmOverlay = styled.div`
-  position: absolute;
-  top: 0px;
-  opacity: 100%;
-  left: 0px;
-  width: 100%;
-  height: 100%;
-  z-index: 999;
-  display: flex;
-  padding-bottom: 30px;
-  align-items: center;
-  justify-content: center;
-  font-family: "Work Sans", sans-serif;
-  font-size: 18px;
-  color: white;
-  flex-direction: column;
-  background: rgb(0, 0, 0, 0.55);
-  backdrop-filter: blur(5px);
-  animation: lindEnter 0.2s;
-  animation-fill-mode: forwards;
-
-  @keyframes lindEnter {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const ButtonRow = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  width: 140px;
-  margin-top: 30px;
-`;
-
-const ConfirmButton = styled.div`
-  outline: none;
-  height: 40px;
-  border: 1px solid white;
-  border-radius: 5px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 60px;
-  cursor: pointer;
-  opacity: 0;
-  font-family: "Work Sans", sans-serif;
-  font-size: 15px;
-  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;
-  }
-`;

+ 0 - 117
dashboard/src/legacy/main/auth/InfoPanel.tsx

@@ -1,117 +0,0 @@
-import React, { useState } from "react";
-import gift from "legacy/assets/gift.svg";
-import logo from "legacy/assets/logo.png";
-import Container from "legacy/components/porter/Container";
-import Image from "legacy/components/porter/Image";
-import Spacer from "legacy/components/porter/Spacer";
-import styled from "styled-components";
-
-const InfoPanel: React.FC = () => {
-  if (window.location.hostname === "cloud.porter.run") {
-    return (
-      <Wrapper>
-        <Container row>
-          <a href="https://porter.run">
-            <Logo src={logo} />
-          </a>
-          <Badge>Cloud</Badge>
-        </Container>
-        <Spacer y={2} />
-        <Jumbotron>
-          Deploy and scale <Shiny>effortlessly</Shiny> with Porter
-        </Jumbotron>
-        <Spacer y={2} />
-        <CheckRow>
-          <Image src={gift} />
-          <Spacer inline width="10px" /> $5 in free credits on sign-up
-        </CheckRow>
-        <Spacer y={0.5} />
-        <CheckRow>
-          <i className="material-icons">done</i> Instantly deploy from any Git
-          repo
-        </CheckRow>
-        <Spacer y={0.5} />
-        <CheckRow>
-          <i className="material-icons">done</i> Eject at any time to your own
-          AWS/Azure/GCP account
-        </CheckRow>
-      </Wrapper>
-    );
-  }
-  return (
-    <Wrapper>
-      <a href="https://porter.run">
-        <Logo src={logo} />
-      </a>
-      <Spacer y={2} />
-      <Jumbotron>
-        Deploy and scale <Shiny>effortlessly</Shiny> with Porter
-      </Jumbotron>
-      <Spacer y={2} />
-      <CheckRow>
-        <i className="material-icons">done</i> 14 day free trial
-      </CheckRow>
-      <Spacer y={0.5} />
-      <CheckRow>
-        <i className="material-icons">done</i> Generous startup program for
-        seed-stage companies
-      </CheckRow>
-      <Spacer y={0.5} />
-      <CheckRow>
-        <i className="material-icons">done</i> Bring your own cloud and use your
-        credits
-      </CheckRow>
-    </Wrapper>
-  );
-};
-
-export default InfoPanel;
-
-const Badge = styled.div`
-  margin-left: 17px;
-  margin-top: -6px;
-  background: ${(props) => props.theme.clickable};
-  padding: 5px 10px;
-  border: 1px solid #aaaabb;
-  border-radius: 5px;
-`;
-
-const CheckRow = styled.div`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb;
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: #4797ff;
-  }
-`;
-
-const Shiny = styled.span`
-  background-image: linear-gradient(225deg, #fff, #7980ff);
-  -webkit-background-clip: text;
-  background-clip: text;
-  -webkit-text-fill-color: transparent;
-`;
-
-const Jumbotron = styled.div`
-  font-size: 32px;
-  font-weight: 500;
-  line-height: 1.5;
-`;
-
-const Logo = styled.img`
-  height: 24px;
-  user-select: none;
-`;
-
-const Wrapper = styled.div`
-  width: 500px;
-  margin-top: -20px;
-  position: relative;
-  padding: 25px;
-  border-radius: 5px;
-  font-size: 13px;
-`;

+ 0 - 239
dashboard/src/legacy/main/auth/Login.tsx

@@ -1,239 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import github from "legacy/assets/github-icon.png";
-import GoogleIcon from "legacy/assets/GoogleIcon";
-import Heading from "legacy/components/form-components/Heading";
-import Button from "legacy/components/porter/Button";
-import Container from "legacy/components/porter/Container";
-import Input from "legacy/components/porter/Input";
-import Link from "legacy/components/porter/Link";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import api from "legacy/shared/api";
-import { emailRegex } from "legacy/shared/regex";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-type Props = {
-  authenticate: () => Promise<void>;
-};
-
-const Login: React.FC<Props> = ({ authenticate }) => {
-  const { setUser, setCurrentError } = useContext(Context);
-  const [email, setEmail] = useState("");
-  const [password, setPassword] = useState("");
-  const [emailError, setEmailError] = useState(false);
-  const [credentialError, setCredentialError] = useState(false);
-  const [hasBasic, setHasBasic] = useState(true);
-  const [hasGithub, setHasGithub] = useState(true);
-  const [hasGoogle, setHasGoogle] = useState(false);
-  const [hasResetPassword, setHasResetPassword] = useState(true);
-
-  const handleLogin = (): void => {
-    if (!emailRegex.test(email)) {
-      setEmailError(true);
-    } else if (password === "") {
-      setCredentialError(true);
-    } else {
-      api
-        .logInUser("", { email, password }, {})
-        .then((res) => {
-          if (res?.data?.redirect) {
-            window.location.href = res.data.redirect;
-          } else {
-            setUser(res?.data?.id, res?.data?.email);
-            authenticate().catch(() => {});
-          }
-        })
-        .catch((err) => {
-          setCurrentError(err.response.data.error);
-        });
-    }
-  };
-
-  const handleKeyDown = (e: any) => {
-    if (e.key === "Enter") {
-      handleLogin();
-    }
-  };
-
-  // Manually re-register event listener on email/password change
-  useEffect(() => {
-    document.removeEventListener("keydown", handleKeyDown);
-    document.addEventListener("keydown", handleKeyDown);
-    return () => {
-      document.removeEventListener("keydown", handleKeyDown);
-    };
-  }, [email, password]);
-
-  useEffect(() => {
-    // Get capabilities to case on login methods
-    api
-      .getMetadata("", {}, {})
-      .then((res) => {
-        setHasBasic(res.data?.basic_login);
-        setHasGithub(res.data?.github_login);
-        setHasGoogle(res.data?.google_login);
-        setHasResetPassword(res.data?.email);
-      })
-      .catch((err) => {
-        console.log(err);
-      });
-
-    const urlParams = new URLSearchParams(window.location.search);
-    const emailFromCLI = urlParams.get("email");
-    emailFromCLI && setEmail(emailFromCLI);
-  }, []);
-
-  const githubRedirect = () => {
-    const redirectUrl = `/api/oauth/login/github`;
-    window.location.href = redirectUrl;
-  };
-
-  const googleRedirect = () => {
-    const redirectUrl = `/api/oauth/login/google`;
-    window.location.href = redirectUrl;
-  };
-
-  return (
-    <Container>
-      <Heading isAtTop>Log in to your Porter account</Heading>
-      <Spacer y={1} />
-      {(hasGithub || hasGoogle) && (
-        <>
-          <Container row>
-            {hasGithub && (
-              <OAuthButton onClick={githubRedirect}>
-                <Icon src={github} />
-                Log in with GitHub
-              </OAuthButton>
-            )}
-            {hasGithub && hasGoogle && <Spacer inline x={2} />}
-            {hasGoogle && (
-              <OAuthButton onClick={googleRedirect}>
-                <StyledGoogleIcon />
-                Log in with Google
-              </OAuthButton>
-            )}
-          </Container>
-          {hasBasic && (
-            <OrWrapper>
-              <Line />
-              <Or>or</Or>
-            </OrWrapper>
-          )}
-        </>
-      )}
-      {hasBasic && (
-        <>
-          <Input
-            autoFocus={true}
-            type="email"
-            placeholder="Email"
-            label="Email"
-            value={email}
-            setValue={(x) => {
-              setEmail(x);
-              setEmailError(false);
-              setCredentialError(false);
-            }}
-            width="100%"
-            height="40px"
-            error={emailError && "Please enter a valid email"}
-          />
-          <Spacer y={1} />
-          <Input
-            type="password"
-            placeholder="Password"
-            label="Password"
-            value={password}
-            setValue={(x) => {
-              setPassword(x);
-              setCredentialError(false);
-            }}
-            width="100%"
-            height="40px"
-            error={credentialError && ""}
-          >
-            {hasResetPassword && (
-              <ForgotPassword>
-                <Link to="/password/reset">Forgot your password?</Link>
-              </ForgotPassword>
-            )}
-          </Input>
-          <Spacer height="30px" />
-          <Button onClick={handleLogin} width="100%" height="40px">
-            Continue
-          </Button>
-        </>
-      )}
-      <Spacer y={1} />
-      <Text size={13} color="helper">
-        Don't have an account?
-        <Spacer width="5px" inline />
-        <Link to="/register">Sign up</Link>
-      </Text>
-    </Container>
-  );
-};
-
-export default Login;
-
-const ForgotPassword = styled.div`
-  position: absolute;
-  right: 0;
-  top: 0;
-  font-size: 13px;
-`;
-
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
-`;
-
-const Line = styled.div`
-  height: 2px;
-  width: 100%;
-  background: #ffffff22;
-  margin: 35px 0px 30px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 50px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 25px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin: 14px;
-`;
-
-const OAuthButton = styled.div`
-  width: 100%;
-  height: 40px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 5px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;

+ 0 - 157
dashboard/src/legacy/main/auth/LoginWrapper.tsx

@@ -1,157 +0,0 @@
-import React, { useEffect, useState } from "react";
-import blog from "legacy/assets/blog.png";
-import docs from "legacy/assets/docs.png";
-import logo from "legacy/assets/logo.png";
-import DynamicLink from "legacy/components/DynamicLink";
-import Container from "legacy/components/porter/Container";
-import Spacer from "legacy/components/porter/Spacer";
-import styled from "styled-components";
-
-import Login from "./Login";
-
-type Props = {
-  authenticate: () => Promise<void>;
-};
-
-const getWindowDimensions = () => {
-  const { innerWidth: width, innerHeight: height } = window;
-  return { width, height };
-};
-
-const LoginWrapper: React.FC<Props> = ({ authenticate }) => {
-  const [windowDimensions, setWindowDimensions] = useState(
-    getWindowDimensions()
-  );
-
-  const handleResize = () => {
-    setWindowDimensions(getWindowDimensions());
-  };
-
-  useEffect(() => {
-    window.addEventListener("resize", handleResize);
-    return () => {
-      window.removeEventListener("resize", handleResize);
-    };
-  }, []);
-
-  return (
-    <StyledLogin>
-      {windowDimensions.width > windowDimensions.height && (
-        <Wrapper>
-          <Container row>
-            <a href="https://porter.run">
-              <Logo src={logo} />
-            </a>
-            {window.location.hostname === "cloud.porter.run" && (
-              <Badge>Cloud</Badge>
-            )}
-          </Container>
-          <Spacer y={2} />
-          <Jumbotron>
-            <Shiny>Welcome back to Porter</Shiny>
-          </Jumbotron>
-          <Spacer y={1} />
-          <LinkRow to="https://porter.run/docs" target="_blank">
-            <img src={docs} /> Read the Porter docs
-          </LinkRow>
-          <Spacer y={0.5} />
-          <LinkRow to="https://porter.run/blog" target="_blank">
-            <img src={blog} /> See what's new with Porter
-          </LinkRow>
-        </Wrapper>
-      )}
-      <Wrapper>
-        {windowDimensions.width <= windowDimensions.height && (
-          <Flex>
-            <a href="https://porter.run">
-              <Logo src={logo} />
-            </a>
-            <Spacer y={2} />
-          </Flex>
-        )}
-        <Spacer y={2} />
-        <Login authenticate={authenticate} />
-      </Wrapper>
-    </StyledLogin>
-  );
-};
-
-export default LoginWrapper;
-
-const Badge = styled.div`
-  margin-left: 17px;
-  margin-top: -6px;
-  background: ${(props) => props.theme.clickable};
-  padding: 5px 10px;
-  border: 1px solid #aaaabb;
-  border-radius: 5px;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
-`;
-
-const LinkRow = styled(DynamicLink)`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  width: 220px;
-  color: #aaaabb;
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: #4797ff;
-  }
-
-  > img {
-    height: 18px;
-    margin-right: 10px;
-  }
-
-  :hover {
-    filter: brightness(2);
-  }
-`;
-
-const Shiny = styled.span`
-  background-image: linear-gradient(225deg, #fff, #7980ff);
-  -webkit-background-clip: text;
-  background-clip: text;
-  -webkit-text-fill-color: transparent;
-`;
-
-const Jumbotron = styled.div`
-  font-size: 32px;
-  font-weight: 500;
-  line-height: 1.5;
-`;
-
-const Logo = styled.img`
-  height: 24px;
-  user-select: none;
-`;
-
-const Wrapper = styled.div`
-  width: 500px;
-  margin-top: -20px;
-  position: relative;
-  padding: 25px;
-  border-radius: 5px;
-  font-size: 13px;
-`;
-
-const StyledLogin = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 600
dashboard/src/legacy/main/auth/Register.tsx

@@ -1,600 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import github from "legacy/assets/github-icon.png";
-import GoogleIcon from "legacy/assets/GoogleIcon";
-import logo from "legacy/assets/logo.png";
-import Heading from "legacy/components/form-components/Heading";
-import Button from "legacy/components/porter/Button";
-import Container from "legacy/components/porter/Container";
-import Input from "legacy/components/porter/Input";
-import Link from "legacy/components/porter/Link";
-import Select from "legacy/components/porter/Select";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import api from "legacy/shared/api";
-import { emailRegex } from "legacy/shared/regex";
-import { useLocation } from "react-router-dom";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-import InfoPanel from "./InfoPanel";
-
-type Props = {
-  authenticate: () => Promise<void>;
-};
-
-const getWindowDimensions = () => {
-  const { innerWidth: width, innerHeight: height } = window;
-  return { width, height };
-};
-
-const Register: React.FC<Props> = ({ authenticate }) => {
-  const { setUser, setCurrentError } = useContext(Context);
-  const [firstName, setFirstName] = useState("");
-  const [firstNameError, setFirstNameError] = useState(false);
-  const [lastName, setLastName] = useState("");
-  const [lastNameError, setLastNameError] = useState(false);
-  const [companyName, setCompanyName] = useState("");
-  const [referralCode, setReferralCode] = useState("");
-  const [referralCodeError, setReferralCodeError] = useState(false);
-
-  const [companyNameError, setCompanyNameError] = useState(false);
-  const [email, setEmail] = useState("");
-  const [emailError, setEmailError] = useState(false);
-  const [disabled, setDisabled] = useState(false);
-  const [password, setPassword] = useState("");
-  const [passwordError, setPasswordError] = useState(false);
-  const [hasBasic, setHasBasic] = useState(true);
-  const [hasGithub, setHasGithub] = useState(true);
-  const [hasGoogle, setHasGoogle] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(
-    getWindowDimensions()
-  );
-  const [buttonDisabled, setButtonDisabled] = useState(false);
-
-  const [chosenReferralOption, setChosenReferralOption] =
-    useState<string>("(None provided)");
-  const [referralOtherText, setReferralOtherText] = useState<string>("");
-
-  const referralOptions = [
-    { value: "(None provided)", label: "Please select an option:" },
-    { value: "Email", label: "Email" },
-    {
-      value: "Word of mouth",
-      label: "Word of mouth (friend, colleague, etc.)",
-    },
-    { value: "YC", label: "YC" },
-    { value: "YC Startup School", label: "YC Startup School" },
-    { value: "Facebook", label: "Facebook" },
-    { value: "Instagram", label: "Instagram" },
-    { value: "Twitter", label: "Twitter" },
-    { value: "Search engine", label: "Search engine (Google, Bing, etc.)" },
-    { value: "LinkedIn", label: "LinkedIn" },
-    { value: "Porter blog", label: "Porter blog" },
-    { value: "Other", label: "Other" },
-  ];
-
-  const { search } = useLocation();
-  const searchParams = new URLSearchParams(search);
-  const referralCodeFromUrl = searchParams.get("referral");
-
-  useEffect(() => {
-    if (referralCodeFromUrl) {
-      setReferralCode(referralCodeFromUrl);
-    }
-  }, [referralCodeFromUrl]); // Only re-run the effect if referralCodeFromUrl changes
-
-  const handleRegister = (): void => {
-    const isHosted = window.location.hostname === "cloud.porter.run";
-    if (!emailRegex.test(email)) {
-      setEmailError(true);
-    }
-
-    if (firstName === "" && !isHosted) {
-      setFirstNameError(true);
-    }
-
-    if (lastName === "" && !isHosted) {
-      setLastNameError(true);
-    }
-
-    if (password === "") {
-      setPasswordError(true);
-    }
-
-    if (companyName === "" && !isHosted) {
-      setCompanyNameError(true);
-    }
-
-    // Check for valid input
-    if (
-      !isHosted &&
-      emailRegex.test(email) &&
-      firstName !== "" &&
-      lastName !== "" &&
-      password !== "" &&
-      companyName !== ""
-    ) {
-      setButtonDisabled(true);
-
-      // Attempt user registration
-      api
-        .registerUser(
-          "",
-          {
-            email,
-            password,
-            first_name: firstName,
-            last_name: lastName,
-            company_name: companyName,
-            referral_method:
-              chosenReferralOption === "Other"
-                ? `Other: ${referralOtherText}`
-                : chosenReferralOption,
-            referred_by_code: referralCode,
-          },
-          {}
-        )
-        .then((res: any) => {
-          if (res?.data?.redirect) {
-            window.location.href = res.data.redirect;
-          } else {
-            setUser(res?.data?.id, res?.data?.email);
-            authenticate().catch(() => {});
-
-            try {
-              window.dataLayer?.push({
-                event: "sign-up",
-                data: {
-                  method: "email",
-                  email: res?.data?.email,
-                },
-              });
-            } catch (err) {
-              console.log(err);
-            }
-          }
-
-          // Temp
-          location.reload();
-          setButtonDisabled(false);
-        })
-        .catch((err) => {
-          console.log("registration:", err);
-          if (err.response?.data?.error) {
-            setCurrentError(err.response.data.error);
-          } else {
-            location.reload();
-          }
-          setButtonDisabled(false);
-        });
-    } else if (isHosted && emailRegex.test(email) && password !== "") {
-      setButtonDisabled(true);
-
-      // Attempt user registration
-      api
-        .registerUser(
-          "",
-          {
-            email,
-            password,
-            first_name: email,
-            last_name: email,
-            company_name: email,
-            referral_method:
-              chosenReferralOption === "Other"
-                ? `Other: ${referralOtherText}`
-                : chosenReferralOption,
-            referred_by_code: referralCode,
-          },
-          {}
-        )
-        .then((res: any) => {
-          if (res?.data?.redirect) {
-            window.location.href = res.data.redirect;
-          } else {
-            setUser(res?.data?.id);
-            authenticate();
-
-            try {
-              window.dataLayer?.push({
-                event: "sign-up",
-                data: {
-                  method: "email",
-                  email: res?.data?.email,
-                },
-              });
-            } catch (err) {
-              console.log(err);
-            }
-          }
-
-          // Temp
-          location.reload();
-          setButtonDisabled(false);
-        })
-        .catch((err) => {
-          console.log("registration:", err);
-          if (err.response?.data?.error) {
-            setCurrentError(err.response.data.error);
-          } else {
-            location.reload();
-          }
-          setButtonDisabled(false);
-        });
-    }
-  };
-
-  const handleResize = () => {
-    setWindowDimensions(getWindowDimensions());
-  };
-
-  const handleKeyDown = (e: any) => {
-    if (e.key === "Enter") {
-      handleRegister();
-    }
-  };
-
-  // Manually re-register event listener on email/password change
-  useEffect(() => {
-    document.removeEventListener("keydown", handleKeyDown);
-    document.addEventListener("keydown", handleKeyDown);
-    return () => {
-      document.removeEventListener("keydown", handleKeyDown);
-    };
-  }, [email, password, firstName, lastName]);
-
-  useEffect(() => {
-    const qs = window.location.search;
-    const urlParams = new URLSearchParams(qs);
-    const email = urlParams.get("email");
-
-    if (email) {
-      setEmail(email);
-      setDisabled(true);
-    }
-  }, []);
-
-  useEffect(() => {
-    // Get capabilities to case on login methods
-    api
-      .getMetadata("", {}, {})
-      .then((res) => {
-        setHasBasic(res.data?.basic_login);
-        setHasGithub(res.data?.github_login);
-        setHasGoogle(res.data?.google_login);
-      })
-      .catch((err) => {
-        console.log(err);
-      });
-
-    window.addEventListener("resize", handleResize);
-    return () => {
-      window.removeEventListener("resize", handleResize);
-    };
-  }, []);
-
-  const githubRedirect = () => {
-    const redirectUrl = `/api/oauth/login/github`;
-    window.location.href = redirectUrl;
-  };
-
-  const googleRedirect = () => {
-    const redirectUrl = `/api/oauth/login/google`;
-    window.location.href = redirectUrl;
-  };
-
-  return (
-    <StyledRegister>
-      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
-      <Wrapper>
-        {windowDimensions.width <= windowDimensions.height && (
-          <Flex>
-            <a href="https://porter.run">
-              <Logo src={logo} />
-            </a>
-            <Spacer y={2} />
-          </Flex>
-        )}
-        <Heading isAtTop>Create your Porter account</Heading>
-        <Spacer y={1} />
-        {(hasGithub || hasGoogle) && !disabled && (
-          <>
-            <Container row>
-              {hasGithub && (
-                <OAuthButton onClick={githubRedirect}>
-                  <Icon src={github} />
-                  Sign up with GitHub
-                </OAuthButton>
-              )}
-              {hasGithub && hasGoogle && <Spacer inline x={2} />}
-              {hasGoogle && (
-                <OAuthButton onClick={googleRedirect}>
-                  <StyledGoogleIcon />
-                  Sign up with Google
-                </OAuthButton>
-              )}
-            </Container>
-            {hasBasic && (
-              <OrWrapper>
-                <Line />
-                <Or>or</Or>
-              </OrWrapper>
-            )}
-          </>
-        )}
-        {hasBasic && (
-          <>
-            {window.location.hostname !== "cloud.porter.run" && (
-              <>
-                <Container row>
-                  <RowWrapper>
-                    <Input
-                      placeholder="First name"
-                      label="First name"
-                      value={firstName}
-                      setValue={(x) => {
-                        setFirstName(x);
-                        setFirstNameError(false);
-                      }}
-                      width="100%"
-                      height="40px"
-                      error={firstNameError && "First name cannot be blank"}
-                    />
-                    {!firstNameError && lastNameError && (
-                      <Spacer height="27px" />
-                    )}
-                  </RowWrapper>
-                  <Spacer inline x={2} />
-                  <RowWrapper>
-                    <Input
-                      placeholder="Last name"
-                      label="Last name"
-                      value={lastName}
-                      setValue={(x) => {
-                        setLastName(x);
-                        setLastNameError(false);
-                      }}
-                      width="100%"
-                      height="40px"
-                      error={lastNameError && "Last name cannot be blank"}
-                    />
-                    {!lastNameError && firstNameError && (
-                      <Spacer height="27px" />
-                    )}
-                  </RowWrapper>
-                </Container>
-                <Spacer y={1} />
-                <Input
-                  placeholder="Company name"
-                  label="Company name"
-                  value={companyName}
-                  setValue={(x) => {
-                    setCompanyName(x);
-                    setCompanyNameError(false);
-                  }}
-                  width="100%"
-                  height="40px"
-                  error={companyNameError && ""}
-                />
-                <Spacer y={1} />
-              </>
-            )}
-            <Input
-              type="email"
-              placeholder="Email"
-              label="Email"
-              value={email}
-              setValue={(x) => {
-                setEmail(x);
-                setEmailError(false);
-              }}
-              width="100%"
-              height="40px"
-              error={emailError && "Please enter a valid email"}
-              disabled={disabled}
-            />
-            <Spacer y={1} />
-            <Input
-              placeholder="Password"
-              label="Password"
-              value={password}
-              setValue={setPassword}
-              width="100%"
-              height="40px"
-              type="password"
-              error={passwordError && ""}
-            />
-            <Spacer y={1} />
-            <Text color="helper">(Optional) How did you hear about us?</Text>
-            <Spacer y={0.5} />
-            <Select
-              width="100%"
-              height="40px"
-              options={referralOptions}
-              setValue={setChosenReferralOption}
-              value={chosenReferralOption}
-            />
-            <Spacer y={0.5} />
-            <Input
-              placeholder="Code"
-              label="Referral code"
-              value={referralCode}
-              setValue={(x) => {
-                setReferralCode(x);
-                setReferralCodeError(false);
-              }}
-              width="100%"
-              height="40px"
-              error={referralCodeError && ""}
-            />
-            <Spacer y={0.5} />
-
-            {chosenReferralOption === "Other" && (
-              <>
-                <Spacer y={0.5} />
-                <FeedbackInput
-                  autoFocus={true}
-                  value={referralOtherText}
-                  onChange={(e) => {
-                    setReferralOtherText(e.target.value);
-                  }}
-                  placeholder="Tell us more..."
-                />
-              </>
-            )}
-            <Spacer y={1} />
-            <Button
-              disabled={buttonDisabled}
-              onClick={handleRegister}
-              width="100%"
-              height="40px"
-            >
-              Continue
-            </Button>
-          </>
-        )}
-        {!disabled && (
-          <>
-            <Spacer y={1} />
-            <Text size={13} color="helper">
-              Already have an account?
-              <Spacer width="5px" inline />
-              <Link to="/login">Log in</Link>
-            </Text>
-          </>
-        )}
-      </Wrapper>
-    </StyledRegister>
-  );
-};
-
-export default Register;
-
-const RowWrapper = styled.div`
-  width: 100%;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
-`;
-
-const CheckRow = styled.div`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb;
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: #4797ff;
-  }
-`;
-
-const Shiny = styled.span`
-  background-image: linear-gradient(225deg, #fff, #7980ff);
-  -webkit-background-clip: text;
-  background-clip: text;
-  -webkit-text-fill-color: transparent;
-`;
-
-const Jumbotron = styled.div`
-  font-size: 32px;
-  font-weight: 500;
-  line-height: 1.5;
-`;
-
-const Logo = styled.img`
-  height: 24px;
-  user-select: none;
-`;
-
-const FeedbackInput = styled.textarea`
-  resize: none;
-  width: 100%;
-  height: 80px;
-  outline: 0;
-  padding: 14px;
-  color: white;
-  border: 0;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  background: #aaaabb11;
-`;
-
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
-`;
-
-const Line = styled.div`
-  height: 2px;
-  width: 100%;
-  background: #ffffff22;
-  margin: 35px 0px 30px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 50px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 25px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin: 14px;
-`;
-
-const OAuthButton = styled.div`
-  width: 100%;
-  height: 40px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 5px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;
-
-const Wrapper = styled.div`
-  width: 500px;
-  margin-top: -20px;
-  position: relative;
-  padding: 25px;
-  border-radius: 5px;
-  font-size: 13px;
-`;
-
-const StyledRegister = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 426
dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx

@@ -1,426 +0,0 @@
-import React, { ChangeEvent, Component } from "react";
-import logo from "legacy/assets/logo.png";
-import Loading from "legacy/components/Loading";
-import api from "legacy/shared/api";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-type PropsType = {};
-
-type StateType = {
-  email: string;
-  token: string;
-  token_id: number;
-  password: string;
-  passwordError: boolean;
-  tokenError: boolean;
-  loading: boolean;
-  submitted: boolean;
-};
-
-export default class ResetPasswordInit extends Component<PropsType, StateType> {
-  state = {
-    email: "",
-    token: "",
-    password: "",
-    token_id: 0,
-    passwordError: false,
-    tokenError: false,
-    loading: true,
-    submitted: false,
-  };
-
-  handleKeyDown = (e: any) => {
-    e.key === "Enter" ? this.handleResetPasswordFinalize() : null;
-  };
-
-  componentDidMount() {
-    let urlParams = new URLSearchParams(window.location.search);
-
-    let emailFromParam = urlParams.get("email");
-    let tokenFromParams = urlParams.get("token");
-    let tokenIDFromParams = urlParams.get("token_id");
-
-    api
-      .createPasswordResetVerify(
-        "",
-        {
-          email: emailFromParam,
-          token: tokenFromParams,
-          token_id: parseInt(tokenIDFromParams),
-        },
-        {}
-      )
-      .then(() => {
-        this.setState({ loading: false });
-      })
-      .catch((err) => this.setState({ loading: false, tokenError: true }));
-
-    document.addEventListener("keydown", this.handleKeyDown);
-
-    this.setState({
-      email: emailFromParam,
-      token: tokenFromParams,
-      token_id: parseInt(tokenIDFromParams),
-    });
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.handleKeyDown);
-  }
-
-  renderPasswordError = () => {
-    let { passwordError } = this.state;
-    if (passwordError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Invalid Password
-        </ErrorHelper>
-      );
-    }
-  };
-
-  handleResetPasswordFinalize = (): void => {
-    let { email, token, token_id, password } = this.state;
-
-    // Call reset password
-    api
-      .createPasswordResetFinalize(
-        "",
-        {
-          email: email,
-          token: token,
-          token_id: token_id,
-          new_password: password,
-        },
-        {}
-      )
-      .then((res) => {
-        // redirect to dashboard with message after timeout
-        this.setState({ submitted: true });
-
-        setTimeout(() => {
-          window.location.href = "/login";
-        }, 2000);
-      })
-      .catch((err) => this.setState({ tokenError: true }));
-  };
-
-  render() {
-    let { password, passwordError, submitted, loading, tokenError } =
-      this.state;
-
-    let inputSection = (
-      <div>
-        <InputWrapper>
-          <Input
-            type="password"
-            placeholder="Password"
-            value={password}
-            onChange={(e: ChangeEvent<HTMLInputElement>) =>
-              this.setState({
-                password: e.target.value,
-                passwordError: false,
-              })
-            }
-            valid={!passwordError}
-          />
-          {this.renderPasswordError()}
-        </InputWrapper>
-        <Button onClick={this.handleResetPasswordFinalize}>Continue</Button>
-      </div>
-    );
-
-    if (loading) {
-      inputSection = (
-        <StatusText>
-          <Loading />
-        </StatusText>
-      );
-    } else if (tokenError) {
-      inputSection = (
-        <StatusText>
-          Link has already been used or has expired. Please
-          <Link href="/password/reset">try again.</Link>
-        </StatusText>
-      );
-    } else if (submitted) {
-      inputSection = (
-        <StatusText>
-          Password changed successfully! Redirecting to login...
-        </StatusText>
-      );
-    }
-
-    return (
-      <StyledLogin>
-        <LoginPanel>
-          <OverflowWrapper>
-            <GradientBg />
-          </OverflowWrapper>
-          <FormWrapper>
-            <Logo src={logo} />
-            <Prompt>Reset Password</Prompt>
-            <DarkMatter />
-            {inputSection}
-            <Helper>
-              Don't have an account?
-              <Link href="/register">Sign up</Link>
-            </Helper>
-          </FormWrapper>
-        </LoginPanel>
-
-        <Footer>
-          © 2021 Porter Technologies Inc. •
-          <Link
-            href="https://docs.porter.run/docs/terms-of-service"
-            target="_blank"
-          >
-            Terms & Privacy
-          </Link>
-        </Footer>
-      </StyledLogin>
-    );
-  }
-}
-
-ResetPasswordInit.contextType = Context;
-
-const Footer = styled.div`
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  margin-bottom: 30px;
-  width: 100vw;
-  text-align: center;
-  color: #aaaabb;
-  font-size: 13px;
-  padding-right: 8px;
-  font:
-    Work Sans,
-    sans-serif;
-`;
-
-const DarkMatter = styled.div`
-  margin-top: -10px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 30px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 15px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const IconWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 10px;
-  height: 100%;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin-right: 20px;
-`;
-
-const OAuthButton = styled.div`
-  width: 200px;
-  height: 30px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 3px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;
-
-const Link = styled.a`
-  margin-left: 5px;
-  color: #819bfd;
-`;
-
-const Helper = styled.div`
-  position: absolute;
-  bottom: 30px;
-  width: 100%;
-  text-align: center;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  color: #ffffff44;
-`;
-
-const OverflowWrapper = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 10px;
-`;
-
-const ErrorHelper = styled.div`
-  position: absolute;
-  right: -185px;
-  top: 8px;
-  height: 30px;
-  width: 170px;
-  user-select: none;
-  background: #272731;
-  font-family: "Work Sans", sans-serif;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ff3b62;
-  border-radius: 3px;
-
-  > div {
-    background: #272731;
-    height: 15px;
-    width: 15px;
-    position: absolute;
-    left: -3px;
-    top: 7px;
-    transform: rotate(45deg);
-    z-index: -1;
-  }
-`;
-
-const Line = styled.div`
-  min-height: 3px;
-  width: 100px;
-  z-index: 999;
-  background: #ffffff22;
-  margin: 30px 0px 30px;
-`;
-
-const Button = styled.button`
-  width: 200px;
-  min-height: 30px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  cursor: pointer;
-  margin-top: 9px;
-  border-radius: 2px;
-  border: 0;
-  background: #819bfd;
-  color: white;
-  font-weight: 500;
-  font-size: 14px;
-`;
-
-const InputWrapper = styled.div`
-  position: relative;
-`;
-
-const Input = styled.input`
-  width: 200px;
-  font-family: "Work Sans", sans-serif;
-  margin: 8px 0px;
-  height: 30px;
-  padding: 8px;
-  background: #ffffff12;
-  color: #ffffff;
-  border: ${(props: { valid?: boolean }) =>
-    props.valid ? "0" : "1px solid #ff3b62"};
-  border-radius: 2px;
-  font-size: 14px;
-`;
-
-const Prompt = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  font-size: 15px;
-  margin-bottom: 18px;
-`;
-
-const StatusText = styled.div`
-  padding: 18px 30px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  line-height: 160%;
-`;
-
-const Logo = styled.img`
-  width: 140px;
-  margin-top: 50px;
-  margin-bottom: 75px;
-  user-select: none;
-`;
-
-const FormWrapper = styled.div`
-  width: calc(100% - 8px);
-  height: calc(100% - 8px);
-  background: #111114;
-  z-index: 1;
-  border-radius: 10px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-`;
-
-const GradientBg = styled.div`
-  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
-  width: 180%;
-  height: 180%;
-  position: absolute;
-  top: -40%;
-  left: -40%;
-  animation: flip 6s infinite linear;
-  @keyframes flip {
-    from {
-      transform: rotate(0deg);
-    }
-    to {
-      transform: rotate(360deg);
-    }
-  }
-`;
-
-const LoginPanel = styled.div`
-  width: 330px;
-  height: 470px;
-  background: white;
-  margin-top: -20px;
-  border-radius: 10px;
-  display: flex;
-  justify-content: center;
-  position: relative;
-  align-items: center;
-`;
-
-const StyledLogin = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 373
dashboard/src/legacy/main/auth/ResetPasswordInit.tsx

@@ -1,373 +0,0 @@
-import React, { ChangeEvent, Component } from "react";
-import logo from "legacy/assets/logo.png";
-import api from "legacy/shared/api";
-import { emailRegex } from "legacy/shared/regex";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-type PropsType = {};
-
-type StateType = {
-  email: string;
-  emailError: boolean;
-  submitted: boolean;
-};
-
-export default class ResetPasswordInit extends Component<PropsType, StateType> {
-  state = {
-    email: "",
-    emailError: false,
-    submitted: false,
-  };
-
-  handleKeyDown = (e: any) => {
-    e.key === "Enter" ? this.handleResetPasswordInit() : null;
-  };
-
-  componentDidMount() {
-    document.addEventListener("keydown", this.handleKeyDown);
-  }
-
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.handleKeyDown);
-  }
-
-  renderEmailError = () => {
-    let { emailError } = this.state;
-    if (emailError) {
-      return (
-        <ErrorHelper>
-          <div />
-          Please enter a valid email
-        </ErrorHelper>
-      );
-    }
-  };
-
-  handleResetPasswordInit = (): void => {
-    let { email } = this.state;
-
-    // Check for valid input
-    if (!emailRegex.test(email)) {
-      this.setState({ emailError: true });
-    } else {
-      // Call reset password
-      api
-        .createPasswordReset(
-          "",
-          {
-            email: email,
-          },
-          {}
-        )
-        .then((res) => {
-          this.setState({ submitted: true });
-        })
-        .catch((err) => this.context.setCurrentError(err.response.data.error));
-    }
-  };
-
-  render() {
-    let { email, emailError, submitted } = this.state;
-
-    let formSection = (
-      <div>
-        <InputWrapper>
-          <Input
-            type="email"
-            placeholder="Email"
-            value={email}
-            onChange={(e: ChangeEvent<HTMLInputElement>) =>
-              this.setState({
-                email: e.target.value,
-                emailError: false,
-              })
-            }
-            valid={!emailError}
-          />
-          {this.renderEmailError()}
-        </InputWrapper>
-        <Button onClick={this.handleResetPasswordInit}>Continue</Button>
-      </div>
-    );
-
-    if (submitted) {
-      formSection = (
-        <StatusText>
-          If we found an account matching {email}, we've sent you password reset
-          instructions. Remember to check your spam folder.
-        </StatusText>
-      );
-    }
-
-    return (
-      <StyledLogin>
-        <LoginPanel>
-          <OverflowWrapper>
-            <GradientBg />
-          </OverflowWrapper>
-          <FormWrapper>
-            <Logo src={logo} />
-            <Prompt>Reset Password</Prompt>
-            <DarkMatter />
-            {formSection}
-            <Helper>
-              Don't have an account?
-              <Link href="/register">Sign up</Link>
-            </Helper>
-          </FormWrapper>
-        </LoginPanel>
-
-        <Footer>
-          © 2021 Porter Technologies Inc. •
-          <Link
-            href="https://docs.porter.run/docs/terms-of-service"
-            target="_blank"
-          >
-            Terms & Privacy
-          </Link>
-        </Footer>
-      </StyledLogin>
-    );
-  }
-}
-
-ResetPasswordInit.contextType = Context;
-
-const Footer = styled.div`
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  margin-bottom: 30px;
-  width: 100vw;
-  text-align: center;
-  color: #aaaabb;
-  font-size: 13px;
-  padding-right: 8px;
-  font:
-    Work Sans,
-    sans-serif;
-`;
-
-const DarkMatter = styled.div`
-  margin-top: -10px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 30px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 15px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const IconWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 10px;
-  height: 100%;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin-right: 20px;
-`;
-
-const OAuthButton = styled.div`
-  width: 200px;
-  height: 30px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 3px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;
-
-const Link = styled.a`
-  margin-left: 5px;
-  color: #819bfd;
-`;
-
-const Helper = styled.div`
-  position: absolute;
-  bottom: 30px;
-  width: 100%;
-  text-align: center;
-  font-size: 13px;
-  font-family: "Work Sans", sans-serif;
-  color: #ffffff44;
-`;
-
-const OverflowWrapper = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  border-radius: 10px;
-`;
-
-const ErrorHelper = styled.div`
-  position: absolute;
-  right: -185px;
-  top: 8px;
-  height: 30px;
-  width: 170px;
-  user-select: none;
-  background: #272731;
-  font-family: "Work Sans", sans-serif;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ff3b62;
-  border-radius: 3px;
-
-  > div {
-    background: #272731;
-    height: 15px;
-    width: 15px;
-    position: absolute;
-    left: -3px;
-    top: 7px;
-    transform: rotate(45deg);
-    z-index: -1;
-  }
-`;
-
-const Line = styled.div`
-  min-height: 3px;
-  width: 100px;
-  z-index: 999;
-  background: #ffffff22;
-  margin: 30px 0px 30px;
-`;
-
-const Button = styled.button`
-  width: 200px;
-  min-height: 30px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  cursor: pointer;
-  margin-top: 9px;
-  border-radius: 2px;
-  border: 0;
-  background: #819bfd;
-  color: white;
-  font-weight: 500;
-  font-size: 14px;
-`;
-
-const InputWrapper = styled.div`
-  position: relative;
-`;
-
-const Input = styled.input`
-  width: 200px;
-  font-family: "Work Sans", sans-serif;
-  margin: 8px 0px;
-  height: 30px;
-  padding: 8px;
-  background: #ffffff12;
-  color: #ffffff;
-  border: ${(props: { valid?: boolean }) =>
-    props.valid ? "0" : "1px solid #ff3b62"};
-  border-radius: 2px;
-  font-size: 14px;
-`;
-
-const Prompt = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  font-size: 15px;
-  margin-bottom: 18px;
-`;
-
-const Logo = styled.img`
-  width: 140px;
-  margin-top: 50px;
-  margin-bottom: 75px;
-  user-select: none;
-`;
-
-const StatusText = styled.div`
-  padding: 18px 30px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  line-height: 160%;
-`;
-
-const FormWrapper = styled.div`
-  width: calc(100% - 8px);
-  height: calc(100% - 8px);
-  background: #111114;
-  z-index: 1;
-  border-radius: 10px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-`;
-
-const GradientBg = styled.div`
-  background: linear-gradient(#8ce1ff, #a59eff, #fba8ff);
-  width: 180%;
-  height: 180%;
-  position: absolute;
-  top: -40%;
-  left: -40%;
-  animation: flip 6s infinite linear;
-  @keyframes flip {
-    from {
-      transform: rotate(0deg);
-    }
-    to {
-      transform: rotate(360deg);
-    }
-  }
-`;
-
-const LoginPanel = styled.div`
-  width: 330px;
-  height: 470px;
-  background: white;
-  margin-top: -20px;
-  border-radius: 10px;
-  display: flex;
-  justify-content: center;
-  position: relative;
-  align-items: center;
-`;
-
-const StyledLogin = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 300
dashboard/src/legacy/main/auth/SetInfo.tsx

@@ -1,300 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import github from "legacy/assets/github-icon.png";
-import GoogleIcon from "legacy/assets/GoogleIcon";
-import logo from "legacy/assets/logo.png";
-import Heading from "legacy/components/form-components/Heading";
-import Button from "legacy/components/porter/Button";
-import Container from "legacy/components/porter/Container";
-import Input from "legacy/components/porter/Input";
-import Link from "legacy/components/porter/Link";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import api from "legacy/shared/api";
-import { emailRegex } from "legacy/shared/regex";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-import InfoPanel from "./InfoPanel";
-
-type Props = {
-  authenticate: () => Promise<void>;
-  handleLogOut: () => void;
-};
-
-const getWindowDimensions = () => {
-  const { innerWidth: width, innerHeight: height } = window;
-  return { width, height };
-};
-
-const SetInfo: React.FC<Props> = ({ authenticate, handleLogOut }) => {
-  const { user, setCurrentError } = useContext(Context);
-  const [firstName, setFirstName] = useState("");
-  const [firstNameError, setFirstNameError] = useState(false);
-  const [lastName, setLastName] = useState("");
-  const [lastNameError, setLastNameError] = useState(false);
-  const [companyName, setCompanyName] = useState("");
-  const [companyNameError, setCompanyNameError] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(
-    getWindowDimensions()
-  );
-
-  const handleResize = () => {
-    setWindowDimensions(getWindowDimensions());
-  };
-
-  const finishAccountSetup = async () => {
-    if (firstName === "") {
-      setFirstNameError(true);
-    }
-
-    if (lastName === "") {
-      setLastNameError(true);
-    }
-
-    if (companyName === "") {
-      setCompanyNameError(true);
-    }
-
-    if (firstName !== "" && lastName !== "" && companyName !== "") {
-      api
-        .updateUserInfo(
-          "",
-          {
-            first_name: firstName,
-            last_name: lastName,
-            company_name: companyName,
-          },
-          { id: user.id }
-        )
-        .then((res: any) => {
-          authenticate().catch(() => {});
-
-          try {
-            window.dataLayer?.push({
-              event: "sign-up",
-              data: {
-                method: "github",
-                email: user?.email,
-              },
-            });
-          } catch (err) {
-            console.log(err);
-          }
-        })
-        .catch((err) => {
-          setCurrentError(err);
-        });
-    }
-  };
-
-  const handleKeyDown = (e: any) => {
-    if (e.key === "Enter") {
-      finishAccountSetup();
-    }
-  };
-
-  // Manually re-register event listener on email/password change
-  useEffect(() => {
-    document.removeEventListener("keydown", handleKeyDown);
-    document.addEventListener("keydown", handleKeyDown);
-    return () => {
-      document.removeEventListener("keydown", handleKeyDown);
-    };
-  }, [firstName, lastName, companyName]);
-
-  useEffect(() => {
-    window.addEventListener("resize", handleResize);
-    return () => {
-      window.removeEventListener("resize", handleResize);
-    };
-  }, []);
-
-  return (
-    <StyledRegister>
-      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
-      <Wrapper>
-        {windowDimensions.width <= windowDimensions.height && (
-          <Flex>
-            <Logo src={logo} />
-            <Spacer y={2} />
-          </Flex>
-        )}
-        <Heading isAtTop>Finish setting up your account</Heading>
-        <Spacer y={1} />
-        <Container row>
-          <RowWrapper>
-            <Input
-              placeholder="First name"
-              label="First name"
-              value={firstName}
-              setValue={(x) => {
-                setFirstName(x);
-                setFirstNameError(false);
-              }}
-              width="100%"
-              height="40px"
-              error={firstNameError && "First name cannot be blank"}
-            />
-            {!firstNameError && lastNameError && <Spacer height="27px" />}
-          </RowWrapper>
-          <Spacer inline x={2} />
-          <RowWrapper>
-            <Input
-              placeholder="Last name"
-              label="Last name"
-              value={lastName}
-              setValue={(x) => {
-                setLastName(x);
-                setLastNameError(false);
-              }}
-              width="100%"
-              height="40px"
-              error={lastNameError && "Last name cannot be blank"}
-            />
-            {!lastNameError && firstNameError && <Spacer height="27px" />}
-          </RowWrapper>
-        </Container>
-        <Spacer y={1} />
-        <Input
-          placeholder="Company name"
-          label="Company name"
-          value={companyName}
-          setValue={(x) => {
-            setCompanyName(x);
-            setCompanyNameError(false);
-          }}
-          width="100%"
-          height="40px"
-          error={companyNameError && ""}
-        />
-        <Spacer height="30px" />
-        <Button onClick={finishAccountSetup} width="100%" height="40px">
-          Continue
-        </Button>
-        <Spacer y={1} />
-        <Text size={13} color="helper">
-          Want to use a different login method?{" "}
-          <Link onClick={handleLogOut}>Log out</Link>
-        </Text>
-      </Wrapper>
-    </StyledRegister>
-  );
-};
-
-export default SetInfo;
-
-const RowWrapper = styled.div`
-  width: 100%;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
-`;
-
-const CheckRow = styled.div`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb;
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: #4797ff;
-  }
-`;
-
-const Shiny = styled.span`
-  background-image: linear-gradient(225deg, #fff, #7980ff);
-  -webkit-background-clip: text;
-  background-clip: text;
-  -webkit-text-fill-color: transparent;
-`;
-
-const Jumbotron = styled.div`
-  font-size: 32px;
-  font-weight: 500;
-  line-height: 1.5;
-`;
-
-const Logo = styled.img`
-  height: 24px;
-  user-select: none;
-`;
-
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
-`;
-
-const Line = styled.div`
-  height: 2px;
-  width: 100%;
-  background: #ffffff22;
-  margin: 35px 0px 30px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 50px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 25px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin: 14px;
-`;
-
-const OAuthButton = styled.div`
-  width: 100%;
-  height: 40px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 5px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;
-
-const Wrapper = styled.div`
-  width: 500px;
-  margin-top: -20px;
-  position: relative;
-  padding: 25px;
-  border-radius: 5px;
-  font-size: 13px;
-`;
-
-const StyledRegister = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 241
dashboard/src/legacy/main/auth/VerifyEmail.tsx

@@ -1,241 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import github from "legacy/assets/github-icon.png";
-import GoogleIcon from "legacy/assets/GoogleIcon";
-import logo from "legacy/assets/logo.png";
-import Heading from "legacy/components/form-components/Heading";
-import Button from "legacy/components/porter/Button";
-import Container from "legacy/components/porter/Container";
-import Input from "legacy/components/porter/Input";
-import Link from "legacy/components/porter/Link";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import api from "legacy/shared/api";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-import InfoPanel from "./InfoPanel";
-
-type Props = {
-  handleLogOut: () => void;
-};
-
-const getWindowDimensions = () => {
-  const { innerWidth: width, innerHeight: height } = window;
-  return { width, height };
-};
-
-const Register: React.FC<Props> = ({ handleLogOut }) => {
-  const { user, setCurrentError } = useContext(Context);
-  const [submitted, setSubmitted] = useState(false);
-  const [windowDimensions, setWindowDimensions] = useState(
-    getWindowDimensions()
-  );
-
-  const handleResize = () => {
-    setWindowDimensions(getWindowDimensions());
-  };
-
-  useEffect(() => {
-    window.addEventListener("resize", handleResize);
-    return () => {
-      window.removeEventListener("resize", handleResize);
-    };
-  }, []);
-
-  const handleSendEmail = (): void => {
-    api
-      .createEmailVerification("", {}, {})
-      .then((res) => {
-        setSubmitted(true);
-      })
-      .catch((err) => {
-        setCurrentError(err.response.data.error);
-      });
-  };
-
-  return (
-    <StyledRegister>
-      {windowDimensions.width > windowDimensions.height && <InfoPanel />}
-      <Wrapper>
-        {windowDimensions.width <= windowDimensions.height && (
-          <Flex>
-            <Logo src={logo} />
-            <Spacer y={2} />
-          </Flex>
-        )}
-        <Heading isAtTop>Verify your email</Heading>
-        <Spacer y={1} />
-        {submitted ? (
-          <>
-            <Text color="helper" size={13}>
-              A new verification email was sent to:
-            </Text>
-            <Spacer y={1} />
-            <Email>{user?.email}</Email>
-            <Spacer y={1} />
-            <Text color="helper" size={13}>
-              Don't forget to check your spam folder.
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper" size={13}>
-              If you still need help, please contact support@porter.run.
-            </Text>
-          </>
-        ) : (
-          <>
-            <Text color="helper" size={13}>
-              We've sent a verification link to the following email address:
-            </Text>
-            <Spacer y={1} />
-            <Email>{user?.email}</Email>
-            <Spacer y={1} />
-            <Text color="helper" size={13}>
-              Please click the link in your inbox to verify your email.
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper" size={13}>
-              Didn't receive anything?
-            </Text>
-            <Spacer height="30px" />
-            <Button onClick={handleSendEmail} width="100%" height="40px">
-              Resend verification email
-            </Button>
-          </>
-        )}
-        <Spacer y={1} />
-        <Text size={13} color="helper">
-          Want to use a different email?
-          <Spacer inline width="5px" />
-          <Link onClick={handleLogOut}>Log out</Link>
-        </Text>
-      </Wrapper>
-    </StyledRegister>
-  );
-};
-
-export default Register;
-
-const Email = styled.div`
-  width: 100%;
-  height: 40px;
-  border-radius: 5px;
-  background: #26292e;
-  border: 1px solid #494b4f;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  padding: 15px;
-  color: #aaaabb;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-direction: column;
-`;
-
-const CheckRow = styled.div`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb;
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: #4797ff;
-  }
-`;
-
-const Shiny = styled.span`
-  background-image: linear-gradient(225deg, #fff, #7980ff);
-  -webkit-background-clip: text;
-  background-clip: text;
-  -webkit-text-fill-color: transparent;
-`;
-
-const Jumbotron = styled.div`
-  font-size: 32px;
-  font-weight: 500;
-  line-height: 1.5;
-`;
-
-const Logo = styled.img`
-  height: 24px;
-  user-select: none;
-`;
-
-const StyledGoogleIcon = styled(GoogleIcon)`
-  width: 38px;
-  height: 38px;
-`;
-
-const Line = styled.div`
-  height: 2px;
-  width: 100%;
-  background: #ffffff22;
-  margin: 35px 0px 30px;
-`;
-
-const Or = styled.div`
-  position: absolute;
-  width: 50px;
-  text-align: center;
-  background: #111114;
-  z-index: 999;
-  left: calc(50% - 25px);
-  margin-top: -1px;
-`;
-
-const OrWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-  position: relative;
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin: 14px;
-`;
-
-const OAuthButton = styled.div`
-  width: 100%;
-  height: 40px;
-  display: flex;
-  background: #ffffff;
-  align-items: center;
-  border-radius: 5px;
-  color: #000000;
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 13px;
-  :hover {
-    background: #ffffffdd;
-  }
-`;
-
-const Wrapper = styled.div`
-  width: 500px;
-  margin-top: -20px;
-  position: relative;
-  padding: 25px;
-  border-radius: 5px;
-  font-size: 13px;
-`;
-
-const StyledRegister = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #111114;
-`;

+ 0 - 185
dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx

@@ -1,185 +0,0 @@
-import React from "react";
-import { AnimatePresence, motion } from "framer-motion";
-import box from "legacy/assets/box.png";
-import postgresql from "legacy/assets/postgresql.svg";
-import redis from "legacy/assets/redis.svg";
-import Spacer from "legacy/components/porter/Spacer";
-import { type ClientAddon } from "legacy/lib/addons";
-import { type UseFieldArrayUpdate } from "react-hook-form";
-import styled from "styled-components";
-import { match } from "ts-pattern";
-
-import { type AppTemplateFormData } from "../cluster-dashboard/preview-environments/v2/EnvTemplateContextProvider";
-import { PostgresTabs } from "./tabs/PostgresTabs";
-import { RedisTabs } from "./tabs/RedisTabs";
-
-type AddonRowProps = {
-  index: number;
-  addon: Omit<ClientAddon, "template">;
-  update: UseFieldArrayUpdate<AppTemplateFormData, "addons">;
-  remove: (index: number) => void;
-};
-
-export const AddonListRow: React.FC<AddonRowProps> = ({
-  index,
-  addon,
-  update,
-  remove,
-}) => {
-  const renderIcon = (type: ClientAddon["config"]["type"]): JSX.Element =>
-    match(type)
-      .with("postgres", () => <Icon src={postgresql} />)
-      .with("redis", () => <Icon src={redis} />)
-      .otherwise(() => <Icon src={box} />);
-
-  return (
-    <>
-      <AddonHeader
-        showExpanded={addon.expanded}
-        onClick={() => {
-          update(index, {
-            ...addon,
-            expanded: !addon.expanded,
-          });
-        }}
-        bordersRounded={!addon.expanded}
-      >
-        <AddonTitle>
-          <ActionButton>
-            <span className="material-icons dropdown">arrow_drop_down</span>
-          </ActionButton>
-          {renderIcon(addon.config.type)}
-          {addon.name.value.trim().length > 0 ? addon.name.value : "New Addon"}
-        </AddonTitle>
-
-        {addon.canDelete && (
-          <ActionButton
-            onClick={(e) => {
-              e.stopPropagation();
-              remove(index);
-            }}
-          >
-            <span className="material-icons">delete</span>
-          </ActionButton>
-        )}
-      </AddonHeader>
-      <AnimatePresence>
-        {addon.expanded && (
-          <StyledSourceBox
-            key={addon.name.value}
-            initial={{
-              height: 0,
-            }}
-            animate={{
-              height: "auto",
-              transition: {
-                duration: 0.3,
-              },
-            }}
-            exit={{
-              height: 0,
-              transition: {
-                duration: 0.3,
-              },
-            }}
-            showExpanded={addon.expanded}
-          >
-            <div
-              style={{
-                padding: "14px 25px 30px",
-                border: "1px solid #494b4f",
-              }}
-            >
-              {match(addon)
-                .with({ config: { type: "postgres" } }, (ao) => (
-                  <PostgresTabs index={index} addon={ao} />
-                ))
-                .with({ config: { type: "redis" } }, (ao) => (
-                  <RedisTabs index={index} addon={ao} />
-                ))
-                .otherwise(() => null)}
-            </div>
-          </StyledSourceBox>
-        )}
-      </AnimatePresence>
-      <Spacer y={0.5} />
-    </>
-  );
-};
-
-const AddonTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const AddonHeader = styled.div<{
-  showExpanded?: boolean;
-  bordersRounded?: boolean;
-}>`
-  flex-direction: row;
-  display: flex;
-  height: 60px;
-  justify-content: space-between;
-  cursor: pointer;
-  padding: 20px;
-  color: ${(props) => props.theme.text.primary};
-  position: relative;
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-
-  border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")};
-  border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")};
-
-  .dropdown {
-    font-size: 30px;
-    cursor: pointer;
-    border-radius: 20px;
-    margin-left: -10px;
-    transform: ${(props: { showExpanded?: boolean }) =>
-      props.showExpanded ? "" : "rotate(-90deg)"};
-  }
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin-right: 15px;
-`;
-
-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 {
-    color: white;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-  margin-right: 5px;
-`;
-
-const StyledSourceBox = styled(motion.div)<{
-  showExpanded?: boolean;
-  hasFooter?: boolean;
-}>`
-  overflow: hidden;
-  color: #ffffff;
-  font-size: 13px;
-  background: ${(props) => props.theme.fg};
-  border-top: 0;
-  border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
-  border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
-`;

+ 0 - 195
dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx

@@ -1,195 +0,0 @@
-import React, { useState } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import postgresql from "legacy/assets/postgresql.svg";
-import redis from "legacy/assets/redis.svg";
-import Button from "legacy/components/porter/Button";
-import Container from "legacy/components/porter/Container";
-import Modal from "legacy/components/porter/Modal";
-import Select from "legacy/components/porter/Select";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import { defaultClientAddon } from "legacy/lib/addons";
-import {
-  Controller,
-  useFieldArray,
-  useForm,
-  useFormContext,
-} from "react-hook-form";
-import styled from "styled-components";
-import { match } from "ts-pattern";
-import { z } from "zod";
-
-import { type AppTemplateFormData } from "../cluster-dashboard/preview-environments/v2/EnvTemplateContextProvider";
-import { AddonListRow } from "./AddonListRow";
-
-const addAddonFormValidator = z.object({
-  type: z.enum(["postgres", "redis"]),
-});
-type AddAddonFormValues = z.infer<typeof addAddonFormValidator>;
-
-export const AddonsList: React.FC = () => {
-  const [showAddAddonModal, setShowAddAddonModal] = useState(false);
-
-  const { control: appTemplateControl } = useFormContext<AppTemplateFormData>();
-
-  // add addon modal form
-  const { watch, control, reset, handleSubmit } = useForm<AddAddonFormValues>({
-    reValidateMode: "onChange",
-    resolver: zodResolver(addAddonFormValidator),
-    defaultValues: {
-      type: "postgres",
-    },
-  });
-
-  const addonType = watch("type");
-
-  const { append, update, remove, fields } = useFieldArray({
-    control: appTemplateControl,
-    name: "addons",
-  });
-
-  const onSubmit = handleSubmit((data) => {
-    const baseAddon = defaultClientAddon(data.type);
-    append(baseAddon);
-
-    reset();
-    setShowAddAddonModal(false);
-  });
-
-  return (
-    <>
-      <AddonsContainer>
-        {fields.map((addon, idx) => (
-          <AddonListRow
-            key={addon.id}
-            index={idx}
-            addon={addon}
-            update={update}
-            remove={remove}
-          />
-        ))}
-      </AddonsContainer>
-      <AddAddonButton
-        onClick={() => {
-          setShowAddAddonModal(true);
-        }}
-      >
-        <I className="material-icons add-icon">add</I>
-        Include add-on in preview environments
-      </AddAddonButton>
-      <Spacer y={0.5} />
-      {showAddAddonModal && (
-        <Modal
-          closeModal={() => {
-            setShowAddAddonModal(false);
-          }}
-          width="500px"
-        >
-          <Text size={16}>Include an addon in your preview environment</Text>
-          <Spacer y={1} />
-          <Text color="helper">Select a service type:</Text>
-          <Spacer y={0.5} />
-          <Container row>
-            <AddonIcon>
-              {match(addonType)
-                .with("postgres", () => <img src={postgresql} />)
-                .with("redis", () => <img src={redis} />)
-                .exhaustive()}
-            </AddonIcon>
-            <Controller
-              name="type"
-              control={control}
-              render={({ field: { onChange } }) => (
-                <Select
-                  value={addonType}
-                  width="100%"
-                  setValue={(value: string) => {
-                    onChange(value);
-                  }}
-                  options={[
-                    { label: "Postgres", value: "postgres" },
-                    { label: "Redis", value: "redis" },
-                  ]}
-                />
-              )}
-            />
-          </Container>
-          <Spacer y={1} />
-          <Button type="button" onClick={onSubmit}>
-            <I className="material-icons">add</I> Add
-          </Button>
-        </Modal>
-      )}
-    </>
-  );
-};
-
-const AddonsContainer = styled.div`
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const AddonIcon = styled.div`
-  border: 1px solid #494b4f;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 35px;
-  width: 35px;
-  min-width: 35px;
-  margin-right: 10px;
-  overflow: hidden;
-  border-radius: 5px;
-  > img {
-    height: 18px;
-    animation: floatIn 0.5s 0s;
-    @keyframes floatIn {
-      from {
-        opacity: 0;
-        transform: translateY(7px);
-      }
-      to {
-        opacity: 1;
-        transform: translateY(0px);
-      }
-    }
-  }
-`;
-
-const AddAddonButton = styled.div`
-  color: #aaaabb;
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  align-items: center;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  .add-icon {
-    width: 30px;
-    font-size: 20px;
-  }
-`;
-
-const I = styled.i`
-  color: white;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 7px;
-  justify-content: center;
-`;

+ 0 - 170
dashboard/src/legacy/main/home/modals/UpgradeChartModal.tsx

@@ -1,170 +0,0 @@
-import React, { Component } from "react";
-import close from "legacy/assets/close.png";
-import Loading from "legacy/components/Loading";
-import SaveButton from "legacy/components/SaveButton";
-import api from "legacy/shared/api";
-import { ChartType } from "legacy/shared/types";
-import Markdown from "markdown-to-jsx";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-
-type PropsType = {
-  currentChart: ChartType;
-  onSubmit: () => void;
-  closeModal: () => void;
-};
-
-type StateType = {
-  notes: string;
-};
-
-export default class UpgradeChartModal extends Component<PropsType, StateType> {
-  state = {
-    notes: "Loading",
-  };
-
-  componentDidMount() {
-    // get the chart update notes from the api
-    let repoURL = this.context.capabilities.default_addon_helm_repo_url;
-    let chartName = this.props.currentChart.chart.metadata.name
-      .toLowerCase()
-      .trim();
-
-    if (chartName == "web" || chartName == "worker" || chartName === "job") {
-      repoURL = this.context.capabilities?.default_app_helm_repo_url;
-    }
-
-    api
-      .getTemplateUpgradeNotes(
-        "<token>",
-        {
-          repo_url: repoURL,
-          prev_version: this.props.currentChart.chart.metadata.version,
-        },
-        {
-          project_id: this.context.currentProject.id,
-          name: chartName,
-          version: this.props.currentChart.latest_version,
-        }
-      )
-      .then((res) => {
-        if (!res.data.upgrade_notes || res.data.upgrade_notes.length == 0) {
-          this.setState({
-            notes: `
-## Version ${this.props.currentChart.chart.metadata.version} -> ${this.props.currentChart.latest_version}
-No upgrade notes available. This update should be backwards-compatible. 
-        `,
-          });
-
-          return;
-        }
-
-        let noteArr = res.data.upgrade_notes.map((note: any) => {
-          return `
-## Version ${note.previous} -> ${note.target}
-${note.note}
-            `;
-        });
-
-        this.setState({ notes: noteArr.join("\n") });
-      })
-      .catch((err) => {
-        console.log(err);
-        this.setState({
-          notes: `
-      ## Version ${this.props.currentChart.chart.metadata.version} -> ${this.props.currentChart.latest_version}
-      No upgrade notes available. This update should be backwards-compatible.
-        `,
-        });
-      });
-  }
-
-  renderContent() {
-    if (this.state.notes == "Loading") {
-      return <Loading />;
-    }
-
-    return <StyledContent>{this.state.notes}</StyledContent>;
-  }
-
-  render() {
-    return (
-      <StyledUpgradeChartModal>
-        {this.renderContent()}
-        <SaveButton
-          disabled={false}
-          text="Upgrade Template"
-          status={""}
-          onClick={this.props.onSubmit}
-        />
-      </StyledUpgradeChartModal>
-    );
-  }
-}
-
-UpgradeChartModal.contextType = Context;
-
-const ModalTitle = styled.div`
-  margin: 0px 0px 13px;
-  display: flex;
-  flex: 1;
-  font-family:
-    Work Sans,
-    sans-serif;
-  font-size: 12px;
-  color: #ffffff;
-  user-select: none;
-  font-weight: 700;
-  align-items: center;
-  position: relative;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-`;
-
-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 StyledUpgradeChartModal = styled.div`
-  width: 100%;
-  position: absolute;
-  left: 0;
-  top: 0;
-  height: 100%;
-  padding: 25px 30px;
-  overflow: hidden;
-  border-radius: 6px;
-  background: #202227;
-  font-size: 13px;
-  line-height: 1.8em;
-  font-family:
-    Work Sans,
-    sans-serif;
-`;
-const StyledContent = styled.div`
-  /* Add your custom styles for the content here */
-  margin-top: 16px;
-  font-size: 16px;
-  line-height: 1.5;
-  color: #ffffff;
-  overflow: auto;
-`;

+ 0 - 42
dashboard/src/legacy/main/home/onboarding/constants.ts

@@ -1,42 +0,0 @@
-export const GCP_REGION_OPTIONS = [
-  { value: "asia-east1", label: "asia-east1" },
-  { value: "asia-east2", label: "asia-east2" },
-  { value: "asia-northeast1", label: "asia-northeast1" },
-  { value: "asia-northeast2", label: "asia-northeast2" },
-  { value: "asia-northeast3", label: "asia-northeast3" },
-  { value: "asia-south1", label: "asia-south1" },
-  { value: "asia-south2", label: "asia-south2" },
-  { value: "asia-southeast1", label: "asia-southeast1" },
-  { value: "asia-southeast2", label: "asia-southeast2" },
-  { value: "australia-southeast1", label: "australia-southeast1" },
-  { value: "australia-southeast2", label: "australia-southeast2" },
-  { value: "europe-central2", label: "europe-central2" },
-  { value: "europe-north1", label: "europe-north1" },
-  { value: "europe-west1", label: "europe-west1" },
-  { value: "europe-west2", label: "europe-west2" },
-  { value: "europe-west3", label: "europe-west3" },
-  { value: "europe-west4", label: "europe-west4" },
-  { value: "europe-west6", label: "europe-west6" },
-  { value: "europe-west8", label: "europe-west8" },
-  { value: "europe-west9", label: "europe-west9" },
-  { value: "europe-southwest1", label: "europe-southwest1" },
-  { value: "northamerica-northeast1", label: "northamerica-northeast1" },
-  { value: "northamerica-northeast2", label: "northamerica-northeast2" },
-  { value: "southamerica-east1", label: "southamerica-east1" },
-  { value: "southamerica-west1", label: "southamerica-west1" },
-  { value: "us-central1", label: "us-central1" },
-  { value: "us-east1", label: "us-east1" },
-  { value: "us-east4", label: "us-east4" },
-  { value: "us-east5", label: "us-east5" },
-  { value: "us-west1", label: "us-west1" },
-  { value: "us-west2", label: "us-west2" },
-  { value: "us-west3", label: "us-west3" },
-  { value: "us-west4", label: "us-west4" },
-  { value: "us-south1", label: "us-south1" },
-];
-
-export const GAR_REGION_OPTIONS = GCP_REGION_OPTIONS.concat([
-  { value: "us", label: "us (multi-region)" },
-  { value: "europe", label: "europe (multi-region)" },
-  { value: "asia", label: "asia (multi-region)" },
-]);

+ 0 - 77
dashboard/src/legacy/main/home/project-settings/Bars.tsx

@@ -1,77 +0,0 @@
-import React from "react";
-import Text from "legacy/components/porter/Text";
-import {
-  Bar,
-  BarChart,
-  CartesianGrid,
-  ResponsiveContainer,
-  Tooltip,
-  XAxis,
-  YAxis,
-  type TooltipProps,
-} from "recharts";
-import styled from "styled-components";
-
-type Props = {
-  data: Array<Record<string, unknown>>;
-  yKey: string;
-  xKey: string;
-  fill?: string;
-  title?: string;
-};
-
-const CustomTooltip = ({ active, payload }: TooltipProps<string, string>) => {
-  if (active && payload?.length) {
-    return (
-      <div
-        className="custom-tooltip"
-        style={{
-          backgroundColor: "#42444933",
-          backdropFilter: "saturate(150%) blur(8px)",
-          border: "1px solid #494b4f",
-          fontSize: "13px",
-          padding: "10px",
-          borderRadius: "5px",
-          color: "white",
-        }}
-      >
-        <p className="intro">{`Value: ${payload[0].value}`}</p>
-      </div>
-    );
-  }
-
-  return null;
-};
-
-const Bars: React.FC<Props> = ({ data, yKey, xKey, fill, title }) => {
-  return (
-    <ResponsiveContainer width="100%" height="100%">
-      <BarChart
-        width={500}
-        height={300}
-        data={data}
-        margin={{
-          top: 5,
-          right: 0,
-          left: -20,
-          bottom: 5,
-        }}
-      >
-        <CartesianGrid vertical={false} stroke="#ffffff22" />
-        <XAxis dataKey={xKey} tick={{ fontSize: 13 }} />
-        <YAxis tick={{ fontSize: 13 }} />
-        <Tooltip content={<CustomTooltip />} cursor={{ fill: "#ffffff11" }} />
-        <Bar dataKey={yKey} fill={fill || "#6A7FC4"} />
-      </BarChart>
-      <Center>
-        <Text color="helper">{title}</Text>
-      </Center>
-    </ResponsiveContainer>
-  );
-};
-
-export default Bars;
-
-const Center = styled.div`
-  text-align: center;
-`;

+ 0 - 35
dashboard/src/legacy/main/home/project-settings/ReferralsPage.tsx

@@ -1,35 +0,0 @@
-import React from "react";
-import Link from "legacy/components/porter/Link";
-import Spacer from "legacy/components/porter/Spacer";
-import Text from "legacy/components/porter/Text";
-import { useReferralDetails } from "legacy/lib/hooks/useStripe";
-
-function ReferralsPage(): JSX.Element {
-  const { referralDetails } = useReferralDetails();
-  const baseUrl = window.location.origin;
-
-  return (
-    <>
-      <Text size={16}>Referrals</Text>
-      <Spacer y={1} />
-      <Text color="helper">Refer people to Porter to earn credits.</Text>
-      <Spacer y={1} />
-      {referralDetails !== null && (
-        <>
-          <Text>Your referral link is </Text>
-          <Link to={baseUrl + "/register?referral=" + referralDetails.code}>
-            {baseUrl + "/register?referral=" + referralDetails.code}
-          </Link>
-          <Spacer y={1} />
-          <Text>
-            You have referred {referralDetails.referral_count}/
-            {referralDetails.max_allowed_referrals} users
-          </Text>
-        </>
-      )}
-      <Spacer y={1} />
-    </>
-  );
-}
-
-export default ReferralsPage;

+ 0 - 30
dashboard/src/legacy/shared/error_handling/MainWrapperErrorBoundary.tsx

@@ -1,30 +0,0 @@
-import React, { useContext } from "react";
-
-import { Context } from "shared/Context";
-
-import PorterErrorBoundary from "./PorterErrorBoundary";
-
-const MainWrapperErrorBoundary: React.FC = ({ children }) => {
-  const location = "MainWrapperErrorBoundary";
-  const { capabilities, currentCluster, currentProject, devOpsMode, projects } =
-    useContext(Context);
-
-  return (
-    <PorterErrorBoundary
-      errorBoundaryLocation={location}
-      context={{
-        "Global context state": {
-          capabilities,
-          currentProject,
-          currentCluster,
-          devOpsMode,
-          projects: JSON.stringify(projects),
-        },
-      }}
-    >
-      {children}
-    </PorterErrorBoundary>
-  );
-};
-
-export default MainWrapperErrorBoundary;

+ 0 - 18
dashboard/src/legacy/shared/error_handling/sentry/setup.ts

@@ -1,18 +0,0 @@
-import * as Sentry from "@sentry/react";
-import { Integrations } from "@sentry/tracing";
-
-const SENTRY_DSN = import.meta.env.SENTRY_DSN;
-const SENTRY_ENV = import.meta.env.SENTRY_ENV || "development";
-
-export const SetupSentry = () => {
-  if (!SENTRY_DSN) {
-    return;
-  }
-  Sentry.init({
-    dsn: SENTRY_DSN,
-    integrations: [new Integrations.BrowserTracing()],
-    environment: SENTRY_ENV,
-    // Check out https://docs.sentry.io/platforms/javascript/guides/react/configuration/sampling/ for a more refined sample rate
-    tracesSampleRate: 1,
-  });
-};

+ 0 - 37
dashboard/src/legacy/shared/error_handling/window_error_handling.ts

@@ -1,37 +0,0 @@
-import * as Sentry from "@sentry/react";
-import StackTrace from "stacktrace-js";
-
-import { stackFramesToString } from "./stack_trace_utils";
-
-export function EnableErrorHandling() {
-  window.onerror = function (msg, file, line, col, err) {
-    StackTrace.fromError(err).then((stackframes) => {
-      const stackFramesStringify = stackFramesToString(stackframes);
-      // Preserve the old stack just in case
-      const originalStack = err.stack;
-      // Update the error stack with the StackTrace stack (this helps for minified environments)
-      err.stack = stackFramesStringify;
-
-      if (import.meta.env.ENABLE_SENTRY) {
-        Sentry.captureException(err, (scope) => {
-          scope.setTags({
-            error_boundary_location: "window_error_handling",
-            error_message: err?.message,
-          });
-          scope.setContext("Original stack", {
-            originalStack,
-          });
-
-          return scope;
-        });
-      }
-
-      window?.analytics?.track("React Error", {
-        location: "window_error_handling",
-        error: stackFramesStringify,
-        componentStack: err.stack,
-        url: window.location.toString(),
-      });
-    });
-  };
-}

+ 0 - 34
dashboard/src/legacy/shared/hooks/useEffectDebugger.ts

@@ -1,34 +0,0 @@
-import { useEffect } from "react";
-import { usePrevious } from "./usePrevious";
-
-export const useEffectDebugger = (
-  effectHook: any,
-  dependencies: any,
-  dependencyNames: any = []
-) => {
-  const previousDeps = usePrevious(dependencies, []);
-
-  const changedDeps = dependencies.reduce(
-    (accum: any, dependency: any, index: any) => {
-      if (dependency !== previousDeps[index]) {
-        const keyName = dependencyNames[index] || index;
-        return {
-          ...accum,
-          [keyName]: {
-            before: previousDeps[index],
-            after: dependency,
-          },
-        };
-      }
-
-      return accum;
-    },
-    {}
-  );
-
-  if (Object.keys(changedDeps).length) {
-    // console.log("[use-effect-debugger] ", changedDeps);
-  }
-
-  useEffect(effectHook, dependencies);
-};

+ 0 - 24
dashboard/src/legacy/shared/icons/MoonBase.tsx

@@ -1,24 +0,0 @@
-import React from "react";
-
-import { type IconProps } from "./types";
-
-const MoonBaseIcon: React.FC<IconProps> = ({ className, styles, fill }) => {
-  return (
-    <svg
-      viewBox="0 0 24 24"
-      xmlns="http://www.w3.org/2000/svg"
-      className={className}
-      style={styles}
-      fill={fill}
-    >
-      <path
-        d="M21.5999 14.6398C20.6977 14.9134 19.7404 15.0606 18.7487 15.0606C13.3307 15.0606 8.93856 10.6684 8.93856 5.25042C8.93856 4.2592 9.08557 3.30232 9.35897 2.40039C5.33153 3.62177 2.40002 7.36343 2.40002 11.7898C2.40002 17.2078 6.79218 21.5999 12.2102 21.5999C16.637 21.5999 20.3789 18.6679 21.5999 14.6398Z"
-        stroke="black"
-        strokeWidth="2"
-        strokeLinejoin="round"
-      />
-    </svg>
-  );
-};
-
-export default MoonBaseIcon;

+ 0 - 185
dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx

@@ -1,185 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import loadable from "@loadable/component";
-import { withRouter, type RouteComponentProps } from "react-router";
-import { Route, Switch } from "react-router-dom";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-
-import api from "shared/api";
-import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import GuardedRoute from "shared/auth/RouteGuard";
-import { Context } from "shared/Context";
-import { getQueryParam, pushQueryParams, type PorterUrl } from "shared/routing";
-import { type ClusterType } from "shared/types";
-
-import AppDashboard from "./apps/AppDashboard";
-import DashboardRoutes from "./dashboard/Routes";
-import EnvGroupDashboard from "./env-groups/EnvGroupDashboard";
-import ExpandedEnvGroupDashboard from "./env-groups/ExpandedEnvGroupDashboard";
-import ExpandedChartWrapper from "./expanded-chart/ExpandedChartWrapper";
-import JobDashboard from "./jobs/JobDashboard";
-
-const LazyPreviewEnvironmentsRoutes = loadable(
-  // @ts-expect-error
-  async () => await import("./preview-environments/routes.tsx"),
-  {
-    fallback: <Loading />,
-  }
-);
-
-const LazyStackRoutes = loadable(
-  // @ts-expect-error
-  async () => await import("./stacks/routes.tsx"),
-  {
-    fallback: <Loading />,
-  }
-);
-
-type Props = RouteComponentProps &
-  WithAuthProps & {
-    currentCluster: ClusterType;
-    setSidebar: (x: boolean) => void;
-    currentView: PorterUrl;
-  };
-
-// TODO: should try to maintain single source of truth b/w router and context/state (ex: namespace -> being managed in parallel right now so highly inextensible and routing is fragile)
-const DashboardRouter: React.FC<Props> = ({
-  setSidebar,
-  currentView,
-  ...props
-}) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [namespace, setNamespace] = useState(null);
-  const [sortType, setSortType] = useState(
-    localStorage.getItem("SortType") || "Newest"
-  );
-  const [currentChart, setCurrentChart] = useState(null);
-  const [isMetricsInstalled, setIsMetricsInstalled] = useState(false);
-
-  useEffect(() => {
-    // Don't add cluster as query param if present in path
-    const { cluster } = props.match?.params as any;
-    if (!cluster) {
-      pushQueryParams(props, { cluster: currentCluster.name });
-    }
-    api
-      .getPrometheusIsInstalled(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        setIsMetricsInstalled(true);
-      })
-      .catch(() => {
-        setIsMetricsInstalled(false);
-      });
-  }, []);
-
-  // Reset namespace filter and close expanded chart on cluster change
-  useEffect(() => {
-    let namespace = "default";
-    const localStorageNamespace = localStorage.getItem(
-      `${currentProject.id}-${currentCluster.id}-namespace`
-    );
-    if (localStorageNamespace) {
-      namespace = localStorageNamespace;
-    }
-    setNamespace(namespace);
-    setSortType(localStorage.getItem("SortType") || "Newest");
-    setCurrentChart(null);
-
-    // ret2
-    pushQueryParams(props, { namespace });
-  }, [currentCluster]);
-
-  useEffect(() => {
-    let { currentNamespace } =
-      currentProject?.simplified_view_enabled &&
-      currentProject?.capi_provisioner_enabled
-        ? "porter-env-group"
-        : (props.match?.params as any);
-    if (!currentNamespace) {
-      currentNamespace =
-        currentProject?.simplified_view_enabled &&
-        currentProject?.capi_provisioner_enabled
-          ? "porter-env-group"
-          : getQueryParam(props, "namespace");
-    }
-    setSortType("Newest");
-    setCurrentChart(null);
-    setNamespace(currentNamespace || "default");
-    pushQueryParams(props, { namespace: currentNamespace || "default" });
-  }, [currentView]);
-
-  return (
-    <Switch>
-      <Route path={"/stacks"}>
-        <LazyStackRoutes />
-      </Route>
-      <Route path={"/preview-environments"}>
-        <LazyPreviewEnvironmentsRoutes />
-      </Route>
-      <Route path="/:baseRoute/:clusterName+/:namespace/:chartName">
-        <ExpandedChartWrapper
-          setSidebar={setSidebar}
-          isMetricsInstalled={isMetricsInstalled}
-        />
-      </Route>
-      <GuardedRoute
-        path={"/applications"}
-        scope="application"
-        resource=""
-        verb={["get", "list"]}
-      >
-        <AppDashboard
-          currentView={currentView}
-          namespace={namespace}
-          setNamespace={setNamespace}
-          sortType={sortType}
-          setSortType={setSortType}
-        />
-      </GuardedRoute>
-      <GuardedRoute
-        path={"/jobs"}
-        scope="job"
-        resource=""
-        verb={["get", "list"]}
-      >
-        <JobDashboard
-          currentView={currentView}
-          namespace={namespace}
-          setNamespace={setNamespace}
-          sortType={sortType}
-        />
-      </GuardedRoute>
-      <GuardedRoute
-        path={"/env-groups/:name"}
-        scope="env_group"
-        resource=""
-        verb={["get", "list"]}
-      >
-        <ExpandedEnvGroupDashboard currentCluster={currentCluster} />
-      </GuardedRoute>
-      <GuardedRoute
-        path={"/env-groups"}
-        scope="env_group"
-        resource=""
-        verb={["get", "list"]}
-      >
-        <EnvGroupDashboard currentCluster={currentCluster} />
-      </GuardedRoute>
-      <Route path={["/cluster-dashboard"]}>
-        <DashboardRoutes />
-      </Route>
-    </Switch>
-  );
-};
-
-export default withRouter(withAuth(DashboardRouter));
-
-const StyledTemplateComponent = styled.div``;

+ 0 - 57
dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx

@@ -1,57 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-import RadioFilter from "components/RadioFilter";
-
-import { JobStatusType } from "shared/types";
-import last_run from "assets/last-run.svg";
-
-type PropsType = {
-  lastRunStatus: JobStatusType;
-  setLastRunStatus: (lastRunStatus: JobStatusType) => void;
-};
-
-const LastRunStatusSelector = (props: PropsType) => {
-  const options = [
-    {
-      label: "All",
-      value: "all",
-    },
-  ].concat(
-    Object.entries(JobStatusType).map((status) => ({
-      label: status[0],
-      value: status[1],
-    }))
-  );
-
-  return (
-    <RadioFilter
-      selected={props.lastRunStatus}
-      setSelected={props.setLastRunStatus}
-      options={options}
-      name="Last run status"
-      icon={last_run}
-    />
-  );
-};
-
-export default LastRunStatusSelector;
-
-const Label = styled.div`
-  display: flex;
-  align-items: center;
-  min-width: 130px;
-  margin-right: 12px;
-
-  > i {
-    margin-right: 8px;
-    font-size: 18px;
-  }
-`;
-
-const StyledLastRunStatusSelector = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: -3px;
-  font-size: 13px;
-`;

+ 0 - 168
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -1,168 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import RadioFilter from "components/RadioFilter";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import folder from "assets/folder-outline.svg";
-
-type Props = {
-  setNamespace: (x: string) => void;
-  namespace: string;
-};
-
-type StateType = {
-  namespaceOptions: Array<{ label: string; value: string }>;
-};
-
-// TODO: fix update to unmounted component
-export const NamespaceSelector: React.FunctionComponent<Props> = ({
-  setNamespace,
-  namespace,
-}) => {
-  const context = useContext(Context);
-  const _isMounted = true;
-  const [namespaceOptions, setNamespaceOptions] = useState<
-    Array<{
-      label: string;
-      value: string;
-    }>
-  >([]);
-  const [defaultNamespace, setDefaultNamespace] = useState<string>(
-    localStorage.getItem(
-      `${context.currentProject.id}-${context.currentCluster.id}-namespace`
-    )
-  );
-
-  const updateOptions = () => {
-    const { currentCluster, currentProject } = context;
-
-    api
-      .getNamespaces(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (_isMounted) {
-          const namespaceOptions: Array<{ label: string; value: string }> = [
-            // { label: "All", value: "ALL" },
-          ];
-
-          // Set namespace from URL if specified
-          const queryString = window.location.search;
-          const urlParams = new URLSearchParams(queryString);
-          let urlNamespace = urlParams.get("namespace");
-          if (urlNamespace === "ALL") {
-            urlNamespace = "ALL";
-          }
-
-          const availableNamespaces = res.data.filter((namespace: any) => {
-            return namespace.status !== "Terminating";
-          });
-          if (
-            localStorage.getItem(
-              `${context.currentProject.id}-${context.currentCluster.id}-namespace`
-            )
-          ) {
-            setDefaultNamespace(
-              localStorage.getItem(
-                `${context.currentProject.id}-${context.currentCluster.id}-namespace`
-              )
-            );
-          } else {
-            setDefaultNamespace("default");
-          }
-          availableNamespaces.forEach((x: { name: string }, i: number) => {
-            if (
-              currentProject?.capi_provisioner_enabled &&
-              x.name.startsWith("porter-stack-")
-            ) {
-              namespaceOptions.push({
-                label: x.name.replace("porter-stack-", ""),
-                value: x.name,
-              });
-            } else if (!x.name.startsWith("pr-")) {
-              namespaceOptions.push({
-                label: x.name,
-                value: x.name,
-              });
-            }
-            if (x.name === urlNamespace) {
-              setDefaultNamespace(urlNamespace);
-            }
-          });
-          setNamespaceOptions(namespaceOptions);
-        }
-      })
-      .catch((err) => {
-        if (_isMounted) {
-          setNamespaceOptions([]);
-        }
-      });
-  };
-
-  useEffect(() => {
-    const urlParams = new URLSearchParams(window.location.search);
-    const urlNamespace = urlParams.get("namespace");
-    if (
-      urlNamespace === "" ||
-      defaultNamespace === "" ||
-      urlNamespace === "ALL"
-    ) {
-      setNamespace("default");
-    } else if (namespace !== defaultNamespace) {
-      setNamespace(defaultNamespace);
-    }
-  }, [namespaceOptions]);
-
-  useEffect(() => {
-    updateOptions();
-  }, [namespace, context.currentCluster]);
-
-  useEffect(() => {
-    setNamespace(
-      localStorage.getItem(
-        `${context.currentProject.id}-${context.currentCluster.id}-namespace`
-      )
-    );
-  }, [context.currentCluster]);
-
-  const handleSetActive = (namespace: any) => {
-    localStorage.setItem(
-      `${context.currentProject.id}-${context.currentCluster.id}-namespace`,
-      namespace
-    );
-    setNamespace(namespace);
-  };
-
-  return (
-    <RadioFilter
-      icon={folder}
-      selected={namespace}
-      setSelected={handleSetActive}
-      options={namespaceOptions}
-      name="Namespace"
-    />
-  );
-};
-
-const Label = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: 12px;
-  > i {
-    margin-right: 8px;
-    font-size: 18px;
-  }
-`;
-
-const StyledNamespaceSelector = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-`;

+ 0 - 83
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -1,83 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-
-import RadioFilter from "components/RadioFilter";
-
-import { Context } from "shared/Context";
-import sort from "assets/sort.svg";
-
-type PropsType = {
-  setSortType: (x: string) => void;
-  sortType: string;
-  currentView: string;
-};
-
-type StateType = {
-  sortOptions: Array<{ label: string; value: string }>;
-};
-
-// TODO: fix update to unmounted component
-export default class SortSelector extends Component<PropsType, StateType> {
-  state = {
-    sortOptions: [
-      { label: "Newest", value: "Newest" },
-      { label: "Oldest", value: "Oldest" },
-      { label: "Alphabetical", value: "Alphabetical" },
-      { label: "Next Run", value: "Next Run" },
-    ] as Array<{ label: string; value: string }>,
-  };
-
-  getSortOptions() {
-    if (this.props.currentView === "jobs") {
-      return [
-        { label: "Newest", value: "Newest" },
-        { label: "Oldest", value: "Oldest" },
-        { label: "Alphabetical", value: "Alphabetical" },
-        { label: "Next Run", value: "Next Run" },
-      ];
-    }
-
-    return [
-      { label: "Newest", value: "Newest" },
-      { label: "Oldest", value: "Oldest" },
-      { label: "Alphabetical", value: "Alphabetical" },
-    ];
-  }
-
-  render() {
-    return (
-      <StyledSortSelector>
-        <RadioFilter
-          selected={this.props.sortType}
-          setSelected={(sortType: any) => {
-            this.props.setSortType(sortType);
-          }}
-          options={this.getSortOptions()}
-          name="Sort"
-          icon={sort}
-          dropdownAlignRight={false}
-          noMargin
-        />
-      </StyledSortSelector>
-    );
-  }
-}
-
-SortSelector.contextType = Context;
-
-const Label = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: 12px;
-
-  > i {
-    margin-right: 8px;
-    font-size: 18px;
-  }
-`;
-
-const StyledSortSelector = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-`;

+ 0 - 70
dashboard/src/main/home/cluster-dashboard/TagFilter.tsx

@@ -1,70 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import RadioFilter from "components/RadioFilter";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import tag from "assets/tag.svg";
-
-const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [selectedTag, setSelectedTag] = useState("none");
-  const [tags, setTags] = useState([]);
-
-  useEffect(() => {
-    let isSubscribed = true;
-    api
-      .getTagsByProjectId("<token>", {}, { project_id: currentProject.id })
-      .then((res) => {
-        const newTags = res.data;
-
-        setTags(newTags);
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [currentProject, currentCluster]);
-
-  useEffect(() => {
-    const currentTag = tags.find((tag) => tag.name === selectedTag);
-    onSelect(currentTag);
-  }, [selectedTag]);
-
-  return (
-    <RadioFilter
-      selected={selectedTag}
-      options={[{ label: "All", value: "none" }].concat(
-        tags.map((tag) => ({
-          value: tag.name,
-          label: tag.name,
-        }))
-      )}
-      setSelected={(newVal: any) => {
-        setSelectedTag(newVal);
-      }}
-      name="Tag"
-      icon={tag}
-    />
-  );
-};
-
-export default TagFilter;
-
-const Label = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: 12px;
-
-  > i {
-    margin-right: 8px;
-    font-size: 18px;
-  }
-`;
-
-const StyledTagSelector = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-`;

+ 0 - 198
dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx

@@ -1,198 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
-import Spacer from "components/porter/Spacer";
-
-import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { Context } from "shared/Context";
-import { pushFiltered, pushQueryParams, type PorterUrl } from "shared/routing";
-import web from "assets/web.png";
-
-import ChartList from "../chart/ChartList";
-import DashboardHeader from "../DashboardHeader";
-import { NamespaceSelector } from "../NamespaceSelector";
-import SortSelector from "../SortSelector";
-import TagFilter from "../TagFilter";
-
-type Props = RouteComponentProps &
-  WithAuthProps & {
-    currentView: PorterUrl;
-    namespace?: string;
-    setNamespace?: (namespace: string) => void;
-    sortType: any;
-    setSortType: (sortType: any) => void;
-  };
-
-// TODO: Pull namespace (and sort) down out of DashboardRouter
-const AppDashboard: React.FC<Props> = ({
-  currentView,
-  namespace,
-  setNamespace,
-  sortType,
-  setSortType,
-  ...props
-}) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [selectedTag, setSelectedTag] = useState("none");
-
-  useEffect(() => {
-    if (currentProject?.simplified_view_enabled) {
-      window.location.replace("/apps");
-    }
-  }, [currentProject]);
-
-  return (
-    <StyledAppDashboard>
-      <DashboardHeader
-        image={web}
-        title={currentView}
-        description="Continuously running web services, workers, and add-ons."
-        disableLineBreak
-      />
-      {currentCluster.status === "UPDATING_UNAVAILABLE" ? (
-        <ClusterProvisioningPlaceholder />
-      ) : (
-        <>
-          <ControlRow>
-            <FilterWrapper>
-              <SortSelector
-                setSortType={setSortType}
-                sortType={sortType}
-                currentView={currentView}
-              />
-              <Spacer inline width="10px" />
-              <NamespaceSelector
-                setNamespace={(x) => {
-                  setNamespace(x);
-                  pushQueryParams(props, {
-                    namespace: x || "ALL",
-                  });
-                }}
-                namespace={namespace}
-              />
-              <TagFilter onSelect={setSelectedTag} />
-            </FilterWrapper>
-            <Flex>
-              {props.isAuthorized("namespace", [], ["get", "create"]) && (
-                <Button
-                  onClick={() => {
-                    pushFiltered(props, "/launch", ["project_id"]);
-                  }}
-                >
-                  <i className="material-icons">add</i> Launch template
-                </Button>
-              )}
-            </Flex>
-          </ControlRow>
-          <ChartList
-            currentView={currentView}
-            currentCluster={currentCluster}
-            namespace={namespace}
-            sortType={sortType}
-            selectedTag={selectedTag}
-          />
-        </>
-      )}
-    </StyledAppDashboard>
-  );
-};
-
-export default withRouter(withAuth(AppDashboard));
-
-const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
-  padding: 0 10px;
-  color: ${(props) => (props.selected ? "" : "#494b4f")};
-  border: 1px solid #494b4f;
-  height: 100%;
-  display: flex;
-  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
-  align-items: center;
-  border-radius: ${(props) =>
-    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
-  :hover {
-    border: 1px solid #7a7b80;
-    z-index: 999;
-  }
-`;
-
-const ToggleButton = styled.div`
-  background: #26292e;
-  border-radius: 5px;
-  font-size: 13px;
-  height: 30px;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  border-bottom: 30px solid transparent;
-`;
-
-const StyledAppDashboard = styled.div``;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-`;
-
-const FilterWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  border-bottom: 30px solid transparent;
-  > div:not(:first-child) {
-  }
-`;
-
-const Button = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-left: 10px;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  font-weight: 500;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  min-width: 155px;
-  padding-right: 13px;
-  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 HidableElement = styled.div<{ show: boolean }>`
-  display: ${(props) => (props.show ? "unset" : "none")};
-`;

+ 0 - 403
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -1,403 +0,0 @@
-import React, {
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useState,
-} from "react";
-import { Tooltip, Zoom } from "@material-ui/core";
-import {
-  createTheme,
-  MuiThemeProvider,
-} from "@material-ui/core/styles";
-import CronParser from "cron-parser";
-import _ from "lodash";
-import { useHistory, useLocation, useRouteMatch } from "react-router";
-import { Link } from "react-router-dom";
-import styled from "styled-components";
-
-import StatusIndicator from "components/StatusIndicator";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { readableDate } from "shared/string_utils";
-import {
-  JobStatusType,
-  type ChartType,
-  type JobStatusWithTimeType,
-} from "shared/types";
-
-type Props = {
-  chart: ChartType;
-  controllers: Record<string, any>;
-  jobStatus: JobStatusWithTimeType;
-  isJob: boolean;
-  closeChartRedirectUrl?: string;
-};
-
-const theme = createTheme({
-  overrides: {
-    MuiTooltip: {
-      tooltip: {
-        backgroundColor: "#3E3F44",
-        border: "1px solid #ffffff33",
-      },
-    },
-  },
-});
-
-const Chart: React.FunctionComponent<Props> = ({
-  chart,
-  controllers,
-  jobStatus,
-  isJob,
-  closeChartRedirectUrl,
-}) => {
-  const [chartControllers, setChartControllers] = useState<any>([]);
-  const [showDescription, setShowDescription] = useState(false);
-  const context = useContext(Context);
-  const location = useLocation();
-  const history = useHistory();
-  const match = useRouteMatch();
-
-  const renderIcon = () => {
-    if (chart.chart.metadata.icon && chart.chart.metadata.icon !== "") {
-      return <Icon src={chart.chart.metadata.icon} />;
-    } else {
-      return <i className="material-icons">tonality</i>;
-    }
-  };
-
-  const getControllerForChart = async (chart: ChartType) => {
-    try {
-      const { currentCluster, currentProject } = context;
-      const res = await api.getChartControllers(
-        "<token>",
-        {},
-        {
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          revision: chart.version,
-          id: currentProject.id,
-        }
-      );
-
-      const controllersUid = res.data.map((c: any) => {
-        return c.metadata.uid;
-      });
-      setChartControllers(controllersUid);
-    } catch (error) {
-      context.setCurrentError(JSON.stringify(error));
-    }
-  };
-
-  useEffect(() => {
-    getControllerForChart(chart);
-  }, [chart]);
-
-  const filteredControllers = useMemo(() => {
-    const tmpControllers: any = {};
-    chartControllers.forEach((uid: any) => {
-      if (!controllers[uid]) {
-        return;
-      }
-      tmpControllers[uid] = controllers[uid];
-    });
-    return tmpControllers;
-  }, [chartControllers, controllers]);
-
-  let interval = null;
-  if (chart?.config?.schedule?.enabled) {
-    interval = CronParser.parseExpression(chart?.config?.schedule.value, {
-      utc: true,
-    });
-  }
-
-  // @ts-expect-error
-  const rtf = new Intl.DateTimeFormat("en", {
-    localeMatcher: "best fit", // other values: "lookup"
-    // @ts-expect-error
-    dateStyle: "full",
-    timeStyle: "long",
-  });
-
-  const getExpandedChartLinkURL = useCallback(() => {
-    const params = new Proxy(new URLSearchParams(window.location.search), {
-      get: (searchParams, prop: string) => searchParams.get(prop),
-    });
-
-    const cluster = context.currentCluster?.name;
-
-    const route = `${isJob ? "/jobs" : "/applications"}/${cluster}/${
-      chart.namespace
-    }/${chart.name}`;
-
-    const newParams = {
-      // @ts-expect-error
-      project_id: params.project_id,
-      closeChartRedirectUrl,
-    };
-
-    const newURLSearchParams = new URLSearchParams(
-      _.omitBy(newParams, _.isNil)
-    );
-
-    return `${route}?${newURLSearchParams.toString()}`;
-  }, [chart, context.currentCluster, isJob, closeChartRedirectUrl]);
-
-  return (
-    <Link to={getExpandedChartLinkURL}>
-      <StyledChart>
-        <Title>
-          <IconWrapper>{renderIcon()}</IconWrapper>
-          {chart.canonical_name === "" ? chart.name : chart.canonical_name}
-          {chart?.config?.description && (
-            <>
-              <Dot style={{ marginLeft: "9px", color: "#ffffff88" }}>•</Dot>
-              <MuiThemeProvider theme={theme}>
-                <Tooltip
-                  TransitionComponent={Zoom}
-                  placement={"bottom-start"}
-                  title={
-                    <div
-                      style={{
-                        fontFamily: "Work Sans, sans-serif",
-                        fontSize: "12px",
-                        fontWeight: "normal",
-                        padding: "5px 6px",
-                        color: "#ffffffdd",
-                        lineHeight: "16px",
-                      }}
-                    >
-                      {chart.config.description as string}
-                    </div>
-                  }
-                >
-                  <Description>{chart.config.description}</Description>
-                </Tooltip>
-              </MuiThemeProvider>
-            </>
-          )}
-        </Title>
-
-        <BottomWrapper>
-          <InfoWrapper>
-            <StatusIndicator
-              controllers={filteredControllers}
-              status={chart.info.status}
-              margin_left={"17px"}
-            />
-            <LastDeployed>
-              {jobStatus?.status ? (
-                <>
-                  <Dot>•</Dot>
-                  <JobStatus status={jobStatus.status}>
-                    {jobStatus.status === JobStatusType.Running
-                      ? "Started running"
-                      : `Last run ${jobStatus.status}`}{" "}
-                    at {readableDate(jobStatus.start_time)}
-                  </JobStatus>
-                </>
-              ) : (
-                <>
-                  <Dot>•</Dot>
-                  <JobStatus>
-                    Last deployed {readableDate(chart.info.last_deployed)}
-                  </JobStatus>
-                </>
-              )}
-              {chart.config?.schedule?.enabled ? (
-                <>
-                  <Dot style={{ marginLeft: "10px" }}>•</Dot>
-                  <JobStatus>
-                    Next run{" "}
-                    {rtf.format(interval?.next().toDate() || new Date())}
-                  </JobStatus>
-                </>
-              ) : null}
-            </LastDeployed>
-          </InfoWrapper>
-
-          <TagWrapper>
-            Namespace
-            <NamespaceTag>
-              {context.currentProject?.capi_provisioner_enabled &&
-              chart.namespace.startsWith("porter-stack-")
-                ? chart.namespace.replace("porter-stack-", "")
-                : chart.namespace}
-            </NamespaceTag>
-          </TagWrapper>
-        </BottomWrapper>
-
-        <TopRightContainer>
-          <span>v{chart.version}</span>
-        </TopRightContainer>
-      </StyledChart>
-    </Link>
-  );
-};
-
-export default Chart;
-
-const Description = styled.div`
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  max-width: 80%;
-  color: #ffffff88;
-  position: relative;
-  font-size: 13px;
-  padding-top: 1px;
-`;
-
-const BottomWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding-right: 11px;
-  margin-top: 3px;
-`;
-
-const TopRightContainer = styled.div`
-  position: absolute;
-  top: 12px;
-  right: 12px;
-  font-size: 12px;
-  color: #aaaabb;
-`;
-
-const Dot = styled.div`
-  margin-right: 9px;
-`;
-
-const StatusDot = styled.span`
-  margin: 0 9px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-right: 8px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 10px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 5px;
-`;
-
-const NamespaceTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #ffffff22;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const Icon = styled.img`
-  width: 100%;
-`;
-
-const IconWrapper = styled.div`
-  color: #efefef;
-  background: none;
-  font-size: 16px;
-  top: 11px;
-  left: 14px;
-  height: 20px;
-  width: 20px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 3px;
-  position: absolute;
-
-  > i {
-    font-size: 17px;
-    margin-top: -1px;
-  }
-`;
-
-const Title = styled.div`
-  display: flex;
-  position: relative;
-  text-decoration: none;
-  padding: 12px 35px 12px 45px;
-  font-size: 14px;
-  font-family: "Work Sans", sans-serif;
-  color: ${(props) => props.theme.text.primary};
-  width: 80%;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  animation: fadeIn 0.5s;
-
-  > img {
-    background: none;
-    top: 12px;
-    left: 13px;
-
-    padding: 5px 4px;
-    width: 24px;
-    position: absolute;
-  }
-`;
-
-const JobStatus = styled.span<{ status?: JobStatusType }>`
-  font-size: 13px;
-  font-weight: ${(props) =>
-    props.status && props.status !== JobStatusType.Running ? "500" : ""};
-  ${(props) => `
-  color: ${
-    props.status === JobStatusType.Succeeded
-      ? "rgb(56, 168, 138)"
-      : props.status === JobStatusType.Failed
-      ? "#ff385d"
-      : "#aaaabb66"
-  }`}
-`;
-
-const StyledChart = styled.div`
-  cursor: pointer;
-  margin-bottom: 15px;
-  padding-top: 2px;
-  padding-bottom: 13px;
-  position: relative;
-  width: calc(100% + 2px);
-  height: calc(100% + 2px);
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;

+ 0 - 506
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -1,506 +0,0 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import CronParser from "cron-parser";
-import _ from "lodash";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import Placeholder from "components/Placeholder";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { useWebsockets } from "shared/hooks/useWebsockets";
-import { type PorterUrl } from "shared/routing";
-import {
-  JobStatusType,
-  type ChartType,
-  type ClusterType,
-  type JobStatusWithTimeType,
-} from "shared/types";
-
-import Chart from "./Chart";
-
-type Props = {
-  currentCluster: ClusterType;
-  lastRunStatus?: JobStatusType | null | "all";
-  namespace: string;
-  // TODO Convert to enum
-  sortType: string;
-  currentView: PorterUrl;
-  disableBottomPadding?: boolean;
-  closeChartRedirectUrl?: string;
-  selectedTag?: any;
-  appFilters?: string[];
-  noPlaceholder?: boolean;
-};
-
-type JobStatusWithTimeAndVersion = {
-  resource_version: number;
-} & JobStatusWithTimeType;
-
-const ChartList: React.FunctionComponent<Props> = ({
-  lastRunStatus,
-  namespace,
-  sortType,
-  currentView,
-  disableBottomPadding,
-  closeChartRedirectUrl,
-  selectedTag,
-  appFilters,
-  noPlaceholder,
-}) => {
-  const { newWebsocket, openWebsocket, closeWebsocket, closeAllWebsockets } =
-    useWebsockets();
-  const [charts, setCharts] = useState<ChartType[]>([]);
-  const [controllers, setControllers] = useState<
-    Record<string, Record<string, any>>
-  >({});
-  const [jobStatus, setJobStatus] = useState<
-    Record<string, JobStatusWithTimeAndVersion>
-  >({});
-  const [isLoading, setIsLoading] = useState(false);
-  const [isError, setIsError] = useState(false);
-
-  const context = useContext(Context);
-
-  const getChartKey = (name: string, namespace: string) =>
-    `${namespace}-${name}`;
-
-  const updateCharts = async () => {
-    try {
-      const { currentCluster, currentProject } = context;
-      setIsLoading(true);
-      const res = await api.getCharts(
-        "<token>",
-        {
-          limit: 50,
-          skip: 0,
-          byDate: false,
-          statusFilter: [
-            "deployed",
-            "uninstalled",
-            "pending",
-            "pending-install",
-            "pending-upgrade",
-            "pending-rollback",
-            "failed",
-          ],
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace,
-        }
-      );
-      const charts = res.data || [];
-      setIsError(false);
-      return charts;
-    } catch (error) {
-      console.log(error);
-      context.setCurrentError(JSON.stringify(error));
-      setIsError(true);
-    }
-  };
-
-  const setupHelmReleasesWebsocket = (
-    websocketID: string,
-    namespace: string
-  ) => {
-    let apiPath = `/api/projects/${context.currentProject.id}/clusters/${context.currentCluster.id}/helm_release`;
-
-    if (namespace) {
-      apiPath += `?namespace=${namespace}`;
-    }
-
-    const wsConfig = {
-      onopen: () => {
-        console.log(`connected to websocket: ${websocketID}`);
-      },
-      onmessage: (evt: MessageEvent) => {
-        const event = JSON.parse(evt.data);
-        const newChart: ChartType = event.Object;
-        const isSameChart = (chart: ChartType) =>
-          getChartKey(chart.name, chart.namespace) ===
-          getChartKey(newChart.name, newChart.namespace);
-        setCharts((currentCharts) => {
-          const tmpCharts = Array.isArray(currentCharts)
-            ? [...currentCharts]
-            : [];
-          switch (event.event_type) {
-            case "ADD":
-              if (tmpCharts.find(isSameChart)) {
-                return tmpCharts;
-              }
-              return tmpCharts.concat(newChart);
-            case "UPDATE":
-              return tmpCharts.map((chart) => {
-                if (isSameChart(chart) && newChart.version >= chart.version) {
-                  return newChart;
-                }
-                return chart;
-              });
-            case "DELETE":
-              const chartToDelete = tmpCharts.find(isSameChart);
-              if (chartToDelete.version === newChart.version) {
-                return tmpCharts.filter((chart) => !isSameChart(chart));
-              }
-            default:
-              return tmpCharts;
-          }
-        });
-      },
-
-      onclose: () => {
-        console.log(`closing websocket: ${websocketID}`);
-      },
-
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(websocketID);
-      },
-    };
-
-    newWebsocket(websocketID, apiPath, wsConfig);
-    openWebsocket(websocketID);
-  };
-
-  const setupControllerWebsocket = (kind: string) => {
-    const { currentCluster, currentProject } = context;
-    const apiPath = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status`;
-
-    const wsConfig = {
-      onopen: () => {
-        console.log(`connected to websocket: ${kind}`);
-      },
-      onmessage: (evt: MessageEvent) => {
-        const event = JSON.parse(evt.data);
-        const object = event?.Object;
-
-        if (!object?.metadata?.kind) {
-          return;
-        }
-
-        object.metadata.kind = event.Kind;
-
-        setControllers((oldControllers) => ({
-          ...oldControllers,
-          [object.metadata.uid]: object,
-        }));
-      },
-      onclose: () => {
-        console.log(`closing websocket: ${kind}`);
-      },
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(kind);
-      },
-    };
-
-    newWebsocket(kind, apiPath, wsConfig);
-
-    openWebsocket(kind);
-  };
-
-  const setupControllerWebsockets = (controllers: string[]) => {
-    controllers.map((kind) => {
-      setupControllerWebsocket(kind);
-    });
-  };
-
-  const setupJobWebsocket = (websocketID: string) => {
-    const kind = "job";
-    const { currentCluster, currentProject } = context;
-    const apiPath = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status`;
-
-    const wsConfig = {
-      onopen: () => {
-        console.log(`connected to websocket: ${websocketID}`);
-      },
-      onmessage: (evt: MessageEvent) => {
-        const event = JSON.parse(evt.data);
-
-        if (event.event_type === "DELETE") {
-          return;
-        }
-
-        const object = event.Object;
-
-        if (_.get(object.metadata, ["annotations", "helm.sh/hook"])) {
-          return;
-        }
-
-        setJobStatus((currentStatus) => {
-          let nextStatus: JobStatusType = null;
-          for (const status of Object.values(JobStatusType)) {
-            if (_.get(object.status, status, 0) > 0) {
-              nextStatus = status;
-              break;
-            }
-          }
-
-          const chartName =
-            object.metadata.labels["app.kubernetes.io/instance"];
-          const chartNamespace = object.metadata.namespace;
-          const key = getChartKey(chartName, chartNamespace);
-
-          const existingValue: JobStatusWithTimeAndVersion = _.get(
-            currentStatus,
-            key,
-            null
-          );
-          const newValue: JobStatusWithTimeAndVersion = {
-            status: nextStatus,
-            start_time: object.status.startTime,
-            resource_version: object.metadata.resourceVersion,
-          };
-
-          if (
-            !existingValue ||
-            Number(newValue.resource_version) >
-              Number(existingValue.resource_version)
-          ) {
-            return {
-              ...currentStatus,
-              [key]: newValue,
-            };
-          }
-
-          return currentStatus;
-        });
-      },
-      onclose: () => {
-        console.log(`closing websocket: ${websocketID}`);
-      },
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(websocketID);
-      },
-    };
-
-    newWebsocket(websocketID, apiPath, wsConfig);
-
-    openWebsocket(websocketID);
-  };
-
-  // Setup basic websockets on start
-  useEffect(() => {
-    const controllers = [
-      "deployment",
-      "statefulset",
-      "daemonset",
-      "replicaset",
-    ];
-    setupControllerWebsockets(controllers);
-
-    const jobWebsocketID = "job";
-    setupJobWebsocket(jobWebsocketID);
-
-    return () => {
-      controllers.map((controller) => {
-        closeWebsocket(controller);
-      });
-      closeWebsocket(jobWebsocketID);
-    };
-  }, [context.currentCluster]);
-
-  useEffect(() => {
-    const websocketID = "helm_releases";
-
-    setupHelmReleasesWebsocket(websocketID, namespace);
-
-    return () => {
-      closeWebsocket(websocketID);
-    };
-  }, [namespace, context.currentCluster]);
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    if (namespace || namespace === "") {
-      updateCharts().then((charts) => {
-        if (isSubscribed) {
-          setCharts(charts);
-          setIsLoading(false);
-        }
-      });
-    }
-    return () => {
-      isSubscribed = false;
-    };
-  }, [namespace, currentView, context.currentCluster]);
-
-  const filteredCharts = useMemo(() => {
-    if (!Array.isArray(charts)) {
-      return [];
-    }
-
-    const result = charts
-      .filter((chart) => {
-        if (!selectedTag) {
-          return true;
-        }
-
-        return !!selectedTag.releases?.find((release: ChartType) => {
-          return release.name === chart.name;
-        });
-      })
-      .filter((chart: ChartType) => {
-        if (currentView === "jobs" && chart.chart.metadata.name === "job") {
-          return true;
-        }
-
-        if (
-          ["applications", "cluster-dashboard"].includes(currentView) &&
-          chart.chart.metadata.name !== "job"
-        ) {
-          return true;
-        }
-
-        if (currentView === "stacks") {
-          return true;
-        }
-
-        return false;
-      })
-      .filter((chart: ChartType) => {
-        if (currentView !== "jobs") {
-          return true;
-        }
-        if (lastRunStatus === null || lastRunStatus === "all") {
-          return true;
-        }
-        const status: JobStatusWithTimeAndVersion = _.get(
-          jobStatus,
-          getChartKey(chart.name, chart.namespace),
-          { status: null } as any
-        );
-        return status.status === lastRunStatus;
-      })
-      .filter((chart: ChartType) => {
-        if (!Array.isArray(appFilters) || appFilters?.length === 0) {
-          return true;
-        }
-
-        return appFilters.some((filter) => {
-          return chart.name.toLowerCase() === filter.toLowerCase();
-        });
-      });
-
-    if (sortType == "Newest") {
-      result.sort((a: any, b: any) =>
-        Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
-          ? -1
-          : 1
-      );
-    } else if (sortType == "Oldest") {
-      result.sort((a: any, b: any) =>
-        Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
-          ? 1
-          : -1
-      );
-    } else if (sortType == "Alphabetical") {
-      result.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-    } else if (sortType == "Next Run" && currentView === "jobs") {
-      const cronJobs = result.filter(
-        (chart) => chart?.config?.schedule?.enabled
-      );
-      const nonCronJobs = result.filter(
-        (chart) => !chart?.config?.schedule?.enabled
-      );
-      cronJobs.sort((a: any, b: any) => {
-        let firstInterval = null;
-        if (a?.config?.schedule?.enabled) {
-          firstInterval = CronParser.parseExpression(
-            a?.config?.schedule.value,
-            {
-              utc: true,
-            }
-          );
-        }
-
-        let secondInterval = null;
-        if (b?.config?.schedule?.enabled) {
-          secondInterval = CronParser.parseExpression(
-            b?.config?.schedule.value,
-            {
-              utc: true,
-            }
-          );
-        }
-
-        return Date.parse(firstInterval.next().toISOString()) >
-          Date.parse(secondInterval.next().toISOString())
-          ? 1
-          : -1;
-      });
-
-      return [...cronJobs, ...nonCronJobs];
-    }
-
-    return result;
-  }, [charts, sortType, jobStatus, lastRunStatus, selectedTag]);
-
-  const renderChartList = () => {
-    if (!noPlaceholder) {
-      if (isLoading || (!namespace && namespace !== "")) {
-        return (
-          <LoadingWrapper>
-            <Loading />
-          </LoadingWrapper>
-        );
-      } else if (isError) {
-        return (
-          <Placeholder height="370px">
-            <i className="material-icons">error</i> Error connecting to cluster.
-          </Placeholder>
-        );
-      } else if (filteredCharts?.length === 0) {
-        return (
-          <Placeholder height="370px">
-            <i className="material-icons">category</i> No
-            {currentView === "jobs" ? ` jobs` : ` charts`} found with the given
-            filters.
-          </Placeholder>
-        );
-      }
-    }
-
-    return filteredCharts?.map((chart: ChartType, i: number) => {
-      return (
-        <Chart
-          key={getChartKey(chart.name, chart.namespace)}
-          chart={chart}
-          controllers={controllers || {}}
-          jobStatus={_.get(
-            jobStatus,
-            getChartKey(chart.name, chart.namespace),
-            null
-          )}
-          isJob={currentView === "jobs"}
-          closeChartRedirectUrl={closeChartRedirectUrl}
-        />
-      );
-    });
-  };
-
-  return (
-    <StyledChartList disableBottomPadding={disableBottomPadding}>
-      {renderChartList()}
-    </StyledChartList>
-  );
-};
-
-export default ChartList;
-
-const LoadingWrapper = styled.div`
-  padding-top: 100px;
-`;
-
-const StyledChartList = styled.div`
-  padding-bottom: ${(props: { disableBottomPadding: boolean }) => {
-    if (props.disableBottomPadding) {
-      return "unset";
-    }
-
-    return "105px";
-  }};
-`;

+ 0 - 541
dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx

@@ -1,541 +0,0 @@
-import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
-import { type CellProps, type Column } from "react-table";
-import styled from "styled-components";
-
-import DynamicLink from "components/DynamicLink";
-import Loading from "components/Loading";
-import Table from "components/OldTable";
-
-import { Context } from "shared/Context";
-import {
-  useWebsockets,
-  type NewWebsocketOptions,
-} from "shared/hooks/useWebsockets";
-import { relativeDate, timeFrom } from "shared/string_utils";
-
-type Props = {
-  lastRunStatus: "failed" | "succeeded" | "active" | "all";
-  namespace: string;
-  sortType: "Newest" | "Oldest" | "Alphabetical";
-};
-
-const runnedFor = (start: string | number, end?: string | number) => {
-  const duration = timeFrom(start, end);
-
-  const unit =
-    duration.time === 1
-      ? duration.unitOfTime.substring(0, duration.unitOfTime.length - 1)
-      : duration.unitOfTime;
-
-  return `${duration.time} ${unit}`;
-};
-
-const JobRunTable: React.FC<Props> = ({
-  lastRunStatus,
-  namespace,
-  sortType,
-}) => {
-  const { currentCluster, currentProject } = useContext(Context);
-  const [jobRuns, setJobRuns] = useState<JobRun[]>(null);
-  const [hasError, setHasError] = useState(false);
-  const tmpJobRuns = useRef([]);
-  const lastStreamStatus = useRef("");
-  const { openWebsocket, newWebsocket, closeAllWebsockets } = useWebsockets();
-
-  const getJobRuns = () => {
-    closeAllWebsockets();
-    tmpJobRuns.current = [];
-    lastStreamStatus.current = "";
-    setJobRuns(null);
-    setHasError(false);
-    const websocketId = `job-runs-for-all-charts-ws`;
-    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/jobs/stream`;
-
-    const config: NewWebsocketOptions = {
-      onopen: console.log,
-      onmessage: (message) => {
-        const data = JSON.parse(message.data);
-
-        if (data.streamStatus === "finished") {
-          setHasError(false);
-          setJobRuns(tmpJobRuns.current);
-          lastStreamStatus.current = data.streamStatus;
-          return;
-        }
-
-        if (data.streamStatus === "errored") {
-          setHasError(true);
-          tmpJobRuns.current = [];
-          setJobRuns([]);
-          return;
-        }
-
-        tmpJobRuns.current = [...tmpJobRuns.current, data];
-      },
-      onclose: (event) => {
-        // console.log(event);
-        closeAllWebsockets();
-      },
-      onerror: (error) => {
-        setHasError(true);
-        console.log(error);
-        closeAllWebsockets();
-      },
-    };
-    newWebsocket(websocketId, endpoint, config);
-    openWebsocket(websocketId);
-  };
-
-  useEffect(() => {
-    if (!namespace) {
-      return;
-    }
-
-    getJobRuns();
-  }, [currentCluster, currentProject, namespace]);
-
-  useEffect(() => {
-    return () => {
-      closeAllWebsockets();
-    };
-  }, []);
-
-  const columns = useMemo<Array<Column<JobRun>>>(
-    () => [
-      {
-        Header: "Namespace / Name",
-        accessor: (originalRow) => {
-          const owners = originalRow.metadata.ownerReferences;
-          let name = "N/A";
-          if (Array.isArray(owners)) {
-            name = owners[0]?.name;
-          }
-          if (originalRow?.metadata?.labels["meta.helm.sh/release-name"]) {
-            name = originalRow.metadata.labels["meta.helm.sh/release-name"];
-          }
-
-          if (name !== "N/A") {
-            return originalRow.metadata?.namespace + "/" + name;
-          }
-
-          return name;
-        },
-        width: "max-content",
-      },
-      {
-        Header: "Run at",
-        accessor: (originalRow) => relativeDate(originalRow.status.startTime),
-      },
-      {
-        Header: "Run for",
-        accessor: (originalRow) => {
-          if (originalRow.status?.completionTime) {
-            return originalRow.status?.completionTime;
-          } else if (
-            Array.isArray(originalRow.status?.conditions) &&
-            originalRow.status?.conditions[0]?.lastTransitionTime
-          ) {
-            return originalRow.status?.conditions[0]?.lastTransitionTime;
-          } else {
-            return "Still running...";
-          }
-        },
-        Cell: ({ row }: CellProps<JobRun>) => {
-          if (row.original.status?.completionTime) {
-            return runnedFor(
-              row.original.status?.startTime,
-              row.original.status?.completionTime
-            );
-          } else if (
-            Array.isArray(row.original.status?.conditions) &&
-            row.original.status?.conditions[0]?.lastTransitionTime
-          ) {
-            return runnedFor(
-              row.original.status?.startTime,
-              row.original.status?.conditions[0]?.lastTransitionTime
-            );
-          } else {
-            return "Still running...";
-          }
-        },
-        styles: {
-          padding: "10px",
-        },
-      },
-      {
-        Header: "Status",
-        id: "status",
-        Cell: ({ row }: CellProps<JobRun>) => {
-          if (row.original.status?.succeeded >= 1) {
-            return <Status color="#38a88a">Succeeded</Status>;
-          }
-
-          if (row.original.status?.failed >= 1) {
-            return <Status color="#cc3d42">Failed</Status>;
-          }
-
-          return <Status color="#ffffff11">Running</Status>;
-        },
-      },
-      {
-        Header: "Image tag",
-        id: "commit_or_image_tag",
-        accessor: (originalRow) => {
-          const container = originalRow.spec?.template?.spec?.containers[0];
-          return container?.image?.split(":")[1] || "N/A";
-        },
-        Cell: ({ row }: CellProps<JobRun>) => {
-          const container = row.original.spec?.template?.spec?.containers[0];
-
-          const tag = container?.image?.split(":")[1];
-          return tag;
-        },
-      },
-      {
-        Header: "Command",
-        id: "command",
-        accessor: (originalRow) => {
-          const container = originalRow.spec?.template?.spec?.containers[0];
-          return container?.command?.join(" ") || "N/A";
-        },
-        Cell: ({ row }: CellProps<JobRun>) => {
-          const container = row.original.spec?.template?.spec?.containers[0];
-
-          return (
-            <CommandString>
-              {container?.command?.join(" ") || "N/A"}
-            </CommandString>
-          );
-        },
-      },
-      {
-        id: "expand",
-        Cell: ({ row }: CellProps<JobRun>) => {
-          /**
-           * project_id: currentProject.id,
-          chart_revision: 0,
-          job: row.original?.metadata?.name,
-           */
-          const urlParams = new URLSearchParams();
-          urlParams.append("project_id", String(currentProject.id));
-          urlParams.append("chart_revision", String(0));
-          urlParams.append("job", row.original.metadata.name);
-
-          return (
-            <RedirectButton
-              to={{
-                pathname: `/jobs/${currentCluster.name}/${row.original?.metadata?.namespace}/${row.original?.metadata?.labels["meta.helm.sh/release-name"]}`,
-                search: urlParams.toString(),
-              }}
-            >
-              <i className="material-icons">open_in_new</i>
-            </RedirectButton>
-          );
-        },
-        maxWidth: 40,
-      },
-    ],
-    []
-  );
-
-  const data = useMemo(() => {
-    if (jobRuns === null) {
-      return [];
-    }
-    let tmp = [...tmpJobRuns.current];
-    const filter = new JobRunsFilter(tmp);
-    switch (lastRunStatus) {
-      case "active":
-        tmp = filter.filterByActive();
-        break;
-      case "failed":
-        tmp = filter.filterByFailed();
-        break;
-      case "succeeded":
-        tmp = filter.filterBySucceded();
-        break;
-      default:
-        tmp = filter.dontFilter();
-        break;
-    }
-
-    const sorter = new JobRunsSorter(tmp);
-    switch (sortType) {
-      case "Alphabetical":
-        tmp = sorter.sortByAlphabetical();
-        break;
-      case "Newest":
-        tmp = sorter.sortByNewest();
-        break;
-      case "Oldest":
-        tmp = sorter.sortByOldest();
-        break;
-      default:
-        break;
-    }
-
-    return tmp;
-  }, [jobRuns, lastRunStatus, sortType]);
-
-  if (hasError && lastStreamStatus.current !== "finished") {
-    return (
-      <ErrorWrapper>
-        Couldn't retrieve jobs, please try again.{" "}
-        <RetryButton
-          onClick={() => {
-            getJobRuns();
-          }}
-        >
-          Retry
-        </RetryButton>
-      </ErrorWrapper>
-    );
-  }
-
-  if (jobRuns === null) {
-    return <Loading />;
-  }
-
-  if (!jobRuns?.length) {
-    return <>No job runs found</>;
-  }
-
-  return (
-    <Table
-      columns={columns}
-      data={data}
-      isLoading={jobRuns === null}
-      enablePagination
-    />
-  );
-};
-
-export default JobRunTable;
-
-const RetryButton = styled.button`
-  margin-left: 10px;
-  border: none;
-  background: #5460c6;
-  color: white;
-  padding: 5px 10px;
-  border-radius: 25px;
-  min-height: 35px;
-  min-width: 65px;
-  cursor: pointer;
-`;
-
-const ErrorWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  min-height: 300px;
-  width: 100%;
-  color: #ffffff88;
-`;
-
-const Status = styled.div<{ color: string }>`
-  padding: 5px 10px;
-  background: ${(props) => props.color};
-  font-size: 13px;
-  border-radius: 3px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: min-content;
-  height: 25px;
-  min-width: 90px;
-`;
-
-const CommandString = styled.div`
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  max-width: 160px;
-  color: #ffffff55;
-  margin-right: 27px;
-  font-family: monospace;
-`;
-
-const RedirectButton = styled(DynamicLink)`
-  user-select: none;
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  > i {
-    border-radius: 20px;
-    font-size: 18px;
-    padding: 5px;
-    margin: 0 5px;
-    color: #ffffff44;
-    :hover {
-      background: #ffffff11;
-    }
-  }
-`;
-
-type JobRun = {
-  metadata: {
-    name: string;
-    namespace: string;
-    selfLink: string;
-    uid: string;
-    resourceVersion: string;
-    creationTimestamp: string;
-    labels: {
-      [key: string]: string;
-      "app.kubernetes.io/instance": string;
-      "app.kubernetes.io/managed-by": string;
-      "app.kubernetes.io/version": string;
-      "helm.sh/chart": string;
-      "helm.sh/revision": string;
-      "meta.helm.sh/release-name": string;
-    };
-    ownerReferences: Array<{
-      apiVersion: string;
-      kind: string;
-      name: string;
-      uid: string;
-      controller: boolean;
-      blockOwnerDeletion: boolean;
-    }>;
-    managedFields: unknown[];
-  };
-  spec: {
-    [key: string]: unknown;
-    parallelism: number;
-    completions: number;
-    backOffLimit?: number;
-    selector: {
-      [key: string]: unknown;
-      matchLabels: {
-        [key: string]: unknown;
-        "controller-uid": string;
-      };
-    };
-    template: {
-      [key: string]: unknown;
-      metadata: {
-        creationTimestamp: string | null;
-        labels: {
-          [key: string]: unknown;
-          "controller-uid": string;
-          "job-name": string;
-        };
-      };
-      spec: {
-        containers: Array<{
-          name: string;
-          image: string;
-          command: string[];
-          env?: Array<{
-            [key: string]: unknown;
-            name: string;
-            value?: string;
-            valueFrom?: {
-              secretKeyRef?: { name: string; key: string };
-              configMapKeyRef?: { name: string; key: string };
-            };
-          }>;
-          resources: {
-            [key: string]: unknown;
-            limits: { [key: string]: unknown; memory: string };
-            requests: { [key: string]: unknown; cpu: string; memory: string };
-          };
-          terminationMessagePath: string;
-          terminationMessagePolicy: string;
-          imagePullPolicy: string;
-        }>;
-
-        restartPolicy: string;
-        terminationGracePeriodSeconds: number;
-        dnsPolicy: string;
-        shareProcessNamespace: boolean;
-        securityContext: unknown;
-        schedulerName: string;
-        tolerations: Array<{
-          [key: string]: unknown;
-          key: string;
-          operator: string;
-          value: string;
-          effect: string;
-        }>;
-      };
-    };
-  };
-  status: {
-    [key: string]: unknown;
-    conditions: Array<{
-      [key: string]: unknown;
-      type: string;
-      status: string;
-      lastProbeTime: string;
-      lastTransitionTime: string;
-    }>;
-    startTime: string;
-    completionTime: string | undefined | null;
-    succeeded?: number;
-    failed?: number;
-    active?: number;
-  };
-};
-
-class JobRunsFilter {
-  jobRuns: JobRun[];
-
-  constructor(newJobRuns: JobRun[]) {
-    this.jobRuns = newJobRuns;
-  }
-
-  filterByFailed() {
-    return this.jobRuns.filter((jobRun) => jobRun?.status?.failed);
-  }
-
-  filterByActive() {
-    return this.jobRuns.filter((jobRun) => jobRun?.status?.active);
-  }
-
-  filterBySucceded() {
-    return this.jobRuns.filter(
-      (jobRun) =>
-        jobRun?.status?.succeeded &&
-        !jobRun?.status?.active &&
-        !jobRun?.status?.failed
-    );
-  }
-
-  dontFilter() {
-    return this.jobRuns;
-  }
-}
-
-class JobRunsSorter {
-  jobRuns: JobRun[];
-
-  constructor(newJobRuns: JobRun[]) {
-    this.jobRuns = newJobRuns;
-  }
-
-  sortByNewest() {
-    return this.jobRuns.sort((a, b) => {
-      return Date.parse(a?.metadata?.creationTimestamp) >
-        Date.parse(b?.metadata?.creationTimestamp)
-        ? -1
-        : 1;
-    });
-  }
-
-  sortByOldest() {
-    return this.jobRuns.sort((a, b) => {
-      return Date.parse(a?.metadata?.creationTimestamp) >
-        Date.parse(b?.metadata?.creationTimestamp)
-        ? 1
-        : -1;
-    });
-  }
-
-  sortByAlphabetical() {
-    return this.jobRuns.sort((a, b) =>
-      a?.metadata?.name > b?.metadata?.name ? 1 : -1
-    );
-  }
-}

+ 0 - 473
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx

@@ -1,473 +0,0 @@
-import React, { useContext, useState } from "react";
-import { type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import CopyToClipboard from "components/CopyToClipboard";
-import CheckboxRow from "components/form-components/CheckboxRow";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import InputRow from "components/form-components/InputRow";
-import Loading from "components/Loading";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { type DetailedIngressError } from "shared/types";
-import { stringifiedDNSRecordType } from "utils/ip";
-
-type Props = RouteComponentProps & {
-  ingressIp: string;
-  ingressError: DetailedIngressError;
-};
-
-const ClusterSettings: React.FC<Props> = (props) => {
-  const {
-    currentProject,
-    currentCluster,
-    setCurrentCluster,
-    setCurrentModal,
-    capabilities,
-  } = useContext(Context);
-  const [newClusterName, setNewClusterName] = useState<string>(
-    currentCluster.name
-  );
-  const [newAWSClusterID, setNewAWSClusterID] = useState<string>(
-    currentCluster.aws_cluster_id
-  );
-  const [successfulRename, setSuccessfulRename] = useState<boolean>(false);
-
-  const [accessKeyId, setAccessKeyId] = useState<string>("");
-  const [secretKey, setSecretKey] = useState<string>("");
-  const [startRotateCreds, setStartRotateCreds] = useState<boolean>(false);
-  const [successfulRotate, setSuccessfulRotate] = useState<boolean>(false);
-
-  const [enableAgent, setEnableAgent] = useState(
-    currentCluster.agent_integration_enabled
-  );
-  const [agentLoading, setAgentLoading] = useState(false);
-  const [enablePreviewEnvs, setEnablePreviewEnvs] = useState(
-    currentCluster.preview_envs_enabled
-  );
-  const [previewEnvsLoading, setPreviewEnvsLoading] = useState(false);
-
-  const rotateCredentials = () => {
-    api
-      .overwriteAWSIntegration(
-        "<token>",
-        {
-          aws_integration_id: currentCluster.aws_integration_id,
-          aws_access_key_id: accessKeyId,
-          aws_secret_access_key: secretKey,
-          cluster_id: currentCluster.id,
-        },
-        {
-          project_id: currentProject.id,
-        }
-      )
-      .then(({ data }) => {
-        setSuccessfulRotate(true);
-      })
-      .catch(() => {
-        setSuccessfulRotate(false);
-      });
-  };
-
-  const updateClusterName = () => {
-    api
-      .updateCluster(
-        "<token>",
-        {
-          name: newClusterName,
-          aws_cluster_id: newAWSClusterID,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then(({ data }) => {
-        setSuccessfulRename(true);
-      })
-      .catch(() => {
-        setSuccessfulRename(false);
-      });
-  };
-
-  const updateAgentIntegrationEnabled = () => {
-    setAgentLoading(true);
-
-    api
-      .updateCluster(
-        "<token>",
-        {
-          agent_integration_enabled: enableAgent,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then(({ data }) => {
-        setCurrentCluster(data);
-        setAgentLoading(false);
-      })
-      .catch(() => {
-        setAgentLoading(false);
-      });
-  };
-
-  const updatePreviewEnvironmentsEnabled = () => {
-    setPreviewEnvsLoading(true);
-
-    api
-      .updateCluster(
-        "<token>",
-        {
-          preview_envs_enabled: enablePreviewEnvs,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then(({ data }) => {
-        setCurrentCluster(data);
-        setPreviewEnvsLoading(false);
-      })
-      .catch(() => {
-        setPreviewEnvsLoading(false);
-      });
-  };
-
-  let helperText = (
-    <Helper>
-      Delete this cluster and underlying infrastructure. To ensure that
-      everything has been properly destroyed, please visit your cloud provider's
-      console. Instructions to properly delete all resources can be found
-      <a
-        target="none"
-        href="https://docs.porter.run/other/deleting-dangling-resources"
-      >
-        {" "}
-        here
-      </a>
-      . Contact support@porter.run if you need guidance.
-    </Helper>
-  );
-
-  if (
-    (!currentCluster?.infra_id && !currentProject?.capi_provisioner_enabled) ||
-    !currentCluster?.service
-  ) {
-    helperText = (
-      <Helper>
-        Remove this cluster from Porter. Since this cluster was not provisioned
-        by Porter, deleting the cluster will only detach this cluster from your
-        project. To delete the cluster itself, you must do so manually. This
-        operation cannot be undone. Contact support@porter.run if you need
-        guidance.
-      </Helper>
-    );
-  }
-
-  let keyRotationSection = null;
-
-  if (
-    currentCluster?.aws_integration_id &&
-    currentCluster?.aws_integration_id != 0
-  ) {
-    if (successfulRotate) {
-      keyRotationSection = (
-        <div>
-          <Heading>Credential rotation</Heading>
-          <Helper>Successfully rotated credentials!</Helper>
-        </div>
-      );
-    } else if (startRotateCreds) {
-      keyRotationSection = (
-        <div>
-          <Heading>Credential rotation</Heading>
-          <Helper>Input the new credentials for the EKS cluster.</Helper>
-          <InputRow
-            type="text"
-            value={accessKeyId}
-            setValue={(x: string) => {
-              setAccessKeyId(x);
-            }}
-            label="👤 AWS Access ID"
-            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
-            width="100%"
-            isRequired={true}
-          />
-          <InputRow
-            type="password"
-            value={secretKey}
-            setValue={(x: string) => {
-              setSecretKey(x);
-            }}
-            label="🔒 AWS Secret Key"
-            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-            width="100%"
-            isRequired={true}
-          />
-          <Button color="#616FEEcc" onClick={rotateCredentials}>
-            Submit
-          </Button>
-        </div>
-      );
-    } else {
-      keyRotationSection = (
-        <div>
-          <Heading>Credential rotation</Heading>
-          <Helper>
-            Rotate the credentials that Porter uses to connect to the cluster.
-          </Helper>
-          <Button
-            color="#616FEEcc"
-            onClick={() => {
-              setStartRotateCreds(true);
-            }}
-          >
-            Rotate credentials
-          </Button>
-        </div>
-      );
-    }
-  }
-
-  const overrideAWSClusterNameSection =
-    currentCluster?.aws_integration_id &&
-    currentCluster?.aws_integration_id != 0 ? (
-      <InputRow
-        type="text"
-        value={newAWSClusterID}
-        setValue={(x: string) => {
-          setNewAWSClusterID(x);
-        }}
-        label="AWS Cluster ID"
-        placeholder="ex: my-awesome-cluster"
-        width="100%"
-        isRequired={false}
-      />
-    ) : null;
-
-  let renameClusterSection = (
-    <div>
-      <Heading>Rename cluster</Heading>
-      <InputRow
-        type="text"
-        value={newClusterName}
-        setValue={(x: string) => {
-          setNewClusterName(x);
-        }}
-        label="Cluster Name"
-        placeholder="ex: my-awesome-cluster"
-        width="100%"
-        isRequired={true}
-      />
-      {overrideAWSClusterNameSection}
-      <Button color="#616FEEcc" onClick={updateClusterName}>
-        Submit
-      </Button>
-    </div>
-  );
-  const configureUrl = (
-    ingressIp: string | undefined,
-    ingressError: DetailedIngressError
-  ) => {
-    if (typeof ingressIp !== "string") {
-      return <></>;
-    }
-
-    if (!ingressIp.length && ingressError) {
-      return <></>;
-    }
-
-    if (!ingressIp.length) {
-      return <></>;
-    }
-    return (
-      <>
-        <div>
-          <Heading>Configure Custom Domain</Heading>
-          <Helper>
-            To configure custom domains for your apps, add{" "}
-            {stringifiedDNSRecordType(ingressIp)} record pointing to the
-            following Ingress IP:
-          </Helper>
-          <CopyToClipboard
-            as={Url}
-            text={ingressIp}
-            wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
-          >
-            <span>{ingressIp}</span>
-            <i className="material-icons-outlined">content_copy</i>
-          </CopyToClipboard>
-        </div>
-      </>
-    );
-  };
-
-  let enableAgentIntegration = (
-    <div>
-      <Heading>Enable agent</Heading>
-      <CheckboxRow
-        label={"Allow the Porter agent to be installed on the cluster"}
-        toggle={() => {
-          setEnableAgent(!enableAgent);
-        }}
-        checked={enableAgent}
-      />
-      <Button color="#616FEEcc" onClick={updateAgentIntegrationEnabled}>
-        Save
-      </Button>
-    </div>
-  );
-
-  if (agentLoading) {
-    enableAgentIntegration = <Loading />;
-  }
-
-  let enablePreviewEnvironments = null;
-
-  if (currentProject.preview_envs_enabled) {
-    if (previewEnvsLoading) {
-      enablePreviewEnvironments = <Loading />;
-    } else {
-      enablePreviewEnvironments = (
-        <div>
-          <Heading>Enable preview environments</Heading>
-          <CheckboxRow
-            label={"Create preview environments on this cluster"}
-            toggle={() => {
-              setEnablePreviewEnvs(!enablePreviewEnvs);
-            }}
-            checked={enablePreviewEnvs}
-          />
-          <Button color="#616FEEcc" onClick={updatePreviewEnvironmentsEnabled}>
-            Save
-          </Button>
-        </div>
-      );
-    }
-  }
-
-  if (capabilities.version == "production") {
-    enableAgentIntegration = null;
-  }
-
-  if (successfulRename) {
-    renameClusterSection = (
-      <div>
-        <Heading>Credential rotation</Heading>
-        <Helper>Successfully renamed the cluster! Reload the page.</Helper>
-      </div>
-    );
-  }
-
-  return (
-    <div>
-      <StyledSettingsSection>
-        <DarkMatter />
-        {props.ingressIp && (
-          <>{configureUrl(props.ingressIp, props.ingressError)}</>
-        )}
-        <DarkMatter />
-        {enableAgentIntegration}
-        <DarkMatter />
-        {enablePreviewEnvironments}
-        <DarkMatter />
-        {keyRotationSection}
-        <DarkMatter />
-        {/* Disabled this field due to https://discord.com/channels/542888846271184896/856554532972134420/1042497537912864788 */}
-        {/* {renameClusterSection}
-        <DarkMatter /> */}
-        <Heading>Delete Infrastructure</Heading>
-        {helperText}
-        <Button
-          disabled={
-            currentProject.capi_provisioner_enabled
-              ? currentCluster.status != "READY"
-              : currentCluster.status == "UPDATING"
-          }
-          color="#b91133"
-          onClick={() => {
-            setCurrentModal("UpdateClusterModal");
-          }}
-        >
-          Delete cluster
-        </Button>
-      </StyledSettingsSection>
-    </div>
-  );
-};
-
-export default ClusterSettings;
-
-const DarkMatter = styled.div`
-  width: 100%;
-  margin-top: -15px;
-`;
-
-const StyledSettingsSection = styled.div`
-  margin-top: 35px;
-  width: 100%;
-  padding: 30px;
-  padding-top: 5px;
-  padding-bottom: 15px;
-  position: relative;
-  border-radius: 8px;
-  overflow: auto;
-  height: 100%;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-`;
-
-const Button = styled.button`
-  height: 35px;
-  font-size: 13px;
-  margin-top: 6px;
-  margin-bottom: 30px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  color: white;
-  padding: 6px 20px 7px 20px;
-  text-align: left;
-  border: 0;
-  border-radius: 5px;
-  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
-  user-select: none;
-  :focus {
-    outline: 0;
-  }
-  :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
-  }
-`;
-
-const Warning = styled.div`
-  font-size: 13px;
-  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
-    props.highlight ? "#f5cb42" : ""};
-  margin-bottom: 20px;
-`;
-const Url = styled.a`
-  font-size: 13px;
-  user-select: text;
-  font-weight: 400;
-  display: flex;
-  align-items: center;
-  margin-left: 20px;
-  cursor: pointer;
-  > i {
-    margin-left: 10px;
-    font-size: 15px;
-  }
-
-  > span {
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-  }
-`;

+ 0 - 151
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettingsModal.tsx

@@ -1,151 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import Button from "components/porter/Button";
-import Input from "components/porter/Input";
-import Spacer from "components/porter/Spacer";
-import Modal from "main/home/modals/Modal";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-
-type Props = {};
-
-const ClusterSettingsModal: React.FC<Props> = ({}) => {
-  const {
-    setCurrentModal,
-    currentCluster,
-    currentProject,
-    setShouldRefreshClusters,
-  } = useContext(Context);
-  const [clusterName, setClusterName] = useState("");
-  const [status, setStatus] = useState("");
-
-  useEffect(() => {
-    setClusterName(currentCluster.vanity_name || currentCluster.name);
-  }, []);
-
-  const renameCluster = async () => {
-    setStatus("loading");
-    try {
-      const res = await api.renameCluster(
-        "<token>",
-        { name: clusterName },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-      setStatus("success");
-      setShouldRefreshClusters(true);
-    } catch (err) {
-      setStatus("error");
-      console.log(err);
-    }
-  };
-
-  return (
-    <Modal
-      width="600px"
-      height="auto"
-      onRequestClose={() => {
-        setCurrentModal(null, null);
-      }}
-      title="Cluster name"
-    >
-      <Spacer height="15px" />
-      <Flex>
-        <IconWrapper>
-          <svg
-            width="18"
-            height="18"
-            viewBox="0 0 19 19"
-            fill="none"
-            xmlns="http://www.w3.org/2000/svg"
-          >
-            <path
-              d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-            <path
-              d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-            <path
-              fillRule="evenodd"
-              clipRule="evenodd"
-              d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-            <path
-              d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-            <path
-              d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-            <path
-              fillRule="evenodd"
-              clipRule="evenodd"
-              d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
-              stroke="white"
-              strokeWidth="1.5"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            />
-          </svg>
-        </IconWrapper>
-        <Spacer inline />
-        <Input
-          placeholder="ex: my-cluster"
-          width="100%"
-          value={clusterName}
-          setValue={setClusterName}
-        />
-      </Flex>
-      <Spacer y={1} />
-      <Button
-        onClick={renameCluster}
-        disabled={clusterName === ""}
-        status={status}
-        helperText="Note: The vanity name for your cluster will not change the cluster's name in your cloud provider."
-      >
-        Save
-      </Button>
-    </Modal>
-  );
-};
-
-export default ClusterSettingsModal;
-
-const IconWrapper = styled.div`
-  min-width: 35px;
-  height: 35px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border: 1px solid #494b4f;
-  border-radius: 5px;
-  cursor: not-allowed;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;

+ 0 - 313
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -1,313 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { useLocation } from "react-router";
-import styled from "styled-components";
-
-import AzureProvisionerSettings from "components/AzureProvisionerSettings";
-import GCPProvisionerSettings from "components/GCPProvisionerSettings";
-import Button from "components/porter/Button";
-import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
-import Image from "components/porter/Image";
-import PorterLink from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import ProvisionerSettings from "components/ProvisionerSettings";
-import TabSelector from "components/TabSelector";
-
-import api from "shared/api";
-import useAuth from "shared/auth/useAuth";
-import { Context } from "shared/Context";
-import { getQueryParam } from "shared/routing";
-import editIcon from "assets/edit-button.svg";
-import infraGrad from "assets/infra-grad.svg";
-
-import DashboardHeader from "../DashboardHeader";
-import ClusterRevisionSelector from "./ClusterRevisionSelector";
-import ClusterSettings from "./ClusterSettings";
-import ClusterSettingsModal from "./ClusterSettingsModal";
-import Metrics from "./Metrics";
-import { NamespaceList } from "./NamespaceList";
-import NodeList from "./NodeList";
-import ProvisionerStatus from "./ProvisionerStatus";
-
-type TabEnum =
-  | "nodes"
-  | "settings"
-  | "namespaces"
-  | "metrics"
-  | "incidents"
-  | "configuration";
-
-const tabOptions: Array<{
-  label: string;
-  value: TabEnum;
-}> = [{ label: "Additional settings", value: "settings" }];
-
-export const Dashboard: React.FunctionComponent = () => {
-  const [currentTab, setCurrentTab] = useState<TabEnum>("settings");
-  const [currentTabOptions, setCurrentTabOptions] = useState(tabOptions);
-  const [isAuthorized] = useAuth();
-  const location = useLocation();
-  const [selectedClusterVersion, setSelectedClusterVersion] = useState(null);
-  const [showProvisionerStatus, setShowProvisionerStatus] = useState(false);
-  const [provisionFailureReason, setProvisionFailureReason] = useState("");
-  const [ingressIp, setIngressIp] = useState("");
-  const [ingressError, setIngressError] = useState("");
-
-  const context = useContext(Context);
-  const renderTab = () => {
-    switch (currentTab) {
-      case "settings":
-        return (
-          <ClusterSettings
-            ingressIp={ingressIp}
-            ingressError={ingressError}
-            history={undefined}
-            location={undefined}
-            match={undefined}
-          />
-        );
-      case "metrics":
-        return <Metrics />;
-      case "namespaces":
-        return <NamespaceList />;
-      case "configuration":
-        return (
-          <>
-            <Br />
-            {context.currentCluster.cloud_provider === "AWS" && (
-              <ProvisionerSettings
-                selectedClusterVersion={selectedClusterVersion}
-                provisionerError={provisionFailureReason}
-                clusterId={context.currentCluster.id}
-                credentialId={
-                  context.currentCluster.cloud_provider_credential_identifier
-                }
-              />
-            )}
-            {context.currentCluster.cloud_provider === "Azure" && (
-              <AzureProvisionerSettings
-                selectedClusterVersion={selectedClusterVersion}
-                provisionerError={provisionFailureReason}
-                clusterId={context.currentCluster.id}
-                credentialId={
-                  context.currentCluster.cloud_provider_credential_identifier
-                }
-              />
-            )}
-            {context.currentCluster.cloud_provider === "GCP" && (
-              <GCPProvisionerSettings
-                selectedClusterVersion={selectedClusterVersion}
-                provisionerError={provisionFailureReason}
-                clusterId={context.currentCluster.id}
-                credentialId={
-                  context.currentCluster.cloud_provider_credential_identifier
-                }
-              />
-            )}
-          </>
-        );
-      default:
-        return <NodeList />;
-    }
-  };
-
-  useEffect(() => {
-    if (
-      context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
-      !tabOptions.find((tab) => tab.value === "nodes")
-    ) {
-      if (!context.currentProject?.capi_provisioner_enabled) {
-        tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
-        tabOptions.unshift({ label: "Metrics", value: "metrics" });
-        tabOptions.unshift({ label: "Nodes", value: "nodes" });
-      }
-    }
-
-    if (
-      context.currentProject?.capi_provisioner_enabled &&
-      !tabOptions.find((tab) => tab.value === "configuration")
-    ) {
-      tabOptions.unshift({ value: "configuration", label: "Configuration" });
-    }
-  }, []);
-
-  useEffect(() => {
-    setCurrentTabOptions(
-      tabOptions.filter((option) => {
-        if (option.value === "settings") {
-          return isAuthorized("cluster", "", ["get", "delete"]);
-        }
-        return true;
-      })
-    );
-  }, [isAuthorized]);
-
-  useEffect(() => {
-    const selectedTab = getQueryParam({ location }, "selected_tab");
-    if (tabOptions.find((tab) => tab.value === selectedTab)) {
-      setCurrentTab(selectedTab as any);
-    }
-  }, [location]);
-
-  // Need to reset tab to reset views that don't auto-update on cluster switch (esp namespaces + settings)
-  useEffect(() => {
-    setShowProvisionerStatus(false);
-    if (context.currentProject?.capi_provisioner_enabled) {
-      setCurrentTab("configuration");
-    } else {
-      setCurrentTab("nodes");
-    }
-  }, [context.currentCluster]);
-
-  const updateClusterWithDetailedData = async () => {
-    try {
-      const res = await api.getCluster(
-        "<token>",
-        {},
-        {
-          project_id: context.currentProject.id,
-          cluster_id: context.currentCluster.id,
-        }
-      );
-      if (res.data) {
-        const { ingress_ip, ingress_error } = res.data;
-        setIngressIp(ingress_ip);
-        setIngressError(ingress_error);
-      }
-    } catch (error) {}
-  };
-
-  useEffect(() => {
-    updateClusterWithDetailedData();
-  }, []);
-
-  useEffect(() => {
-    updateClusterWithDetailedData();
-  }, [context.currentCluster]);
-
-  const renderContents = () => {
-    if (context.currentProject?.sandbox_enabled) {
-      return (
-        <DashboardPlaceholder>
-          <Text size={16}>
-            Infrastructure settings are not enabled on the Porter Cloud.
-          </Text>
-          <Spacer y={0.5} />
-          <Text color={"helper"}>
-            Eject to your own cloud account to enable managed infrastructure.
-          </Text>
-          <Spacer y={1} />
-          <PorterLink to="https://docs.porter.run/other/eject">
-            <Button alt height="35px">
-              Request ejection
-            </Button>
-          </PorterLink>
-        </DashboardPlaceholder>
-      );
-    } else if (context.currentProject?.capi_provisioner_enabled) {
-      return (
-        <>
-          <ClusterRevisionSelector
-            selectedClusterVersion={selectedClusterVersion}
-            setSelectedClusterVersion={setSelectedClusterVersion}
-            setShowProvisionerStatus={setShowProvisionerStatus}
-            setProvisionFailureReason={setProvisionFailureReason}
-          />
-          {showProvisionerStatus &&
-            (context.currentCluster.status === "UPDATING" ||
-              context.currentCluster.status === "UPDATING_UNAVAILABLE") && (
-              <>
-                <ProvisionerStatus
-                  provisionFailureReason={provisionFailureReason}
-                />
-                <Spacer y={1} />
-              </>
-            )}
-          <TabSelector
-            options={currentTabOptions}
-            currentTab={currentTab}
-            setCurrentTab={(value: TabEnum) => {
-              setCurrentTab(value);
-            }}
-          />
-          {renderTab()}
-        </>
-      );
-    } else {
-      return (
-        <>
-          <TabSelector
-            options={currentTabOptions}
-            currentTab={currentTab}
-            setCurrentTab={(value: TabEnum) => {
-              setCurrentTab(value);
-            }}
-          />
-          {renderTab()}
-        </>
-      );
-    }
-  };
-
-  return (
-    <>
-      <DashboardHeader
-        title={
-          <Flex>
-            <Flex>
-              <Image size={25} src={infraGrad} />
-              <Spacer inline />
-              {context.currentCluster.vanity_name ||
-                context.currentCluster.name}
-              <Spacer inline />
-            </Flex>
-            <EditIconStyle
-              onClick={() => {
-                context.setCurrentModal(<ClusterSettingsModal />);
-              }}
-            >
-              <img src={editIcon} />
-            </EditIconStyle>
-          </Flex>
-        }
-        description={`Cluster settings and status for ${
-          context.currentCluster.vanity_name || context.currentCluster.name
-        }.`}
-        disableLineBreak
-        capitalize={false}
-      />
-
-      {renderContents()}
-    </>
-  );
-};
-
-const EditIconStyle = styled.div`
-  width: 20px;
-  height: 20px;
-  margin-left: -5px;
-  cursor: pointer;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 40px;
-  margin-bottom: 3px;
-  :hover {
-    background: #ffffff18;
-  }
-  > img {
-    width: 22px;
-    opacity: 0.4;
-    margin-bottom: -4px;
-  }
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const Br = styled.div`
-  width: 100%;
-  height: 35px;
-`;

+ 0 - 618
dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx

@@ -1,618 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import ParentSize from "@visx/responsive/lib/components/ParentSize";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import Placeholder from "components/OldPlaceholder";
-import TabSelector from "components/TabSelector";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import settings from "assets/settings.svg";
-
-import SelectRow from "../../../../components/form-components/SelectRow";
-import AggregatedDataLegend from "../expanded-chart/metrics/AggregatedDataLegend";
-import AreaChart from "../expanded-chart/metrics/AreaChart";
-import { MetricNormalizer } from "../expanded-chart/metrics/MetricNormalizer";
-import {
-  resolutions,
-  secondsBeforeNow,
-} from "../expanded-chart/metrics/MetricsSection";
-import {
-  type AvailableMetrics,
-  type GenericMetricResponse,
-  type NormalizedMetricsData,
-} from "../expanded-chart/metrics/types";
-
-const Metrics: React.FC = () => {
-  const { currentProject, currentCluster, setCurrentError } =
-    useContext(Context);
-  const [loading, setLoading] = useState(true);
-  const [detected, setDetected] = useState(false);
-  const [metricsOptions, setMetricsOptions] = useState([]);
-  const [dropdownExpanded, setDropdownExpanded] = useState(false);
-  const [ingressOptions, setIngressOptions] = useState([]);
-  const [selectedIngress, setSelectedIngress] = useState(null);
-  const [selectedRange, setSelectedRange] = useState("1H");
-  const [selectedMetric, setSelectedMetric] = useState("nginx:errors");
-  const [selectedMetricLabel, setSelectedMetricLabel] = useState(
-    "5XX Error Percentage"
-  );
-  const [selectedPercentile, setSelectedPercentile] = useState("0.99");
-  const [data, setData] = useState<NormalizedMetricsData[]>([]);
-  const [aggregatedData, setAggregatedData] = useState<
-    Record<string, NormalizedMetricsData[]>
-  >({});
-  const [showMetricsSettings, setShowMetricsSettings] = useState(false);
-  const [isLoading, setIsLoading] = useState(0);
-  const [hpaData, setHpaData] = useState([]);
-
-  useEffect(() => {
-    if (selectedMetric && selectedRange && selectedIngress) {
-      getMetrics();
-    }
-  }, [
-    selectedMetric,
-    selectedRange,
-    selectedIngress,
-    selectedPercentile,
-    currentCluster,
-  ]);
-
-  useEffect(() => {
-    Promise.all([
-      api.getCluster(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      ),
-      api.getPrometheusIsInstalled(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      ),
-    ])
-      .then(() => {
-        setDetected(true);
-        setIsLoading((prev) => prev + 1);
-
-        api
-          .getNGINXIngresses(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              cluster_id: currentCluster.id,
-            }
-          )
-          .then((res) => {
-            const ingressOptions = res.data.map((ingress: any) => ({
-              value: ingress,
-              label: ingress.name,
-            }));
-            setIngressOptions(ingressOptions);
-            setSelectedIngress(ingressOptions[0]?.value);
-            setMetricsOptions([
-              ...metricsOptions,
-              {
-                value: "nginx:errors",
-                label: "5XX Error Percentage",
-              },
-              {
-                value: "nginx:latency",
-                label: "Request Latency (s)",
-              },
-              {
-                value: "nginx:latency-histogram",
-                label: "Percentile Response Times (s)",
-              },
-            ]);
-            setLoading(false);
-          })
-          .catch((err) => {
-            setCurrentError(JSON.stringify(err));
-          })
-          .finally(() => {
-            setIsLoading((prev) => prev - 1);
-          });
-      })
-      .catch(() => {
-        setDetected(false);
-        setLoading(false);
-      });
-  }, []);
-
-  const renderMetricsSettings = () => {
-    if (showMetricsSettings) {
-      return (
-        <>
-          <DropdownOverlay
-            onClick={() => {
-              setShowMetricsSettings(false);
-            }}
-          />
-          <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
-            <Label>Additional Settings</Label>
-            <SelectRow
-              label="Target Ingress"
-              value={selectedIngress}
-              setActiveValue={(x: any) => {
-                setSelectedIngress(x);
-              }}
-              options={ingressOptions}
-              width="100%"
-            />
-            {selectedMetric == "nginx:latency-histogram" && (
-              <SelectRow
-                label="Percentile"
-                value={selectedPercentile}
-                setActiveValue={(x) => {
-                  setSelectedPercentile(x);
-                }}
-                options={[
-                  {
-                    label: "99",
-                    value: "0.99",
-                  },
-                  {
-                    label: "95",
-                    value: "0.95",
-                  },
-                  {
-                    label: "50",
-                    value: "0.5",
-                  },
-                ]}
-                width="100%"
-              />
-            )}
-          </DropdownAlt>
-        </>
-      );
-    }
-  };
-
-  const renderDropdown = () => {
-    if (dropdownExpanded) {
-      return (
-        <>
-          <DropdownOverlay
-            onClick={() => {
-              setDropdownExpanded(false);
-            }}
-          />
-          <Dropdown
-            dropdownWidth="230px"
-            dropdownMaxHeight="200px"
-            onClick={() => {
-              setDropdownExpanded(false);
-            }}
-          >
-            {renderOptionList()}
-          </Dropdown>
-        </>
-      );
-    }
-  };
-
-  const renderOptionList = () => {
-    return metricsOptions.map(
-      (option: { value: string; label: string }, i: number) => {
-        return (
-          <Option
-            key={i}
-            selected={option.value === selectedMetric}
-            onClick={() => {
-              setSelectedMetric(option.value);
-              setSelectedMetricLabel(option.label);
-            }}
-            lastItem={i === metricsOptions.length - 1}
-          >
-            {option.label}
-          </Option>
-        );
-      }
-    );
-  };
-
-  const getMetrics = async () => {
-    try {
-      const shouldsum = true;
-      const namespace = "default";
-
-      // calculate start and end range
-      const d = new Date();
-      const end = Math.round(d.getTime() / 1000);
-      const start = end - secondsBeforeNow[selectedRange];
-
-      let podNames = [] as string[];
-
-      podNames = [selectedIngress?.name];
-
-      setIsLoading((prev) => prev + 1);
-      setData([]);
-      setAggregatedData({});
-
-      const allPodsRes = await api.getMetrics(
-        "<token>",
-        {
-          metric: selectedMetric,
-          shouldsum: false,
-          kind: "Ingress",
-          namespace: selectedIngress?.namespace || "default",
-          percentile:
-            selectedMetric == "nginx:latency-histogram"
-              ? parseFloat(selectedPercentile)
-              : undefined,
-          startrange: start,
-          endrange: end,
-          resolution: resolutions[selectedRange],
-          pods: [],
-          name: selectedIngress?.name,
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-
-      const allPodsData: GenericMetricResponse[] = allPodsRes.data ?? [];
-      const allPodsMetrics = allPodsData.flatMap((d) => d.results);
-      const allPodsMetricsNormalized = new MetricNormalizer(
-        [{ results: allPodsMetrics }],
-        selectedMetric as AvailableMetrics
-      );
-      setAggregatedData(allPodsMetricsNormalized.getAggregatedData());
-      //
-
-      const res = await api.getMetrics(
-        "<token>",
-        {
-          metric: selectedMetric,
-          shouldsum: false,
-          kind: "Ingress",
-          namespace: selectedIngress?.namespace || "default",
-          percentile:
-            selectedMetric == "nginx:latency-histogram"
-              ? parseFloat(selectedPercentile)
-              : undefined,
-          startrange: start,
-          endrange: end,
-          resolution: resolutions[selectedRange],
-          pods: podNames,
-          name: selectedIngress?.name,
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-
-      setHpaData([]);
-
-      const metrics = new MetricNormalizer(
-        res.data,
-        selectedMetric as AvailableMetrics
-      );
-
-      // transform the metrics to expected form
-      setData(metrics.getParsedData());
-    } catch (error) {
-      setCurrentError(JSON.stringify(error));
-    } finally {
-      setIsLoading((prev) => prev - 1);
-    }
-  };
-
-  return loading ? (
-    <LoadingWrapper>
-      <Loading />
-    </LoadingWrapper>
-  ) : !detected ? (
-    <>
-      <br />
-      <br />
-      <Placeholder height="calc(50vh - 50px)" minHeight="400px">
-        Cluster metrics unavailable. Make sure nginx-ingress and Prometheus are
-        installed.
-        <A href="/launch">Go to Launch</A>
-      </Placeholder>
-    </>
-  ) : (
-    <StyledMetricsSection>
-      <Header>
-        <Flex>
-          <MetricSelector
-            onClick={() => {
-              setDropdownExpanded(!dropdownExpanded);
-            }}
-          >
-            <MetricsLabel>{selectedMetricLabel}</MetricsLabel>
-            <i className="material-icons">arrow_drop_down</i>
-            {renderDropdown()}
-          </MetricSelector>
-          <Relative>
-            <IconWrapper
-              onClick={() => {
-                setShowMetricsSettings(true);
-              }}
-            >
-              <SettingsIcon src={settings} />
-            </IconWrapper>
-            {renderMetricsSettings()}
-          </Relative>
-
-          <Highlight color={"#7d7d81"} onClick={getMetrics}>
-            <i className="material-icons">autorenew</i>
-          </Highlight>
-        </Flex>
-        <RangeWrapper>
-          <TabSelector
-            noBuffer={true}
-            options={[
-              { value: "1H", label: "1H" },
-              { value: "6H", label: "6H" },
-              { value: "1D", label: "1D" },
-              { value: "1M", label: "1M" },
-            ]}
-            currentTab={selectedRange}
-            setCurrentTab={(x: string) => {
-              setSelectedRange(x);
-            }}
-          />
-        </RangeWrapper>
-      </Header>
-      {isLoading > 0 && <Loading />}
-      {data.length === 0 && isLoading === 0 && (
-        <Message>
-          No data available yet.
-          <Highlight color={"#8590ff"} onClick={getMetrics}>
-            <i className="material-icons">autorenew</i>
-            Refresh
-          </Highlight>
-        </Message>
-      )}
-      {data.length > 0 && isLoading === 0 && (
-        <>
-          <AggregatedDataLegend data={data} hideAvg={false} />
-          <ParentSize>
-            {({ width, height }) => (
-              <AreaChart
-                dataKey={selectedMetricLabel}
-                data={data}
-                aggregatedData={aggregatedData}
-                hpaData={hpaData}
-                hpaEnabled={false}
-                width={width}
-                height={height - 10}
-                resolution={selectedRange}
-                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
-              />
-            )}
-          </ParentSize>
-        </>
-      )}
-    </StyledMetricsSection>
-  );
-};
-
-export default Metrics;
-
-const A = styled.a`
-  margin-left: 5px;
-`;
-
-const LoadingWrapper = styled.div`
-  padding: 100px 0px;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-  justify-content: center;
-  color: #ffffff44;
-`;
-
-const Highlight = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-left: 8px;
-  color: ${(props: { color: string }) => props.color};
-  cursor: pointer;
-
-  > i {
-    font-size: 20px;
-    margin-right: 3px;
-  }
-`;
-
-const Label = styled.div`
-  font-weight: bold;
-`;
-
-const Relative = styled.div`
-  position: relative;
-`;
-
-const Message = styled.div`
-  display: flex;
-  height: 100%;
-  width: calc(100% - 150px);
-  align-items: center;
-  justify-content: center;
-  margin-left: 75px;
-  text-align: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
-const IconWrapper = styled.div`
-  display: flex;
-  position: relative;
-  align-items: center;
-  justify-content: center;
-  margin-top: 2px;
-  border-radius: 30px;
-  height: 25px;
-  width: 25px;
-  margin-left: 8px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff22;
-  }
-`;
-
-const SettingsIcon = styled.img`
-  opacity: 0.4;
-  width: 20px;
-  height: 20px;
-  margin-left: -1px;
-  margin-bottom: -2px;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const MetricsHeader = styled.div`
-  width: 100%;
-  display: flex;
-  align-items: center;
-  overflow: visible;
-  justify-content: space-between;
-`;
-
-const DropdownOverlay = styled.div`
-  position: fixed;
-  width: 100%;
-  height: 100%;
-  z-index: 10;
-  left: 0px;
-  top: 0px;
-  cursor: default;
-`;
-
-const Option = styled.div`
-  width: 100%;
-  border-top: 1px solid #00000000;
-  border-bottom: 1px solid
-    ${(props: { selected: boolean; lastItem: boolean }) =>
-      props.lastItem ? "#ffffff00" : "#ffffff15"};
-  height: 37px;
-  font-size: 13px;
-  padding-top: 9px;
-  align-items: center;
-  padding-left: 15px;
-  cursor: pointer;
-  padding-right: 10px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  background: ${(props: { selected: boolean; lastItem: boolean }) =>
-    props.selected ? "#ffffff11" : ""};
-
-  :hover {
-    background: #ffffff22;
-  }
-`;
-
-const Dropdown = styled.div`
-  position: absolute;
-  left: 0;
-  top: calc(100% + 10px);
-  background: #26282f;
-  width: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
-    props.dropdownWidth};
-  max-height: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
-    props.dropdownMaxHeight || "300px"};
-  border-radius: 3px;
-  z-index: 999;
-  overflow-y: auto;
-  margin-bottom: 20px;
-  box-shadow: 0px 4px 10px 0px #00000088;
-`;
-
-const DropdownAlt = styled(Dropdown)`
-  padding: 20px 20px 7px;
-  overflow: visible;
-`;
-
-const RangeWrapper = styled.div`
-  float: right;
-  font-weight: bold;
-  width: 156px;
-  margin-top: -8px;
-`;
-
-const MetricSelector = styled.div`
-  font-size: 13px;
-  font-weight: 500;
-  position: relative;
-  color: #ffffff;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  border-radius: 5px;
-  :hover {
-    > i {
-      background: #ffffff22;
-    }
-  }
-
-  > i {
-    border-radius: 20px;
-    font-size: 20px;
-    margin-left: 10px;
-  }
-`;
-
-const MetricsLabel = styled.div`
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  max-width: 200px;
-`;
-
-const StyledMetricsSection = styled.div`
-  width: 100%;
-  min-height: 400px;
-  height: 50vh;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  border-radius: 5px;
-  border: 1px solid #ffffff33;
-  padding: 18px 22px;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  margin-top: 34px;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;

+ 0 - 316
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -1,316 +0,0 @@
-import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
-import { useHistory, useLocation } from "react-router";
-import styled from "styled-components";
-
-import OptionsDropdown from "components/OptionsDropdown";
-
-import useAuth from "shared/auth/useAuth";
-import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
-import { type ClusterType, type ProjectType } from "shared/types";
-
-const useWebsocket = (
-  currentProject: ProjectType,
-  currentCluster: ClusterType
-) => {
-  const wsRef = useRef<WebSocket | undefined>(undefined);
-
-  useEffect(() => {
-    const protocol = window.location.protocol == "https:" ? "wss" : "ws";
-    wsRef.current = new WebSocket(
-      `${protocol}://${window.location.host}/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespace/status`
-    );
-
-    wsRef.current.onopen = () => {
-      console.log("Connected to websocket");
-    };
-
-    wsRef.current.onclose = () => {
-      console.log("closing websocket");
-    };
-
-    return () => {
-      wsRef.current.close();
-    };
-  }, []);
-
-  return wsRef;
-};
-
-export const NamespaceList: React.FunctionComponent = () => {
-  const { currentCluster, currentProject, setCurrentModal, setCurrentError } =
-    useContext(Context);
-  const location = useLocation();
-  const history = useHistory();
-  const [namespaces, setNamespaces] = useState([]);
-  const websocket = useWebsocket(currentProject, currentCluster);
-  const onDelete = (namespace: any) => {
-    setCurrentModal("DeleteNamespaceModal", namespace);
-  };
-
-  const [isAuthorized] = useAuth();
-
-  const isAvailableForDeletion = (namespaceName: string) => {
-    // Only the namespaces that doesn't start with kube- or has by name default will be
-    // available for deletion (as those are the k8s namespaces)
-    return !/(^default$)|(^kube-.*)/.test(namespaceName);
-  };
-
-  useEffect(() => {
-    if (!websocket) {
-      return;
-    }
-
-    websocket.current.onerror = (err: ErrorEvent) => {
-      setCurrentError(err.message);
-      websocket.current.close();
-    };
-
-    websocket.current.onmessage = (evt: MessageEvent) => {
-      const data = JSON.parse(evt.data);
-      if (data.Kind !== "namespace") {
-        return;
-      }
-      if (data.event_type === "ADD") {
-        setNamespaces((oldNamespaces) => [...oldNamespaces, data.Object]);
-      }
-
-      if (data.event_type === "DELETE") {
-        setNamespaces((oldNamespaces) => {
-          const oldNamespaceIndex = oldNamespaces.findIndex(
-            (namespace) => namespace.metadata.name === data.Object.metadata.name
-          );
-          oldNamespaces.splice(oldNamespaceIndex, 1);
-          return [...oldNamespaces];
-        });
-      }
-
-      if (data.event_type === "UPDATE") {
-        setNamespaces((oldNamespaces) => {
-          const oldNamespaceIndex = oldNamespaces.findIndex(
-            (namespace) => namespace.metadata.name === data.Object.metadata.name
-          );
-          oldNamespaces.splice(oldNamespaceIndex, 1, data.Object);
-          return [...oldNamespaces];
-        });
-      }
-    };
-  }, [websocket]);
-
-  const sortAlphabetically = (prev: any, current: any) => {
-    return prev.metadata.name > current.metadata.name ? 1 : -1;
-  };
-
-  const sortedNamespaces = useMemo<any[]>(() => {
-    const nonDeletableNamespaces = namespaces
-      .filter((namespace) => !isAvailableForDeletion(namespace.metadata.name))
-      .sort(sortAlphabetically);
-    const deletableNamespaces = namespaces
-      .filter((namespace) => isAvailableForDeletion(namespace.metadata.name))
-      .sort(sortAlphabetically);
-
-    return [...deletableNamespaces, ...nonDeletableNamespaces];
-  }, [namespaces]);
-
-  return (
-    <NamespaceListWrapper>
-      <ControlRow>
-        {isAuthorized("namespace", "", ["get", "create"]) && (
-          <Button
-            onClick={() => {
-              setCurrentModal(
-                "NamespaceModal",
-                namespaces.map((namespace) => ({
-                  value: namespace.metadata.name,
-                }))
-              );
-            }}
-          >
-            <i className="material-icons">add</i> Add namespace
-          </Button>
-        )}
-      </ControlRow>
-      <NamespacesGrid>
-        {sortedNamespaces.map((namespace) => {
-          return (
-            <StyledCard
-              key={namespace?.metadata?.name}
-              onClick={() => {
-                pushFiltered({ location, history }, `/applications`, [], {
-                  cluster: currentCluster.name,
-                  namespace: namespace.metadata.name,
-                });
-              }}
-            >
-              <ContentContainer>
-                <Title>{namespace?.metadata?.name}</Title>
-                <Status margin_left={"0px"}>
-                  <StatusColor status={namespace.status.phase} />
-                  {namespace?.status?.phase}
-                </Status>
-              </ContentContainer>
-              {isAuthorized("namespace", "", ["get", "delete"]) &&
-                isAvailableForDeletion(namespace?.metadata?.name) &&
-                namespace?.status?.phase === "Active" && (
-                  <OptionsDropdown.Dropdown
-                    expandIcon="more_vert"
-                    shrinkIcon="more_vert"
-                  >
-                    <OptionsDropdown.Option
-                      onClick={() => {
-                        onDelete(namespace);
-                      }}
-                    >
-                      <i className="material-icons-outlined">delete</i>
-                      <span>Delete</span>
-                    </OptionsDropdown.Option>
-                  </OptionsDropdown.Dropdown>
-                )}
-            </StyledCard>
-          );
-        })}
-      </NamespacesGrid>
-    </NamespaceListWrapper>
-  );
-};
-
-const NamespaceListWrapper = styled.div`
-  margin-top: 35px;
-  padding-bottom: 80px;
-`;
-
-const NamespacesGrid = styled.div`
-  margin-top: 32px;
-  padding-bottom: 150px;
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(2, minmax(200px, 1fr));
-`;
-
-const Title = styled.div`
-  font-size: 14px;
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-`;
-
-const StatusColor = styled.div`
-  margin-top: 1px;
-  width: 8px;
-  height: 8px;
-  background: ${(props: { status: string }) =>
-    props.status === "Active"
-      ? "#4797ff"
-      : props.status === "Terminating"
-      ? "#ed5f85"
-      : "#f5cb42"};
-  border-radius: 20px;
-  margin-left: 3px;
-  margin-right: 16px;
-`;
-
-const Status = styled.div`
-  display: flex;
-  height: 20px;
-  font-size: 13px;
-  flex-direction: row;
-  text-transform: capitalize;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  color: #aaaabb;
-  animation: fadeIn 0.5s;
-  margin-left: ${(props: { margin_left: string }) => props.margin_left};
-
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 35px;
-  padding-left: 0px;
-`;
-
-const Button = styled.div`
-  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: 5px;
-  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 StyledCard = styled.div`
-  min-height: 80px;
-  width: 100%;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 14px;
-  animation: fadeIn 0.5s;
-  cursor: pointer;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;
-
-const ContentContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  height: 100%;
-`;

+ 0 - 198
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -1,198 +0,0 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import { useHistory, useLocation } from "react-router";
-import { type Column } from "react-table";
-import styled from "styled-components";
-
-import Table from "components/OldTable";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
-
-const NodeList: React.FC = () => {
-  const context = useContext(Context);
-  const [nodeList, setNodeList] = useState([]);
-  const [loading, setLoading] = useState<boolean>(false);
-  const history = useHistory();
-  const location = useLocation();
-
-  const columns = useMemo<Array<Column<any>>>(
-    () => [
-      {
-        Header: "Node Name",
-        accessor: "name",
-        Cell: ({ row }) => {
-          return <NameWrapper>{row.values.name}</NameWrapper>;
-        },
-        width: "max-content",
-      },
-      {
-        Header: "Machine Type",
-        accessor: "machine_type",
-      },
-      {
-        Header: "CPU Usage",
-        accessor: "cpu_usage",
-      },
-      {
-        Header: "RAM Usage",
-        accessor: "ram_usage",
-      },
-      {
-        Header: () => <StatusHeader>Node Condition</StatusHeader>,
-        accessor: "is_node_healthy",
-        Cell: ({ row }) => {
-          return (
-            <StatusButtonWrapper>
-              <StatusButton success={row.values.is_node_healthy}>
-                {row.values.is_node_healthy ? "Healthy" : "Unhealthy"}
-              </StatusButton>
-            </StatusButtonWrapper>
-          );
-        },
-      },
-    ],
-    []
-  );
-
-  const data = useMemo(() => {
-    const percentFormatter = (number: number) =>
-      `${Number(number).toFixed(2)}%`;
-
-    const getMachineType = (labels: any) => {
-      return labels?.["node.kubernetes.io/instance-type"] || "N/A";
-    };
-
-    return nodeList
-      .map((node) => {
-        return {
-          name: node.name,
-          machine_type: getMachineType(node?.labels),
-          cpu_usage: percentFormatter(node.fraction_cpu_reqs),
-          ram_usage: percentFormatter(node.fraction_memory_reqs),
-          node_conditions: node.node_conditions,
-          is_node_healthy: node.node_conditions.reduce(
-            (prevValue: boolean, current: any) => {
-              if (current.type !== "Ready" && current.status !== "False") {
-                return false;
-              }
-              if (current.type === "Ready" && current.status !== "True") {
-                return false;
-              }
-              return prevValue;
-            },
-            true
-          ),
-        };
-      })
-      .sort((firstEl, secondElement) =>
-        firstEl.is_node_healthy === secondElement.is_node_healthy
-          ? 0
-          : firstEl.is_node_healthy
-          ? 1
-          : -1
-      );
-  }, [nodeList]);
-
-  useEffect(() => {
-    const { currentCluster, currentProject } = context;
-    setLoading(true);
-    api
-      .getClusterNodes(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-        }
-      )
-      .then(({ data }) => {
-        if (data) {
-          setNodeList(data);
-        }
-      })
-      .catch(() => {
-        console.log({ error: true });
-      })
-      .finally(() => {
-        setLoading(false);
-      });
-  }, [context, setNodeList]);
-
-  const handleOnRowClick = (row: any) => {
-    pushFiltered(
-      {
-        history,
-        location,
-      },
-      `/cluster-dashboard/node-view/${row.original.name}`,
-      []
-    );
-  };
-
-  return (
-    <NodeListWrapper>
-      <StyledChart>
-        <Table
-          columns={columns}
-          data={data}
-          isLoading={loading}
-          onRowClick={handleOnRowClick}
-        />
-      </StyledChart>
-    </NodeListWrapper>
-  );
-};
-
-export default NodeList;
-
-const NodeListWrapper = styled.div`
-  margin-top: 35px;
-`;
-
-const StyledChart = styled.div`
-  padding: 14px;
-  position: relative;
-  width: 100%;
-  height: 100%;
-  :not(:last-child) {
-    margin-bottom: 25px;
-  }
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-`;
-
-const StatusHeader = styled.div`
-  width: 100%;
-  text-align: center;
-`;
-
-const StatusButtonWrapper = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: center;
-`;
-
-const StatusButton = styled.div`
-  cursor: pointer;
-  display: flex;
-  border-radius: 3px;
-  align-items: center;
-  justify-content: center;
-  font-weight: 500;
-  height: 21px;
-  font-size: 13px;
-  width: 70px;
-  background: ${(props: { success: boolean }) =>
-    props.success ? "#616FEEcc" : "#ed5f85"};
-  :hover {
-    background: ${(props: { success: boolean }) =>
-      props.success ? "#405eddbb" : "#e83162"};
-  }
-`;
-
-const NameWrapper = styled.span`
-  white-space: nowrap;
-  margin-right: 10px;
-`;

+ 0 - 171
dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx

@@ -1,171 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import LoadingBar from "components/porter/LoadingBar";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import aws from "assets/aws.png";
-import azure from "assets/azure.png";
-
-type Props = {
-  provisionFailureReason: string;
-};
-
-const PROVISIONING_STATUS_POLL_INTERVAL = 60 * 1000; // poll every minute
-
-const ProvisionerStatus: React.FC<Props> = (props) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [progress, setProgress] = useState<number>(1);
-  const [provisionType, setProvisionType] = useState("CREATE");
-
-  // Continuously poll provisioning status and cluster status
-  const pollProvisioningAndClusterStatus = async () => {
-    if (currentProject && currentCluster) {
-      try {
-        const resState = await api.getClusterState(
-          "<token>",
-          {},
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        );
-        const { is_control_plane_ready, is_infrastructure_ready, phase } =
-          resState.data;
-        let newProgress = 1;
-        if (is_control_plane_ready) {
-          newProgress += 1;
-        }
-        if (is_infrastructure_ready) {
-          newProgress += 1;
-        }
-        if (phase === "Provisioned") {
-          newProgress += 1;
-        }
-        setProgress(newProgress);
-        if (newProgress >= 4) {
-          const resStatus = await api.getCluster(
-            "<token>",
-            {},
-            {
-              project_id: currentProject.id,
-              cluster_id: currentCluster.id,
-            }
-          );
-          const status = resStatus.data.status;
-          if (status === "READY") {
-            window.location.reload();
-          }
-        }
-      } catch (err) {
-        console.log(err);
-      }
-    }
-  };
-
-  useEffect(() => {
-    const intervalId = setInterval(
-      pollProvisioningAndClusterStatus,
-      PROVISIONING_STATUS_POLL_INTERVAL
-    );
-    pollProvisioningAndClusterStatus();
-    return () => {
-      clearInterval(intervalId);
-    };
-  }, []);
-
-  useEffect(() => {
-    // check if this is create or update operation
-    // TODO: CCP should distinguish between create vs update.
-    api
-      .getContracts("<token>", {}, { project_id: currentProject.id })
-      .then(({ data }) => {
-        const filtered_data = data.filter((x: any) => {
-          return x.cluster_id === currentCluster.id;
-        });
-
-        if (filtered_data.length > 1) {
-          setProvisionType("UPDATE");
-        }
-      })
-      .catch((err) => {
-        console.error(err);
-      });
-  });
-
-  return (
-    <StyledProvisionerStatus>
-      <HeaderSection>
-        <Flex>
-          {currentCluster?.cloud_provider == "AWS" && (
-            <>
-              <Icon src={aws} />
-              AWS provisioning status
-            </>
-          )}
-
-          {currentCluster?.cloud_provider == "Azure" && (
-            <>
-              <Icon src={azure} />
-              Azure provisioning status
-            </>
-          )}
-        </Flex>
-        <Spacer height="18px" />
-        <LoadingBar
-          color={props?.provisionFailureReason ? "failed" : undefined}
-          completed={progress}
-          total={5}
-        />
-        <Spacer height="18px" />
-        <Text color="#aaaabb">
-          {provisionType == "UPDATE"
-            ? "Updating your infrastructure may take up to 30 minutes and will not incur any downtime for your applications. You can still deploy and manage your applications while the update is in effect."
-            : "Setup can take up to 20 minutes. You can close this window and come back later."}
-        </Text>
-      </HeaderSection>
-      {props?.provisionFailureReason && (
-        <DummyLogs>Error: {props?.provisionFailureReason}</DummyLogs>
-      )}
-    </StyledProvisionerStatus>
-  );
-};
-
-export default ProvisionerStatus;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const HeaderSection = styled.div`
-  padding: 15px;
-  padding-bottom: 18px;
-`;
-
-const DummyLogs = styled.div`
-  padding: 15px;
-  width: 100%;
-  display: flex;
-  font-size: 13px;
-  background: #101420;
-  font-family: monospace;
-`;
-
-const Icon = styled.img`
-  height: 16px;
-  margin-right: 10px;
-  margin-bottom: -1px;
-`;
-
-const StyledProvisionerStatus = styled.div`
-  border-radius: 5px;
-  background: #26292e;
-  border: 1px solid #494b4f;
-  font-size: 13px;
-  width: 100%;
-  overflow: hidden;
-`;

+ 0 - 26
dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx

@@ -1,26 +0,0 @@
-import React, { useContext } from "react";
-import { Route, Switch, useRouteMatch } from "react-router";
-
-import { Context } from "shared/Context";
-
-import { Dashboard } from "./Dashboard";
-import ExpandedNodeView from "./node-view/ExpandedNodeView";
-
-export const Routes = () => {
-  const { url } = useRouteMatch();
-  const { currentProject } = useContext(Context);
-  return (
-    <>
-      <Switch>
-        <Route path={`${url}/node-view/:nodeId`}>
-          <ExpandedNodeView />
-        </Route>
-        <Route path={`${url}/`}>
-          <Dashboard />
-        </Route>
-      </Switch>
-    </>
-  );
-};
-
-export default Routes;

+ 0 - 463
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -1,463 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import Helper from "components/form-components/Helper";
-import InputRow from "components/form-components/InputRow";
-import SaveButton from "components/SaveButton";
-import Selector from "components/Selector";
-
-import api from "shared/api";
-import { isAlphanumeric } from "shared/common";
-import { Context } from "shared/Context";
-import { type ClusterType } from "shared/types";
-
-import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray";
-
-type PropsType = {
-  goBack: () => void;
-  currentCluster: ClusterType;
-};
-
-const CreateEnvGroup = ({ goBack, currentCluster }: PropsType) => {
-  const [envGroupName, setEnvGroupName] = useState("");
-  const [selectedNamespace, setSelectedNamespace] = useState("default");
-  const [namespaceOptions, setNamespaceOptions] = useState<any[]>([]);
-  const [envVariables, setEnvVariables] = useState<KeyValueType[]>([]);
-  const [submitStatus, setSubmitStatus] = useState("");
-
-  const context = useContext(Context);
-
-  useEffect(() => {
-    updateNamespaces();
-  }, []);
-
-  const isDisabled = (): boolean => {
-    const isEnvGroupNameInvalid =
-      !isAlphanumeric(envGroupName) ||
-      envGroupName === "" ||
-      envGroupName.length > 60;
-
-    const isAnyEnvVariableBlank = envVariables.some(
-      (envVar) => !envVar.key.trim() || !envVar.value.trim()
-    );
-
-    return isEnvGroupNameInvalid || isAnyEnvVariableBlank;
-  };
-
-  const onSubmit = (): void => {
-    setSubmitStatus("loading");
-
-    const apiEnvVariables: Record<string, string> = {};
-    const secretEnvVariables: Record<string, string> = {};
-
-    const envVariable = envVariables;
-
-    if (context.currentProject.simplified_view_enabled) {
-      api
-        .createNamespace(
-          "<token>",
-          {
-            name: "porter-env-group",
-          },
-          {
-            id: context.currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        )
-        .catch((error) => {
-          if (error.response && error.response.status === 412) {
-            // do nothing
-          } else {
-            // do nothing still
-          }
-        });
-    }
-    envVariable
-      .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => {
-        // remove any collisions that are marked as deleted and are duplicates
-        const numCollisions = self.reduce((n, _envVar: KeyValueType) => {
-          return n + (_envVar.key === envVar.key ? 1 : 0);
-        }, 0);
-
-        if (numCollisions === 1) {
-          return true;
-        } else {
-          return (
-            index ===
-            self.findIndex(
-              (_envVar: KeyValueType) =>
-                _envVar.key === envVar.key && !_envVar.deleted
-            )
-          );
-        }
-      })
-      .forEach((envVar: KeyValueType) => {
-        if (!envVar.deleted) {
-          if (envVar.hidden) {
-            secretEnvVariables[envVar.key] = envVar.value;
-          } else {
-            apiEnvVariables[envVar.key] = envVar.value;
-          }
-        }
-      });
-
-    api
-      .createEnvGroup(
-        "<token>",
-        {
-          name: envGroupName,
-          variables: apiEnvVariables,
-          secret_variables: secretEnvVariables,
-        },
-        {
-          id: context.currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace: context.currentProject.simplified_view_enabled
-            ? "porter-env-group"
-            : selectedNamespace,
-        }
-      )
-      .then((res) => {
-        setSubmitStatus("successful");
-        // console.log(res);
-        goBack();
-      })
-      .catch((err) => {
-        setSubmitStatus("Could not create");
-      });
-  };
-
-  const createEnv = () => {
-    setSubmitStatus("loading");
-
-    const apiEnvVariables: Record<string, string> = {};
-    const secretEnvVariables: Record<string, string> = {};
-
-    const envVariable = envVariables;
-    envVariable
-      .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => {
-        // remove any collisions that are marked as deleted and are duplicates
-        const numCollisions = self.reduce((n, _envVar: KeyValueType) => {
-          return n + (_envVar.key === envVar.key ? 1 : 0);
-        }, 0);
-
-        if (numCollisions === 1) {
-          return true;
-        } else {
-          return (
-            index ===
-            self.findIndex(
-              (_envVar: KeyValueType) =>
-                _envVar.key === envVar.key && !_envVar.deleted
-            )
-          );
-        }
-      })
-      .forEach((envVar: KeyValueType) => {
-        if (!envVar.deleted) {
-          if (envVar.hidden) {
-            secretEnvVariables[envVar.key] = envVar.value;
-          } else {
-            apiEnvVariables[envVar.key] = envVar.value;
-          }
-        }
-      });
-
-    api
-      .createEnvironmentGroups(
-        "<token>",
-        {
-          name: envGroupName,
-          variables: apiEnvVariables,
-          secret_variables: secretEnvVariables,
-        },
-        {
-          id: context.currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        setSubmitStatus("successful");
-        // console.log(res);
-        goBack();
-      })
-      .catch((err) => {
-        if (err) {
-          setSubmitStatus("Could not create");
-        }
-      });
-  };
-
-  const updateNamespaces = () => {
-    const { currentProject } = context;
-    api
-      .getNamespaces(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (res.data) {
-          const availableNamespaces = res.data.filter((namespace: any) => {
-            return namespace.status !== "Terminating";
-          });
-          const namespaceOptions = availableNamespaces.map(
-            (x: { name: string }) => {
-              return { label: x.name, value: x.name };
-            }
-          );
-          if (availableNamespaces.length > 0) {
-            setNamespaceOptions(namespaceOptions);
-          }
-        }
-      })
-      .catch(console.log);
-  };
-
-  return (
-    <>
-      <StyledCreateEnvGroup>
-        <HeaderSection>
-          <Button onClick={goBack}>
-            <i className="material-icons">keyboard_backspace</i>
-            Back
-          </Button>
-          <Title>Create an environment group</Title>
-        </HeaderSection>
-        <Wrapper>
-          <DarkMatter antiHeight="-13px" />
-          <Heading isAtTop={true}>Name</Heading>
-          <Subtitle>
-            <Warning
-              makeFlush={true}
-              highlight={
-                (!isAlphanumeric(envGroupName) || envGroupName.length > 60) &&
-                envGroupName !== ""
-              }
-            >
-              Lowercase letters, numbers, and "-" only. Maximum 60 characters.
-            </Warning>
-          </Subtitle>
-          <DarkMatter antiHeight="-29px" />
-          <InputRow
-            type="text"
-            value={envGroupName}
-            setValue={(x: string) => {
-              setEnvGroupName(x);
-            }}
-            placeholder="ex: my-env-group"
-            width="100%"
-          />
-          {!context?.currentProject?.simplified_view_enabled && (
-            <>
-              <Heading>Destination</Heading>
-              <Subtitle>
-                Specify the namespace you would like to create this environment
-                group in.
-              </Subtitle>
-              <DestinationSection>
-                <NamespaceLabel>
-                  <i className="material-icons">view_list</i>Namespace
-                </NamespaceLabel>
-                <Selector
-                  key={"namespace"}
-                  activeValue={selectedNamespace}
-                  setActiveValue={(namespace: string) => {
-                    setSelectedNamespace(namespace);
-                  }}
-                  options={namespaceOptions}
-                  width="250px"
-                  dropdownWidth="335px"
-                  closeOverlay={true}
-                />
-              </DestinationSection>
-            </>
-          )}
-          <Heading>Environment variables</Heading>
-          <Helper>
-            Set environment variables for your secrets and environment-specific
-            configuration.
-          </Helper>
-          <EnvGroupArray
-            namespace={selectedNamespace}
-            values={envVariables}
-            setValues={(x: any) => {
-              setEnvVariables(x);
-            }}
-            fileUpload={true}
-            secretOption={true}
-          />
-        </Wrapper>
-        <SaveButton
-          disabled={isDisabled()}
-          text="Create env group"
-          clearPosition={true}
-          statusPosition="right"
-          onClick={
-            context.currentProject.simplified_view_enabled
-              ? createEnv
-              : onSubmit
-          }
-          status={isDisabled() ? "Missing required fields" : submitStatus}
-          makeFlush={true}
-        />
-      </StyledCreateEnvGroup>
-      <Buffer />
-    </>
-  );
-};
-
-export default CreateEnvGroup;
-
-const Wrapper = styled.div`
-  padding: 30px;
-  padding-bottom: 25px;
-  border-radius: 5px;
-  margin-top: -15px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  margin-bottom: 30px;
-`;
-
-const Buffer = styled.div`
-  width: 100%;
-  height: 150px;
-`;
-
-const StyledCreateEnvGroup = styled.div`
-  padding-bottom: 70px;
-  position: relative;
-`;
-
-const NamespaceLabel = styled.div`
-  margin-right: 10px;
-  display: flex;
-  align-items: center;
-  > i {
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;
-
-const DestinationSection = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff;
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  margin-top: 2px;
-  font-weight: 500;
-  margin-bottom: 32px;
-
-  > i {
-    font-size: 25px;
-    color: #ffffff44;
-    margin-right: 13px;
-  }
-`;
-
-const Button = styled.div`
-  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;
-  margin-left: -2px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: pointer;
-  border: 2px solid #969fbbaa;
-  :hover {
-    background: #ffffff11;
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    color: #969fbbaa;
-    font-weight: 600;
-    font-size: 14px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const DarkMatter = styled.div<{ antiHeight?: string }>`
-  width: 100%;
-  margin-top: ${(props) => props.antiHeight || "-15px"};
-`;
-
-const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
-  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
-  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
-`;
-
-const Subtitle = styled.div`
-  padding: 11px 0px 16px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #aaaabb;
-  line-height: 1.6em;
-  display: flex;
-  align-items: center;
-`;
-
-const Title = styled.div`
-  font-size: 20px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  margin-left: 15px;
-  border-radius: 2px;
-  color: #ffffff;
-`;
-
-const HeaderSection = styled.div`
-  display: flex;
-  align-items: center;
-  margin-bottom: 40px;
-
-  > i {
-    cursor: pointer;
-    font-size: 20px;
-    color: #969fbbaa;
-    padding: 2px;
-    border: 2px solid #969fbbaa;
-    border-radius: 100px;
-    :hover {
-      background: #ffffff11;
-    }
-  }
-
-  > img {
-    width: 20px;
-    margin-left: 17px;
-    margin-right: 7px;
-  }
-`;
-
-const Heading = styled.div<{ isAtTop?: boolean }>`
-  color: white;
-  font-weight: 500;
-  font-size: 16px;
-  margin-bottom: 5px;
-  margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
-  display: flex;
-  align-items: center;
-`;

+ 0 - 217
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx

@@ -1,217 +0,0 @@
-import React, { Component } from "react";
-import { Link } from "react-router-dom";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-import { readableDate } from "shared/string_utils";
-import doppler from "assets/doppler.png";
-import sliders from "assets/sliders.svg";
-
-export type EnvGroupData = {
-  name: string;
-  type?: string;
-  namespace: string;
-  created_at?: string;
-  version: number;
-};
-
-type PropsType = {
-  envGroup: EnvGroupData;
-};
-
-type StateType = {
-  update: any[];
-};
-
-export default class EnvGroup extends Component<PropsType, StateType> {
-  state = {
-    update: [] as any[],
-  };
-
-  render() {
-    const { envGroup } = this.props;
-    const name = envGroup?.name;
-    const timestamp = envGroup?.created_at;
-    const namespace = envGroup?.namespace;
-    const version = this.context?.currentProject.simplified_view_enabled
-      ? envGroup?.latest_version
-      : envGroup?.version;
-
-    return (
-      <Link to={`/env-groups/${name}${window.location.search}`} target="_self">
-        <StyledEnvGroup>
-          <Title>
-            <IconWrapper>
-              <Icon src={envGroup.type === "doppler" ? doppler : sliders} />
-            </IconWrapper>
-            {name}
-          </Title>
-
-          <BottomWrapper>
-            <InfoWrapper>
-              <LastDeployed>
-                Last updated {readableDate(timestamp)}
-              </LastDeployed>
-            </InfoWrapper>
-
-            {!this.context?.currentProject.simplified_view_enabled && (
-              <TagWrapper>
-                Namespace
-                <NamespaceTag>
-                  {namespace.startsWith("porter-stack-")
-                    ? namespace.replace("porter-stack-", "")
-                    : namespace}
-                </NamespaceTag>
-              </TagWrapper>
-            )}
-          </BottomWrapper>
-
-          <Version>v{version}</Version>
-        </StyledEnvGroup>
-      </Link>
-    );
-  }
-}
-
-export function formattedEnvironmentValue(value: string) {
-  if (value.startsWith("PORTERSECRET_")) {
-    return "••••";
-  }
-  return value;
-}
-
-EnvGroup.contextType = Context;
-
-const BottomWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding-right: 11px;
-  margin-top: 3px;
-`;
-
-const Version = styled.div`
-  position: absolute;
-  top: 12px;
-  right: 12px;
-  font-size: 12px;
-  color: #aaaabb;
-`;
-
-const Dot = styled.div`
-  margin-right: 9px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-right: 8px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 14px;
-  margin-bottom: -1px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 5px;
-`;
-
-const NamespaceTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #ffffff22;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const Icon = styled.img`
-  width: 100%;
-`;
-
-const IconWrapper = styled.div`
-  color: #efefef;
-  background: none;
-  font-size: 16px;
-  top: 11px;
-  left: 14px;
-  height: 20px;
-  width: 20px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 3px;
-  position: absolute;
-
-  > i {
-    font-size: 17px;
-    margin-top: -1px;
-  }
-`;
-
-const Title = styled.div`
-  position: relative;
-  text-decoration: none;
-  padding: 12px 35px 12px 45px;
-  font-size: 14px;
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-  width: 80%;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  animation: fadeIn 0.5s;
-
-  > img {
-    background: none;
-    top: 12px;
-    left: 13px;
-
-    padding: 5px 4px;
-    width: 24px;
-    position: absolute;
-  }
-`;
-
-const StyledEnvGroup = styled.div`
-  cursor: pointer;
-  margin-bottom: 15px;
-  padding-top: 2px;
-  padding-bottom: 13px;
-  position: relative;
-  width: calc(100% + 2px);
-  height: calc(100% + 2px);
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;

+ 0 - 275
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -1,275 +0,0 @@
-import React, { useContext, useState } from "react";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
-import PorterButton from "components/porter/Button";
-import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
-import PorterLink from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-
-import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { Context } from "shared/Context";
-import { getQueryParam, pushQueryParams } from "shared/routing";
-import { type ClusterType } from "shared/types";
-import sliders from "assets/env-groups.svg";
-
-import DashboardHeader from "../DashboardHeader";
-import { NamespaceSelector } from "../NamespaceSelector";
-import SortSelector from "../SortSelector";
-import CreateEnvGroup from "./CreateEnvGroup";
-import EnvGroupList from "./EnvGroupList";
-import ExpandedEnvGroup from "./ExpandedEnvGroup";
-
-type PropsType = RouteComponentProps &
-  WithAuthProps & {
-    currentCluster: ClusterType;
-  };
-
-type StateType = {
-  expand: boolean;
-  update: any[];
-  sortType: string;
-  expandedEnvGroup: any;
-  namespace: string;
-  createEnvMode: boolean;
-};
-
-const EnvGroupDashboard = (props: PropsType) => {
-  const [state, setState] = useState<StateType>({
-    expand: false,
-    update: [] as any[],
-    namespace: null as string,
-    expandedEnvGroup: null as any,
-    createEnvMode: false,
-    sortType: localStorage.getItem("SortType")
-      ? localStorage.getItem("SortType")
-      : "Newest",
-  });
-
-  const { currentProject } = useContext(Context);
-
-  const setNamespace = (namespace: string) => {
-    setState((state) => ({ ...state, namespace }));
-    pushQueryParams(props, {
-      namespace: currentProject.simplified_view_enabled
-        ? "porter-env-group"
-        : namespace ?? "ALL",
-    });
-  };
-
-  const setSortType = (sortType: string) => {
-    setState((state) => ({ ...state, sortType }));
-  };
-
-  const toggleCreateEnvMode = () => {
-    setState((state) => ({
-      ...state,
-      createEnvMode: !state.createEnvMode,
-    }));
-  };
-
-  const setExpandedEnvGroup = (envGroup: any | null) => {
-    setState((state) => ({ ...state, expandedEnvGroup: envGroup }));
-  };
-
-  const closeExpanded = () => {
-    pushQueryParams(props, {}, ["selected_env_group"]);
-    const redirectUrlOnClose = getQueryParam(props, "redirect_url");
-    if (redirectUrlOnClose) {
-      props.history.push(redirectUrlOnClose);
-      return;
-    }
-    setExpandedEnvGroup(null);
-  };
-
-  const renderBody = () => {
-    if (props.currentCluster.status === "UPDATING_UNAVAILABLE") {
-      return <ClusterProvisioningPlaceholder />;
-    }
-
-    if (currentProject?.sandbox_enabled) {
-      return (
-        <DashboardPlaceholder>
-          <Text size={16}>
-            Environment groups are not enabled on the Porter Cloud.
-          </Text>
-          <Spacer y={0.5} />
-          <Text color={"helper"}>
-            Eject to your own cloud account to enable environment groups.
-          </Text>
-          <Spacer y={1} />
-          <PorterLink to="https://docs.porter.run/other/eject">
-            <PorterButton alt height="35px">
-              Request ejection
-            </PorterButton>
-          </PorterLink>
-        </DashboardPlaceholder>
-      );
-    }
-
-    const goBack = () => {
-      setState((state) => ({ ...state, createEnvMode: false }));
-    };
-
-    if (state.createEnvMode) {
-      return (
-        <CreateEnvGroup goBack={goBack} currentCluster={props.currentCluster} />
-      );
-    } else {
-      const isAuthorizedToAdd = props.isAuthorized("env_group", "", [
-        "get",
-        "create",
-      ]);
-
-      return (
-        <>
-          <ControlRow hasMultipleChilds={isAuthorizedToAdd}>
-            <SortFilterWrapper>
-              <SortSelector
-                currentView="env-groups"
-                setSortType={setSortType}
-                sortType={state.sortType}
-              />
-              <Spacer inline width="10px" />
-              {!currentProject.simplified_view_enabled && (
-                <NamespaceSelector
-                  setNamespace={setNamespace}
-                  namespace={
-                    currentProject.simplified_view_enabled
-                      ? "porter-env-group"
-                      : state.namespace
-                  }
-                />
-              )}
-            </SortFilterWrapper>
-            <Flex>
-              {isAuthorizedToAdd && (
-                <Button onClick={toggleCreateEnvMode}>
-                  <i className="material-icons">add</i> Create env group
-                </Button>
-              )}
-            </Flex>
-          </ControlRow>
-
-          <EnvGroupList
-            currentCluster={props.currentCluster}
-            namespace={
-              currentProject?.simplified_view_enabled
-                ? "porter-env-group"
-                : state.namespace
-            }
-            sortType={state.sortType}
-            setExpandedEnvGroup={setExpandedEnvGroup}
-          />
-        </>
-      );
-    }
-  };
-
-  const renderContents = () => {
-    if (state.expandedEnvGroup) {
-      return (
-        <ExpandedEnvGroup
-          isAuthorized={props.isAuthorized}
-          namespace={
-            currentProject?.simplified_view_enabled
-              ? "porter-env-group"
-              : state.expandedEnvGroup?.namespace || state.namespace
-          }
-          currentCluster={props.currentCluster}
-          envGroup={state.expandedEnvGroup}
-          closeExpanded={() => {
-            closeExpanded();
-          }}
-        />
-      );
-    } else {
-      return (
-        <>
-          <DashboardHeader
-            image={sliders}
-            title="Environment groups"
-            description="Groups of environment variables for storing secrets and configuration."
-            disableLineBreak
-            capitalize={false}
-          />
-          {renderBody()}
-        </>
-      );
-    }
-  };
-
-  return <>{renderContents()}</>;
-};
-
-export default withRouter(withAuth(EnvGroupDashboard));
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  border-bottom: 30px solid transparent;
-`;
-
-const SortFilterWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  border-bottom: 30px solid transparent;
-  > div:not(:first-child) {
-  }
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: ${(props: { hasMultipleChilds: boolean }) => {
-    if (props.hasMultipleChilds) {
-      return "space-between";
-    }
-    return "flex-end";
-  }};
-  align-items: center;
-  flex-wrap: wrap;
-`;
-
-const Button = styled.div`
-  display: flex;
-  margin-left: 10px;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  min-width: 155px;
-  padding-right: 13px;
-  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;
-  }
-`;

+ 0 - 158
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx

@@ -1,158 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import Placeholder from "components/Placeholder";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { getQueryParam, pushQueryParams } from "shared/routing";
-import { type ClusterType } from "shared/types";
-
-import EnvGroup from "./EnvGroup";
-
-type Props = RouteComponentProps & {
-  currentCluster: ClusterType;
-  namespace: string;
-  sortType: string;
-  setExpandedEnvGroup: (envGroup: any) => void;
-};
-
-type State = {
-  envGroups: any[];
-  loading: boolean;
-  error: boolean;
-};
-
-const EnvGroupList: React.FunctionComponent<Props> = (props) => {
-  const context = useContext(Context);
-
-  const { currentCluster, namespace, sortType, setExpandedEnvGroup } = props;
-
-  const [envGroups, setEnvGroups] = useState<any[]>([]);
-  const [isLoading, setIsLoading] = useState<boolean>(true);
-  const [hasError, setHasError] = useState<boolean>(false);
-
-  const updateEnvGroups = async () => {
-    const { currentProject, currentCluster } = context;
-    try {
-      let envGroups: any[] = [];
-      if (currentProject?.simplified_view_enabled) {
-        envGroups = await api
-          .getAllEnvGroups(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              cluster_id: currentCluster.id,
-            }
-          )
-          .then((res) => {
-            return res.data?.environment_groups;
-          });
-      } else {
-        envGroups = await api
-          .listEnvGroups(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              namespace,
-              cluster_id: currentCluster.id,
-            }
-          )
-          .then((res) => {
-            return res.data;
-          });
-      }
-      const sortedGroups = envGroups;
-      if (sortedGroups) {
-        switch (sortType) {
-          case "Oldest":
-            sortedGroups.sort((a: any, b: any) =>
-              Date.parse(a.created_at) > Date.parse(b.created_at) ? 1 : -1
-            );
-            break;
-          case "Alphabetical":
-            sortedGroups.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-            break;
-          default:
-            sortedGroups.sort((a: any, b: any) =>
-              Date.parse(a.created_at) > Date.parse(b.created_at) ? -1 : 1
-            );
-        }
-      }
-      return sortedGroups;
-    } catch (error) {
-      console.log(error);
-      setIsLoading(false);
-      setHasError(true);
-    }
-  };
-
-  useEffect(() => {
-    // Prevents reload when opening ClusterConfigModal
-    (namespace || namespace === "") &&
-      updateEnvGroups().then((envGroups) => {
-        const selectedEnvGroup = getQueryParam(props, "selected_env_group");
-
-        setEnvGroups(envGroups);
-        if (envGroups && envGroups.length > 0) {
-          setHasError(false);
-        }
-        setIsLoading(false);
-
-        if (selectedEnvGroup) {
-          // find env group by selectedEnvGroup
-          const envGroup = envGroups.find(
-            (envGroup: any) => envGroup.name === selectedEnvGroup
-          );
-          if (envGroup) {
-            setExpandedEnvGroup(envGroup);
-          } else {
-            pushQueryParams(props, {}, ["selected_env_group"]);
-          }
-        }
-      });
-  }, [currentCluster, namespace, sortType]);
-
-  const renderEnvGroupList = () => {
-    if (isLoading || (!namespace && namespace !== "")) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (hasError) {
-      return (
-        <Placeholder height="370px">
-          <i className="material-icons">error</i> Error connecting to cluster.
-        </Placeholder>
-      );
-    } else if (!envGroups || envGroups.length === 0) {
-      return (
-        <Placeholder height="370px">
-          <i className="material-icons">category</i>
-          No environment groups found with the given filters.
-        </Placeholder>
-      );
-    }
-
-    return envGroups.map((envGroup: any, i: number) => {
-      return <EnvGroup key={i} envGroup={envGroup} />;
-    });
-  };
-
-  return <StyledEnvGroupList>{renderEnvGroupList()}</StyledEnvGroupList>;
-};
-
-export default withRouter(EnvGroupList);
-
-const LoadingWrapper = styled.div`
-  padding-top: 100px;
-`;
-
-const StyledEnvGroupList = styled.div`
-  padding-bottom: 85px;
-`;

+ 0 - 1303
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -1,1303 +0,0 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import { remove } from "lodash";
-import styled, { keyframes } from "styled-components";
-
-import DocsHelper from "components/DocsHelper";
-import DynamicLink from "components/DynamicLink";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import InputRow from "components/form-components/InputRow";
-import {
-  type NewPopulatedEnvGroup,
-  type PopulatedEnvGroup,
-} from "components/porter-form/types";
-import Spacer from "components/porter/Spacer";
-import SaveButton from "components/SaveButton";
-import TabRegion from "components/TabRegion";
-import TitleSection from "components/TitleSection";
-
-import api from "shared/api";
-import { type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import useAuth from "shared/auth/useAuth";
-import { Context } from "shared/Context";
-import { type ClusterType } from "shared/types";
-import key from "assets/key.svg";
-import leftArrow from "assets/left-arrow.svg";
-import loading from "assets/loading.gif";
-
-import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray";
-
-type PropsType = WithAuthProps & {
-  namespace: string;
-  envGroup: any;
-  currentCluster: ClusterType;
-  closeExpanded: () => void;
-  allEnvGroups?: NewPopulatedEnvGroup[];
-};
-
-type StateType = {
-  loading: boolean;
-  currentTab: string | null;
-  deleting: boolean;
-  saveValuesStatus: string | null;
-  envGroup: EnvGroup;
-  tabOptions: Array<{ value: string; label: string }>;
-  newEnvGroupName: string;
-};
-
-type EnvGroup = {
-  name: string;
-  // timestamp: string;
-  variables: KeyValueType[];
-  version: number;
-};
-
-// export default withAuth(ExpandedEnvGroup);
-
-type EditableEnvGroup = Omit<PopulatedEnvGroup, "variables"> & {
-  variables: KeyValueType[];
-  linked_applications?: string[];
-  secret_variables?: KeyValueType[];
-};
-
-export const ExpandedEnvGroupFC = ({
-  envGroup,
-  namespace,
-  closeExpanded,
-  allEnvGroups,
-}: PropsType) => {
-  const { currentProject, currentCluster, setCurrentOverlay, setCurrentError } =
-    useContext(Context);
-  const [isAuthorized] = useAuth();
-
-  const [currentTab, setCurrentTab] = useState("variables-editor");
-  const [isDeleting, setIsDeleting] = useState(false);
-  const [buttonStatus, setButtonStatus] = useState("");
-
-  const [currentEnvGroup, setCurrentEnvGroup] =
-    useState<EditableEnvGroup>(null);
-  const [originalEnvVars, setOriginalEnvVars] = useState<
-    Array<{
-      key: string;
-      value: string;
-    }>
-  >();
-
-  const tabOptions = useMemo(() => {
-    if (!isAuthorized("env_group", "", ["get", "delete"])) {
-      return [{ value: "variables-editor", label: "Environment variables" }];
-    }
-    if (
-      !isAuthorized("env_group", "", ["get", "delete"]) &&
-      (currentProject?.simplified_view_enabled
-        ? currentEnvGroup?.linked_applications?.length
-        : currentEnvGroup?.applications?.length)
-    ) {
-      return [
-        { value: "variables-editor", label: "Environment variables" },
-        { value: "applications", label: "Linked applications" },
-      ];
-    }
-
-    if (
-      currentProject?.simplified_view_enabled
-        ? currentEnvGroup?.linked_applications?.length
-        : currentEnvGroup?.applications?.length
-    ) {
-      return [
-        { value: "variables-editor", label: "Environment variables" },
-        { value: "applications", label: "Linked applications" },
-        { value: "settings", label: "Settings" },
-      ];
-    }
-
-    return [
-      { value: "variables-editor", label: "Environment variables" },
-      { value: "settings", label: "Settings" },
-    ];
-  }, [currentEnvGroup]);
-  const populateEnvGroup = async () => {
-    if (currentProject?.simplified_view_enabled) {
-      try {
-        const populatedEnvGroup = await api
-          .getAllEnvGroups(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              cluster_id: currentCluster.id,
-            }
-          )
-          .then((res) => res.data.environment_groups);
-        updateEnvGroup(
-          populatedEnvGroup.find((i: any) => i.name === envGroup.name)
-        );
-      } catch (error) {
-        console.log(error);
-      }
-    } else {
-      try {
-        const populatedEnvGroup = await api
-          .getEnvGroup<NewPopulatedEnvGroup>(
-            "<token>",
-            {},
-            {
-              name: envGroup.name,
-              id: currentProject.id,
-              namespace,
-              cluster_id: currentCluster.id,
-            }
-          )
-          .then((res) => res.data);
-        updateEnvGroup(populatedEnvGroup);
-      } catch (error) {
-        console.log(error);
-      }
-    }
-  };
-
-  const updateEnvGroup = (populatedEnvGroup: NewPopulatedEnvGroup) => {
-    if (currentProject?.simplified_view_enabled) {
-      const normal_variables: KeyValueType[] = Object.entries(
-        populatedEnvGroup.variables || {}
-      ).map(([key, value]) => ({
-        key,
-        value,
-        hidden: value.includes("PORTERSECRET"),
-        locked: value.includes("PORTERSECRET"),
-        deleted: false,
-      }));
-      const secret_variables: KeyValueType[] = Object.entries(
-        populatedEnvGroup.secret_variables || {}
-      ).map(([key, value]) => ({
-        key,
-        value,
-        hidden: true,
-        locked: true,
-        deleted: false,
-      }));
-      const variables = [...normal_variables, ...secret_variables];
-
-      setOriginalEnvVars(
-        Object.entries({
-          ...(populatedEnvGroup?.variables || {}),
-          ...(populatedEnvGroup.secret_variables || {}),
-        }).map(([key, value]) => ({
-          key,
-          value,
-        }))
-      );
-
-      setCurrentEnvGroup({
-        ...populatedEnvGroup,
-        variables,
-      });
-    } else {
-      const variables: KeyValueType[] = Object.entries(
-        populatedEnvGroup.variables || {}
-      ).map(([key, value]) => ({
-        key,
-        value,
-        hidden: value.includes("PORTERSECRET"),
-        locked: value.includes("PORTERSECRET"),
-        deleted: false,
-      }));
-
-      setOriginalEnvVars(
-        Object.entries(populatedEnvGroup?.variables || {}).map(
-          ([key, value]) => ({
-            key,
-            value,
-          })
-        )
-      );
-
-      setCurrentEnvGroup({
-        ...populatedEnvGroup,
-        variables,
-      });
-    }
-  };
-
-  const deleteEnvGroup = async () => {
-    const { name, stack_id, type } = currentEnvGroup;
-    if (currentProject?.simplified_view_enabled) {
-      return await api.deleteNewEnvGroup(
-        "<token>",
-        {
-          name,
-          type,
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-    }
-
-    if (stack_id?.length) {
-      return await api.removeStackEnvGroup(
-        "<stack>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace,
-          stack_id,
-          env_group_name: name,
-        }
-      );
-    }
-
-    return await api.deleteEnvGroup(
-      "<token>",
-      {
-        name,
-      },
-      {
-        id: currentProject.id,
-        cluster_id: currentCluster.id,
-        namespace,
-      }
-    );
-  };
-
-  const handleDeleteEnvGroup = () => {
-    setIsDeleting(true);
-    setCurrentOverlay(null);
-
-    deleteEnvGroup()
-      .then(() => {
-        closeExpanded();
-        setIsDeleting(true);
-      })
-      .catch(() => {
-        setIsDeleting(true);
-      });
-  };
-
-  const handleUpdateValues = async () => {
-    setButtonStatus("loading");
-    const name = currentEnvGroup.name;
-    const variables = currentEnvGroup?.variables;
-    if (
-      currentEnvGroup.meta_version === 2 ||
-      currentProject?.simplified_view_enabled
-    ) {
-      const secretVariables = remove(variables, (envVar) => {
-        return !envVar.value.includes("PORTERSECRET") && envVar.hidden;
-      }).reduce(
-        (acc, variable) => ({
-          ...acc,
-          [variable.key]: variable?.value,
-        }),
-        {}
-      );
-
-      const normalVariables = variables?.reduce(
-        (acc, variable) => ({
-          ...acc,
-          [variable.key]: variable?.value,
-        }),
-        {}
-      );
-
-      if (currentProject?.simplified_view_enabled) {
-        try {
-          const normal_variables: KeyValueType[] = Object.entries(
-            normalVariables || {}
-          ).map(([key, value]) => ({
-            key,
-            value,
-            hidden: value.includes("PORTERSECRET"),
-            locked: value.includes("PORTERSECRET"),
-            deleted: false,
-          }));
-
-          const secret_variables: KeyValueType[] = Object.entries(
-            secretVariables || {}
-          ).map(([key, value]) => ({
-            key,
-            value,
-            hidden: true,
-            locked: true,
-            deleted: false,
-          }));
-          const variables = [...normal_variables, ...secret_variables];
-
-          setCurrentEnvGroup({
-            ...currentEnvGroup,
-            variables,
-          });
-
-          const linkedApp: string[] = currentEnvGroup?.linked_applications;
-          // doppler env groups update themselves, and we don't want to increment the version
-          if (
-            currentEnvGroup?.type !== "doppler" &&
-            currentEnvGroup.type !== "infisical"
-          ) {
-            await api.createEnvironmentGroups(
-              "<token>",
-              {
-                name,
-                variables: normalVariables,
-                secret_variables: secretVariables,
-              },
-              {
-                id: currentProject.id,
-                cluster_id: currentCluster.id,
-              }
-            );
-          }
-
-          try {
-            const res = await api.updateAppsLinkedToEnvironmentGroup(
-              "<token>",
-              {
-                name: currentEnvGroup?.name,
-              },
-              {
-                id: currentProject.id,
-                cluster_id: currentCluster.id,
-              }
-            );
-          } catch (error) {
-            setCurrentError(error);
-          }
-
-          const populatedEnvGroup = await api
-            .getAllEnvGroups(
-              "<token>",
-              {},
-              {
-                id: currentProject.id,
-                cluster_id: currentCluster.id,
-              }
-            )
-            .then((res) => res.data.environment_groups);
-
-          const newEnvGroup = populatedEnvGroup.find(
-            (i: any) => i.name === name
-          );
-
-          updateEnvGroup(newEnvGroup);
-          setButtonStatus("successful");
-        } catch (error) {
-          setButtonStatus("Couldn't update successfully");
-          setCurrentError(error);
-          setTimeout(() => {
-            setButtonStatus("");
-          }, 1000);
-        }
-      } else {
-        try {
-          const updatedEnvGroup = await api
-            .updateEnvGroup<PopulatedEnvGroup>(
-              "<token>",
-              {
-                name,
-                variables: normalVariables,
-                secret_variables: secretVariables,
-              },
-              {
-                project_id: currentProject.id,
-                cluster_id: currentCluster.id,
-                namespace,
-              }
-            )
-            .then((res) => res.data);
-          if (!currentProject?.simplified_view_enabled) {
-            setButtonStatus("successful");
-          }
-          updateEnvGroup(updatedEnvGroup);
-
-          setTimeout(() => {
-            setButtonStatus("");
-          }, 1000);
-        } catch (error) {
-          setButtonStatus("Couldn't update successfully");
-          setCurrentError(error);
-          setTimeout(() => {
-            setButtonStatus("");
-          }, 1000);
-        }
-      }
-    } else {
-      // SEPARATE THE TWO KINDS OF VARIABLES
-      let secret = variables.filter(
-        (variable) =>
-          variable.hidden && !variable.value.includes("PORTERSECRET")
-      );
-
-      let normal = variables.filter(
-        (variable) =>
-          !variable.hidden && !variable.value.includes("PORTERSECRET")
-      );
-
-      // Filter variables that weren't updated
-      normal = normal.reduce((acc, variable) => {
-        const originalVar = originalEnvVars.find(
-          (orgVar) => orgVar.key === variable.key
-        );
-
-        // Remove variables that weren't updated
-        if (variable.value === originalVar?.value) {
-          return acc;
-        }
-
-        // add the variable that's going to be updated
-        return [...acc, variable];
-      }, []);
-
-      secret = secret.reduce((acc, variable) => {
-        const originalVar = originalEnvVars.find(
-          (orgVar) => orgVar.key === variable.key
-        );
-
-        // Remove variables that weren't updated
-        if (variable.value === originalVar?.value) {
-          return acc;
-        }
-
-        // add the variable that's going to be updated
-        return [...acc, variable];
-      }, []);
-
-      // Check through the original env vars to see if there's a missing variable, if it is, then means it was removed
-      const removedNormal = originalEnvVars.reduce((acc, orgVar) => {
-        if (orgVar.value.includes("PORTERSECRET")) {
-          return acc;
-        }
-
-        const variableFound = variables.find(
-          (variable) => orgVar.key === variable.key
-        );
-        if (variableFound) {
-          return acc;
-        }
-        return [
-          ...acc,
-          {
-            key: orgVar.key,
-            value: null,
-          },
-        ];
-      }, []);
-
-      const removedSecret = originalEnvVars.reduce((acc, orgVar) => {
-        if (!orgVar.value.includes("PORTERSECRET")) {
-          return acc;
-        }
-
-        const variableFound = variables.find(
-          (variable) => orgVar.key === variable.key
-        );
-        if (variableFound) {
-          return acc;
-        }
-        return [
-          ...acc,
-          {
-            key: orgVar.key,
-            value: null,
-          },
-        ];
-      }, []);
-
-      normal = [...normal, ...removedNormal];
-      secret = [...secret, ...removedSecret];
-
-      const normalObject = normal.reduce((acc, val) => {
-        return {
-          ...acc,
-          [val.key]: val.value,
-        };
-      }, {});
-
-      const secretObject = secret.reduce((acc, val) => {
-        return {
-          ...acc,
-          [val.key]: val.value,
-        };
-      }, {});
-
-      try {
-        const updatedEnvGroup = await api
-          .updateConfigMap(
-            "<token>",
-            {
-              name,
-              variables: normalObject,
-              secret_variables: secretObject,
-            },
-            {
-              id: currentProject.id,
-              cluster_id: currentCluster.id,
-              namespace,
-            }
-          )
-          .then((res) => res.data);
-        setButtonStatus("successful");
-        updateEnvGroup(updatedEnvGroup);
-        setTimeout(() => {
-          setButtonStatus("");
-        }, 1000);
-      } catch (error) {
-        setButtonStatus("Couldn't update successfully");
-        setCurrentError(error);
-        setTimeout(() => {
-          setButtonStatus("");
-        }, 1000);
-      }
-    }
-  };
-
-  const renderTabContents = () => {
-    const { variables, secret_variables } = currentEnvGroup;
-
-    // const mergeVar = variables.concat(secret_variables);
-
-    switch (currentTab) {
-      case "variables-editor":
-        return (
-          <EnvGroupVariablesEditor
-            onChange={(x) => {
-              setCurrentEnvGroup((prev) => ({ ...prev, variables: x }));
-            }}
-            handleUpdateValues={handleUpdateValues}
-            variables={variables}
-            buttonStatus={buttonStatus}
-            setButtonStatus={setButtonStatus}
-          />
-        );
-      case "applications":
-        return <ApplicationsList envGroup={currentEnvGroup} />;
-      default:
-        return (
-          <EnvGroupSettings
-            namespace={namespace}
-            envGroup={currentEnvGroup}
-            handleDeleteEnvGroup={handleDeleteEnvGroup}
-          />
-        );
-    }
-  };
-
-  useEffect(() => {
-    populateEnvGroup();
-  }, [envGroup]);
-
-  if (!currentEnvGroup) {
-    return null;
-  }
-
-  return (
-    <StyledExpandedChart>
-      <BreadcrumbRow>
-        <Breadcrumb onClick={closeExpanded}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <TitleSection icon={key} iconWidth="33px">
-          {envGroup.name}
-          {!currentProject?.simplified_view_enabled && (
-            <TagWrapper>
-              Namespace{" "}
-              <NamespaceTag>
-                {currentProject?.capi_provisioner_enabled &&
-                namespace.startsWith("porter-stack-")
-                  ? namespace.replace("porter-stack-", "")
-                  : namespace}
-              </NamespaceTag>
-            </TagWrapper>
-          )}
-        </TitleSection>
-      </HeaderWrapper>
-
-      <Spacer y={1} />
-
-      {isDeleting ? (
-        <>
-          <LineBreak />
-          <Placeholder>
-            <TextWrap>
-              <Header>
-                <Spinner src={loading} /> Deleting "{currentEnvGroup.name}"
-              </Header>
-              You will be automatically redirected after deletion is complete.
-            </TextWrap>
-          </Placeholder>
-        </>
-      ) : (
-        <TabRegion
-          currentTab={currentTab}
-          setCurrentTab={(x: string) => {
-            setCurrentTab(x);
-          }}
-          options={tabOptions}
-          color={null}
-        >
-          {renderTabContents()}
-        </TabRegion>
-      )}
-    </StyledExpandedChart>
-  );
-};
-
-export default ExpandedEnvGroupFC;
-
-const EnvGroupVariablesEditor = ({
-  onChange,
-  handleUpdateValues,
-  variables,
-  buttonStatus,
-  setButtonStatus,
-}: {
-  variables: KeyValueType[];
-  buttonStatus: any;
-  onChange: (newValues: any) => void;
-  handleUpdateValues: () => void;
-  setButtonStatus: (status: string) => void;
-}) => {
-  const [isAuthorized] = useAuth();
-  const [buttonDisabled, setButtonDisabled] = useState(false);
-
-  return (
-    <TabWrapper>
-      <InnerWrapper>
-        <Heading isAtTop>Environment variables</Heading>
-        <Helper>
-          Set environment variables for your secrets and environment-specific
-          configuration.
-        </Helper>
-        <EnvGroupArray
-          setButtonDisabled={setButtonDisabled}
-          values={variables}
-          setValues={(x: any) => {
-            onChange(x);
-          }}
-          fileUpload={true}
-          secretOption={true}
-          disabled={
-            !isAuthorized("env_group", "", [
-              "get",
-              "create",
-              "delete",
-              "update",
-            ])
-          }
-        />
-      </InnerWrapper>
-      {isAuthorized("env_group", "", ["get", "update"]) && (
-        <SaveButton
-          text="Update"
-          onClick={() => {
-            handleUpdateValues();
-          }}
-          status={buttonStatus}
-          disabled={buttonStatus == "loading" || buttonDisabled}
-          makeFlush={true}
-          clearPosition={true}
-          statusPosition="right"
-        />
-      )}
-    </TabWrapper>
-  );
-};
-
-const EnvGroupSettings = ({
-  envGroup,
-  handleDeleteEnvGroup,
-  namespace,
-}: {
-  envGroup: EditableEnvGroup;
-  handleDeleteEnvGroup: () => void;
-  namespace?: string;
-}) => {
-  const { setCurrentOverlay, currentProject, currentCluster, setCurrentError } =
-    useContext(Context);
-  const [isAuthorized] = useAuth();
-
-  // When cloning an env group, append "-2" for the default name
-  // (i.e. my-env-group-2)
-  const [name, setName] = useState<string>(envGroup.name + "-2");
-  const [cloneNamespace, setCloneNamespace] = useState<string>("default");
-  const [cloneSuccess, setCloneSuccess] = useState(false);
-
-  const canDelete = useMemo(() => {
-    // add a case for when applications is null - in this case this is a deprecated env group version
-    if (currentProject?.simplified_view_enabled) {
-      if (!envGroup?.linked_applications) {
-        return true;
-      }
-
-      return envGroup?.linked_applications?.length === 0;
-    } else {
-      if (!envGroup?.applications) {
-        return true;
-      }
-
-      return envGroup?.applications?.length === 0;
-    }
-  }, [envGroup]);
-
-  const cloneEnvGroup = async () => {
-    setCloneSuccess(false);
-    try {
-      await api.cloneEnvGroup(
-        "<token>",
-        {
-          name: envGroup.name,
-          namespace: cloneNamespace,
-          clone_name: name,
-          version: envGroup.version,
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace,
-        }
-      );
-      setCloneSuccess(true);
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
-  return (
-    <TabWrapper>
-      {isAuthorized("env_group", "", ["get", "delete"]) && (
-        <InnerWrapper full={true}>
-          <Heading isAtTop>Manage environment group</Heading>
-          <Helper>
-            Permanently delete this set of environment variables. This action
-            cannot be undone.
-          </Helper>
-          {!canDelete && (
-            <Helper color="#f5cb42">
-              Applications are still synced to this env group. Navigate to
-              "Linked applications" and remove this env group from all
-              applications to delete.
-            </Helper>
-          )}
-          <Button
-            color="#b91133"
-            onClick={() => {
-              setCurrentOverlay({
-                message: `Are you sure you want to delete ${envGroup.name}?`,
-                onYes: handleDeleteEnvGroup,
-                onNo: () => {
-                  setCurrentOverlay(null);
-                },
-              });
-            }}
-            disabled={!canDelete}
-          >
-            Delete {envGroup.name}
-          </Button>
-          {!currentProject?.simplified_view_enabled && (
-            <>
-              <DarkMatter />
-              <Heading>Clone environment group</Heading>
-              <Helper>
-                Clone this set of environment variables into a new env group.
-              </Helper>
-              <InputRow
-                type="string"
-                value={name}
-                setValue={(x: string) => {
-                  setName(x);
-                }}
-                label="New env group name"
-                placeholder="ex: my-cloned-env-group"
-              />
-              <InputRow
-                type="string"
-                value={cloneNamespace}
-                setValue={(x: string) => {
-                  setCloneNamespace(x);
-                }}
-                label="New env group namespace"
-                placeholder="ex: default"
-              />
-              <FlexAlt>
-                <Button onClick={cloneEnvGroup}>Clone {envGroup.name}</Button>
-                {cloneSuccess && (
-                  <StatusWrapper position="right" successful={true}>
-                    <i className="material-icons">done</i>
-                    <StatusTextWrapper>Successfully cloned</StatusTextWrapper>
-                  </StatusWrapper>
-                )}
-              </FlexAlt>
-            </>
-          )}
-        </InnerWrapper>
-      )}
-    </TabWrapper>
-  );
-};
-
-const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => {
-  const { currentCluster, currentProject } = useContext(Context);
-
-  return (
-    <>
-      <HeadingWrapper>
-        <Heading isAtTop>Linked applications:</Heading>
-        <DocsHelper
-          link="https://docs.porter.run/deploying-applications/environment-groups#syncing-environment-groups-to-applications"
-          tooltipText="When env group sync is enabled, the applications are automatically restarted when the env groups are updated."
-          placement="top-start"
-          disableMargin
-        />
-      </HeadingWrapper>
-      {currentProject?.simplified_view_enabled
-        ? envGroup.linked_applications.map((appName) => {
-            return (
-              <StyledCard>
-                <Flex>
-                  <ContentContainer>
-                    <EventInformation>
-                      <EventName>{appName}</EventName>
-                    </EventInformation>
-                  </ContentContainer>
-                  <ActionContainer>
-                    {currentProject?.simplified_view_enabled ? (
-                      <ActionButton to={`/apps/${appName}`} target="_blank">
-                        <span className="material-icons-outlined">
-                          open_in_new
-                        </span>
-                      </ActionButton>
-                    ) : (
-                      <ActionButton
-                        to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
-                        target="_blank"
-                      >
-                        <span className="material-icons-outlined">
-                          open_in_new
-                        </span>
-                      </ActionButton>
-                    )}
-                  </ActionContainer>
-                </Flex>
-              </StyledCard>
-            );
-          })
-        : envGroup.applications.map((appName) => {
-            return (
-              <StyledCard>
-                <Flex>
-                  <ContentContainer>
-                    <EventInformation>
-                      <EventName>{appName}</EventName>
-                    </EventInformation>
-                  </ContentContainer>
-                  <ActionContainer>
-                    {currentProject?.simplified_view_enabled ? (
-                      <ActionButton to={`/apps/${appName}`} target="_blank">
-                        <span className="material-icons-outlined">
-                          open_in_new
-                        </span>
-                      </ActionButton>
-                    ) : (
-                      <ActionButton
-                        to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
-                        target="_blank"
-                      >
-                        <span className="material-icons-outlined">
-                          open_in_new
-                        </span>
-                      </ActionButton>
-                    )}
-                  </ActionContainer>
-                </Flex>
-              </StyledCard>
-            );
-          })}
-    </>
-  );
-};
-
-const FlexAlt = styled.div`
-  display: flex;
-  align-items: center;
-  margin-top: 20px;
-`;
-
-const StatusTextWrapper = styled.p`
-  display: -webkit-box;
-  line-clamp: 2;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-  line-height: 19px;
-  margin: 0;
-`;
-
-const StatusWrapper = styled.div<{
-  successful: boolean;
-  position: "right" | "left";
-}>`
-  display: flex;
-  align-items: center;
-  max-width: 170px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #ffffff55;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-top: 5px;
-  margin-bottom: 30px;
-  height: 35px;
-  margin-left: 15px;
-
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-    float: left;
-    color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
-  }
-
-  animation-fill-mode: forwards;
-
-  @keyframes statusFloatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const DarkMatter = styled.div`
-  width: 100%;
-  height: 1px;
-  margin-top: -20px;
-`;
-
-const ArrowIcon = styled.img`
-  width: 15px;
-  margin-right: 8px;
-  opacity: 50%;
-`;
-
-const BreadcrumbRow = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: flex-start;
-`;
-
-const Breadcrumb = styled.div`
-  color: #aaaabb88;
-  font-size: 13px;
-  margin-bottom: 15px;
-  display: flex;
-  align-items: center;
-  margin-top: -10px;
-  z-index: 999;
-  padding: 5px;
-  padding-right: 7px;
-  border-radius: 5px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Wrap = styled.div`
-  z-index: 999;
-`;
-
-const HeadingWrapper = styled.div`
-  display: flex;
-  margin-bottom: 15px;
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;
-
-const Placeholder = styled.div`
-  min-height: 400px;
-  height: 50vh;
-  padding: 30px;
-  padding-bottom: 90px;
-  font-size: 13px;
-  color: #ffffff44;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-`;
-
-const Spinner = styled.img`
-  width: 15px;
-  height: 15px;
-  margin-right: 12px;
-  margin-bottom: -2px;
-`;
-
-const TextWrap = styled.div``;
-
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 1px;
-  background: #494b4f;
-  margin: 15px 0px 55px;
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-`;
-
-const BackButton = styled.div`
-  position: absolute;
-  top: 0px;
-  right: 0px;
-  display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
-`;
-
-const Button = styled.button`
-  height: 35px;
-  font-size: 13px;
-  margin-top: 5px;
-  margin-bottom: 30px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  color: white;
-  padding: 6px 20px 7px 20px;
-  text-align: left;
-  border: 0;
-  border-radius: 5px;
-  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
-  user-select: none;
-  :focus {
-    outline: 0;
-  }
-  :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
-  }
-`;
-
-const CloneButton = styled(Button)`
-  display: flex;
-  width: fit-content;
-  align-items: center;
-  justify-content: center;
-  background-color: #ffffff11;
-  :hover {
-    background-color: #ffffff18;
-  }
-`;
-
-const InnerWrapper = styled.div<{ full?: boolean }>`
-  width: 100%;
-  height: ${(props) => (props.full ? "100%" : "calc(100% - 65px)")};
-  padding: 30px;
-  padding-bottom: 15px;
-  position: relative;
-  overflow: auto;
-  margin-bottom: 30px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-`;
-
-const TabWrapper = styled.div`
-  height: 100%;
-  width: 100%;
-  padding-bottom: 65px;
-  overflow: hidden;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin: 10px 0px 17px 0px;
-  height: 20px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 0;
-  margin-top: -1px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  margin-left: 20px;
-  margin-bottom: -3px;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 5px;
-  background: #26282e;
-`;
-
-const NamespaceTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #43454a;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-`;
-
-const StyledExpandedChart = styled.div`
-  width: 100%;
-  z-index: 0;
-  animation: fadeIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  display: flex;
-  overflow-y: auto;
-  padding-bottom: 120px;
-  flex-direction: column;
-  overflow: visible;
-
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
-  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
-  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
-`;
-
-const Subtitle = styled.div`
-  padding: 11px 0px 16px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #aaaabb;
-  line-height: 1.6em;
-  display: flex;
-  align-items: center;
-`;
-
-const fadeIn = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-const StyledCard = styled.div`
-  border-radius: 8px;
-  padding: 10px 18px;
-  overflow: hidden;
-  font-size: 13px;
-  animation: ${fadeIn} 0.5s;
-
-  background: #2b2e3699;
-  margin-bottom: 15px;
-  overflow: hidden;
-  border: 1px solid #ffffff0a;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-`;
-
-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 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(DynamicLink)`
-  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;
-  border: 1px solid #ffffff00;
-
-  :hover {
-    background: #ffffff11;
-    border: 1px solid #ffffff44;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-`;

+ 0 - 211
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx

@@ -1,211 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { useQuery } from "@tanstack/react-query";
-import { useParams, withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import Placeholder from "components/Placeholder";
-
-import api from "shared/api";
-import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { Context } from "shared/Context";
-import { getQueryParam } from "shared/routing";
-import { type ClusterType } from "shared/types";
-
-import ExpandedEnvGroup from "./ExpandedEnvGroup";
-
-type PropsType = RouteComponentProps &
-  WithAuthProps & {
-    currentCluster: ClusterType;
-  };
-
-const EnvGroupDashboard = (props: PropsType) => {
-  const namespace =
-    currentProject?.simplified_view_enabled &&
-    currentProject?.capi_provisioner_enabled
-      ? "porter-env-group"
-      : getQueryParam(props, "namespace");
-  const params = useParams<{ name: string }>();
-  const { currentProject } = useContext(Context);
-  const [expandedEnvGroup, setExpandedEnvGroup] = useState<any>();
-  const isTabActive = () => {
-    return !document.hidden;
-  };
-
-  const {
-    data: envGroups,
-    isLoading: listEnvGroupsLoading,
-    isError,
-    refetch,
-  } = useQuery<any[]>(
-    ["envGroupList", currentProject.id, namespace, props.currentCluster.id],
-    async () => {
-      try {
-        if (!namespace) {
-          if (!currentProject?.simplified_view_enabled) {
-            return [];
-          }
-        }
-        let res: any[] = [];
-        if (currentProject?.simplified_view_enabled) {
-          res = await api.getAllEnvGroups(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              cluster_id: props.currentCluster.id,
-            }
-          );
-        } else {
-          res = await api.listEnvGroups(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              namespace: currentProject?.simplified_view_enabled
-                ? "porter-env-group"
-                : namespace,
-              cluster_id: props.currentCluster.id,
-            }
-          );
-        }
-        return currentProject?.simplified_view_enabled
-          ? res.data?.environment_groups
-          : res.data;
-      } catch (err) {
-        throw err;
-      }
-    },
-    {
-      enabled: false, // Initially disable the query
-    }
-  );
-
-  useEffect(() => {
-    const name = params.name;
-
-    if (!envGroups || !isTabActive()) {
-      return;
-    }
-
-    const envGroup = envGroups.find((envGroup) => envGroup.name === name);
-    setExpandedEnvGroup(envGroup);
-  }, [envGroups, params]);
-
-  useEffect(() => {
-    if (isTabActive()) {
-      refetch(); // Run the query when the component mounts and the tab is active
-    }
-  }, []);
-  if (listEnvGroupsLoading) {
-    return (
-      <Placeholder>
-        <Loading />
-      </Placeholder>
-    );
-  }
-
-  const renderContents = () => {
-    if (!expandedEnvGroup) {
-      return null;
-    }
-
-    return (
-      <ExpandedEnvGroup
-        allEnvGroups={envGroups}
-        isAuthorized={props.isAuthorized}
-        namespace={
-          currentProject?.simplified_view_enabled &&
-          currentProject?.capi_provisioner_enabled
-            ? "porter-env-group"
-            : expandedEnvGroup?.namespace ?? namespace
-        }
-        currentCluster={props.currentCluster}
-        envGroup={expandedEnvGroup}
-        closeExpanded={() => {
-          props.history.push("/env-groups");
-        }}
-      />
-    );
-  };
-
-  if (listEnvGroupsLoading) {
-    return (
-      <Placeholder>
-        <Loading />
-      </Placeholder>
-    );
-  }
-
-  return <>{renderContents()}</>;
-};
-
-export default withRouter(withAuth(EnvGroupDashboard));
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  border-bottom: 30px solid transparent;
-`;
-
-const SortFilterWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  border-bottom: 30px solid transparent;
-  > div:not(:first-child) {
-  }
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: ${(props: { hasMultipleChilds: boolean }) => {
-    if (props.hasMultipleChilds) {
-      return "space-between";
-    }
-    return "flex-end";
-  }};
-  align-items: center;
-  flex-wrap: wrap;
-`;
-
-const Button = styled.div`
-  display: flex;
-  margin-left: 10px;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  min-width: 155px;
-  padding-right: 13px;
-  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;
-  }
-`;

+ 0 - 157
dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx

@@ -1,157 +0,0 @@
-import React, { useContext, useMemo, useState } from "react";
-import Color from "color";
-import styled from "styled-components";
-
-import InputRow from "components/form-components/InputRow";
-import SaveButton from "components/SaveButton";
-
-import api from "shared/api";
-import { isAlphanumeric } from "shared/common";
-import { Context } from "shared/Context";
-import { type ChartType } from "shared/types";
-
-type Props = {
-  onSave: (() => void) | (() => Promise<void>);
-  release: ChartType;
-};
-
-const CanonicalName = ({ onSave, release }: Props) => {
-  const { currentProject, currentCluster, setCurrentError } =
-    useContext(Context);
-  const [buttonStatus, setButtonStatus] = useState("");
-  const [canonicalName, setCanonicalName] = useState<string>(
-    release.canonical_name
-  );
-
-  const handleSave = async () => {
-    setButtonStatus("loading");
-
-    try {
-      await api.updateCanonicalName(
-        "<token>",
-        { canonical_name: canonicalName },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace: release.namespace,
-          release_name: release.name,
-        }
-      );
-      await onSave();
-      setButtonStatus("successful");
-    } catch (error) {
-      console.log(error);
-      setCurrentError(
-        "We couldn't change the canonical name. Please try again."
-      );
-      setButtonStatus("Canonical name not changed.");
-      return;
-    } finally {
-      setTimeout(() => {
-        setButtonStatus("");
-      }, 800);
-    }
-  };
-
-  const shouldDisableSave = useMemo(() => {
-    if (canonicalName !== release.canonical_name) {
-      if (canonicalName === "") {
-        return false;
-      }
-
-      return !isAlphanumeric(canonicalName) || canonicalName.length > 63;
-    }
-
-    return true;
-  }, [canonicalName]);
-
-  const saveButtonHelper = useMemo(() => {
-    if (canonicalName !== release.canonical_name) {
-      if (canonicalName !== "") {
-        if (!isAlphanumeric(canonicalName)) {
-          return "Invalid characters in the name";
-        } else if (canonicalName.length > 63) {
-          return "Name cannot exceed 63 characters";
-        }
-      }
-
-      return "Unsaved changes";
-    }
-
-    return "";
-  }, [canonicalName]);
-
-  return (
-    <>
-      <InputRow
-        type="text"
-        value={canonicalName}
-        setValue={(x: string) => {
-          setCanonicalName(x);
-        }}
-        placeholder="ex: my-app"
-        isRequired={true}
-        width={"100%"}
-      />
-      <Flex
-        style={{
-          marginTop: "25px",
-        }}
-      >
-        <SaveButton
-          helper={saveButtonHelper}
-          clearPosition
-          disabled={shouldDisableSave}
-          statusPosition="right"
-          text="Save changes"
-          onClick={async () => {
-            await handleSave();
-          }}
-          status={buttonStatus}
-        ></SaveButton>
-      </Flex>
-      <Br />
-    </>
-  );
-};
-
-const Br = styled.div`
-  width: 100%;
-  height: 10px;
-`;
-
-export default CanonicalName;
-
-const Flex = styled.div`
-  display: flex;
-  position: relative;
-`;
-
-const Tag = styled.div<{ color: string }>`
-  display: inline-flex;
-  color: ${(props) => Color(props.color).darken(0.4).string() || "inherit"};
-  user-select: none;
-  border: 1px solid ${(props) => Color(props.color).darken(0.4).string()};
-  border-radius: 5px;
-  padding: 4px 8px;
-  position: relative;
-  margin-bottom: 20px;
-  text-align: center;
-  align-items: center;
-  font-size: 13px;
-  background-color: ${(props) => props.color || "inherit"};
-
-  max-width: 150px;
-  min-width: 60px;
-
-  :not(:last-child) {
-    margin-right: 10px;
-  }
-
-  > .material-icons {
-    font-size: 16px;
-    :hover {
-      cursor: pointer;
-    }
-  }
-`;

+ 0 - 104
dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx

@@ -1,104 +0,0 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-
-import { integrationList } from "shared/common";
-import { type ChartType } from "shared/types";
-
-type Props = {
-  currentChart: ChartType;
-};
-
-const DeploymentType: React.FC<Props> = ({ currentChart }) => {
-  const [showRepoTooltip, setShowRepoTooltip] = useState(false);
-
-  const githubRepository = currentChart?.git_action_config?.git_repo;
-  const icon = githubRepository
-    ? integrationList.repo.icon
-    : integrationList.registry.icon;
-
-  const repository =
-    githubRepository ||
-    currentChart?.image_repo_uri ||
-    currentChart?.config?.image?.repository;
-
-  if (repository?.includes("hello-porter")) {
-    return null;
-  }
-
-  return (
-    <DeploymentImageContainer>
-      <DeploymentTypeIcon src={icon} />
-      <RepositoryName
-        onMouseOver={() => {
-          setShowRepoTooltip(true);
-        }}
-        onMouseOut={() => {
-          setShowRepoTooltip(false);
-        }}
-      >
-        {repository}
-      </RepositoryName>
-      {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
-    </DeploymentImageContainer>
-  );
-};
-
-export default DeploymentType;
-
-const DeploymentImageContainer = styled.div`
-  height: 20px;
-  font-size: 13px;
-  position: relative;
-  display: flex;
-  margin-left: 15px;
-  margin-bottom: -3px;
-  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: -40px;
-  top: 28px;
-  min-height: 18px;
-  max-width: calc(700px);
-  padding: 5px 7px;
-  background: #272731;
-  z-index: 999;
-  color: white;
-  font-size: 12px;
-  font-family: "Work Sans", sans-serif;
-  outline: 1px solid #ffffff55;
-  opacity: 0;
-  animation: faded-in 0.2s 0.15s;
-  animation-fill-mode: forwards;
-  @keyframes faded-in {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;

+ 0 - 1331
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,1331 +0,0 @@
-import React, { useCallback, useContext, useEffect, useState } from "react";
-import yaml from "js-yaml";
-import _, { cloneDeep } from "lodash";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import Banner from "components/porter/Banner";
-import Spacer from "components/porter/Spacer";
-import TitleSection from "components/TitleSection";
-
-import api from "shared/api";
-import useAuth from "shared/auth/useAuth";
-import { Context } from "shared/Context";
-import { useWebsockets } from "shared/hooks/useWebsockets";
-import {
-  type ChartType,
-  type ClusterType,
-  type ResourceType,
-} from "shared/types";
-import leftArrow from "assets/left-arrow.svg";
-import loadingSrc from "assets/loading.gif";
-
-import BuildSettingsTab from "./build-settings/BuildSettingsTab";
-import DeployStatusSection from "./deploy-status-section/DeployStatusSection";
-import DeploymentType from "./DeploymentType";
-import EventsTab from "./events/EventsTab";
-import GraphSection from "./GraphSection";
-import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces";
-import ListSection from "./ListSection";
-import LogsSection, { type InitLogData } from "./logs-section/LogsSection";
-import MetricsSection from "./metrics/MetricsSection";
-import RevisionSection from "./RevisionSection";
-import SettingsSection from "./SettingsSection";
-import StatusSection from "./status/StatusSection";
-import { useStackEnvGroups } from "./useStackEnvGroups";
-import ValuesYaml from "./ValuesYaml";
-
-type Props = {
-  namespace: string;
-  currentChart: ChartType;
-  currentCluster: ClusterType;
-  closeChart: () => void;
-  setSidebar: (x: boolean) => void;
-  isMetricsInstalled: boolean;
-};
-
-const getReadableDate = (s: string) => {
-  const ts = new Date(s);
-  const date = ts.toLocaleDateString();
-  const time = ts.toLocaleTimeString([], {
-    hour: "numeric",
-    minute: "2-digit",
-  });
-  return `${time} on ${date}`;
-};
-
-const templateWhitelist = [
-  "elasticache-redis",
-  "rds-postgresql",
-  "rds-postgresql-aurora",
-];
-
-const ExpandedChart: React.FC<Props> = (props) => {
-  const [currentChart, setCurrentChart] = useState<ChartType>(
-    props.currentChart
-  );
-  const [showRevisions, setShowRevisions] = useState<boolean>(false);
-  const [loading, setLoading] = useState<boolean>(false);
-  const [components, setComponents] = useState<ResourceType[]>([]);
-  const [isPreview, setIsPreview] = useState<boolean>(false);
-  const [devOpsMode, setDevOpsMode] = useState<boolean>(
-    localStorage.getItem("devOpsMode") === "true"
-  );
-  const [rightTabOptions, setRightTabOptions] = useState<any[]>([]);
-  const [leftTabOptions, setLeftTabOptions] = useState<any[]>([]);
-  const [saveValuesStatus, setSaveValueStatus] = useState<string>(null);
-  const [forceRefreshRevisions, setForceRefreshRevisions] =
-    useState<boolean>(false);
-  const [controllers, setControllers] = useState<
-    Record<string, Record<string, any>>
-  >({});
-  const [url, setUrl] = useState<string>(null);
-  const [deleting, setDeleting] = useState<boolean>(false);
-  const [imageIsPlaceholder, setImageIsPlaceholer] = useState<boolean>(false);
-  const [newestImage, setNewestImage] = useState<string>(null);
-  const [isLoadingChartData, setIsLoadingChartData] = useState<boolean>(true);
-  const [isAuthorized] = useAuth();
-  const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
-  const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
-  const [logData, setLogData] = useState<InitLogData>({});
-  const [overrideCurrentTab, setOverrideCurrentTab] = useState("");
-  const [isAgentInstalled, setIsAgentInstalled] = useState<boolean>(false);
-  const [databaseStatus, setDatabaseStatus] = useState<boolean>(true);
-
-  const { isStack, stackEnvGroups, isLoadingStackEnvGroups } =
-    useStackEnvGroups(currentChart);
-
-  const { newWebsocket, openWebsocket, closeAllWebsockets, closeWebsocket } =
-    useWebsockets();
-
-  const { currentCluster, currentProject, setCurrentError, setCurrentOverlay } =
-    useContext(Context);
-
-  const renderLogsAtTimestamp = (initLogData: InitLogData) => {
-    setLogData(initLogData);
-    setOverrideCurrentTab("logs");
-  };
-
-  // Retrieve full chart data (includes form and values)
-  const getChartData = async (chart: ChartType) => {
-    setIsLoadingChartData(true);
-    const res = await api.getChart(
-      "<token>",
-      {},
-      {
-        name: chart.name,
-        namespace: chart.namespace,
-        cluster_id: currentCluster.id,
-        revision: chart.version,
-        id: currentProject.id,
-      }
-    );
-    const image = res.data?.config?.image?.repository;
-    const tag = res.data?.config?.image?.tag?.toString();
-    const newNewestImage = tag ? image + ":" + tag : image;
-    let imageIsPlaceholder = false;
-    if (
-      (image === "porterdev/hello-porter" ||
-        image === "public.ecr.aws/o1j4x7p4/hello-porter") &&
-      !newestImage
-    ) {
-      imageIsPlaceholder = true;
-    }
-    setImageIsPlaceholer(imageIsPlaceholder);
-    setNewestImage(newNewestImage);
-
-    const updatedChart = res.data;
-
-    setCurrentChart(updatedChart);
-
-    updateComponents(updatedChart).finally(() => {
-      setIsLoadingChartData(false);
-    });
-  };
-
-  const getControllers = async (chart: ChartType) => {
-    // don't retrieve controllers for chart that failed to even deploy.
-    if (chart.info.status == "failed") return;
-
-    try {
-      const { data: chartControllers } = await api.getChartControllers(
-        "<token>",
-        {},
-        {
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          revision: chart.version,
-          id: currentProject.id,
-        }
-      );
-
-      chartControllers.forEach((c: any) => {
-        c.metadata.kind = c.kind;
-
-        setControllers((oldControllers) => ({
-          ...oldControllers,
-          [c.metadata.uid]: c,
-        }));
-      });
-    } catch (error) {
-      if (typeof error !== "string") {
-        setCurrentError(JSON.stringify(error));
-      }
-      setCurrentError(error);
-    }
-  };
-
-  const setupWebsocket = (kind: string) => {
-    const apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status`;
-
-    const wsConfig = {
-      onmessage(evt: MessageEvent) {
-        const event = JSON.parse(evt.data);
-        const object = event.Object;
-        object.metadata.kind = event.Kind;
-
-        if (event.event_type != "UPDATE") {
-          return;
-        }
-
-        setControllers((oldControllers) => {
-          if (
-            oldControllers &&
-            oldControllers[object.metadata.uid]?.status?.conditions ==
-              object.status?.conditions
-          ) {
-            return oldControllers;
-          }
-          return {
-            ...oldControllers,
-            [object.metadata.uid]: object,
-          };
-        });
-      },
-      onerror() {
-        closeWebsocket(kind);
-      },
-    };
-
-    newWebsocket(kind, apiEndpoint, wsConfig);
-  };
-
-  const updateComponents = async (currentChart: ChartType) => {
-    setLoading(true);
-    try {
-      const res = await api.getChartComponents(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          name: currentChart.name,
-          namespace: currentChart.namespace,
-          cluster_id: currentCluster.id,
-          revision: currentChart.version,
-        }
-      );
-      setComponents(res.data.Objects);
-      setLoading(false);
-    } catch (error) {
-      console.log(error);
-      setLoading(false);
-    }
-  };
-
-  const onSubmit = async (props: any) => {
-    const rawValues = props.values;
-
-    // Convert dotted keys to nested objects
-    let values: any = {};
-
-    // Weave in preexisting values and convert to yaml
-    if (props?.currentChart?.config) {
-      values = props.currentChart.config;
-    }
-
-    // Override config from currentChart prop if we have it on the current state
-    if (currentChart.config) {
-      values = currentChart.config;
-    }
-
-    for (const key in rawValues) {
-      _.set(values, key, rawValues[key]);
-    }
-
-    const valuesYaml = yaml.dump({
-      ...values,
-    });
-
-    const syncedEnvGroups = props?.metadata
-      ? props?.metadata["container.env"]
-      : {};
-
-    const deletedEnvGroups = syncedEnvGroups?.deleted || [];
-
-    const addedEnvGroups = syncedEnvGroups?.added || [];
-
-    const addApplicationToEnvGroupPromises = addedEnvGroups.map(
-      async (envGroup: any) => {
-        return await api.addApplicationToEnvGroup(
-          "<token>",
-          {
-            name: envGroup?.name,
-            app_name: currentChart.name,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            namespace: currentChart.namespace,
-          }
-        );
-      }
-    );
-
-    try {
-      await Promise.all(addApplicationToEnvGroupPromises);
-    } catch (error) {
-      setCurrentError(
-        "We coudln't sync the env group to the application, please try again."
-      );
-    }
-
-    const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
-      async (envGroup: any) => {
-        return await api.removeApplicationFromEnvGroup(
-          "<token>",
-          {
-            name: envGroup?.name,
-            app_name: currentChart.name,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            namespace: currentChart.namespace,
-          }
-        );
-      }
-    );
-    try {
-      await Promise.all(removeApplicationToEnvGroupPromises);
-    } catch (error) {
-      setCurrentError(
-        "We coudln't remove the synced env group from the application, please try again."
-      );
-    }
-
-    setSaveValueStatus("loading");
-
-    try {
-      await api.upgradeChartValues(
-        "<token>",
-        {
-          values: valuesYaml,
-          // this is triggered from the Porter form, so we set the latest revision to ensure that the release is
-          // up to date
-          latest_revision: currentChart.version,
-        },
-        {
-          id: currentProject.id,
-          namespace: currentChart.namespace,
-          name: currentChart.name,
-          cluster_id: currentCluster.id,
-        }
-      );
-
-      getChartData(currentChart);
-
-      setSaveValueStatus("successful");
-      setForceRefreshRevisions(true);
-
-      window.analytics?.track("Chart Upgraded", {
-        chart: currentChart.name,
-        values: valuesYaml,
-      });
-    } catch (err) {
-      const parsedErr = err?.response?.data?.error;
-
-      if (parsedErr) {
-        err = parsedErr;
-      }
-
-      setSaveValueStatus("The api answered with an error");
-
-      setCurrentError(JSON.stringify(parsedErr));
-
-      window.analytics?.track("Failed to Upgrade Chart", {
-        chart: currentChart.name,
-        values: valuesYaml,
-        error: err,
-      });
-    }
-  };
-
-  const handleUpgradeVersion = useCallback(
-    async (version: string, cb: () => void) => {
-      // convert current values to yaml
-      const values = currentChart.config;
-
-      const valuesYaml = yaml.dump({
-        ...values,
-      });
-
-      setSaveValueStatus("loading");
-      getChartData(currentChart);
-
-      try {
-        await api.upgradeChartValues(
-          "<token>",
-          {
-            values: valuesYaml,
-            version,
-            latest_revision: currentChart.version,
-          },
-          {
-            id: currentProject.id,
-            namespace: currentChart.namespace,
-            name: currentChart.name,
-            cluster_id: currentCluster.id,
-          }
-        );
-        setSaveValueStatus("successful");
-        setForceRefreshRevisions(true);
-
-        window.analytics?.track("Chart Upgraded", {
-          chart: currentChart.name,
-          values: valuesYaml,
-        });
-
-        cb && cb();
-      } catch (err) {
-        const parsedErr = err?.response?.data?.error;
-
-        if (parsedErr) {
-          err = parsedErr;
-        }
-
-        setSaveValueStatus(err);
-        setCurrentError(parsedErr);
-
-        window.analytics?.track("Failed to Upgrade Chart", {
-          chart: currentChart.name,
-          values: valuesYaml,
-          error: err,
-        });
-      }
-    },
-    [currentChart]
-  );
-
-  const renderTabContents = (currentTab: string) => {
-    const { setSidebar } = props;
-    const chart = currentChart; // // Reset the logData when navigating to a different tab
-
-    switch (currentTab) {
-      case "logs":
-        if (!isAgentInstalled) {
-          return null;
-        }
-
-        return (
-          <LogsSection
-            currentChart={chart}
-            isFullscreen={isFullscreen}
-            setIsFullscreen={setIsFullscreen}
-            initData={logData}
-            setInitData={setLogData}
-          />
-        );
-      case "metrics":
-        return <MetricsSection currentChart={chart} />;
-      case "events":
-        if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
-          return null;
-        }
-        return (
-          <EventsTab currentChart={chart} setLogData={renderLogsAtTimestamp} />
-        );
-      case "status":
-        if (isLoadingChartData) {
-          return (
-            <Placeholder>
-              <Loading />
-            </Placeholder>
-          );
-        }
-        if (imageIsPlaceholder) {
-          return (
-            <Placeholder>
-              <TextWrap>
-                <Header>
-                  <Spinner src={loadingSrc} /> This application is currently
-                  being deployed
-                </Header>
-                {props.currentChart.git_action_config?.gitlab_integration_id ? (
-                  <>
-                    Navigate to the{" "}
-                    <A
-                      href={`https://gitlab.com/${props.currentChart.git_action_config?.git_repo}/-/jobs`}
-                      target={"_blank"}
-                    >
-                      Jobs
-                    </A>{" "}
-                    tab of your GitLab repo to view live build logs.
-                  </>
-                ) : (
-                  <>
-                    Navigate to the{" "}
-                    <A
-                      href={`https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`}
-                      target={"_blank"}
-                    >
-                      Actions
-                    </A>{" "}
-                    tab of your GitHub repo to view live build logs.
-                  </>
-                )}
-              </TextWrap>
-            </Placeholder>
-          );
-        } else {
-          return (
-            <StatusSection
-              currentChart={chart}
-              setFullScreenLogs={() => {
-                setFullScreenLogs(true);
-              }}
-            />
-          );
-        }
-      case "settings":
-        return (
-          <SettingsSection
-            currentChart={chart}
-            refreshChart={async () => {
-              await getChartData(currentChart);
-            }}
-            setShowDeleteOverlay={(x: boolean) => {
-              if (x) {
-                setCurrentOverlay({
-                  message: `Are you sure you want to delete ${currentChart.name}?`,
-                  onYes: handleUninstallChart,
-                  onNo: () => {
-                    setCurrentOverlay(null);
-                  },
-                });
-              } else {
-                setCurrentOverlay(null);
-              }
-            }}
-          />
-        );
-      case "graph":
-        return (
-          <GraphSection
-            components={components}
-            currentChart={chart}
-            setSidebar={setSidebar}
-            // Handle resize YAML wrapper
-            showRevisions={showRevisions}
-          />
-        );
-      case "list":
-        return (
-          <ListSection
-            currentChart={chart}
-            components={components}
-            // Handle resize YAML wrapper
-            showRevisions={showRevisions}
-          />
-        );
-      case "values":
-        return (
-          <ValuesYaml
-            currentChart={chart}
-            refreshChart={async () => {
-              await getChartData(currentChart);
-            }}
-            disabled={!isAuthorized("application", "", ["get", "update"])}
-          />
-        );
-      case "build-settings":
-        return (
-          <BuildSettingsTab
-            chart={chart}
-            isPreviousVersion={isPreview}
-            onSave={() => {
-              getChartData(currentChart);
-            }}
-          />
-        );
-      default:
-    }
-  };
-
-  const updateTabs = () => {
-    // Collate non-form tabs
-    let rightTabOptions = [] as any[];
-    let leftTabOptions = [] as any[];
-    if (
-      currentChart.chart.metadata.home === "https://getporter.dev/" &&
-      (currentChart.chart.metadata.name === "web" ||
-        currentChart.chart.metadata.name === "worker" ||
-        currentChart.chart.metadata.name === "job") &&
-      currentCluster.agent_integration_enabled
-    ) {
-      leftTabOptions.push({ label: "Events", value: "events" });
-
-      if (isAgentInstalled) {
-        leftTabOptions.push({ label: "Logs", value: "logs" });
-      }
-    }
-    leftTabOptions.push({ label: "Status", value: "status" });
-
-    if (props.isMetricsInstalled) {
-      leftTabOptions.push({ label: "Metrics", value: "metrics" });
-    }
-
-    rightTabOptions.push({ label: "Chart Overview", value: "graph" });
-
-    if (devOpsMode) {
-      rightTabOptions.push(
-        { label: "Manifests", value: "list" },
-        { label: "Helm Values", value: "values" }
-      );
-    }
-
-    if (currentChart?.git_action_config?.git_repo && !isStack) {
-      rightTabOptions.push({
-        label: "Build Settings",
-        value: "build-settings",
-      });
-    }
-
-    // Settings tab is always last
-    if (isAuthorized("application", "", ["get", "delete"])) {
-      rightTabOptions.push({ label: "Settings", value: "settings" });
-    }
-
-    // Filter tabs if previewing an old revision or updating the chart version
-    if (isPreview) {
-      const liveTabs = ["status", "events", "settings", "deploy", "metrics"];
-      rightTabOptions = rightTabOptions.filter(
-        (tab: any) => !liveTabs.includes(tab.value)
-      );
-      leftTabOptions = leftTabOptions.filter(
-        (tab: any) => !liveTabs.includes(tab.value)
-      );
-    }
-
-    setLeftTabOptions(leftTabOptions);
-    setRightTabOptions(rightTabOptions);
-  };
-
-  const setRevision = (chart: ChartType, isCurrent?: boolean) => {
-    // if we've set the revision, we also override the revision in log data
-    const newLogData = logData;
-
-    newLogData.revision = `${chart.version}`;
-
-    setLogData(newLogData);
-
-    setIsPreview(!isCurrent);
-    getChartData(chart);
-  };
-
-  // TODO: consolidate with pop + push in refreshTabs
-  const toggleDevOpsMode = () => {
-    setDevOpsMode(!devOpsMode);
-  };
-
-  const renderUrl = () => {
-    if (url) {
-      return (
-        <Url>
-          <i className="material-icons">link</i>
-          <a href={url} target="_blank" rel="noreferrer">
-            {url}
-          </a>
-        </Url>
-      );
-    }
-
-    const service: any = components?.find((c) => {
-      return c.Kind === "Service";
-    });
-
-    if (loading) {
-      return (
-        <Url>
-          <Bolded>Loading...</Bolded>
-        </Url>
-      );
-    }
-
-    if (!service?.Name || !service?.Namespace) {
-      return;
-    }
-
-    return (
-      <Url>
-        <Bolded>Internal URI:</Bolded>
-        {`${service.Name}.${service.Namespace}.svc.cluster.local`}
-      </Url>
-    );
-  };
-
-  const renderHelmReleaseName = () => {
-    return (
-      <Url>
-        <Bolded>Helm Release Name:</Bolded>
-        {currentChart.name}
-      </Url>
-    );
-  };
-
-  const handleUninstallChart = async () => {
-    setDeleting(true);
-    setCurrentOverlay(null);
-    const syncedEnvGroups = currentChart.config?.container?.env?.synced || [];
-    const removeApplicationToEnvGroupPromises = syncedEnvGroups.map(
-      async (envGroup: any) => {
-        return await api.removeApplicationFromEnvGroup(
-          "<token>",
-          {
-            name: envGroup?.name,
-            app_name: currentChart.name,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            namespace: currentChart.namespace,
-          }
-        );
-      }
-    );
-    try {
-      await Promise.all(removeApplicationToEnvGroupPromises);
-    } catch (error) {
-      setCurrentError(
-        "We coudln't remove the synced env group from the application, please remove it manually before uninstalling the chart, or try again."
-      );
-      return;
-    }
-
-    try {
-      if (currentChart.stack_id) {
-        await api.removeStackAppResource(
-          "<token>",
-          {},
-          {
-            namespace: currentChart.namespace,
-            app_resource_name: currentChart.name,
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            stack_id: currentChart.stack_id,
-          }
-        );
-      } else {
-        await api.uninstallTemplate(
-          "<token>",
-          {},
-          {
-            namespace: currentChart.namespace,
-            name: currentChart.name,
-            id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        );
-      }
-
-      props.closeChart();
-    } catch (error) {
-      console.log(error);
-      setCurrentError("Couldn't uninstall chart, please try again");
-    }
-  };
-
-  // Check if porter agent is installed. If not installed hide the `Logs` component
-  useEffect(() => {
-    if (
-      !currentCluster.agent_integration_enabled ||
-      // If chart is an add on, we don't need to check if agent is installed
-      !["web", "worker", "job"].includes(currentChart?.chart?.metadata?.name)
-    ) {
-      return;
-    }
-
-    api
-      .detectPorterAgent(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (res.data?.version == "v3") {
-          setIsAgentInstalled(true);
-        } else {
-          setIsAgentInstalled(false);
-        }
-      })
-      .catch((err) => {
-        setIsAgentInstalled(false);
-
-        if (err.status !== 404) {
-          setCurrentError(
-            "We could not detect the Porter agent installation status, please try again."
-          );
-        }
-      });
-  }, [currentChart]);
-
-  useEffect(() => {
-    if (logData.revision) {
-      api
-        .getChart(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            namespace: props.currentChart.namespace,
-            cluster_id: currentCluster.id,
-            name: props.currentChart.name,
-            revision: parseInt(logData.revision),
-          }
-        )
-        .then((res) => {
-          setCurrentChart(res.data || props.currentChart);
-        })
-        .catch(console.log);
-
-      return;
-    }
-
-    setCurrentChart(props.currentChart);
-  }, [logData, props.currentChart]);
-
-  useEffect(() => {
-    window.analytics?.track("Opened Chart", {
-      chart: currentChart.name,
-    });
-
-    getChartData(currentChart).then(() => {
-      getControllers(currentChart).then(() => {
-        ["deployment", "statefulset", "daemonset", "replicaset"]
-          .map((kind) => {
-            setupWebsocket(kind);
-            return kind;
-          })
-          .forEach((kind) => {
-            openWebsocket(kind);
-          });
-      });
-    });
-    return () => {
-      closeAllWebsockets();
-    };
-  }, []);
-
-  useEffect(() => {
-    updateTabs();
-    localStorage.setItem("devOpsMode", devOpsMode.toString());
-  }, [devOpsMode, currentChart?.form, isPreview, isAgentInstalled]);
-
-  useEffect((): any => {
-    let isSubscribed = true;
-
-    const ingressComponent = components?.find(
-      (c) =>
-        c.Kind === "Ingress" ||
-        (c.Kind === "Gateway" &&
-          c.RawYAML?.apiVersion?.startsWith("networking.istio.io"))
-    );
-
-    const ingressName = ingressComponent?.Name;
-
-    if (!ingressName) return;
-
-    api
-      .getIngress(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          name: ingressName,
-          cluster_id: currentCluster.id,
-          namespace: `${currentChart.namespace}`,
-        }
-      )
-      .then((res) => {
-        if (!isSubscribed) {
-          return;
-        }
-        if (res.data?.spec?.rules?.[0]?.host) {
-          setUrl(`https://${res.data?.spec?.rules[0]?.host}`);
-          return;
-        }
-
-        if (res.data?.status?.loadBalancer?.ingress) {
-          setUrl(
-            `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}`
-          );
-          return;
-        }
-
-        if (res.data?.spec?.servers?.[0]?.hosts?.[0]) {
-          setUrl(`http://${res.data?.spec?.servers[0]?.hosts[0]}`);
-        }
-      })
-      .catch(console.log);
-
-    return () => (isSubscribed = false);
-  }, [components, currentCluster, currentProject, currentChart]);
-
-  return (
-    <>
-      {fullScreenLogs ? (
-        <StatusSection
-          fullscreen={true}
-          currentChart={currentChart}
-          setFullScreenLogs={() => {
-            setFullScreenLogs(false);
-          }}
-        />
-      ) : (
-        <>
-          {isFullscreen ? (
-            <LogsSection
-              isFullscreen={true}
-              setIsFullscreen={setIsFullscreen}
-              currentChart={currentChart}
-              setInitData={() => {}}
-            />
-          ) : (
-            <StyledExpandedChart>
-              <BreadcrumbRow>
-                <Breadcrumb onClick={props.closeChart}>
-                  <ArrowIcon src={leftArrow} />
-                  <Wrap>Back</Wrap>
-                </Breadcrumb>
-              </BreadcrumbRow>
-              <HeaderWrapper>
-                <TitleSection
-                  icon={currentChart.chart.metadata.icon}
-                  iconWidth="33px"
-                >
-                  {currentChart.canonical_name === ""
-                    ? currentChart.name
-                    : currentChart.canonical_name}
-                  <DeploymentType currentChart={currentChart} />
-                  <TagWrapper>
-                    Namespace{" "}
-                    <NamespaceTag>{currentChart.namespace}</NamespaceTag>
-                  </TagWrapper>
-                </TitleSection>
-
-                {currentChart.chart.metadata.name != "worker" &&
-                  currentChart.chart.metadata.name != "job" &&
-                  renderUrl()}
-
-                {currentChart.canonical_name !== "" && renderHelmReleaseName()}
-                <InfoWrapper>
-                  {/*
-                  <StatusIndicator
-                    controllers={controllers}
-                    status={currentChart.info.status}
-                    margin_left={"0px"}
-                  />
-                  */}
-                  {!templateWhitelist.includes(
-                    currentChart.chart.metadata.name
-                  ) && (
-                    <>
-                      <DeployStatusSection
-                        chart={currentChart}
-                        setLogData={renderLogsAtTimestamp}
-                      />
-                      <LastDeployed>
-                        <Dot>•</Dot>Last deployed
-                        {" " + getReadableDate(currentChart.info.last_deployed)}
-                      </LastDeployed>
-                    </>
-                  )}
-                </InfoWrapper>
-
-                {!databaseStatus && (
-                  <>
-                    <Banner>
-                      <BannerContents>
-                        <b>Database is being created</b>
-                      </BannerContents>
-                      <Spacer inline width="5px" />
-                    </Banner>
-                  </>
-                )}
-              </HeaderWrapper>
-              {deleting ? (
-                <>
-                  <LineBreak />
-                  <Placeholder>
-                    <TextWrap>
-                      <Header>
-                        <Spinner src={loadingSrc} /> Deleting "
-                        {currentChart.name}"
-                      </Header>
-                      You will be automatically redirected after deletion is
-                      complete.
-                    </TextWrap>
-                  </Placeholder>
-                </>
-              ) : (
-                <>
-                  <RevisionSection
-                    showRevisions={showRevisions}
-                    toggleShowRevisions={() => {
-                      setShowRevisions(!showRevisions);
-                    }}
-                    chart={currentChart}
-                    refreshChart={async () => {
-                      await getChartData(currentChart);
-                    }}
-                    setRevision={setRevision}
-                    forceRefreshRevisions={forceRefreshRevisions}
-                    refreshRevisionsOff={() => {
-                      setForceRefreshRevisions(false);
-                    }}
-                    shouldUpdate={
-                      currentChart.latest_version &&
-                      currentChart.latest_version !==
-                        currentChart.chart.metadata.version
-                    }
-                    latestVersion={currentChart.latest_version}
-                    upgradeVersion={handleUpgradeVersion}
-                  />
-                  {isStack && isLoadingStackEnvGroups ? (
-                    <>
-                      <LineBreak />
-                      <Placeholder>
-                        <TextWrap>
-                          <Header>
-                            <Spinner src={loadingSrc} />
-                          </Header>
-                        </TextWrap>
-                      </Placeholder>
-                    </>
-                  ) : (
-                    <>
-                      {(isPreview || leftTabOptions.length > 0) && (
-                        <BodyWrapper>
-                          <PorterFormWrapper
-                            formData={cloneDeep(currentChart.form)}
-                            valuesToOverride={{
-                              namespace: props.namespace,
-                              clusterId: currentCluster.id,
-                            }}
-                            renderTabContents={renderTabContents}
-                            isReadOnly={
-                              isPreview ||
-                              imageIsPlaceholder ||
-                              !isAuthorized("application", "", [
-                                "get",
-                                "update",
-                              ])
-                            }
-                            onSubmit={onSubmit}
-                            includeMetadata
-                            rightTabOptions={rightTabOptions}
-                            leftTabOptions={leftTabOptions}
-                            color={isPreview ? "#f5cb42" : null}
-                            addendum={
-                              <TabButton
-                                onClick={toggleDevOpsMode}
-                                devOpsMode={devOpsMode}
-                              >
-                                <i className="material-icons">offline_bolt</i>{" "}
-                                DevOps Mode
-                              </TabButton>
-                            }
-                            saveValuesStatus={saveValuesStatus}
-                            injectedProps={{
-                              "key-value-array": {
-                                availableSyncEnvGroups:
-                                  isStack && !isPreview
-                                    ? stackEnvGroups
-                                    : undefined,
-                              },
-                              "url-link": {
-                                chart: currentChart,
-                              },
-                            }}
-                            overrideCurrentTab={overrideCurrentTab}
-                            onTabChange={(newTab) => {
-                              if (newTab !== "logs") {
-                                setOverrideCurrentTab("");
-                                setLogData({
-                                  revision: `${currentChart.version}`,
-                                });
-                              }
-                            }}
-                          />
-                        </BodyWrapper>
-                      )}
-                    </>
-                  )}
-                </>
-              )}
-            </StyledExpandedChart>
-          )}
-        </>
-      )}
-    </>
-  );
-};
-
-export default ExpandedChart;
-
-const ArrowIcon = styled.img`
-  width: 15px;
-  margin-right: 8px;
-  opacity: 50%;
-`;
-
-const BreadcrumbRow = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: flex-start;
-  z-index: 999;
-`;
-
-const Breadcrumb = styled.div`
-  color: #aaaabb88;
-  font-size: 13px;
-  margin-bottom: 15px;
-  display: flex;
-  align-items: center;
-  margin-top: -10px;
-  z-index: 999;
-  padding: 5px;
-  padding-right: 7px;
-  border-radius: 5px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Wrap = styled.div`
-  z-index: 999;
-`;
-
-const TextWrap = styled.div``;
-
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 1px;
-  background: #494b4f;
-  margin: 35px 0px;
-`;
-
-const BodyWrapper = styled.div`
-  position: relative;
-  padding-bottom: 0;
-  margin-bottom: 0;
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;
-
-const Placeholder = styled.div`
-  width: 100%;
-  min-height: 300px;
-  height: 40vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  font-size: 14px;
-
-  > i {
-    font-size: 18px;
-    margin-right: 10px;
-  }
-`;
-
-const Spinner = styled.img`
-  width: 15px;
-  height: 15px;
-  margin-right: 12px;
-  margin-bottom: -2px;
-`;
-
-const Bolded = styled.div`
-  font-weight: 500;
-  color: #ffffff44;
-  margin-right: 6px;
-`;
-
-const Url = styled.div`
-  display: block;
-  margin-left: 5px;
-  font-size: 13px;
-  margin-top: 16px;
-  user-select: all;
-  margin-bottom: -5px;
-  user-select: text;
-  display: flex;
-  color: #949eff;
-  align-items: center;
-
-  > i {
-    font-size: 15px;
-    margin-right: 10px;
-  }
-`;
-
-const TabButton = styled.div`
-  position: absolute;
-  right: 0px;
-  height: 30px;
-  background: linear-gradient(
-    to right,
-    #00000000,
-    ${(props) => props.theme.bg} 20%
-  );
-  padding-left: 30px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  color: ${(props: { devOpsMode: boolean }) =>
-    props.devOpsMode ? "#aaaabb" : "#aaaabb55"};
-  margin-left: 35px;
-  border-radius: 20px;
-  text-shadow: 0px 0px 8px
-    ${(props: { devOpsMode: boolean }) =>
-      props.devOpsMode ? "#ffffff66" : "none"};
-  cursor: pointer;
-  :hover {
-    color: ${(props: { devOpsMode: boolean }) =>
-      props.devOpsMode ? "" : "#aaaabb99"};
-  }
-
-  > i {
-    font-size: 17px;
-    margin-right: 9px;
-  }
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-`;
-
-const Dot = styled.div`
-  margin-right: 16px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-left: 3px;
-  margin-top: 22px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 8px;
-  margin-top: -1px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  margin-left: 15px;
-  margin-bottom: -3px;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 5px;
-  background: #26282e;
-`;
-
-const NamespaceTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #43454a;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-`;
-
-const StyledExpandedChart = styled.div`
-  width: 100%;
-  z-index: 0;
-  animation: fadeIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  display: flex;
-  flex-direction: column;
-
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const A = styled.a`
-  color: #8590ff;
-  text-decoration: underline;
-  cursor: pointer;
-`;
-
-const BannerContents = styled.div`
-  display: flex;
-  flex-direction: column;
-  row-gap: 0.5rem;
-`;
-
-const CloseButton = styled.div`
-  display: block;
-  width: 40px;
-  height: 40px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1;
-  border-radius: 50%;
-  cursor: pointer;
-  :hover {
-    background-color: #ffffff11;
-  }
-
-  > i {
-    font-size: 20px;
-    color: #aaaabb;
-  }
-`;

+ 0 - 160
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx

@@ -1,160 +0,0 @@
-import React, { Component } from "react";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import Loading from "components/Loading";
-import PageNotFound from "components/PageNotFound";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
-import {
-  type ChartType,
-  type ChartTypeWithExtendedConfig,
-} from "shared/types";
-
-import ExpandedChart from "./ExpandedChart";
-import ExpandedJobChart from "./ExpandedJobChart";
-
-type PropsType = RouteComponentProps<{
-  baseRoute: string;
-  namespace: string;
-}> & {
-  setSidebar: (x: boolean) => void;
-  isMetricsInstalled: boolean;
-};
-
-type StateType = {
-  loading: boolean;
-  currentChart: ChartType;
-};
-
-class ExpandedChartWrapper extends Component<PropsType, StateType> {
-  state = {
-    loading: true,
-    currentChart: null as ChartType,
-  };
-
-  // Retrieve full chart data (includes form and values)
-  getChartData = () => {
-    const { match } = this.props;
-    const { namespace, chartName } = match.params as any;
-    const { currentProject, currentCluster } = this.context;
-    if (currentProject && currentCluster) {
-      api
-        .getChart<ChartTypeWithExtendedConfig>(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            namespace,
-            cluster_id: currentCluster.id,
-            name: chartName,
-            revision: 0,
-          }
-        )
-        .then((res) => {
-          const chart = res.data;
-          this.setState({ currentChart: res.data, loading: false });
-          const isJob = res.data.form?.name?.toLowerCase() === "job";
-          const route = `${isJob ? "/jobs" : "/applications"}/${
-            currentCluster.name
-          }/${chart.namespace}/${chart.name}`;
-
-          if (isJob && this.props.match.params?.baseRoute === "applications") {
-            pushFiltered(this.props, route, [
-              "project_id",
-              "closeChartRedirectUrl",
-            ]);
-            return;
-          }
-
-          if (!isJob && this.props.match.params?.baseRoute !== "applications") {
-            pushFiltered(this.props, route, [
-              "project_id",
-              "closeChartRedirectUrl",
-            ]);
-          }
-        })
-        .catch((err) => {
-          console.log(err);
-          console.log("err", err?.response?.data);
-          this.setState({ loading: false });
-        });
-    }
-  };
-
-  componentDidMount() {
-    this.setState({ loading: true });
-    this.getChartData();
-  }
-
-  render() {
-    const { setSidebar, location, match } = this.props;
-    const { baseRoute, namespace } = match.params as any;
-    const { loading, currentChart } = this.state;
-
-    if (loading) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (currentChart && baseRoute === "jobs") {
-      return (
-        <ExpandedJobChart
-          namespace={namespace}
-          currentChart={currentChart}
-          currentCluster={this.context.currentCluster}
-          closeChart={() => {
-            const urlParams = new URLSearchParams(window.location.search);
-
-            if (urlParams.get("closeChartRedirectUrl")) {
-              this.props.history.push(urlParams.get("closeChartRedirectUrl"));
-              return;
-            }
-
-            pushFiltered(this.props, "/jobs", ["project_id"], {
-              cluster: this.context.currentCluster.name,
-              namespace,
-            });
-          }}
-          setSidebar={setSidebar}
-        />
-      );
-    } else if (currentChart && baseRoute === "applications") {
-      return (
-        <ExpandedChart
-          namespace={namespace}
-          isMetricsInstalled={this.props.isMetricsInstalled}
-          currentChart={currentChart}
-          currentCluster={this.context.currentCluster}
-          closeChart={() => {
-            const urlParams = new URLSearchParams(window.location.search);
-
-            if (urlParams.get("closeChartRedirectUrl")) {
-              this.props.history.push(urlParams.get("closeChartRedirectUrl"));
-              return;
-            }
-
-            pushFiltered(this.props, "/applications", ["project_id"], {
-              cluster: this.context.currentCluster.name,
-              namespace,
-            });
-          }}
-          setSidebar={setSidebar}
-        />
-      );
-    }
-    return <PageNotFound />;
-  }
-}
-
-ExpandedChartWrapper.contextType = Context;
-
-export default withRouter(ExpandedChartWrapper);
-
-const LoadingWrapper = styled.div`
-  width: 100%;
-  height: 100vh;
-`;

+ 0 - 78
dashboard/src/main/home/project-settings/Bars.tsx

@@ -1,78 +0,0 @@
-import React from "react";
-import {
-  Bar,
-  BarChart,
-  CartesianGrid,
-  ResponsiveContainer,
-  Tooltip,
-  XAxis,
-  YAxis,
-  type TooltipProps,
-} from "recharts";
-import styled from "styled-components";
-
-import Text from "components/porter/Text";
-
-type Props = {
-  data: Array<Record<string, unknown>>;
-  yKey: string;
-  xKey: string;
-  fill?: string;
-  title?: string;
-};
-
-const CustomTooltip = ({ active, payload }: TooltipProps<string, string>) => {
-  if (active && payload?.length) {
-    return (
-      <div
-        className="custom-tooltip"
-        style={{
-          backgroundColor: "#42444933",
-          backdropFilter: "saturate(150%) blur(8px)",
-          border: "1px solid #494b4f",
-          fontSize: "13px",
-          padding: "10px",
-          borderRadius: "5px",
-          color: "white",
-        }}
-      >
-        <p className="intro">{`Value: ${payload[0].value}`}</p>
-      </div>
-    );
-  }
-
-  return null;
-};
-
-const Bars: React.FC<Props> = ({ data, yKey, xKey, fill, title }) => {
-  return (
-    <ResponsiveContainer width="100%" height="100%">
-      <BarChart
-        width={500}
-        height={300}
-        data={data}
-        margin={{
-          top: 5,
-          right: 0,
-          left: -20,
-          bottom: 5,
-        }}
-      >
-        <CartesianGrid vertical={false} stroke="#ffffff22" />
-        <XAxis dataKey={xKey} tick={{ fontSize: 13 }} />
-        <YAxis tick={{ fontSize: 13 }} />
-        <Tooltip content={<CustomTooltip />} cursor={{ fill: "#ffffff11" }} />
-        <Bar dataKey={yKey} fill={fill || "#6A7FC4"} />
-      </BarChart>
-      <Center>
-        <Text color="helper">{title}</Text>
-      </Center>
-    </ResponsiveContainer>
-  );
-};
-
-export default Bars;
-
-const Center = styled.div`
-  text-align: center;
-`;

+ 0 - 36
dashboard/src/main/home/project-settings/ReferralsPage.tsx

@@ -1,36 +0,0 @@
-import React from "react";
-
-import Link from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import { useReferralDetails } from "lib/hooks/useStripe";
-
-function ReferralsPage(): JSX.Element {
-  const { referralDetails } = useReferralDetails();
-  const baseUrl = window.location.origin;
-
-  return (
-    <>
-      <Text size={16}>Referrals</Text>
-      <Spacer y={1} />
-      <Text color="helper">Refer people to Porter to earn credits.</Text>
-      <Spacer y={1} />
-      {referralDetails !== null && (
-        <>
-          <Text>Your referral link is </Text>
-          <Link to={baseUrl + "/register?referral=" + referralDetails.code}>
-            {baseUrl + "/register?referral=" + referralDetails.code}
-          </Link>
-          <Spacer y={1} />
-          <Text>
-            You have referred {referralDetails.referral_count}/
-            {referralDetails.max_allowed_referrals} users
-          </Text>
-        </>
-      )}
-      <Spacer y={1} />
-    </>
-  );
-}
-
-export default ReferralsPage;