Selaa lähdekoodia

basic doppler env group minus app attach

jusrhee 2 vuotta sitten
vanhempi
sitoutus
a4c6158de6

BIN
dashboard/src/assets/doppler.png


+ 4 - 0
dashboard/src/components/porter/Banner.tsx

@@ -20,6 +20,10 @@ const Banner: React.FC<Props> = ({
   suffix,
 }) => {
   const renderIcon = () => {
+    if (icon === "none") {
+      return null;
+    }
+    
     if (icon) {
       return icon;
     }

+ 50 - 14
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupModal.tsx

@@ -3,12 +3,14 @@ import React, {
   Dispatch,
   SetStateAction,
   useCallback,
+  useEffect,
   useMemo,
   useState,
 } from "react";
 import { UseFieldArrayAppend, useFormContext } from "react-hook-form";
 
 import sliders from "assets/sliders.svg";
+import doppler from "assets/doppler.png";
 
 import { PopulatedEnvGroup } from "./types";
 import Text from "components/porter/Text";
@@ -29,6 +31,28 @@ const EnvGroupModal: React.FC<Props> = ({ append, setOpen, baseEnvGroups }) => {
     selectedEnvGroup,
     setSelectedEnvGroup,
   ] = useState<PopulatedEnvGroup | null>(null);
+  const [
+    selectedDopplerEnvGroup,
+    setSelectedDopplerEnvGroup
+  ] = useState<string>("");
+
+  const [dopplerEnvGroups, setDopplerEnvGroups] = useState<any[]>([]);
+
+  // DOPPLER_TODO: call endpoint to get doppler env groups
+  const loadDopplerEnvGroups = (): void => {
+    setDopplerEnvGroups([
+      {
+        name: "doppler-env-group-1",
+      },
+      {
+        name: "doppler-env-group-2",
+      }
+    ]);
+  };
+
+  useEffect(() => {
+    loadDopplerEnvGroups();
+  }, []);
 
   const { watch } = useFormContext<PorterAppFormData>();
   const envGroups = watch("app.envGroups", []);
@@ -60,7 +84,7 @@ const EnvGroupModal: React.FC<Props> = ({ append, setOpen, baseEnvGroups }) => {
               <Text color="helper">
                 Select an Env Group to load into your application.
               </Text>
-              <Spacer y={0.5} />
+              <Spacer y={1} />
               <GroupModalSections>
                 <SidebarSection $expanded={!selectedEnvGroup}>
                   <EnvGroupList>
@@ -71,13 +95,30 @@ const EnvGroupModal: React.FC<Props> = ({ append, setOpen, baseEnvGroups }) => {
                           Boolean(selectedEnvGroup) &&
                           selectedEnvGroup?.name === eg.name
                         }
-                        lastItem={i === remainingEnvGroupOptions?.length - 1}
-                        onClick={() => setSelectedEnvGroup(eg)}
+                        lastItem={i === remainingEnvGroupOptions?.length - 1 && dopplerEnvGroups.length === 0}
+                        onClick={() => {
+                          setSelectedEnvGroup(eg);
+                          setSelectedDopplerEnvGroup("");
+                        }}
                       >
                         <img src={sliders} />
                         {eg.name}
                       </EnvGroupRow>
                     ))}
+                    {dopplerEnvGroups.map((x, i) => (
+                      <EnvGroupRow
+                        key={i}
+                        lastItem={i === dopplerEnvGroups.length - 1}
+                        isSelected={x.name === selectedDopplerEnvGroup}
+                        onClick={() => { 
+                          setSelectedDopplerEnvGroup(x.name);
+                          setSelectedEnvGroup(null);
+                        }}
+                      >
+                        <img src={doppler} />
+                        {x.name}
+                      </EnvGroupRow>
+                    ))}
                   </EnvGroupList>
                 </SidebarSection>
                 {selectedEnvGroup && (
@@ -114,17 +155,18 @@ const EnvGroupModal: React.FC<Props> = ({ append, setOpen, baseEnvGroups }) => {
           )}
         </ScrollableContainer>
       </ColumnContainer>
-      <SubmitButtonContainer>
-        <Button onClick={onSubmit} disabled={!selectedEnvGroup}>
-          Load Env Group
-        </Button>
-      </SubmitButtonContainer>
+      <Button onClick={onSubmit} disabled={!selectedEnvGroup}>
+        Load env group
+      </Button>
     </Modal>
   );
 };
 
 export default EnvGroupModal;
 
