2
0
Эх сурвалжийг харах

Stacks improvements (#2987)

* cleaned up github modal

* statusfooter into separate component

* hide status if no built image

---------

Co-authored-by: Justin Rhee <jusrhee@Justins-MacBook-Air.local>
jusrhee 3 жил өмнө
parent
commit
6ed48f4524

+ 6 - 9
dashboard/src/components/repo-selector/RepoList.tsx

@@ -308,14 +308,11 @@ const RepoList: React.FC<Props> = ({
 
     return (
       <>
-                <Text size={16}>
-                  No connected repositories were found.
-                </Text>
-                <ConnectToGithubButton
-                  href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
-                >
-                  <GitHubIcon src={github} /> Connect to GitHub
-                </ConnectToGithubButton>
+        <ConnectToGithubButton
+          href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
+        >
+          <GitHubIcon src={github} /> Install the Porter GitHub app
+        </ConnectToGithubButton>
       </>
     );
   }
@@ -332,7 +329,7 @@ const GitHubIcon = styled.img`
 `;
 
 const ConnectToGithubButton = styled.a`
-  width: 180px;
+  width: 240px;
   justify-content: center;
   border-radius: 5px;
   display: flex;

+ 0 - 1
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -173,7 +173,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         newAppData
       );
       setPorterJson(porterJson);
-      console.log(newAppData)
       setAppData(newAppData);
       updateServicesAndEnvVariables(resChartData?.data, porterJson);
     } catch (err) {

+ 288 - 0
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -0,0 +1,288 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Button from "components/porter/Button";
+import {
+  NewWebsocketOptions,
+  useWebsockets,
+} from "shared/hooks/useWebsockets";
+import {
+  getAvailability,
+  getAvailabilityStacks,
+} from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
+import Spacer from "components/porter/Spacer";
+
+type Props = {
+  chart: any;
+  service: any;
+};
+
+const StatusFooter: React.FC<Props> = ({
+  chart,
+  service,
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [controller, setController] = React.useState<any>(null);
+  const [available, setAvailable] = React.useState<number>(0);
+  const [total, setTotal] = React.useState<number>(0);
+  const [stale, setStale] = React.useState<number>(0);
+
+  useEffect(() => {
+    // Do something
+  }, []);
+
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    closeWebsocket,
+  } = useWebsockets();
+
+  const getSelectors = () => {
+    let ml =
+      controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
+    let i = 1;
+    let selector = "";
+    for (var key in ml) {
+      selector += key + "=" + ml[key];
+      if (i != Object.keys(ml).length) {
+        selector += ",";
+      }
+      i += 1;
+    }
+    return selector;
+  };
+
+  useEffect(() => {
+    const selectors = getSelectors();
+
+    if (selectors.length > 0) {
+      // updatePods();
+      [controller?.kind].forEach((kind) => {
+        setupWebsocket(kind, controller?.metadata?.uid, selectors);
+      });
+      return () => closeAllWebsockets();
+    }
+  }, [controller]);
+
+  const getName = (service: any) => {
+    const name = chart.name + "-" + service.name;
+
+    switch (service.type) {
+      case "web":
+        return name + "-web";
+      case "worker":
+        return name + "-wkr";
+      case "job":
+        return name + "job";
+    }
+  };
+
+  useEffect(() => {
+    if (chart) {
+      api
+        .getChartControllers(
+          "<token>",
+          {},
+          {
+            namespace: chart.namespace,
+            cluster_id: currentCluster.id,
+            id: currentProject.id,
+            name: chart.name,
+            revision: chart.version,
+          }
+        )
+        .then((res: any) => {
+          const controllers =
+            chart.chart.metadata.name == "job"
+              ? res.data[0]?.status.active
+              : res.data;
+          const filteredControllers = controllers.filter((controller: any) => {
+            const name = getName(service);
+            return name == controller.metadata.name;
+          });
+          if (filteredControllers.length == 1) {
+            setController(filteredControllers[0]);
+          }
+        })
+        .catch((err) => {
+          console.log(err);
+        });
+    }
+  }, [chart]);
+
+  const setupWebsocket = (
+    kind: string,
+    controllerUid: string,
+    selectors: string
+  ) => {
+    let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
+    if (kind == "pod" && selectors) {
+      apiEndpoint += `selectors=${selectors}`;
+    }
+
+
+    const options: NewWebsocketOptions = {};
+    options.onopen = () => {
+    };
+
+    options.onmessage = (evt: MessageEvent) => {
+      let event = JSON.parse(evt.data);
+      let object = event.Object;
+      object.metadata.kind = event.Kind;
+
+      // update pods no matter what if ws message is a pod event.
+      // If controller event, check if ws message corresponds to the designated controller in props.
+      if (event.Kind != "pod" && object.metadata.uid !== controllerUid) {
+        return;
+      }
+
+      if (event.event_type == "ADD" && total == 0) {
+        let [available, total, stale] = getAvailabilityStacks(
+          object.metadata.kind,
+          object
+        );
+
+        setAvailable(available);
+        setTotal(total);
+        setStale(stale);
+        return;
+      }
+
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
+
+      // testing hot reload
+
+      if (event.Kind != "pod") {
+        let [available, total, stale] = getAvailabilityStacks(
+          object.metadata.kind,
+          object
+        );
+
+        setAvailable(available);
+        setTotal(total);
+        setStale(stale);
+        return;
+      }
+      // updatePods();
+    };
+
+    options.onclose = () => {
+    };
+
+    options.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      closeWebsocket(kind);
+    };
+
+    newWebsocket(kind, apiEndpoint, options);
+    openWebsocket(kind);
+  };
+
+  const percentage = Number(1 - available / total).toLocaleString(undefined, {
+    style: "percent",
+    minimumFractionDigits: 2,
+  });
+
+  return (
+    <StyledStatusFooter>
+      {service.type === "job" && (
+        <Container row>
+          <Mi className="material-icons">check</Mi>
+          <Text color="helper">
+            Last run succeeded at 12:39 PM on 4/13/23
+          </Text>
+          <Spacer inline x={1} />
+          <Button
+            onClick={() => { }}
+            height="30px"
+            width="87px"
+            color="#ffffff11"
+            withBorder
+          >
+            <I className="material-icons">open_in_new</I>
+            History
+          </Button>
+        </Container>
+      )}
+      {service.type !== "job" && (
+        <Container row>
+          {percentage === "0.00%" ? (
+            <StatusDot />
+          ) : (
+            <StatusCircle percentage={percentage} />
+          )}
+          <Text color="helper">
+            Running {available}/{total} instances{" "}
+            {stale == 1 ? `(${stale} old instance)` : ""}
+            {stale > 1 ? `(${stale} old instances)` : ""}
+          </Text>
+          <Spacer inline x={1} />
+          <Button
+            onClick={() => { }}
+            height="30px"
+            width="70px"
+            color="#ffffff11"
+            withBorder
+          >
+            <I className="material-icons">open_in_new</I>
+            Logs
+          </Button>
+        </Container>
+      )}
+    </StyledStatusFooter>
+  );
+};
+
+export default StatusFooter;
+
+const StatusDot = styled.div`
+  min-width: 7px;
+  max-width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  margin-right: 10px;
+  background: #38a88a;
+`;
+
+const Mi = styled.i`
+  font-size: 16px;
+  margin-right: 7px;
+  margin-top: -1px;
+  color: rgb(56, 168, 138);
+`;
+
+const I = styled.i`
+  font-size: 14px;
+  margin-right: 5px;
+`;
+
+const StatusCircle = styled.div<{ percentage?: any }>`
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  margin-right: 10px;
+  background: conic-gradient(
+    from 0deg,
+    #ffffff33 ${(props) => props.percentage},
+    #ffffffaa 0% ${(props) => props.percentage}
+  );
+`;
+
+const StyledStatusFooter = styled.div`
+  width: 100%;
+  padding: 10px 15px;
+  background: ${(props) => props.theme.fg2};
+  border-bottom-left-radius: 5px;
+  border-bottom-right-radius: 5px;
+  border: 1px solid #494b4f;
+  border-top: 0;
+`;

+ 50 - 32
dashboard/src/main/home/app-dashboard/new-app-flow/GithubConnectModal.tsx

@@ -53,20 +53,20 @@ const GithubConnectModal: React.FC<Props> = ({
     if (accessError) {
       return (
         <>
+          <Text color="helper">To deploy from GitHub, authorize Porter to view your repos.</Text> 
           <ListWrapper>
             <Helper>
-              No connected repositories found.
+              No connected repos found.
               <A href={"/api/integrations/github-app/oauth"}>
-                Authorize Porter to view your repositories.
+                Authorize Porter to view your repos.
               </A>
             </Helper>
           </ListWrapper>
-          <Spacer y={0.5} />
-
+          <Spacer y={1} />
           <Button
             onClick={handleDoNotConnect}
-            width={"110px"}
-            loadingText={"Submitting..."}
+            loadingText="Submitting..."
+            color="#ffffff11"
             status={loading ? "loading" : undefined}
           >
             Dismiss
@@ -76,29 +76,29 @@ const GithubConnectModal: React.FC<Props> = ({
     } else if (!accessData.accounts || accessData.accounts?.length == 0) {
       return (
         <>
-          <Text size={16}>No connected repositories were found.</Text>
-          <Spacer y={0.5} />
-
-          <ButtonWrapper>
-            <ConnectToGithubButton
-              href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
-              target="_blank"
-              rel="noopener noreferrer"
-              onClick={closeModal}
-            >
-              <GitHubIcon src={github} />
-              Connect to GitHub
-            </ConnectToGithubButton>
-
-            <Button
-              onClick={handleDoNotConnect}
-              width={"110px"}
-              loadingText={"Submitting..."}
-              status={loading ? "loading" : undefined}
-            >
-              Dismiss
-            </Button>
-          </ButtonWrapper>
+          <Text color="helper">
+            You are currently authorized as <B>{accessData.username}</B>.
+          </Text>
+          <Spacer y={1} />
+          <ConnectToGithubButton
+            href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
+            target="_blank"
+            rel="noopener noreferrer"
+            onClick={closeModal}
+          >
+            <GitHubIcon src={github} />
+            Install the Porter GitHub app
+          </ConnectToGithubButton>
+          <Spacer y={1} />
+          <Button
+            onClick={handleDoNotConnect}
+            loadingText="Submitting..."
+            color="#ffffff11"
+            withBorder
+            status={loading ? "loading" : undefined}
+          >
+            Dismiss
+          </Button>
         </>
       );
     }
@@ -122,7 +122,12 @@ const GithubConnectModal: React.FC<Props> = ({
       accessData.accounts?.length === 0) && (
       <>
         <Modal closeModal={closeModal}>
-          <>{renderGithubConnect()}</>
+          <Text size={16}>
+            <GitIcon src={github} />
+            Configure GitHub
+          </Text>
+          <Spacer y={0.5} />
+          {renderGithubConnect()}
         </Modal>
       </>
     )
@@ -131,6 +136,20 @@ const GithubConnectModal: React.FC<Props> = ({
 
 export default withRouter(GithubConnectModal);
 
+const B = styled.b`
+  display: inline;
+  color: #ffffff;
+  margin-left: 5px;
+`;
+
+const GitIcon = styled.img`
+  width: 15px;
+  height: 15px;
+  opacity: 0.9;
+  margin-right: 10px;
+  filter: brightness(120%);
+`;
+
 const Tab = styled.span`
   margin-left: 20px;
   height: 1px;
@@ -164,7 +183,7 @@ const A = styled.a`
 `;
 
 const ConnectToGithubButton = styled.a`
-  width: 180px;
+  width: 240px;
   justify-content: center;
   border-radius: 5px;
   display: flex;
@@ -220,5 +239,4 @@ const GitHubIcon = styled.img`
 const ButtonWrapper = styled.div`
   display: flex;
   justify-content: space-between;
-  margin-top: 20px;
 `;

+ 40 - 260
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -1,6 +1,7 @@
 import React, { useContext, useEffect, useMemo } from "react";
 import AnimateHeight, { Height } from "react-animate-height";
 import styled from "styled-components";
+import _ from "lodash";
 
 import web from "assets/web.png";
 import worker from "assets/worker.png";
@@ -11,19 +12,7 @@ import WebTabs from "./WebTabs";
 import WorkerTabs from "./WorkerTabs";
 import JobTabs from "./JobTabs";
 import { Service } from "./serviceTypes";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Button from "components/porter/Button";
-import {
-  NewWebsocketOptions,
-  useWebsockets,
-} from "../../../../shared/hooks/useWebsockets";
-import { Context } from "../../../../shared/Context";
-import api from "../../../../shared/api";
-import {
-  getAvailability,
-  getAvailabilityStacks,
-} from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
+import StatusFooter from "../expanded-app/StatusFooter";
 
 interface ServiceProps {
   service: Service;
@@ -40,163 +29,6 @@ const ServiceContainer: React.FC<ServiceProps> = ({
 }) => {
   const [showExpanded, setShowExpanded] = React.useState<boolean>(false);
   const [height, setHeight] = React.useState<Height>("auto");
-  const [controller, setController] = React.useState<any>(null);
-  const [available, setAvailable] = React.useState<number>(0);
-  const [total, setTotal] = React.useState<number>(0);
-  const [stale, setStale] = React.useState<number>(0);
-
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
-
-  const getSelectors = () => {
-    let ml =
-      controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
-    let i = 1;
-    let selector = "";
-    for (var key in ml) {
-      selector += key + "=" + ml[key];
-      if (i != Object.keys(ml).length) {
-        selector += ",";
-      }
-      i += 1;
-    }
-    return selector;
-  };
-
-  useEffect(() => {
-    const selectors = getSelectors();
-
-    if (selectors.length > 0) {
-      // updatePods();
-      [controller?.kind].forEach((kind) => {
-        setupWebsocket(kind, controller?.metadata?.uid, selectors);
-      });
-      return () => closeAllWebsockets();
-    }
-  }, [controller]);
-
-  const { currentProject, currentCluster } = useContext(Context);
-
-  useEffect(() => {
-    if (chart != null) {
-      api
-        .getChartControllers(
-          "<token>",
-          {},
-          {
-            namespace: chart.namespace,
-            cluster_id: currentCluster.id,
-            id: currentProject.id,
-            name: chart.name,
-            revision: chart.version,
-          }
-        )
-        .then((res: any) => {
-          const controllers =
-            chart.chart.metadata.name == "job"
-              ? res.data[0]?.status.active
-              : res.data;
-          const filteredControllers = controllers.filter((controller: any) => {
-            const name = getName(service);
-            return name == controller.metadata.name;
-          });
-          if (filteredControllers.length == 1) {
-            setController(filteredControllers[0]);
-          }
-        })
-        .catch((err) => {
-          console.log(err);
-        });
-    }
-  }, [chart]);
-
-  const getName = (service: any) => {
-    const name = chart.name + "-" + service.name;
-
-    switch (service.type) {
-      case "web":
-        return name + "-web";
-      case "worker":
-        return name + "-wkr";
-      case "job":
-        return name + "job";
-    }
-  };
-
-  const setupWebsocket = (
-    kind: string,
-    controllerUid: string,
-    selectors: string
-  ) => {
-    let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
-    if (kind == "pod" && selectors) {
-      apiEndpoint += `selectors=${selectors}`;
-    }
-
-
-    const options: NewWebsocketOptions = {};
-    options.onopen = () => {
-    };
-
-    options.onmessage = (evt: MessageEvent) => {
-      let event = JSON.parse(evt.data);
-      let object = event.Object;
-      object.metadata.kind = event.Kind;
-
-      // update pods no matter what if ws message is a pod event.
-      // If controller event, check if ws message corresponds to the designated controller in props.
-      if (event.Kind != "pod" && object.metadata.uid !== controllerUid) {
-        return;
-      }
-
-      if (event.event_type == "ADD" && total == 0) {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
-
-        setAvailable(available);
-        setTotal(total);
-        setStale(stale);
-        return;
-      }
-
-      // Make a new API call to update pods only when the event type is UPDATE
-      if (event.event_type !== "UPDATE") {
-        return;
-      }
-
-      // testing hot reload
-
-      if (event.Kind != "pod") {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
-
-        setAvailable(available);
-        setTotal(total);
-        setStale(stale);
-        return;
-      }
-      // updatePods();
-    };
-
-    options.onclose = () => {
-    };
-
-    options.onerror = (err: ErrorEvent) => {
-      console.log(err);
-      closeWebsocket(kind);
-    };
-
-    newWebsocket(kind, apiEndpoint, options);
-    openWebsocket(kind);
-  };
 
   // TODO: calculate heights instead of hardcoding them
   const renderTabs = (service: Service) => {
@@ -239,10 +71,13 @@ const ServiceContainer: React.FC<ServiceProps> = ({
     }
   };
 
-  const percentage = Number(1 - available / total).toLocaleString(undefined, {
-    style: "percent",
-    minimumFractionDigits: 2,
-  });
+  const getHasBuiltImage = () => {
+    return (
+      !_.isEmpty((
+        Object.values(chart?.chart?.values)[0] as any
+      )?.global)
+    );
+  }
 
   return (
     <>
@@ -250,6 +85,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         showExpanded={showExpanded}
         onClick={() => setShowExpanded(!showExpanded)}
         chart={chart}
+        bordersRounded={!getHasBuiltImage() && !showExpanded}
       >
         <ServiceTitle>
           <ActionButton>
@@ -271,52 +107,25 @@ const ServiceContainer: React.FC<ServiceProps> = ({
       <AnimateHeight
         height={showExpanded ? height : 0}
       >
-        <StyledSourceBox showExpanded={showExpanded} chart={chart}>
+        <StyledSourceBox 
+          showExpanded={showExpanded}
+          chart={chart}
+          hasFooter={getHasBuiltImage()}
+        >
           {renderTabs(service)}
         </StyledSourceBox>
       </AnimateHeight>
-      {chart != null && <StatusFooter showExpanded={showExpanded}>
-        {service.type === "job" && (
-          <Container row>
-            <Mi className="material-icons">check</Mi>
-            <Text color="helper">
-              Last run succeeded at 12:39 PM on 4/13/23
-            </Text>
-            <Spacer inline x={1} />
-            <Button
-              onClick={() => { }}
-              height="30px"
-              width="87px"
-              color="#ffffff11"
-              withBorder
-            >
-              <I className="material-icons">open_in_new</I>
-              History
-            </Button>
-          </Container>
-        )}
-        {service.type !== "job" && (
-          <Container row>
-            <StatusCircle percentage={percentage} />
-            <Text color="helper">
-              Running {available}/{total} instances{" "}
-              {stale == 1 ? `(${stale} old instance)` : ""}
-              {stale > 1 ? `(${stale} old instances)` : ""}
-            </Text>
-            <Spacer inline x={1} />
-            <Button
-              onClick={() => { }}
-              height="30px"
-              width="70px"
-              color="#ffffff11"
-              withBorder
-            >
-              <I className="material-icons">open_in_new</I>
-              Logs
-            </Button>
-          </Container>
-        )}
-      </StatusFooter>}
+      {(
+        chart &&
+        service &&
+        // Check if has built image
+        getHasBuiltImage()
+      ) && (
+        <StatusFooter
+          chart={chart}
+          service={service}
+        />
+      )}
       <Spacer y={0.5} />
     </>
   );
@@ -324,46 +133,16 @@ const ServiceContainer: React.FC<ServiceProps> = ({
 
 export default ServiceContainer;
 
-const Mi = styled.i`
-  font-size: 16px;
-  margin-right: 7px;
-  margin-top: -1px;
-  color: rgb(56, 168, 138);
-`;
-
-const I = styled.i`
-  font-size: 14px;
-  margin-right: 5px;
-`;
-
-const StatusCircle = styled.div<{ percentage?: any }>`
-  width: 16px;
-  height: 16px;
-  border-radius: 50%;
-  margin-right: 10px;
-  background: conic-gradient(
-    from 0deg,
-    #ffffff33 ${(props) => props.percentage},
-    #ffffffaa 0% ${(props) => props.percentage}
-  );
-`;
-
-const StatusFooter = styled.div<{ showExpanded: boolean }>`
-  width: 100%;
-  padding: 10px;
-  background: ${(props) => props.theme.fg2};
-  border-bottom-left-radius: 5px;
-  border-bottom-right-radius: 5px;
-  border: 1px solid #494b4f;
-  border-top: 0;
-`;
-
 const ServiceTitle = styled.div`
   display: flex;
   align-items: center;
 `;
 
-const StyledSourceBox = styled.div<{ showExpanded: boolean, chart: any }>`
+const StyledSourceBox = styled.div<{ 
+  showExpanded: boolean,
+  chart: any,
+  hasFooter?: boolean,
+}>`
   width: 100%;
   color: #ffffff;
   padding: 14px 25px 30px;
@@ -372,11 +151,8 @@ const StyledSourceBox = styled.div<{ showExpanded: boolean, chart: any }>`
   background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
   border-top: 0;
-  border-bottom: ${(props) => props.chart != null ? "0" : "1px solid #494b4f"};
-  border-top-left-radius: 0;
-  border-top-right-radius: 0;
-  border-bottom-left-radius: ${(props) => props.chart != null ? "0" : "5px"};
-  border-bottom-right-radius: ${(props) => props.chart != null ? "0" : "5px"};
+  border-bottom-left-radius: ${props => props.hasFooter ? "0" : "5px"};
+  border-bottom-right-radius: ${props => props.hasFooter ? "0" : "5px"};
 `;
 
 const ActionButton = styled.button`
@@ -402,7 +178,11 @@ const ActionButton = styled.button`
   margin-right: 5px;
 `;
 
-const ServiceHeader = styled.div`
+const ServiceHeader = styled.div<{
+  showExpanded: boolean,
+  chart: any,
+  bordersRounded?: boolean,
+}>`
   flex-direction: row;
   display: flex;
   height: 60px;
@@ -418,8 +198,8 @@ const ServiceHeader = styled.div`
     border: 1px solid #7a7b80;
   }
 
-  border-bottom-left-radius: ${(props) => props.chart != null ? "0" : props.showExpanded ? "0" : "5px"};
-  border-bottom-right-radius: ${(props) => props.chart != null ? "0" : props.showExpanded ? "0" : "5px"};
+  border-bottom-left-radius: ${props => props.bordersRounded ? "" : "0"};
+  border-bottom-right-radius: ${props => props.bordersRounded ? "" : "0"};
 
   .dropdown {
     font-size: 30px;