From 55d4e8a1ec755220b6dcd65ea61b2854dbf2e9c7 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 21 May 2024 17:09:26 -0400 Subject: [PATCH] deletions part 3 --- .../components/porter/ConfirmOverlay.tsx | 106 -- dashboard/src/legacy/main/auth/InfoPanel.tsx | 117 -- dashboard/src/legacy/main/auth/Login.tsx | 239 --- .../src/legacy/main/auth/LoginWrapper.tsx | 157 -- dashboard/src/legacy/main/auth/Register.tsx | 600 -------- .../main/auth/ResetPasswordFinalize.tsx | 426 ------ .../legacy/main/auth/ResetPasswordInit.tsx | 373 ----- dashboard/src/legacy/main/auth/SetInfo.tsx | 300 ---- .../src/legacy/main/auth/VerifyEmail.tsx | 241 --- .../main/home/managed-addons/AddonListRow.tsx | 185 --- .../main/home/managed-addons/AddonsList.tsx | 195 --- .../main/home/modals/UpgradeChartModal.tsx | 170 --- .../legacy/main/home/onboarding/constants.ts | 42 - .../main/home/project-settings/Bars.tsx | 77 - .../home/project-settings/ReferralsPage.tsx | 35 - .../MainWrapperErrorBoundary.tsx | 30 - .../shared/error_handling/sentry/setup.ts | 18 - .../error_handling/window_error_handling.ts | 37 - .../legacy/shared/hooks/useEffectDebugger.ts | 34 - .../src/legacy/shared/icons/MoonBase.tsx | 24 - .../cluster-dashboard/DashboardRouter.tsx | 185 --- .../LastRunStatusSelector.tsx | 57 - .../cluster-dashboard/NamespaceSelector.tsx | 168 --- .../home/cluster-dashboard/SortSelector.tsx | 83 - .../main/home/cluster-dashboard/TagFilter.tsx | 70 - .../cluster-dashboard/apps/AppDashboard.tsx | 199 --- .../home/cluster-dashboard/chart/Chart.tsx | 406 ----- .../cluster-dashboard/chart/ChartList.tsx | 507 ------- .../cluster-dashboard/chart/JobRunTable.tsx | 543 ------- .../dashboard/ClusterSettings.tsx | 473 ------ .../dashboard/ClusterSettingsModal.tsx | 151 -- .../cluster-dashboard/dashboard/Dashboard.tsx | 313 ---- .../cluster-dashboard/dashboard/Metrics.tsx | 618 -------- .../dashboard/NamespaceList.tsx | 316 ---- .../cluster-dashboard/dashboard/NodeList.tsx | 198 --- .../dashboard/ProvisionerStatus.tsx | 171 --- .../cluster-dashboard/dashboard/Routes.tsx | 26 - .../env-groups/CreateEnvGroup.tsx | 463 ------ .../cluster-dashboard/env-groups/EnvGroup.tsx | 218 --- .../env-groups/EnvGroupDashboard.tsx | 275 ---- .../env-groups/EnvGroupList.tsx | 158 -- .../env-groups/ExpandedEnvGroup.tsx | 1304 ---------------- .../env-groups/ExpandedEnvGroupDashboard.tsx | 212 --- .../expanded-chart/CanonicalName.tsx | 157 -- .../expanded-chart/DeploymentType.tsx | 104 -- .../expanded-chart/ExpandedChart.tsx | 1332 ----------------- .../expanded-chart/ExpandedChartWrapper.tsx | 161 -- .../src/main/home/project-settings/Bars.tsx | 78 - .../home/project-settings/ReferralsPage.tsx | 36 - 49 files changed, 12388 deletions(-) delete mode 100644 dashboard/src/legacy/components/porter/ConfirmOverlay.tsx delete mode 100644 dashboard/src/legacy/main/auth/InfoPanel.tsx delete mode 100644 dashboard/src/legacy/main/auth/Login.tsx delete mode 100644 dashboard/src/legacy/main/auth/LoginWrapper.tsx delete mode 100644 dashboard/src/legacy/main/auth/Register.tsx delete mode 100644 dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx delete mode 100644 dashboard/src/legacy/main/auth/ResetPasswordInit.tsx delete mode 100644 dashboard/src/legacy/main/auth/SetInfo.tsx delete mode 100644 dashboard/src/legacy/main/auth/VerifyEmail.tsx delete mode 100644 dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx delete mode 100644 dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx delete mode 100644 dashboard/src/legacy/main/home/modals/UpgradeChartModal.tsx delete mode 100644 dashboard/src/legacy/main/home/onboarding/constants.ts delete mode 100644 dashboard/src/legacy/main/home/project-settings/Bars.tsx delete mode 100644 dashboard/src/legacy/main/home/project-settings/ReferralsPage.tsx delete mode 100644 dashboard/src/legacy/shared/error_handling/MainWrapperErrorBoundary.tsx delete mode 100644 dashboard/src/legacy/shared/error_handling/sentry/setup.ts delete mode 100644 dashboard/src/legacy/shared/error_handling/window_error_handling.ts delete mode 100644 dashboard/src/legacy/shared/hooks/useEffectDebugger.ts delete mode 100644 dashboard/src/legacy/shared/icons/MoonBase.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/SortSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/TagFilter.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettingsModal.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx delete mode 100644 dashboard/src/main/home/project-settings/Bars.tsx delete mode 100644 dashboard/src/main/home/project-settings/ReferralsPage.tsx diff --git a/dashboard/src/legacy/components/porter/ConfirmOverlay.tsx b/dashboard/src/legacy/components/porter/ConfirmOverlay.tsx deleted file mode 100644 index afdef3a820..0000000000 --- a/dashboard/src/legacy/components/porter/ConfirmOverlay.tsx +++ /dev/null @@ -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 = ({ message, onYes, onNo, loading }) => { - return ( - <> - {createPortal( - - {loading ? ( - - ) : ( - <> - {message} - - Yes - No - - - )} - , - 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; - } -`; diff --git a/dashboard/src/legacy/main/auth/InfoPanel.tsx b/dashboard/src/legacy/main/auth/InfoPanel.tsx deleted file mode 100644 index 316c195e5a..0000000000 --- a/dashboard/src/legacy/main/auth/InfoPanel.tsx +++ /dev/null @@ -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 ( - - - - - - Cloud - - - - Deploy and scale effortlessly with Porter - - - - - $5 in free credits on sign-up - - - - done Instantly deploy from any Git - repo - - - - done Eject at any time to your own - AWS/Azure/GCP account - - - ); - } - return ( - - - - - - - Deploy and scale effortlessly with Porter - - - - done 14 day free trial - - - - done Generous startup program for - seed-stage companies - - - - done Bring your own cloud and use your - credits - - - ); -}; - -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; -`; diff --git a/dashboard/src/legacy/main/auth/Login.tsx b/dashboard/src/legacy/main/auth/Login.tsx deleted file mode 100644 index 5f7403c8dd..0000000000 --- a/dashboard/src/legacy/main/auth/Login.tsx +++ /dev/null @@ -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; -}; - -const Login: React.FC = ({ 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 ( - - Log in to your Porter account - - {(hasGithub || hasGoogle) && ( - <> - - {hasGithub && ( - - - Log in with GitHub - - )} - {hasGithub && hasGoogle && } - {hasGoogle && ( - - - Log in with Google - - )} - - {hasBasic && ( - - - or - - )} - - )} - {hasBasic && ( - <> - { - setEmail(x); - setEmailError(false); - setCredentialError(false); - }} - width="100%" - height="40px" - error={emailError && "Please enter a valid email"} - /> - - { - setPassword(x); - setCredentialError(false); - }} - width="100%" - height="40px" - error={credentialError && ""} - > - {hasResetPassword && ( - - Forgot your password? - - )} - - - - - )} - - - Don't have an account? - - Sign up - - - ); -}; - -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; - } -`; diff --git a/dashboard/src/legacy/main/auth/LoginWrapper.tsx b/dashboard/src/legacy/main/auth/LoginWrapper.tsx deleted file mode 100644 index 5adb72026c..0000000000 --- a/dashboard/src/legacy/main/auth/LoginWrapper.tsx +++ /dev/null @@ -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; -}; - -const getWindowDimensions = () => { - const { innerWidth: width, innerHeight: height } = window; - return { width, height }; -}; - -const LoginWrapper: React.FC = ({ authenticate }) => { - const [windowDimensions, setWindowDimensions] = useState( - getWindowDimensions() - ); - - const handleResize = () => { - setWindowDimensions(getWindowDimensions()); - }; - - useEffect(() => { - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - return ( - - {windowDimensions.width > windowDimensions.height && ( - - - - - - {window.location.hostname === "cloud.porter.run" && ( - Cloud - )} - - - - Welcome back to Porter - - - - Read the Porter docs - - - - See what's new with Porter - - - )} - - {windowDimensions.width <= windowDimensions.height && ( - - - - - - - )} - - - - - ); -}; - -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; -`; diff --git a/dashboard/src/legacy/main/auth/Register.tsx b/dashboard/src/legacy/main/auth/Register.tsx deleted file mode 100644 index f540d87ade..0000000000 --- a/dashboard/src/legacy/main/auth/Register.tsx +++ /dev/null @@ -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; -}; - -const getWindowDimensions = () => { - const { innerWidth: width, innerHeight: height } = window; - return { width, height }; -}; - -const Register: React.FC = ({ 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("(None provided)"); - const [referralOtherText, setReferralOtherText] = useState(""); - - 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 ( - - {windowDimensions.width > windowDimensions.height && } - - {windowDimensions.width <= windowDimensions.height && ( - - - - - - - )} - Create your Porter account - - {(hasGithub || hasGoogle) && !disabled && ( - <> - - {hasGithub && ( - - - Sign up with GitHub - - )} - {hasGithub && hasGoogle && } - {hasGoogle && ( - - - Sign up with Google - - )} - - {hasBasic && ( - - - or - - )} - - )} - {hasBasic && ( - <> - {window.location.hostname !== "cloud.porter.run" && ( - <> - - - { - setFirstName(x); - setFirstNameError(false); - }} - width="100%" - height="40px" - error={firstNameError && "First name cannot be blank"} - /> - {!firstNameError && lastNameError && ( - - )} - - - - { - setLastName(x); - setLastNameError(false); - }} - width="100%" - height="40px" - error={lastNameError && "Last name cannot be blank"} - /> - {!lastNameError && firstNameError && ( - - )} - - - - { - setCompanyName(x); - setCompanyNameError(false); - }} - width="100%" - height="40px" - error={companyNameError && ""} - /> - - - )} - { - setEmail(x); - setEmailError(false); - }} - width="100%" - height="40px" - error={emailError && "Please enter a valid email"} - disabled={disabled} - /> - - - - (Optional) How did you hear about us? - - { - setReferralCode(x); - setReferralCodeError(false); - }} - width="100%" - height="40px" - error={referralCodeError && ""} - /> - - - {chosenReferralOption === "Other" && ( - <> - - { - setReferralOtherText(e.target.value); - }} - placeholder="Tell us more..." - /> - - )} - - - - )} - {!disabled && ( - <> - - - Already have an account? - - Log in - - - )} - - - ); -}; - -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; -`; diff --git a/dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx b/dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx deleted file mode 100644 index 84ed0061b8..0000000000 --- a/dashboard/src/legacy/main/auth/ResetPasswordFinalize.tsx +++ /dev/null @@ -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 { - 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 ( - -
- Invalid Password - - ); - } - }; - - 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 = ( -
- - ) => - this.setState({ - password: e.target.value, - passwordError: false, - }) - } - valid={!passwordError} - /> - {this.renderPasswordError()} - - -
- ); - - if (loading) { - inputSection = ( - - - - ); - } else if (tokenError) { - inputSection = ( - - Link has already been used or has expired. Please - try again. - - ); - } else if (submitted) { - inputSection = ( - - Password changed successfully! Redirecting to login... - - ); - } - - return ( - - - - - - - - Reset Password - - {inputSection} - - Don't have an account? - Sign up - - - - -
- © 2021 Porter Technologies Inc. • - - Terms & Privacy - -
-
- ); - } -} - -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; -`; diff --git a/dashboard/src/legacy/main/auth/ResetPasswordInit.tsx b/dashboard/src/legacy/main/auth/ResetPasswordInit.tsx deleted file mode 100644 index 56bc9e6aba..0000000000 --- a/dashboard/src/legacy/main/auth/ResetPasswordInit.tsx +++ /dev/null @@ -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 { - 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 ( - -
- Please enter a valid email - - ); - } - }; - - 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 = ( -
- - ) => - this.setState({ - email: e.target.value, - emailError: false, - }) - } - valid={!emailError} - /> - {this.renderEmailError()} - - -
- ); - - if (submitted) { - formSection = ( - - If we found an account matching {email}, we've sent you password reset - instructions. Remember to check your spam folder. - - ); - } - - return ( - - - - - - - - Reset Password - - {formSection} - - Don't have an account? - Sign up - - - - -
- © 2021 Porter Technologies Inc. • - - Terms & Privacy - -
-
- ); - } -} - -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; -`; diff --git a/dashboard/src/legacy/main/auth/SetInfo.tsx b/dashboard/src/legacy/main/auth/SetInfo.tsx deleted file mode 100644 index dbf5b54314..0000000000 --- a/dashboard/src/legacy/main/auth/SetInfo.tsx +++ /dev/null @@ -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; - handleLogOut: () => void; -}; - -const getWindowDimensions = () => { - const { innerWidth: width, innerHeight: height } = window; - return { width, height }; -}; - -const SetInfo: React.FC = ({ 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 ( - - {windowDimensions.width > windowDimensions.height && } - - {windowDimensions.width <= windowDimensions.height && ( - - - - - )} - Finish setting up your account - - - - { - setFirstName(x); - setFirstNameError(false); - }} - width="100%" - height="40px" - error={firstNameError && "First name cannot be blank"} - /> - {!firstNameError && lastNameError && } - - - - { - setLastName(x); - setLastNameError(false); - }} - width="100%" - height="40px" - error={lastNameError && "Last name cannot be blank"} - /> - {!lastNameError && firstNameError && } - - - - { - setCompanyName(x); - setCompanyNameError(false); - }} - width="100%" - height="40px" - error={companyNameError && ""} - /> - - - - - Want to use a different login method?{" "} - Log out - - - - ); -}; - -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; -`; diff --git a/dashboard/src/legacy/main/auth/VerifyEmail.tsx b/dashboard/src/legacy/main/auth/VerifyEmail.tsx deleted file mode 100644 index 7d1385b4df..0000000000 --- a/dashboard/src/legacy/main/auth/VerifyEmail.tsx +++ /dev/null @@ -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 = ({ 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 ( - - {windowDimensions.width > windowDimensions.height && } - - {windowDimensions.width <= windowDimensions.height && ( - - - - - )} - Verify your email - - {submitted ? ( - <> - - A new verification email was sent to: - - - {user?.email} - - - Don't forget to check your spam folder. - - - - If you still need help, please contact support@porter.run. - - - ) : ( - <> - - We've sent a verification link to the following email address: - - - {user?.email} - - - Please click the link in your inbox to verify your email. - - - - Didn't receive anything? - - - - - )} - - - Want to use a different email? - - Log out - - - - ); -}; - -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; -`; diff --git a/dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx b/dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx deleted file mode 100644 index e10d1147f2..0000000000 --- a/dashboard/src/legacy/main/home/managed-addons/AddonListRow.tsx +++ /dev/null @@ -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; - update: UseFieldArrayUpdate; - remove: (index: number) => void; -}; - -export const AddonListRow: React.FC = ({ - index, - addon, - update, - remove, -}) => { - const renderIcon = (type: ClientAddon["config"]["type"]): JSX.Element => - match(type) - .with("postgres", () => ) - .with("redis", () => ) - .otherwise(() => ); - - return ( - <> - { - update(index, { - ...addon, - expanded: !addon.expanded, - }); - }} - bordersRounded={!addon.expanded} - > - - - arrow_drop_down - - {renderIcon(addon.config.type)} - {addon.name.value.trim().length > 0 ? addon.name.value : "New Addon"} - - - {addon.canDelete && ( - { - e.stopPropagation(); - remove(index); - }} - > - delete - - )} - - - {addon.expanded && ( - -
- {match(addon) - .with({ config: { type: "postgres" } }, (ao) => ( - - )) - .with({ config: { type: "redis" } }, (ao) => ( - - )) - .otherwise(() => null)} -
-
- )} -
- - - ); -}; - -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")}; -`; diff --git a/dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx b/dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx deleted file mode 100644 index 7f2814c1c1..0000000000 --- a/dashboard/src/legacy/main/home/managed-addons/AddonsList.tsx +++ /dev/null @@ -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; - -export const AddonsList: React.FC = () => { - const [showAddAddonModal, setShowAddAddonModal] = useState(false); - - const { control: appTemplateControl } = useFormContext(); - - // add addon modal form - const { watch, control, reset, handleSubmit } = useForm({ - 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 ( - <> - - {fields.map((addon, idx) => ( - - ))} - - { - setShowAddAddonModal(true); - }} - > - add - Include add-on in preview environments - - - {showAddAddonModal && ( - { - setShowAddAddonModal(false); - }} - width="500px" - > - Include an addon in your preview environment - - Select a service type: - - - - {match(addonType) - .with("postgres", () => ) - .with("redis", () => ) - .exhaustive()} - - ( - - - - - - ); -}; - -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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx deleted file mode 100644 index 66c4b10dc6..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx +++ /dev/null @@ -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("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 ( - - ); - case "metrics": - return ; - case "namespaces": - return ; - case "configuration": - return ( - <> -
- {context.currentCluster.cloud_provider === "AWS" && ( - - )} - {context.currentCluster.cloud_provider === "Azure" && ( - - )} - {context.currentCluster.cloud_provider === "GCP" && ( - - )} - - ); - default: - return ; - } - }; - - 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( - "", - {}, - { - 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 ( - - - Infrastructure settings are not enabled on the Porter Cloud. - - - - Eject to your own cloud account to enable managed infrastructure. - - - - - - - ); - } else if (context.currentProject?.capi_provisioner_enabled) { - return ( - <> - - {showProvisionerStatus && - (context.currentCluster.status === "UPDATING" || - context.currentCluster.status === "UPDATING_UNAVAILABLE") && ( - <> - - - - )} - { - setCurrentTab(value); - }} - /> - {renderTab()} - - ); - } else { - return ( - <> - { - setCurrentTab(value); - }} - /> - {renderTab()} - - ); - } - }; - - return ( - <> - - - - - {context.currentCluster.vanity_name || - context.currentCluster.name} - - - { - context.setCurrentModal(); - }} - > - - - - } - 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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx deleted file mode 100644 index d88e1b4ec4..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx +++ /dev/null @@ -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([]); - const [aggregatedData, setAggregatedData] = useState< - Record - >({}); - 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( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ), - api.getPrometheusIsInstalled( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ), - ]) - .then(() => { - setDetected(true); - setIsLoading((prev) => prev + 1); - - api - .getNGINXIngresses( - "", - {}, - { - 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 ( - <> - { - setShowMetricsSettings(false); - }} - /> - - - { - setSelectedIngress(x); - }} - options={ingressOptions} - width="100%" - /> - {selectedMetric == "nginx:latency-histogram" && ( - { - setSelectedPercentile(x); - }} - options={[ - { - label: "99", - value: "0.99", - }, - { - label: "95", - value: "0.95", - }, - { - label: "50", - value: "0.5", - }, - ]} - width="100%" - /> - )} - - - ); - } - }; - - const renderDropdown = () => { - if (dropdownExpanded) { - return ( - <> - { - setDropdownExpanded(false); - }} - /> - { - setDropdownExpanded(false); - }} - > - {renderOptionList()} - - - ); - } - }; - - const renderOptionList = () => { - return metricsOptions.map( - (option: { value: string; label: string }, i: number) => { - return ( - - ); - } - ); - }; - - 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( - "", - { - 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( - "", - { - 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 ? ( - - - - ) : !detected ? ( - <> -
-
- - Cluster metrics unavailable. Make sure nginx-ingress and Prometheus are - installed. - Go to Launch - - - ) : ( - -
- - { - setDropdownExpanded(!dropdownExpanded); - }} - > - {selectedMetricLabel} - arrow_drop_down - {renderDropdown()} - - - { - setShowMetricsSettings(true); - }} - > - - - {renderMetricsSettings()} - - - - autorenew - - - - { - setSelectedRange(x); - }} - /> - -
- {isLoading > 0 && } - {data.length === 0 && isLoading === 0 && ( - - No data available yet. - - autorenew - Refresh - - - )} - {data.length > 0 && isLoading === 0 && ( - <> - - - {({ width, height }) => ( - - )} - - - )} -
- ); -}; - -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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx deleted file mode 100644 index 5f384bf7cf..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx +++ /dev/null @@ -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(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(() => { - 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 ( - - - {isAuthorized("namespace", "", ["get", "create"]) && ( - - )} - - - {sortedNamespaces.map((namespace) => { - return ( - { - pushFiltered({ location, history }, `/applications`, [], { - cluster: currentCluster.name, - namespace: namespace.metadata.name, - }); - }} - > - - {namespace?.metadata?.name} - - - {namespace?.status?.phase} - - - {isAuthorized("namespace", "", ["get", "delete"]) && - isAvailableForDeletion(namespace?.metadata?.name) && - namespace?.status?.phase === "Active" && ( - - { - onDelete(namespace); - }} - > - delete - Delete - - - )} - - ); - })} - - - ); -}; - -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%; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx deleted file mode 100644 index 9cb6756f82..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx +++ /dev/null @@ -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(false); - const history = useHistory(); - const location = useLocation(); - - const columns = useMemo>>( - () => [ - { - Header: "Node Name", - accessor: "name", - Cell: ({ row }) => { - return {row.values.name}; - }, - width: "max-content", - }, - { - Header: "Machine Type", - accessor: "machine_type", - }, - { - Header: "CPU Usage", - accessor: "cpu_usage", - }, - { - Header: "RAM Usage", - accessor: "ram_usage", - }, - { - Header: () => Node Condition, - accessor: "is_node_healthy", - Cell: ({ row }) => { - return ( - - - {row.values.is_node_healthy ? "Healthy" : "Unhealthy"} - - - ); - }, - }, - ], - [] - ); - - 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( - "", - {}, - { - 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 ( - - - - - - ); -}; - -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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx deleted file mode 100644 index d5097fe708..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx +++ /dev/null @@ -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) => { - const { currentProject, currentCluster } = useContext(Context); - const [progress, setProgress] = useState(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( - "", - {}, - { - 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( - "", - {}, - { - 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("", {}, { 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 ( - - - - {currentCluster?.cloud_provider == "AWS" && ( - <> - - AWS provisioning status - - )} - - {currentCluster?.cloud_provider == "Azure" && ( - <> - - Azure provisioning status - - )} - - - - - - {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."} - - - {props?.provisionFailureReason && ( - Error: {props?.provisionFailureReason} - )} - - ); -}; - -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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx deleted file mode 100644 index 447580dd91..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useContext } from "react"; -import { Redirect, 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 ( - <> - - - - - - - - - - ); -}; - -export default Routes; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx deleted file mode 100644 index 92d54cdf40..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx +++ /dev/null @@ -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([]); - const [envVariables, setEnvVariables] = useState([]); - 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 = {}; - const secretEnvVariables: Record = {}; - - const envVariable = envVariables; - - if (context.currentProject.simplified_view_enabled) { - api - .createNamespace( - "", - { - 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( - "", - { - 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 = {}; - const secretEnvVariables: Record = {}; - - 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( - "", - { - 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( - "", - {}, - { - 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 ( - <> - - - - Create an environment group - - - - Name - - 60) && - envGroupName !== "" - } - > - Lowercase letters, numbers, and "-" only. Maximum 60 characters. - - - - { - setEnvGroupName(x); - }} - placeholder="ex: my-env-group" - width="100%" - /> - {!context?.currentProject?.simplified_view_enabled && ( - <> - Destination - - Specify the namespace you would like to create this environment - group in. - - - - view_listNamespace - - { - setSelectedNamespace(namespace); - }} - options={namespaceOptions} - width="250px" - dropdownWidth="335px" - closeOverlay={true} - /> - - - )} - Environment variables - - Set environment variables for your secrets and environment-specific - configuration. - - { - setEnvVariables(x); - }} - fileUpload={true} - secretOption={true} - /> - - - - - - ); -}; - -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; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx deleted file mode 100644 index e5af022cc1..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { Component } from "react"; -import _ from "lodash"; -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 { - 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 ( - - - - <IconWrapper> - <Icon src={envGroup.type === "doppler" ? doppler : sliders} /> - </IconWrapper> - {name} - - - - - - Last updated {readableDate(timestamp)} - - - - {!this.context?.currentProject.simplified_view_enabled && ( - - Namespace - - {namespace.startsWith("porter-stack-") - ? namespace.replace("porter-stack-", "") - : namespace} - - - )} - - - v{version} - - - ); - } -} - -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; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx deleted file mode 100644 index e4c4a7b3b3..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import React, { Component, useContext, useEffect, 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, pushFiltered, 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({ - 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 ; - } - - if (currentProject?.sandbox_enabled) { - return ( - - - Environment groups are not enabled on the Porter Cloud. - - - - Eject to your own cloud account to enable environment groups. - - - - - Request ejection - - - - ); - } - - const goBack = () => { - setState((state) => ({ ...state, createEnvMode: false })); - }; - - if (state.createEnvMode) { - return ( - - ); - } else { - const isAuthorizedToAdd = props.isAuthorized("env_group", "", [ - "get", - "create", - ]); - - return ( - <> - - - - - {!currentProject.simplified_view_enabled && ( - - )} - - - {isAuthorizedToAdd && ( - - )} - - - - - - ); - } - }; - - const renderContents = () => { - if (state.expandedEnvGroup) { - return ( - { - closeExpanded(); - }} - /> - ); - } else { - return ( - <> - - {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; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx deleted file mode 100644 index 7f5508a96d..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useContext, useEffect, useMemo, 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) => { - const context = useContext(Context); - - const { currentCluster, namespace, sortType, setExpandedEnvGroup } = props; - - const [envGroups, setEnvGroups] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - const updateEnvGroups = async () => { - const { currentProject, currentCluster } = context; - try { - let envGroups: any[] = []; - if (currentProject?.simplified_view_enabled) { - envGroups = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - return res.data?.environment_groups; - }); - } else { - envGroups = await api - .listEnvGroups( - "", - {}, - { - 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 ( - - - - ); - } else if (hasError) { - return ( - - error Error connecting to cluster. - - ); - } else if (!envGroups || envGroups.length === 0) { - return ( - - category - No environment groups found with the given filters. - - ); - } - - return envGroups.map((envGroup: any, i: number) => { - return ; - }); - }; - - return {renderEnvGroupList()}; -}; - -export default withRouter(EnvGroupList); - -const LoadingWrapper = styled.div` - padding-top: 100px; -`; - -const StyledEnvGroupList = styled.div` - padding-bottom: 85px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx deleted file mode 100644 index 9bbb8bcd8c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx +++ /dev/null @@ -1,1304 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import yaml from "js-yaml"; -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 & { - 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(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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - { - name, - type, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - } - - if (stack_id?.length) { - return await api.removeStackEnvGroup( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - stack_id, - env_group_name: name, - } - ); - } - - return await api.deleteEnvGroup( - "", - { - 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( - "", - { - name, - variables: normalVariables, - secret_variables: secretVariables, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - } - - try { - const res = await api.updateAppsLinkedToEnvironmentGroup( - "", - { - name: currentEnvGroup?.name, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - } catch (error) { - setCurrentError(error); - } - - const populatedEnvGroup = await api - .getAllEnvGroups( - "", - {}, - { - 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( - "", - { - 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( - "", - { - 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 ( - { - setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); - }} - handleUpdateValues={handleUpdateValues} - variables={variables} - buttonStatus={buttonStatus} - setButtonStatus={setButtonStatus} - /> - ); - case "applications": - return ; - default: - return ( - - ); - } - }; - - useEffect(() => { - populateEnvGroup(); - }, [envGroup]); - - if (!currentEnvGroup) { - return null; - } - - return ( - - - - - Back - - - - - {envGroup.name} - {!currentProject?.simplified_view_enabled && ( - - Namespace{" "} - - {currentProject?.capi_provisioner_enabled && - namespace.startsWith("porter-stack-") - ? namespace.replace("porter-stack-", "") - : namespace} - - - )} - - - - - - {isDeleting ? ( - <> - - - -
- Deleting "{currentEnvGroup.name}" -
- You will be automatically redirected after deletion is complete. -
-
- - ) : ( - { - setCurrentTab(x); - }} - options={tabOptions} - color={null} - > - {renderTabContents()} - - )} -
- ); -}; - -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 ( - - - Environment variables - - Set environment variables for your secrets and environment-specific - configuration. - - { - onChange(x); - }} - fileUpload={true} - secretOption={true} - disabled={ - !isAuthorized("env_group", "", [ - "get", - "create", - "delete", - "update", - ]) - } - /> - - {isAuthorized("env_group", "", ["get", "update"]) && ( - { - handleUpdateValues(); - }} - status={buttonStatus} - disabled={buttonStatus == "loading" || buttonDisabled} - makeFlush={true} - clearPosition={true} - statusPosition="right" - /> - )} - - ); -}; - -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(envGroup.name + "-2"); - const [cloneNamespace, setCloneNamespace] = useState("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( - "", - { - 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 ( - - {isAuthorized("env_group", "", ["get", "delete"]) && ( - - Manage environment group - - Permanently delete this set of environment variables. This action - cannot be undone. - - {!canDelete && ( - - Applications are still synced to this env group. Navigate to - "Linked applications" and remove this env group from all - applications to delete. - - )} - - {!currentProject?.simplified_view_enabled && ( - <> - - Clone environment group - - Clone this set of environment variables into a new env group. - - { - setName(x); - }} - label="New env group name" - placeholder="ex: my-cloned-env-group" - /> - { - setCloneNamespace(x); - }} - label="New env group namespace" - placeholder="ex: default" - /> - - - {cloneSuccess && ( - - done - Successfully cloned - - )} - - - )} - - )} - - ); -}; - -const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => { - const { currentCluster, currentProject } = useContext(Context); - - return ( - <> - - Linked applications: - - - {currentProject?.simplified_view_enabled - ? envGroup.linked_applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - - open_in_new - - - ) : ( - - - open_in_new - - - )} - - - - ); - }) - : envGroup.applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - - open_in_new - - - ) : ( - - - open_in_new - - - )} - - - - ); - })} - - ); -}; - -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; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx deleted file mode 100644 index 8d2433cd7c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { Component, 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, pushQueryParams } from "shared/routing"; -import { type ClusterType } from "shared/types"; -import sliders from "assets/sliders.svg"; - -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(); - const isTabActive = () => { - return !document.hidden; - }; - - const { - data: envGroups, - isLoading: listEnvGroupsLoading, - isError, - refetch, - } = useQuery( - ["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( - "", - {}, - { - id: currentProject.id, - cluster_id: props.currentCluster.id, - } - ); - } else { - res = await api.listEnvGroups( - "", - {}, - { - 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 ( - - - - ); - } - - const renderContents = () => { - if (!expandedEnvGroup) { - return null; - } - - return ( - { - props.history.push("/env-groups"); - }} - /> - ); - }; - - if (listEnvGroupsLoading) { - return ( - - - - ); - } - - 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; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx deleted file mode 100644 index c4c0831c94..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/CanonicalName.tsx +++ /dev/null @@ -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); - release: ChartType; -}; - -const CanonicalName = ({ onSave, release }: Props) => { - const { currentProject, currentCluster, setCurrentError } = - useContext(Context); - const [buttonStatus, setButtonStatus] = useState(""); - const [canonicalName, setCanonicalName] = useState( - release.canonical_name - ); - - const handleSave = async () => { - setButtonStatus("loading"); - - try { - await api.updateCanonicalName( - "", - { 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 ( - <> - { - setCanonicalName(x); - }} - placeholder="ex: my-app" - isRequired={true} - width={"100%"} - /> - - { - await handleSave(); - }} - status={buttonStatus} - > - -
- - ); -}; - -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; - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx deleted file mode 100644 index 94d7a38c97..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/DeploymentType.tsx +++ /dev/null @@ -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 = ({ 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 ( - - - { - setShowRepoTooltip(true); - }} - onMouseOut={() => { - setShowRepoTooltip(false); - }} - > - {repository} - - {showRepoTooltip && {repository}} - - ); -}; - -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; - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx deleted file mode 100644 index ec631d1333..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx +++ /dev/null @@ -1,1332 +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 StatusIndicator from "components/StatusIndicator"; -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) => { - const [currentChart, setCurrentChart] = useState( - props.currentChart - ); - const [showRevisions, setShowRevisions] = useState(false); - const [loading, setLoading] = useState(false); - const [components, setComponents] = useState([]); - const [isPreview, setIsPreview] = useState(false); - const [devOpsMode, setDevOpsMode] = useState( - localStorage.getItem("devOpsMode") === "true" - ); - const [rightTabOptions, setRightTabOptions] = useState([]); - const [leftTabOptions, setLeftTabOptions] = useState([]); - const [saveValuesStatus, setSaveValueStatus] = useState(null); - const [forceRefreshRevisions, setForceRefreshRevisions] = - useState(false); - const [controllers, setControllers] = useState< - Record> - >({}); - const [url, setUrl] = useState(null); - const [deleting, setDeleting] = useState(false); - const [imageIsPlaceholder, setImageIsPlaceholer] = useState(false); - const [newestImage, setNewestImage] = useState(null); - const [isLoadingChartData, setIsLoadingChartData] = useState(true); - const [isAuthorized] = useAuth(); - const [fullScreenLogs, setFullScreenLogs] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [logData, setLogData] = useState({}); - const [overrideCurrentTab, setOverrideCurrentTab] = useState(""); - const [isAgentInstalled, setIsAgentInstalled] = useState(false); - const [databaseStatus, setDatabaseStatus] = useState(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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - { - 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( - "", - { - 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( - "", - { - 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( - "", - { - 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 ( - - ); - case "metrics": - return ; - case "events": - if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) { - return null; - } - return ( - - ); - case "status": - if (isLoadingChartData) { - return ( - - - - ); - } - if (imageIsPlaceholder) { - return ( - - -
- This application is currently - being deployed -
- {props.currentChart.git_action_config?.gitlab_integration_id ? ( - <> - Navigate to the{" "} - - Jobs - {" "} - tab of your GitLab repo to view live build logs. - - ) : ( - <> - Navigate to the{" "} - - Actions - {" "} - tab of your GitHub repo to view live build logs. - - )} -
-
- ); - } else { - return ( - { - setFullScreenLogs(true); - }} - /> - ); - } - case "settings": - return ( - { - 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 ( - - ); - case "list": - return ( - - ); - case "values": - return ( - { - await getChartData(currentChart); - }} - disabled={!isAuthorized("application", "", ["get", "update"])} - /> - ); - case "build-settings": - return ( - { - 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 ( - - link - - {url} - - - ); - } - - const service: any = components?.find((c) => { - return c.Kind === "Service"; - }); - - if (loading) { - return ( - - Loading... - - ); - } - - if (!service?.Name || !service?.Namespace) { - return; - } - - return ( - - Internal URI: - {`${service.Name}.${service.Namespace}.svc.cluster.local`} - - ); - }; - - const renderHelmReleaseName = () => { - return ( - - Helm Release Name: - {currentChart.name} - - ); - }; - - 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( - "", - { - 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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - {}, - { - 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( - "", - {}, - { - 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 ? ( - { - setFullScreenLogs(false); - }} - /> - ) : ( - <> - {isFullscreen ? ( - {}} - /> - ) : ( - - - - - Back - - - - - {currentChart.canonical_name === "" - ? currentChart.name - : currentChart.canonical_name} - - - Namespace{" "} - {currentChart.namespace} - - - - {currentChart.chart.metadata.name != "worker" && - currentChart.chart.metadata.name != "job" && - renderUrl()} - - {currentChart.canonical_name !== "" && renderHelmReleaseName()} - - {/* - - */} - {!templateWhitelist.includes( - currentChart.chart.metadata.name - ) && ( - <> - - - Last deployed - {" " + getReadableDate(currentChart.info.last_deployed)} - - - )} - - - {!databaseStatus && ( - <> - - - Database is being created - - - - - )} - - {deleting ? ( - <> - - - -
- Deleting " - {currentChart.name}" -
- You will be automatically redirected after deletion is - complete. -
-
- - ) : ( - <> - { - 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 ? ( - <> - - - -
- -
-
-
- - ) : ( - <> - {(isPreview || leftTabOptions.length > 0) && ( - - - offline_bolt{" "} - DevOps Mode - - } - 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}`, - }); - } - }} - /> - - )} - - )} - - )} -
- )} - - )} - - ); -}; - -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; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx deleted file mode 100644 index 67b11ef80f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx +++ /dev/null @@ -1,161 +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 { - StorageType, - 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 { - 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( - "", - {}, - { - 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 ( - - - - ); - } else if (currentChart && baseRoute === "jobs") { - return ( - { - 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 ( - { - 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 ; - } -} - -ExpandedChartWrapper.contextType = Context; - -export default withRouter(ExpandedChartWrapper); - -const LoadingWrapper = styled.div` - width: 100%; - height: 100vh; -`; diff --git a/dashboard/src/main/home/project-settings/Bars.tsx b/dashboard/src/main/home/project-settings/Bars.tsx deleted file mode 100644 index 4daa83a6ea..0000000000 --- a/dashboard/src/main/home/project-settings/Bars.tsx +++ /dev/null @@ -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>; - yKey: string; - xKey: string; - fill?: string; - title?: string; -}; - -const CustomTooltip = ({ active, payload }: TooltipProps) => { - if (active && payload?.length) { - return ( -
-

{`Value: ${payload[0].value}`}

-
- ); - } - - return null; -}; - -const Bars: React.FC = ({ data, yKey, xKey, fill, title }) => { - return ( - - - - - - } cursor={{ fill: "#ffffff11" }} /> - - -
- {title} -
-
- ); -}; - -export default Bars; - -const Center = styled.div` - text-align: center; -`; diff --git a/dashboard/src/main/home/project-settings/ReferralsPage.tsx b/dashboard/src/main/home/project-settings/ReferralsPage.tsx deleted file mode 100644 index 2f2a19cef9..0000000000 --- a/dashboard/src/main/home/project-settings/ReferralsPage.tsx +++ /dev/null @@ -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 ( - <> - Referrals - - Refer people to Porter to earn credits. - - {referralDetails !== null && ( - <> - Your referral link is -