+const DopplerEnvGroupList = styled.div`
+`;
+
 const EnvGroupRow = styled.div<{ lastItem?: boolean; isSelected: boolean }>`
   display: flex;
   width: 100%;
@@ -182,7 +224,6 @@ const GroupEnvPreview = styled.pre`
   }
 `;
 const GroupModalSections = styled.div`
-  margin-top: 20px;
   width: 100%;
   height: 100%;
   display: grid;
@@ -201,8 +242,3 @@ const ScrollableContainer = styled.div`
   overflow-y: auto;
   max-height: 300px;
 `;
-
-const SubmitButtonContainer = styled.div`
-  margin-top: 10px;
-  text-align: right;
-`;

+ 233 - 0
dashboard/src/main/home/integrations/DopplerIntegrationList.tsx

@@ -0,0 +1,233 @@
+import React, { useContext, useRef, useState, useEffect } from "react";
+import ConfirmOverlay from "../../../components/ConfirmOverlay";
+import styled from "styled-components";
+import { Context } from "../../../shared/Context";
+import api from "../../../shared/api";
+
+import doppler from "assets/doppler.png";
+
+import Placeholder from "components/Placeholder";
+import Banner from "components/porter/Banner";
+import ToggleRow from "components/porter/ToggleRow";
+import Spacer from "components/porter/Spacer";
+import Button from "components/porter/Button";
+import Modal from "components/porter/Modal";
+import Text from "components/porter/Text";
+import Input from "components/porter/Input";
+import Container from "components/porter/Container";
+
+const DopplerIntegrationList: React.FC = (_) => {
+  const [dopplerEnabled, setDopplerEnabled] = useState<boolean>(true);
+  const [dopplerToggled, setDopplerToggled] = useState<boolean>(false);
+  const [showServiceTokenModal, setShowServiceTokenModal] = useState<boolean>(false);
+  const [envGroupName, setEnvGroupName] = useState<string>("");
+  const [dopplerServiceToken, setDopplerServiceToken] = useState<string>("");
+  const [dopplerEnvGroups, setDopplerEnvGroups] = useState<any[]>([]);
+
+  const { currentCluster, currentProject } = useContext(
+    Context
+  );
+
+  // Check for the "doppler-integration" namespace
+  const checkDopplerEnabled = (): void => {
+    api
+    .getNamespaces(
+      "<token>",
+      {},
+      { id: currentProject.id, cluster_id: currentCluster.id }
+    )
+    .then((res) => {
+      res.data?.forEach((namespace: { name: string }) => {
+        if (namespace.name === "doppler-integration") {
+          setDopplerEnabled(true);
+        }
+      });
+    })
+    .catch((_) => {});
+  };
+
+  const installDoppler = (): void => {
+    setDopplerToggled(true);
+
+    // DOPPLER_TODO: call endpoint to install doppler
+    // Will automatically check for doppler-integration namespace in 10 seconds or on refresh
+
+    setTimeout(() => {
+      checkDopplerEnabled();
+    }, 10000);
+  };
+
+  const loadDopplerEnvGroups = (): void => {
+    // DOPPLER_TODO: replace dummy data
+    setDopplerEnvGroups([
+      {
+        name: "my-doppler-dev",
+        serviceToken: "dp.st...abcdef",
+      },
+      {
+        name: "my-doppler-staging",
+        serviceToken: "dp.st...abcdef",
+      },
+      {
+        name: "my-doppler-prod",
+        serviceToken: "dp.st...abcdef",
+      }
+    ]);
+  };
+
+  // Install the CRD for a new Doppler secret
+  const addDopplerEnvGroup = (): void => {
+    // DOPPLER_TODO: call endpoint to install doppler CRD
+    // Call the following after the API call succeeds:
+    setShowServiceTokenModal(false);
+    loadDopplerEnvGroups();
+  };
+
+  const deleteDopplerEnvGroup = (envGroupName: string): void => {
+    // DOPPLER_TODO: call endpoint to delete doppler CRD
+    // Call the following after the API call succeeds:
+    loadDopplerEnvGroups();
+  };
+
+  useEffect(() => {
+    checkDopplerEnabled();
+  }, []);
+
+  useEffect(() => {
+    if (dopplerEnabled) {
+      loadDopplerEnvGroups();
+    }
+  }, [dopplerEnabled]);
+
+  if (!dopplerEnabled) {
+    return (
+      <>
+        <Banner icon="none">
+          <ToggleRow
+            isToggled={dopplerToggled}
+            onToggle={installDoppler}
+            disabled={dopplerToggled}
+          >
+            {dopplerToggled ? "Enabling Doppler integration . . ." : "Enable Doppler integration"}
+          </ToggleRow>
+        </Banner>
+        <Spacer y={1} />
+        <Placeholder>
+          You need to enable the Doppler integration to add environment groups from Doppler.
+        </Placeholder>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Button onClick={() => { setShowServiceTokenModal(true) }}>
+        + Add Doppler env group
+      </Button>
+      <Spacer y={1} />
+
+      {dopplerEnvGroups.length > 0 ? (
+        <>
+          {dopplerEnvGroups.map((envGroup: any, i: number) => {
+            return (
+              <DopplerRow key={i}>
+                <Container row>
+                  <Icon src={doppler} />
+                  <Text>
+                    {envGroup.name}
+                  </Text>
+                </Container>
+                <DeleteButton onClick={() => deleteDopplerEnvGroup(envGroup.name)}>
+                  <i className="material-icons">delete</i>
+                </DeleteButton>
+              </DopplerRow>
+            );
+          })}
+        </>
+      ) : (
+        <Placeholder>No environment groups have been added from Doppler yet</Placeholder>
+      )}
+
+      {showServiceTokenModal &&
+        <Modal closeModal={() => { setShowServiceTokenModal(false) }}>
+          <Text size={16}>
+            Add a new Doppler service token
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            Your Doppler secrets will be made available to Porter apps as an environment group.
+          </Text>
+          <Spacer y={1} />
+          <Input
+            placeholder="ex: my-doppler-env"
+            label="Env group name (vanity name for Porter)"
+            value={envGroupName}
+            setValue={(x) => { setEnvGroupName(x) }}
+            width="100%"
+            height="40px"
+          />
+          <Spacer y={1} />
+          <Input
+            type="password"
+            placeholder="ex: dp.st...abcdef"
+            label="Doppler service token"
+            value={dopplerServiceToken}
+            setValue={(x) => { setDopplerServiceToken(x) }}
+            width="100%"
+            height="40px"
+          />
+          <Spacer y={1} />
+          <Button 
+            onClick={addDopplerEnvGroup}
+            disabled={envGroupName === "" || dopplerServiceToken === ""}
+          >
+            Add Doppler env group 
+          </Button>
+        </Modal>
+      }
+    </>
+  );
+};
+
+export default DopplerIntegrationList;
+
+const DeleteButton = styled.div`
+  display: flex;
+  visibility: ${(props: { invis?: boolean }) =>
+    props.invis ? "hidden" : "visible"};
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  float: right;
+  height: 30px;
+  :hover {
+    background: #ffffff11;
+    border-radius: 20px;
+    cursor: pointer;
+  }
+
+  > i {
+    font-size: 20px;
+    color: #ffffff44;
+    border-radius: 20px;
+  }
+`;
+
+const Icon = styled.img`
+  height: 20px;
+  margin-right: 10px;
+`;
+
+const DopplerRow = styled.div`
+  position: relative;
+  padding: 15px;
+  border-radius: 5px;
+  background: ${props => props.theme.clickable.bg};
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+  cursor: not-allowed;
+  justify-content: space-between;
+`;

+ 36 - 25
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -5,6 +5,7 @@ import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
 import { RouteComponentProps, withRouter } from "react-router";
 import IntegrationList from "./IntegrationList";
+import DopplerIntegrationList from "./DopplerIntegrationList";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
 import Loading from "../../../components/Loading";
@@ -102,6 +103,9 @@ const IntegrationCategories: React.FC<Props> = (props) => {
 
   useEffect(() => {
     getIntegrationsForCategory(props.category);
+    if (props.category === "doppler") {
+      setLoading(false);
+    }
   }, [props.category]);
 
   const { category: currentCategory } = props;
@@ -127,30 +131,35 @@ const IntegrationCategories: React.FC<Props> = (props) => {
         <TitleSection icon={icon} iconWidth="32px">
           {label}
         </TitleSection>
-        <Button
-          onClick={() => {
-            if (props.category === "gitlab") {
-              pushFiltered(props, `/integrations/gitlab/create/gitlab`, [
-                "project_id",
-              ]);
-            } else if (props.category != "slack") {
-              setCurrentModal("IntegrationsModal", {
-                category: currentCategory,
-                setCurrentIntegration: (x: string) =>
-                  pushFiltered(
-                    props,
-                    `/integrations/${props.category}/create/${x}`,
-                    ["project_id"]
-                  ),
-              });
-            } else {
-              window.location.href = `/api/projects/${currentProject.id}/oauth/slack`;
-            }
-          }}
-        >
-          <i className="material-icons">add</i>
-          {buttonText}
-        </Button>
+        {props.category === "doppler" ? null : (
+          <Button
+            onClick={() => {
+              if (props.category === "gitlab") {
+                pushFiltered(props, `/integrations/gitlab/create/gitlab`, [
+                  "project_id",
+                ]);
+              } else if (props.category === "doppler") {
+                // ret2
+              } else if (props.category !== "slack") {
+                setCurrentModal("IntegrationsModal", {
+                  category: currentCategory,
+                  setCurrentIntegration: (x: string) => {
+                    pushFiltered(
+                      props,
+                      `/integrations/${props.category}/create/${x}`,
+                      ["project_id"]
+                    )
+                  }
+                });
+              } else {
+                window.location.href = `/api/projects/${currentProject.id}/oauth/slack`;
+              }
+            }}
+          >
+            <i className="material-icons">add</i>
+            {buttonText}
+          </Button>
+        )}
       </Flex>
       <Spacer y={1} />
       {loading ? (
@@ -162,8 +171,10 @@ const IntegrationCategories: React.FC<Props> = (props) => {
             getIntegrationsForCategory(props.category)
           }
         />
-      ) : props.category == "slack" ? (
+      ) : props.category === "slack" ? (
         <SlackIntegrationList slackData={slackData} />
+      ) : props.category === "doppler" ? (
+        <DopplerIntegrationList />
       ) : (
         <IntegrationList
           currentCategory={props.category}

+ 2 - 2
dashboard/src/main/home/integrations/Integrations.tsx

@@ -21,10 +21,10 @@ const Integrations: React.FC<PropsType> = (props) => {
 
   const IntegrationCategoryStrings = useMemo(() => {
     if (!enableGitlab) {
-      return ["registry", "slack"];
+      return ["registry", "slack", "doppler"];
     }
 
-    return ["registry", "slack", "gitlab"];
+    return ["registry", "slack", "doppler", "gitlab"];
   }, [enableGitlab]);
 
   return (

+ 240 - 0
dashboard/src/main/home/integrations/edit-integration/GitlabIntegrationList.tsx

@@ -0,0 +1,240 @@
+import React, { useContext, useRef, useState } from "react";
+import ConfirmOverlay from "../../../components/ConfirmOverlay";
+import styled from "styled-components";
+import { Context } from "../../../shared/Context";
+import api from "../../../shared/api";
+import { integrationList } from "shared/common";
+import DynamicLink from "components/DynamicLink";
+
+interface Props {
+  gitlabData: any[];
+  updateIntegrationList: () => void;
+}
+
+type StateType = {
+  isDelete: boolean;
+  deleteName: string;
+  deleteID: number;
+};
+
+const GitlabIntegrationList: React.FC<Props> = (props) => {
+  const [currentState, setCurrentState] = useState<StateType>({
+    isDelete: false,
+    deleteName: "",
+    deleteID: 0,
+  });
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+
+  const handleDeleteIntegration = () => {
+    api
+      .deleteGitlabIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          integration_id: currentState.deleteID,
+        }
+      )
+      .then(() => {
+        setCurrentState({
+          isDelete: false,
+          deleteName: "",
+          deleteID: 0,
+        });
+        props.updateIntegrationList();
+      })
+      .catch((err) => {
+        setCurrentError(err);
+      });
+  };
+
+  return (
+    <>
+      <ConfirmOverlay
+        show={currentState.isDelete}
+        message={`Are you sure you want to delete the GitLab integration for instance ${currentState.deleteName}?`}
+        onYes={handleDeleteIntegration}
+        onNo={() =>
+          setCurrentState({
+            isDelete: false,
+            deleteName: "",
+            deleteID: 0,
+          })
+        }
+      />
+      <StyledIntegrationList>
+        {props.gitlabData?.length > 0 ? (
+          props.gitlabData.map((inst, idx) => {
+            return (
+              <Integration onClick={() => {}} disabled={false} key={inst.id}>
+                <MainRow disabled={false}>
+                  <Flex>
+                    <Icon src={integrationList.gitlab.icon} />
+                    <Label>{inst.instance_url}</Label>
+                    {inst.username.includes("Unable") ? (
+                      <ErrorLabel>[{inst.username}]</ErrorLabel>
+                    ) : (
+                      <UsernameLabel>({inst.username})</UsernameLabel>
+                    )}
+                  </Flex>
+                  <MaterialIconTray disabled={false}>
+                    <i
+                      className="material-icons"
+                      onClick={() => {
+                        setCurrentState({
+                          isDelete: true,
+                          deleteName: inst.instance_url,
+                          deleteID: inst.id,
+                        });
+                      }}
+                    >
+                      delete
+                    </i>
+                    <i
+                      className="material-icons"
+                      onClick={() => {
+                        window.open(inst.instance_url, "_blank");
+                      }}
+                    >
+                      launch
+                    </i>
+                  </MaterialIconTray>
+                </MainRow>
+              </Integration>
+            );
+          })
+        ) : (
+          <Placeholder>No GitLab instances found</Placeholder>
+        )}
+      </StyledIntegrationList>
+    </>
+  );
+};
+
+export default GitlabIntegrationList;
+
+const Placeholder = styled.div`
+  width: 100%;
+  height: 250px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+  justify-content: center;
+  margin-top: 30px;
+  background: #ffffff11;
+  color: #ffffff44;
+  border-radius: 5px;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+`;
+
+const UsernameLabel = styled.div`
+  color: #ffffff66;
+  font-size: 14px;
+  font-weight: 500;
+  padding: 10px;
+`;
+
+const ErrorLabel = styled.div`
+  color: #f6685e;
+  font-size: 14px;
+  font-weight: 500;
+  padding: 10px;
+`;
+
+const StyledIntegrationList = styled.div`
+  margin-top: 20px;
+  margin-bottom: 80px;
+`;
+
+const MainRow = styled.div`
+  height: 70px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25px;
+  border-radius: 5px;
+  :hover {
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
+    > i {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+
+  > i {
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    color: #ffffff44;
+    margin-right: -7px;
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;
+
+const Integration = styled.div`
+  margin-left: -2px;
+  display: flex;
+  flex-direction: column;
+  background: #26282f;
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+  margin-bottom: 15px;
+  border-radius: 8px;
+  box-shadow: 0 4px 15px 0px #00000055;
+`;
+
+const Icon = styled.img`
+  width: 27px;
+  margin-right: 12px;
+  margin-bottom: -1px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+
+  > i {
+    cursor: pointer;
+    font-size: 24px;
+    color: #969fbbaa;
+    padding: 3px;
+    margin-right: 11px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const MaterialIconTray = styled.div`
+  max-width: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > i {
+    background: #26282f;
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    margin: 0 5px;
+    color: #ffffff44;
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;

+ 8 - 2
dashboard/src/shared/common.tsx

@@ -3,6 +3,7 @@ import digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
 import azure from "assets/azure.png";
+import doppler from "assets/doppler.png";
 
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
@@ -14,16 +15,21 @@ export const infraNames: any = {
 };
 
 export const integrationList: any = {
+  doppler: {
+    icon: doppler,
+    label: "Doppler",
+    buttonText: "Add a service token",
+  },
   kubernetes: {
     icon:
       "https://upload.wikimedia.org/wikipedia/labs/thumb/b/ba/Kubernetes-icon-color.svg/2110px-Kubernetes-icon-color.svg.png",
     label: "Kubernetes",
-    buttonText: "Add a Cluster",
+    buttonText: "Add a cluster",
   },
   repo: {
     icon: "https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png",
     label: "Git Repository",
-    buttonText: "Link a Github Account",
+    buttonText: "Link a GitHub account",
   },
   slack: {
     icon: