Ver código fonte

Merge branch 'alerting-fe' of github.com-meehawk:porter-dev/porter into belanger/agent-v3-integration

Soham Parekh 3 anos atrás
pai
commit
23fb91c8ad
23 arquivos alterados com 1433 adições e 45 exclusões
  1. 15 10
      api/server/shared/config/metadata.go
  2. 2 2
      dashboard/src/components/ProvisionerStatus.tsx
  3. 1 1
      dashboard/src/components/events/useLastSeenPodStatus.ts
  4. 1 1
      dashboard/src/components/porter-form/PorterForm.tsx
  5. 2 2
      dashboard/src/main/home/Home.tsx
  6. 11 2
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  7. 30 2
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  8. 12 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  9. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  10. 2 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  11. 378 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx
  12. 201 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx
  13. 147 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx
  14. 222 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx
  15. 316 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx
  16. 53 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts
  17. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  18. 1 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  19. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  20. 13 0
      dashboard/src/main/home/launch/Boilerplate.tsx
  21. 6 4
      dashboard/src/main/home/launch/Launch.tsx
  22. 16 0
      dashboard/src/main/home/launch/LaunchWrapper.tsx
  23. 2 0
      dashboard/src/shared/types.tsx

+ 15 - 10
api/server/shared/config/metadata.go

@@ -15,20 +15,25 @@ type Metadata struct {
 	Analytics          bool   `json:"analytics"`
 	Version            string `json:"version"`
 	Gitlab             bool   `json:"gitlab"`
+
+	DefaultAppHelmRepoURL   string `json:"default_app_helm_repo_url"`
+	DefaultAddonHelmRepoURL string `json:"default_addon_helm_repo_url"`
 }
 
 func MetadataFromConf(sc *env.ServerConf, version string) *Metadata {
 	return &Metadata{
-		Provisioning:       sc.ProvisionerServerURL != "" && sc.ProvisionerToken != "",
-		Github:             hasGithubAppVars(sc),
-		GithubLogin:        sc.GithubClientID != "" && sc.GithubClientSecret != "" && sc.GithubLoginEnabled,
-		BasicLogin:         sc.BasicLoginEnabled,
-		GoogleLogin:        sc.GoogleClientID != "" && sc.GoogleClientSecret != "",
-		SlackNotifications: sc.SlackClientID != "" && sc.SlackClientSecret != "",
-		Email:              sc.SendgridAPIKey != "",
-		Analytics:          sc.SegmentClientKey != "",
-		Version:            version,
-		Gitlab:             sc.EnableGitlab,
+		Provisioning:            sc.ProvisionerServerURL != "" && sc.ProvisionerToken != "",
+		Github:                  hasGithubAppVars(sc),
+		GithubLogin:             sc.GithubClientID != "" && sc.GithubClientSecret != "" && sc.GithubLoginEnabled,
+		BasicLogin:              sc.BasicLoginEnabled,
+		GoogleLogin:             sc.GoogleClientID != "" && sc.GoogleClientSecret != "",
+		SlackNotifications:      sc.SlackClientID != "" && sc.SlackClientSecret != "",
+		Email:                   sc.SendgridAPIKey != "",
+		Analytics:               sc.SegmentClientKey != "",
+		Version:                 version,
+		Gitlab:                  sc.EnableGitlab,
+		DefaultAppHelmRepoURL:   sc.DefaultApplicationHelmRepoURL,
+		DefaultAddonHelmRepoURL: sc.DefaultAddonHelmRepoURL,
 	}
 }
 

+ 2 - 2
dashboard/src/components/ProvisionerStatus.tsx

@@ -464,11 +464,11 @@ export const OperationDetails: React.FunctionComponent<OperationDetailsProps> =
 
     const wsConfig = {
       onopen: () => {
-        console.log(`connected to websocket:`, websocketID);
+        // console.log(`connected to websocket:`, websocketID);
       },
       onmessage: parseOperationWebsocketEvent,
       onclose: () => {
-        console.log(`closing websocket:`, websocketID);
+        // console.log(`closing websocket:`, websocketID);
       },
       onerror: (err: ErrorEvent) => {
         console.log(err);

+ 1 - 1
dashboard/src/components/events/useLastSeenPodStatus.ts

@@ -59,7 +59,7 @@ const useLastSeenPodStatus = ({
           name: podName,
         }
       );
-      //console.log(getPodStatus(res.data.status));
+      // console.log(getPodStatus(res.data.status));
 
       setCurrentStatus(getPodStatus(res.data.status));
     } catch (error) {

+ 1 - 1
dashboard/src/components/porter-form/PorterForm.tsx

@@ -254,7 +254,7 @@ const StyledPorterForm = styled.div<{ showSave?: boolean }>`
   height: ${(props) => (props.showSave ? "calc(100% - 50px)" : "100%")};
   background: #ffffff11;
   color: #ffffff;
-  padding: 0px 35px 0;
+  padding: 0px 35px 20px;
   position: relative;
   border-radius: 8px;
   font-size: 13px;

+ 2 - 2
dashboard/src/main/home/Home.tsx

@@ -14,7 +14,7 @@ import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
 import Dashboard from "./dashboard/Dashboard";
 import WelcomeForm from "./WelcomeForm";
 import Integrations from "./integrations/Integrations";
-import Templates from "./launch/Launch";
+import LaunchWrapper from "./launch/LaunchWrapper";
 
 import Navbar from "./navbar/Navbar";
 import ProjectSettings from "./project-settings/ProjectSettings";
@@ -503,7 +503,7 @@ class Home extends Component<PropsType, StateType> {
               path={"/project-settings"}
               render={() => <GuardedProjectSettings />}
             />
-            <Route path={"*"} render={() => <Templates />} />
+            <Route path={"*"} render={() => <LaunchWrapper />} />
           </Switch>
         </ViewWrapper>
 

+ 11 - 2
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -111,9 +111,19 @@ class ClusterDashboard extends Component<PropsType, StateType> {
   componentDidUpdate(prevProps: PropsType) {
     // Reset namespace filter and close expanded chart on cluster change
     if (prevProps.currentCluster !== this.props.currentCluster) {
+      let namespace = "default";
+      if (
+        localStorage.getItem(
+          `${this.context.currentProject.id}-${this.context.currentCluster.id}-namespace`
+        )
+      ) {
+        namespace = localStorage.getItem(
+          `${this.context.currentProject.id}-${this.context.currentCluster.id}-namespace`
+        );
+      }
       this.setState(
         {
-          namespace: "default",
+          namespace,
           sortType: localStorage.getItem("SortType")
             ? localStorage.getItem("SortType")
             : "Newest",
@@ -151,7 +161,6 @@ class ClusterDashboard extends Component<PropsType, StateType> {
         <NamespaceSelector
           setNamespace={(namespace) =>
             this.setState({ namespace }, () => {
-              console.log(window.location, namespace);
               pushQueryParams(this.props, {
                 namespace: this.state.namespace || "ALL",
               });

+ 30 - 2
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -30,7 +30,11 @@ export const NamespaceSelector: React.FunctionComponent<Props> = ({
       value: string;
     }[]
   >([]);
-  const [defaultNamespace, setDefaultNamespace] = useState<string>("default");
+  const [defaultNamespace, setDefaultNamespace] = useState<string>(
+    localStorage.getItem(
+      `${context.currentProject.id}-${context.currentCluster.id}-namespace`
+    )
+  );
 
   const updateOptions = () => {
     let { currentCluster, currentProject } = context;
@@ -61,7 +65,19 @@ export const NamespaceSelector: React.FunctionComponent<Props> = ({
           const availableNamespaces = res.data.filter((namespace: any) => {
             return namespace.status !== "Terminating";
           });
-          setDefaultNamespace("default");
+          if (
+            localStorage.getItem(
+              `${context.currentProject.id}-${context.currentCluster.id}-namespace`
+            )
+          ) {
+            setDefaultNamespace(
+              localStorage.getItem(
+                `${context.currentProject.id}-${context.currentCluster.id}-namespace`
+              )
+            );
+          } else {
+            setDefaultNamespace("default");
+          }
           availableNamespaces.forEach((x: { name: string }, i: number) => {
             namespaceOptions.push({
               label: x.name,
@@ -99,7 +115,19 @@ export const NamespaceSelector: React.FunctionComponent<Props> = ({
     updateOptions();
   }, [namespace, context.currentCluster]);
 
+  useEffect(() => {
+    setNamespace(
+      localStorage.getItem(
+        `${context.currentProject.id}-${context.currentCluster.id}-namespace`
+      )
+    );
+  }, [context.currentCluster]);
+
   const handleSetActive = (namespace: any) => {
+    localStorage.setItem(
+      `${context.currentProject.id}-${context.currentCluster.id}-namespace`,
+      namespace
+    );
     setNamespace(namespace);
   };
 

+ 12 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -27,6 +27,7 @@ import EventsTab from "./events/EventsTab";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces";
 import { useStackEnvGroups } from "./useStackEnvGroups";
+import DeployStatusSection from "./deploy-status-section/DeployStatusSection";
 
 type Props = {
   namespace: string;
@@ -140,6 +141,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   };
 
   const getControllers = async (chart: ChartType) => {
+    
     // don't retrieve controllers for chart that failed to even deploy.
     if (chart.info.status == "failed") return;
 
@@ -234,7 +236,6 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const onSubmit = async (props: any) => {
     const rawValues = props.values;
 
-    // console.log("raw", rawValues);
     // Convert dotted keys to nested objects
     let values: any = {};
 
@@ -615,9 +616,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const renderUrl = () => {
     if (url) {
       return (
-        <Url href={url} target="_blank">
+        <Url>
           <i className="material-icons">link</i>
-          {url}
+          <a href={url} target="_blank">{url}</a>
         </Url>
       );
     }
@@ -874,11 +875,14 @@ const ExpandedChart: React.FC<Props> = (props) => {
                   currentChart.chart.metadata.name != "job" &&
                   renderUrl()}
                 <InfoWrapper>
+                  {/*
                   <StatusIndicator
                     controllers={controllers}
                     status={currentChart.info.status}
                     margin_left={"0px"}
                   />
+                  */}
+                  <DeployStatusSection chart={currentChart} />
                   <LastDeployed>
                     <Dot>•</Dot>Last deployed
                     {" " + getReadableDate(currentChart.info.last_deployed)}
@@ -1084,15 +1088,16 @@ const Bolded = styled.div`
   margin-right: 6px;
 `;
 
-const Url = styled.a`
+const Url = styled.div`
   display: block;
-  margin-left: 2px;
+  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 {
@@ -1135,7 +1140,7 @@ const HeaderWrapper = styled.div`
 `;
 
 const Dot = styled.div`
-  margin-right: 9px;
+  margin-right: 16px;
 `;
 
 const InfoWrapper = styled.div`
@@ -1147,7 +1152,7 @@ const InfoWrapper = styled.div`
 
 const LastDeployed = styled.div`
   font-size: 13px;
-  margin-left: 10px;
+  margin-left: 8px;
   margin-top: -1px;
   display: flex;
   align-items: center;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -498,7 +498,7 @@ const StyledRevisionSection = styled.div`
   max-height: ${(props: { showRevisions: boolean }) =>
     props.showRevisions ? "255px" : "40px"};
   background: #ffffff11;
-  margin: 25px 0px 18px;
+  margin: 20px 0px 18px;
   overflow: hidden;
   border-radius: 8px;
   animation: ${(props: { showRevisions: boolean }) =>

+ 2 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx

@@ -100,7 +100,7 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
             value={this.state.values}
             onChange={(e: any) => this.setState({ values: e })}
             readOnly={this.props.disabled}
-            height="calc(100vh - 462px)"
+            height="calc(100vh - 412px)"
           />
         </Wrapper>
         {!this.props.disabled && (
@@ -120,7 +120,6 @@ ValuesYaml.contextType = Context;
 
 const Wrapper = styled.div`
   overflow: auto;
-  height: calc(100% - 60px);
   border-radius: 8px;
   border: 1px solid #ffffff33;
 `;
@@ -129,8 +128,7 @@ const StyledValuesYaml = styled.div`
   display: flex;
   flex-direction: column;
   width: 100%;
-  min-height: 400px;
-  height: calc(100vh - 400px);
+  height: calc(100vh - 350px);
   font-size: 13px;
   overflow: hidden;
   border-radius: 8px;

+ 378 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx

@@ -0,0 +1,378 @@
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import styled from "styled-components";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import ResourceTab from "./ResourceTab";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
+import PodRow from "./PodRow";
+import { timeFormat } from "d3-time-format";
+import { getAvailability, getPodStatus } from "./util";
+import _ from "lodash";
+
+type Props = {
+  controller: any;
+  selectedPod: any;
+  selectPod: (newPod: any) => unknown;
+  selectors: any;
+  isLast?: boolean;
+  isFirst?: boolean;
+  setPodError: (x: string) => void;
+  onUpdate: (update: any) => void;
+};
+
+// Controller tab in log section that displays list of pods on click.
+export type ControllerTabPodType = {
+  namespace: string;
+  name: string;
+  phase: string;
+  status: any;
+  replicaSetName: string;
+  restartCount: number | string;
+  podAge: string;
+  revisionNumber?: number;
+  containerStatus: any;
+};
+
+const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
+
+const ControllerTabFC: React.FunctionComponent<Props> = ({
+  controller,
+  selectPod,
+  isFirst,
+  isLast,
+  selectors,
+  setPodError,
+  selectedPod,
+  onUpdate,
+}) => {
+  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
+  const [rawPodList, setRawPodList] = useState<any[]>([]);
+  const [podPendingDelete, setPodPendingDelete] = useState<any>(null);
+  const [available, setAvailable] = useState<number>(null);
+  const [total, setTotal] = useState<number>(null);
+  const [userSelectedPod, setUserSelectedPod] = useState<boolean>(false);
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    closeWebsocket,
+  } = useWebsockets();
+
+  const currentSelectors = useMemo(() => {
+    if (controller.kind.toLowerCase() == "job" && selectors) {
+      return [...selectors];
+    }
+    let newSelectors = [] as string[];
+    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;
+    }
+    newSelectors.push(selector);
+    return [...newSelectors];
+  }, [controller, selectors]);
+
+  useEffect(() => {
+    updatePods();
+    [controller?.kind, "pod"].forEach((kind) => {
+      setupWebsocket(kind, controller?.metadata?.uid);
+    });
+    () => closeAllWebsockets();
+  }, [currentSelectors, controller, currentCluster, currentProject]);
+
+  const updatePods = async () => {
+    try {
+      const res = await api.getMatchingPods(
+        "<token>",
+        {
+          namespace: controller?.metadata?.namespace,
+          selectors: currentSelectors,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+      const data = res?.data as any[];
+      let newPods = data
+        // Parse only data that we need
+        .map<ControllerTabPodType>((pod: any) => {
+          const replicaSetName =
+            Array.isArray(pod?.metadata?.ownerReferences) &&
+            pod?.metadata?.ownerReferences[0]?.name;
+          const containerStatus =
+            Array.isArray(pod?.status?.containerStatuses) &&
+            pod?.status?.containerStatuses[0];
+
+          const restartCount = containerStatus
+            ? containerStatus.restartCount
+            : "N/A";
+
+          const podAge = formatCreationTimestamp(
+            new Date(pod?.metadata?.creationTimestamp)
+          );
+
+          return {
+            namespace: pod?.metadata?.namespace,
+            name: pod?.metadata?.name,
+            phase: pod?.status?.phase,
+            status: pod?.status,
+            replicaSetName,
+            restartCount,
+            containerStatus,
+            podAge: pod?.metadata?.creationTimestamp ? podAge : "N/A",
+            revisionNumber:
+              (pod?.metadata?.annotations &&
+                pod?.metadata?.annotations["helm.sh/revision"]) ||
+              "N/A",
+          };
+        });
+
+      setPods(newPods);
+      setRawPodList(data);
+      // If the user didn't click a pod, select the first returned from list.
+      if (!userSelectedPod) {
+        let status = getPodStatus(newPods[0].status);
+        status === "failed" &&
+          newPods[0].status?.message &&
+          setPodError(newPods[0].status?.message);
+        handleSelectPod(newPods[0], data);
+      }
+    } catch (error) {}
+  };
+
+  /**
+   * handleSelectPod is a wrapper for the selectPod function received from parent.
+   * Internally we use the ControllerPodType but we want to pass to the parent the
+   * raw pod returned from the API.
+   *
+   * @param pod A ControllerPodType pod that will be used to search the raw pod to pass
+   * @param rawList A rawList of pods in case we don't want to use the state one. Useful to
+   * avoid problems with reactivity
+   */
+  const handleSelectPod = (pod: ControllerTabPodType, rawList?: any[]) => {
+    const rawPod = [...rawPodList, ...(rawList || [])].find(
+      (rawPod) => rawPod?.metadata?.name === pod?.name
+    );
+    selectPod(rawPod);
+  };
+
+  const currentSelectedPod = useMemo(() => {
+    const pod = selectedPod;
+    const replicaSetName =
+      Array.isArray(pod?.metadata?.ownerReferences) &&
+      pod?.metadata?.ownerReferences[0]?.name;
+    return {
+      namespace: pod?.metadata?.namespace,
+      name: pod?.metadata?.name,
+      phase: pod?.status?.phase,
+      status: pod?.status,
+      replicaSetName,
+    } as ControllerTabPodType;
+  }, [selectedPod]);
+
+  const currentControllerStatus = useMemo(() => {
+    let status = available == total ? "running" : "waiting";
+
+    controller?.status?.conditions?.forEach((condition: any) => {
+      if (
+        condition.type == "Progressing" &&
+        condition.status == "False" &&
+        condition.reason == "ProgressDeadlineExceeded"
+      ) {
+        status = "failed";
+      }
+    });
+
+    if (controller.kind.toLowerCase() === "job" && pods.length == 0) {
+      status = "completed";
+    }
+    return status;
+  }, [controller, available, total, pods]);
+
+  const handleDeletePod = (pod: any) => {
+    api
+      .deletePod(
+        "<token>",
+        {},
+        {
+          cluster_id: currentCluster.id,
+          name: pod?.name,
+          namespace: pod?.namespace,
+          id: currentProject.id,
+        }
+      )
+      .then((res) => {
+        updatePods();
+        setPodPendingDelete(null);
+      })
+      .catch((err) => {
+        setCurrentError(JSON.stringify(err));
+        setPodPendingDelete(null);
+      });
+  };
+
+  const replicaSetArray = useMemo(() => {
+    const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"]).reverse().reduce<
+      Array<Array<ControllerTabPodType>>
+    >(function (prev, currentPod, i) {
+      if (
+        !i ||
+        prev[prev.length - 1][0].replicaSetName !== currentPod.replicaSetName
+      ) {
+        return prev.concat([[currentPod]]);
+      }
+      prev[prev.length - 1].push(currentPod);
+      return prev;
+    }, []);
+
+    return podsDividedByReplicaSet.length === 1 ? [] : podsDividedByReplicaSet;
+  }, [pods]);
+
+  const setupWebsocket = (kind: string, controllerUid: string) => {
+    let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
+    if (kind == "pod" && currentSelectors) {
+      apiEndpoint += `selectors=${currentSelectors[0]}`;
+    }
+
+    const options: NewWebsocketOptions = {};
+    options.onopen = () => {
+      console.log("connected to websocket");
+    };
+
+    options.onmessage = (evt: MessageEvent) => {
+      let event = JSON.parse(evt.data);
+      let object = event.Object;
+      object.metadata.kind = event.Kind;
+
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
+      // 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.Kind != "pod") {
+        let [available, total] = getAvailability(object.metadata.kind, object);
+        setAvailable(available);
+        setTotal(total);
+        return;
+      }
+      updatePods();
+    };
+
+    options.onclose = () => {
+      console.log("closing websocket");
+    };
+
+    options.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      closeWebsocket(kind);
+    };
+
+    newWebsocket(kind, apiEndpoint, options);
+    openWebsocket(kind);
+  };
+
+  const mapPods = (podList: ControllerTabPodType[]) => {
+    return podList.map((pod, i, arr) => {
+      let status = getPodStatus(pod.status);
+      return (
+        <PodRow
+          key={i}
+          pod={pod}
+          isSelected={currentSelectedPod?.name === pod?.name}
+          podStatus={status}
+          isLastItem={i === arr.length - 1}
+          onTabClick={() => {
+            setPodError("");
+            status === "failed" &&
+              pod.status?.message &&
+              setPodError(pod.status?.message);
+            handleSelectPod(pod);
+            setUserSelectedPod(true);
+          }}
+          onDeleteClick={() => setPodPendingDelete(pod)}
+        />
+      );
+    });
+  };
+
+  useEffect(() => {
+    onUpdate({ pods, available, total, replicaSetArray });
+  }, [pods, replicaSetArray, available, total]);
+
+  return (
+    <ResourceTab
+      label={controller.kind}
+      // handle CronJob case
+      name={controller.metadata?.name || controller.name}
+      status={{ label: currentControllerStatus, available, total }}
+      isLast={isLast}
+      expanded={isFirst}
+    >
+      {!!replicaSetArray.length &&
+        replicaSetArray.map((subArray, index) => {
+          const firstItem = subArray[0];
+          return (
+            <div key={firstItem.replicaSetName + index}>
+              <ReplicaSetContainer>
+                <ReplicaSetName>
+                  {firstItem?.revisionNumber &&
+                    firstItem?.revisionNumber.toString() != "N/A" && (
+                      <Bold>Revision {firstItem.revisionNumber}:</Bold>
+                    )}{" "}
+                  {firstItem.replicaSetName}
+                </ReplicaSetName>
+              </ReplicaSetContainer>
+              {mapPods(subArray)}
+            </div>
+          );
+        })}
+      {!replicaSetArray.length && mapPods(pods)}
+      <ConfirmOverlay
+        message="Are you sure you want to delete this pod?"
+        show={podPendingDelete}
+        onYes={() => handleDeletePod(podPendingDelete)}
+        onNo={() => setPodPendingDelete(null)}
+      />
+    </ResourceTab>
+  );
+};
+
+export default ControllerTabFC;
+
+const Bold = styled.span`
+  font-weight: 500;
+  display: inline;
+  color: #ffffff;
+`;
+
+const ReplicaSetContainer = styled.div`
+  padding: 10px 5px;
+  display: flex;
+  overflow-wrap: anywhere;
+  justify-content: space-between;
+`;
+
+const ReplicaSetName = styled.span`
+  padding-left: 10px;
+  overflow-wrap: anywhere;
+  max-width: calc(100% - 45px);
+  line-height: 1.5em;
+  color: #ffffff33;
+`;

+ 201 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx

@@ -0,0 +1,201 @@
+import React, { useState, useRef, useEffect } from "react";
+import PodDropdown from "./PodDropdown";
+
+import styled from "styled-components";
+import { getPodStatus } from "./util";
+
+type Props = {
+  chart?: any;
+};
+
+type DeployStatus = "Deploying" | "Deployed" | "Failed";
+
+const DeployStatusSection: React.FC<Props> = (props) => {
+  const [status, setStatus] = useState<DeployStatus>("Deployed");
+  const [percentage, setPercentage] = useState("0%");
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  const wrapperRef = useRef<HTMLInputElement>(null);
+  const parentRef = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    document.addEventListener("mousedown", handleClickOutside.bind(this));
+    return () =>
+      document.removeEventListener("mousedown", handleClickOutside.bind(this));
+  }, []);
+
+  const handleClickOutside = (event: any) => {
+    if (
+      wrapperRef &&
+      wrapperRef.current &&
+      !wrapperRef.current.contains(event.target) &&
+      parentRef &&
+      parentRef.current &&
+      !parentRef.current.contains(event.target)
+    ) {
+      setIsExpanded(false);
+    }
+  };
+
+  const onUpdate = (props: any) => {
+    const { available, total, replicaSetArray } = props;
+    let pods = props.pods;
+
+    if (total) {
+      const remaining = (total - available) / props.total;
+      setPercentage(Math.floor(remaining * 100) + "%");
+    }
+
+    if (replicaSetArray.length) {
+      pods = replicaSetArray[0];
+    }
+
+    const podStatuses = pods.map((pod: any) => getPodStatus(pod.status));
+
+    if (
+      podStatuses.every((status: string) =>
+        ["running", "Ready", "completed", "Completed"].includes(status)
+      )
+    ) {
+      setStatus("Deployed");
+      return;
+    }
+
+    if (
+      podStatuses.some((status: string) =>
+        ["failed", "failedValidation"].includes(status)
+      )
+    ) {
+      setStatus("Failed");
+      return;
+    }
+
+    setStatus("Deploying");
+  };
+
+  return (
+    <>
+      <StyledDeployStatusSection
+        onClick={() => setIsExpanded(!isExpanded)}
+        ref={parentRef}
+        isExpanded={isExpanded}
+      >
+        {status === "Deploying" ? (
+          <>
+            <StatusCircle percentage={percentage} />
+            {status}
+          </>
+        ) : (
+          <StatusWrapper>
+            <StatusColor status={status} />
+            {status}
+          </StatusWrapper>
+        )}
+      </StyledDeployStatusSection>
+      <DropdownWrapper expanded={isExpanded}>
+        <Dropdown ref={wrapperRef}>
+          <PodDropdown currentChart={props.chart} onUpdate={onUpdate} />
+        </Dropdown>
+      </DropdownWrapper>
+    </>
+  );
+};
+
+export default DeployStatusSection;
+
+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 DropdownWrapper = styled.div<{
+  dropdownAlignRight?: boolean;
+  expanded?: boolean;
+}>`
+  display: ${(props) => (props.expanded ? "block" : "none")};
+  position: absolute;
+  left: ${(props) => (props.dropdownAlignRight ? "" : "0")};
+  right: ${(props) => (props.dropdownAlignRight ? "0" : "")};
+  z-index: 1;
+  top: calc(100% + 7px);
+  width: 35%;
+  min-width: 400px;
+  animation: floatIn 0.2s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const Dropdown = styled.div`
+  z-index: 999;
+  overflow-y: auto;
+  background: #2f3135;
+  padding: 0;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>`
+  font-size: 13px;
+  height: 30px;
+  border-radius: 5px;
+  padding: 0 9px;
+  padding-left: 7px;
+  display: flex;
+  margin-left: -1px;
+  align-items: center;
+  ${(props) =>
+    props.isExpanded &&
+    `
+  background: #26292e;
+  border: 1px solid #494b4f;
+  border: 1px solid #7a7b80;
+  margin-left: -2px;
+  margin-right: -1px;
+  `}
+  justify-content: center;
+  cursor: pointer;
+  :hover {
+    background: #26292e;
+    border: 1px solid #494b4f;
+    border: 1px solid #7a7b80;
+    margin-left: -2px;
+    margin-right: -1px;
+  }
+`;
+
+const StatusWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 5px;
+`;
+
+const StatusColor = styled.div`
+  width: 8px;
+  min-width: 8px;
+  height: 8px;
+  background: ${(props: { status: DeployStatus }) =>
+    props.status === "Deployed"
+      ? "#4797ff"
+      : props.status === "Failed"
+      ? "#ed5f85"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;

+ 147 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx

@@ -0,0 +1,147 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ChartType } from "shared/types";
+import Loading from "components/Loading";
+
+import ControllerTab from "./ControllerTab";
+
+type Props = {
+  selectors?: string[];
+  currentChart: ChartType;
+  onUpdate: (props: any) => void;
+};
+
+const PodDropdown: React.FunctionComponent<Props> = ({
+  currentChart,
+  selectors,
+  onUpdate,
+}) => {
+  const [selectedPod, setSelectedPod] = useState<any>({});
+  const [controllers, setControllers] = useState<any[]>([]);
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [podError, setPodError] = useState<string>("");
+
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  useEffect(() => {
+    let isSubscribed = true;
+    api
+      .getChartControllers(
+        "<token>",
+        {},
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          id: currentProject.id,
+          name: currentChart.name,
+          revision: currentChart.version,
+        }
+      )
+      .then((res: any) => {
+        if (!isSubscribed) {
+          return;
+        }
+        let controllers =
+          currentChart.chart.metadata.name == "job"
+            ? res.data[0]?.status.active
+            : res.data;
+        setControllers(controllers);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        if (!isSubscribed) {
+          return;
+        }
+        setCurrentError(JSON.stringify(err));
+        setControllers([]);
+        setIsLoading(false);
+      });
+    return () => {
+      isSubscribed = false;
+    };
+  }, [currentProject, currentCluster, setCurrentError, currentChart]);
+
+  const renderTabs = () => {
+    return controllers.map((c, i) => {
+      return (
+        <ControllerTab
+          // handle CronJob case
+          key={c.metadata?.uid || c.uid}
+          selectedPod={selectedPod}
+          selectPod={setSelectedPod}
+          selectors={selectors ? [selectors[i]] : null}
+          controller={c}
+          isLast={i === controllers?.length - 1}
+          isFirst={i === 0}
+          setPodError={(x: string) => setPodError(x)}
+          onUpdate={onUpdate}
+        />
+      );
+    });
+  };
+
+  const renderStatusSection = () => {
+    if (isLoading) {
+      return (
+        <NoControllers>
+          <Loading />
+        </NoControllers>
+      );
+    }
+    if (controllers?.length > 0) {
+      return (
+        <TabWrapper>{renderTabs()}</TabWrapper>
+      );
+    }
+
+    return (
+      <NoControllers>
+        <i className="material-icons">category</i>
+        No objects to display. This might happen while your app is still
+        deploying.
+      </NoControllers>
+    );
+  };
+
+  return (
+    <StyledStatusSection>
+      {renderStatusSection()}
+    </StyledStatusSection>
+  );
+};
+
+export default PodDropdown;
+
+const TabWrapper = styled.div`
+  width: 100%; 
+  min-height: 50px;
+`;
+
+const StyledStatusSection = styled.div`
+  padding: 0px;
+  user-select: text;
+  overflow: hidden;
+  width: 100%;
+  font-size: 13px;
+`;
+
+const NoControllers = styled.div`
+  position: relative;
+  width: 100%;
+  display: flex;
+  min-height: 50px;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;

+ 222 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx

@@ -0,0 +1,222 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import { ControllerTabPodType } from "./ControllerTab";
+
+type PodRowProps = {
+  pod: ControllerTabPodType;
+  isSelected: boolean;
+  isLastItem: boolean;
+  onTabClick: any;
+  onDeleteClick: any;
+  podStatus: string;
+};
+
+const PodRow: React.FunctionComponent<PodRowProps> = ({
+  pod,
+  isSelected,
+  onTabClick,
+  onDeleteClick,
+  isLastItem,
+  podStatus,
+}) => {
+  const [showTooltip, setShowTooltip] = useState(false);
+
+  return (
+    <Tab key={pod?.name}>
+      <Gutter>
+        <Rail />
+        <Circle />
+        <Rail lastTab={isLastItem} />
+      </Gutter>
+      <Name
+        onMouseOver={() => {
+          // setShowTooltip(true);
+        }}
+        onMouseOut={() => {
+          setShowTooltip(false);
+        }}
+      >
+        {pod?.name}
+      </Name>
+      {showTooltip && (
+        <Tooltip>
+          {pod?.name}
+          <Grey>Restart count: {pod.restartCount}</Grey>
+          <Grey>Created on: {pod.podAge}</Grey>
+          {podStatus === "failed" ? (
+            <FailedStatusContainer>
+              <Grey>
+                Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}
+              </Grey>
+              <Grey>{pod?.containerStatus?.state?.waiting?.message}</Grey>
+            </FailedStatusContainer>
+          ) : null}
+        </Tooltip>
+      )}
+
+      <Status>
+        {podStatus}
+        <StatusColor status={podStatus} />
+        {podStatus === "failed" && (
+          <CloseIcon
+            className="material-icons-outlined"
+            onClick={onDeleteClick}
+          >
+            close
+          </CloseIcon>
+        )}
+      </Status>
+    </Tab>
+  );
+};
+
+export default PodRow;
+
+const Grey = styled.div`
+  margin-top: 5px;
+  color: #aaaabb;
+`;
+
+const FailedStatusContainer = styled.div`
+  width: 100%;
+  border: 1px solid hsl(0deg, 100%, 30%);
+  padding: 5px;
+  margin-block: 5px;
+`;
+
+const Tooltip = styled.div`
+  position: absolute;
+  left: 35px;
+  word-wrap: break-word;
+  top: 38px;
+  min-height: 18px;
+  max-width: calc(100% - 75px);
+  padding: 5px 7px;
+  background: #272731;
+  z-index: 999;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  flex: 1;
+  color: white;
+  text-transform: none;
+  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;
+    }
+  }
+`;
+
+const CloseIcon = styled.i`
+  font-size: 14px;
+  display: flex;
+  font-weight: bold;
+  align-items: center;
+  justify-content: center;
+  border-radius: 5px;
+  background: #ffffff22;
+  width: 18px;
+  height: 18px;
+  margin-right: -6px;
+  margin-left: 10px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff44;
+  }
+`;
+
+const Tab = styled.div`
+  width: 100%;
+  height: 50px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  padding: 20px 18px 20px 42px;
+  text-shadow: 0px 0px 8px none;
+  overflow: visible;
+`;
+
+const Rail = styled.div`
+  width: 2px;
+  background: ${(props: { lastTab?: boolean }) =>
+    props.lastTab ? "" : "#52545D"};
+  height: 50%;
+`;
+
+const Circle = styled.div`
+  min-width: 10px;
+  min-height: 2px;
+  margin-bottom: -2px;
+  margin-left: 8px;
+  background: #52545d;
+`;
+
+const Gutter = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 10px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  overflow: visible;
+`;
+
+const Status = styled.div`
+  display: flex;
+  font-size: 12px;
+  text-transform: capitalize;
+  margin-left: 10px;
+  justify-content: flex-end;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StatusColor = styled.div`
+  margin-left: 12px;
+  mnargin-right: -1px;
+  width: 8px;
+  min-width: 7px;
+  height: 8px;
+  background: ${(props: { status: string }) =>
+    props.status === "running"
+      ? "#4797ff"
+      : props.status === "failed"
+      ? "#ed5f85"
+      : props.status === "completed"
+      ? "#00d12a"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;
+
+const Name = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.5em;
+  display: -webkit-box;
+  overflow-wrap: anywhere;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+`;

+ 316 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx

@@ -0,0 +1,316 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { kindToIcon } from "shared/rosettaStone";
+
+type PropsType = {
+  label: string;
+  name: string;
+  handleClick?: () => void;
+  selected?: boolean;
+  isLast?: boolean;
+  roundAllCorners?: boolean;
+  status?: {
+    label: string;
+    available?: number;
+    total?: number;
+  } | null;
+  expanded?: boolean;
+};
+
+type StateType = {
+  expanded: boolean;
+  showTooltip: boolean;
+};
+
+export default class ResourceTab extends Component<PropsType, StateType> {
+  state = {
+    expanded: this.props.expanded || false,
+    showTooltip: false,
+  };
+
+  renderDropdownIcon = () => {
+    if (this.props.children) {
+      return (
+        <DropdownIcon expanded={this.state.expanded}>
+          <i className="material-icons">arrow_right</i>
+        </DropdownIcon>
+      );
+    }
+  };
+
+  renderIcon = (kind: string) => {
+    let icon = "tonality";
+    if (Object.keys(kindToIcon).includes(kind)) {
+      icon = kindToIcon[kind];
+    }
+
+    return (
+      <IconWrapper>
+        <i className="material-icons">{icon}</i>
+      </IconWrapper>
+    );
+  };
+
+  renderTooltip = (x: string): JSX.Element | undefined => {
+    if (this.state.showTooltip) {
+      return <Tooltip>{x}</Tooltip>;
+    }
+  };
+
+  getStatusText = () => {
+    let { status } = this.props;
+    if (status.available && status.total) {
+      return `${status.available}/${status.total}`;
+    } else if (status.label) {
+      return status.label;
+    }
+  };
+
+  renderStatus = () => {
+    let { status } = this.props;
+    if (status) {
+      return (
+        <Status>
+          {this.getStatusText()}
+
+          <StatusColor status={status.label} />
+        </Status>
+      );
+    }
+  };
+
+  renderExpanded = () => {
+    if (this.props.children && this.state.expanded) {
+      return <ExpandWrapper>{this.props.children}</ExpandWrapper>;
+    }
+  };
+
+  render() {
+    let {
+      label,
+      name,
+      children,
+      isLast,
+      handleClick,
+      selected,
+      status,
+      roundAllCorners,
+    } = this.props;
+    return (
+      <StyledResourceTab
+        isLast={isLast}
+        onClick={() => handleClick && handleClick()}
+        roundAllCorners={roundAllCorners}
+      >
+        <ResourceHeader
+          hasChildren={children && true}
+          expanded={this.state.expanded || selected}
+          onClick={() => {
+            if (children) {
+              this.setState({ expanded: !this.state.expanded });
+            }
+          }}
+        >
+          <Info>
+            {this.renderDropdownIcon()}
+            <Metadata hasStatus={status && true}>
+              {this.renderIcon(label)}
+              {label}
+              <ResourceName
+                showKindLabels={true}
+                onMouseOver={() => {
+                  this.setState({ showTooltip: true });
+                }}
+                onMouseOut={() => {
+                  this.setState({ showTooltip: false });
+                }}
+              >
+                {name}
+              </ResourceName>
+              {this.renderTooltip(name)}
+            </Metadata>
+          </Info>
+          {this.renderStatus()}
+        </ResourceHeader>
+        {this.renderExpanded()}
+      </StyledResourceTab>
+    );
+  }
+}
+
+const StyledResourceTab = styled.div`
+  width: 100%;
+  font-size: 13px;
+  border-bottom-left-radius: ${(props: {
+    isLast: boolean;
+    roundAllCorners: boolean;
+  }) => (props.isLast ? "10px" : "")};
+  animation: fadeIn 0.2s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const Tooltip = styled.div`
+  position: absolute;
+  right: 0px;
+  top: 25px;
+  white-space: nowrap;
+  height: 18px;
+  padding: 2px 5px;
+  background: #383842dd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  color: white;
+  text-transform: none;
+  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;
+    }
+  }
+`;
+
+const ExpandWrapper = styled.div``;
+
+const ResourceHeader = styled.div`
+  width: 100%;
+  height: 50px;
+  display: flex;
+  font-size: 13px;
+  align-items: center;
+  justify-content: space-between;
+  color: #ffffff66;
+  user-select: none;
+  padding: 8px 18px;
+  padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) =>
+    props.hasChildren ? "10px" : "22px"};
+  cursor: pointer;
+  :hover {
+    background: #ffffff18;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+`;
+
+const Info = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  width: 80%;
+  height: 100%;
+`;
+
+const Metadata = styled.div`
+  display: flex;
+  align-items: center;
+  position: relative;
+  max-width: ${(props: { hasStatus: boolean }) =>
+    props.hasStatus ? "calc(100% - 20px)" : "100%"};
+`;
+
+const Status = styled.div`
+  display: flex;
+  width; 20%;
+  font-size: 12px;
+  text-transform: capitalize;
+  justify-content: flex-end;
+  align-items: center;
+  font-family: 'Work Sans', sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
+`;
+
+const StatusColor = styled.div`
+  margin-left: 12px;
+  width: 8px;
+  min-width: 8px;
+  height: 8px;
+  background: ${(props: { status: string }) =>
+    props.status === "running" ||
+    props.status === "Ready" ||
+    props.status === "Completed"
+      ? "#4797ff"
+      : props.status === "failed" || props.status === "FailedValidation"
+      ? "#ed5f85"
+      : props.status === "completed"
+      ? "#00d12a"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;
+
+const ResourceName = styled.div`
+  color: #ffffff;
+  margin-right: 15px;
+  margin-left: ${(props: { showKindLabels: boolean }) =>
+    props.showKindLabels ? "10px" : ""};
+  text-transform: none;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const IconWrapper = styled.div`
+  width: 25px;
+  height: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 15px;
+    color: #ffffff;
+    margin-right: 14px;
+  }
+`;
+
+const DropdownIcon = styled.div`
+  > i {
+    margin-top: 2px;
+    margin-right: 11px;
+    font-size: 20px;
+    color: #ffffff66;
+    cursor: pointer;
+    border-radius: 20px;
+    background: ${(props: { expanded: boolean }) =>
+      props.expanded ? "#ffffff18" : ""};
+    transform: ${(props: { expanded: boolean }) =>
+      props.expanded ? "rotate(180deg)" : ""};
+    animation: ${(props: { expanded: boolean }) =>
+      props.expanded ? "quarterTurn 0.3s" : ""};
+    animation-fill-mode: forwards;
+
+    @keyframes quarterTurn {
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(90deg);
+      }
+    }
+  }
+`;

+ 53 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts

@@ -0,0 +1,53 @@
+export const getPodStatus = (status: any) => {
+  if (status?.phase === "Pending" && status?.containerStatuses !== undefined) {
+    return status.containerStatuses[0].state?.waiting?.reason || "Pending";
+  } else if (status?.phase === "Pending") {
+    return "Pending";
+  }
+
+  if (status?.phase === "Failed") {
+    return "failed";
+  }
+
+  if (status?.phase === "Running") {
+    let collatedStatus = "running";
+
+    status?.containerStatuses?.forEach((s: any) => {
+      if (s.state?.waiting) {
+        collatedStatus =
+          s.state?.waiting?.reason === "CrashLoopBackOff"
+            ? "failed"
+            : "waiting";
+      } else if (
+        s.state?.terminated &&
+        (s.state.terminated?.exitCode !== 0 ||
+          s.state.terminated?.reason !== "Completed")
+      ) {
+        collatedStatus = "failed";
+      }
+    });
+    return collatedStatus;
+  }
+};
+
+export const getAvailability = (kind: string, c: any) => {
+  switch (kind?.toLowerCase()) {
+    case "deployment":
+    case "replicaset":
+      return [
+        c.status?.availableReplicas ||
+          c.status?.replicas - c.status?.unavailableReplicas ||
+          0,
+        c.status?.replicas || 0,
+      ];
+    case "statefulset":
+      return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
+    case "daemonset":
+      return [
+        c.status?.numberAvailable || 0,
+        c.status?.desiredNumberScheduled || 0,
+      ];
+    case "job":
+      return [1, 1];
+  }
+};

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -650,7 +650,7 @@ const DropdownAlt = styled(Dropdown)`
 const RangeWrapper = styled.div`
   float: right;
   font-weight: bold;
-  width: 156px;
+  width: 158px;
   margin-top: -8px;
 `;
 

+ 1 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -195,13 +195,7 @@ const LogsFC: React.FC<{
             Show previous logs
           </Scroll>
         )}
-        <Refresh
-          onClick={() => {
-            // this.refreshLogs();
-            // console.log("Refresh logs");
-            refresh();
-          }}
-        >
+        <Refresh onClick={() => refresh()}>
           <i className="material-icons">autorenew</i>
           Refresh
         </Refresh>

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -32,7 +32,6 @@ const StatusSectionFC: React.FunctionComponent<Props> = ({
   );
 
   useEffect(() => {
-    console.log(currentChart);
     let isSubscribed = true;
     api
       .getChartControllers(

+ 13 - 0
dashboard/src/main/home/launch/Boilerplate.tsx

@@ -0,0 +1,13 @@
+import React, { useState } from "react";
+
+import styled from "styled-components";
+
+type Props = {};
+
+export const Boilerplate: React.FC<Props> = (props) => {
+  const [someState, setSomeState] = useState("");
+
+  return <StyledBoilerplate></StyledBoilerplate>;
+};
+
+const StyledBoilerplate = styled.div``;

+ 6 - 4
dashboard/src/main/home/launch/Launch.tsx

@@ -59,11 +59,15 @@ class Templates extends Component<PropsType, StateType> {
   };
 
   async componentDidMount() {
+    let default_addon_helm_repo_url = this.context?.capabilities
+      ?.default_addon_helm_repo_url;
+    let default_app_helm_repo_url = this.context?.capabilities
+      ?.default_app_helm_repo_url;
     try {
       const res = await api.getTemplates(
         "<token>",
         {
-          repo_url: process.env.ADDON_CHART_REPO_URL,
+          repo_url: default_addon_helm_repo_url,
         },
         {}
       );
@@ -90,7 +94,7 @@ class Templates extends Component<PropsType, StateType> {
       const res = await api.getTemplates(
         "<token>",
         {
-          repo_url: process.env.APPLICATION_CHART_REPO_URL,
+          repo_url: default_app_helm_repo_url,
         },
         {}
       );
@@ -117,8 +121,6 @@ class Templates extends Component<PropsType, StateType> {
         currentTemplate = sortedVersionData.find(
           (v: any) => v.name === template_name
         );
-
-        // console.log(currentTemplate);
         if (currentTemplate.versions.find((v: any) => v === version)) {
           currentTemplate.currentVersion = version;
         }

+ 16 - 0
dashboard/src/main/home/launch/LaunchWrapper.tsx

@@ -0,0 +1,16 @@
+import React, { useState, useContext } from "react";
+import { Context } from "shared/Context";
+
+import styled from "styled-components";
+import Launch from "./Launch";
+
+type Props = {};
+
+const LaunchWrapper: React.FC<Props> = (props) => {
+  const { capabilities } = useContext(Context);
+  return <>{capabilities && <Launch />}</>;
+};
+
+export default LaunchWrapper;
+
+const StyledLaunchWrapper = styled.div``;

+ 2 - 0
dashboard/src/shared/types.tsx

@@ -322,6 +322,8 @@ export interface CapabilityType {
   github: boolean;
   provisioner: boolean;
   version?: string;
+  default_app_helm_repo_url?: string;
+  default_addon_helm_repo_url?: string;
 }
 
 export interface ContextProps {