Explorar el Código

remove files only used in v1 (#4601)

Feroze Mohideen hace 2 años
padre
commit
4dfa55de67
Se han modificado 72 ficheros con 249 adiciones y 17024 borrados
  1. 0 6
      dashboard/src/components/porter/InputSlider.tsx
  2. 29 55
      dashboard/src/main/home/Home.tsx
  3. 0 474
      dashboard/src/main/home/app-dashboard/AppDashboard.tsx
  4. 0 104
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx
  5. 2 2
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx
  6. 0 43
      dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx
  7. 0 195
      dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx
  8. 0 240
      dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx
  9. 0 308
      dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx
  10. 0 9
      dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts
  11. 0 1090
      dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx
  12. 0 84
      dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx
  13. 0 119
      dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx
  14. 0 557
      dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx
  15. 0 548
      dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx
  16. 0 57
      dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx
  17. 0 508
      dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx
  18. 0 301
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx
  19. 0 99
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx
  20. 0 113
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx
  21. 0 229
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx
  22. 0 61
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx
  23. 0 90
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx
  24. 0 126
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx
  25. 0 278
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx
  26. 0 71
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx
  27. 0 129
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx
  28. 0 70
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx
  29. 0 44
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts
  30. 0 91
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts
  31. 0 344
      dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx
  32. 0 332
      dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx
  33. 0 262
      dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx
  34. 0 221
      dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx
  35. 0 559
      dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx
  36. 0 45
      dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx
  37. 0 72
      dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterContainer.tsx
  38. 0 572
      dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx
  39. 0 186
      dashboard/src/main/home/app-dashboard/expanded-app/logs/StyledLogs.tsx
  40. 0 536
      dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts
  41. 0 370
      dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsSection.tsx
  42. 0 81
      dashboard/src/main/home/app-dashboard/expanded-app/status/AppEventModal.tsx
  43. 0 52
      dashboard/src/main/home/app-dashboard/expanded-app/status/ConnectToLogsInstructionModal.tsx
  44. 0 448
      dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx
  45. 0 157
      dashboard/src/main/home/app-dashboard/expanded-app/status/ExpandedIncidentLogs.tsx
  46. 0 231
      dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx
  47. 0 398
      dashboard/src/main/home/app-dashboard/expanded-app/status/Logs.tsx
  48. 0 50
      dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx
  49. 0 234
      dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx
  50. 0 278
      dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx
  51. 0 19
      dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts
  52. 0 218
      dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts
  53. 0 781
      dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx
  54. 0 325
      dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx
  55. 0 248
      dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx
  56. 0 695
      dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts
  57. 0 102
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx
  58. 0 116
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx
  59. 0 325
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx
  60. 0 35
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx
  61. 0 275
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx
  62. 0 50
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx
  63. 0 914
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx
  64. 0 402
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx
  65. 0 88
      dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts
  66. 0 4
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx
  67. 1 6
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
  68. 0 404
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx
  69. 202 460
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  70. 1 4
      dashboard/src/main/home/modals/LoadEnvGroupModal.tsx
  71. 13 21
      dashboard/src/main/home/sidebar/ProjectButton.tsx
  72. 1 3
      dashboard/src/shared/util.ts

+ 0 - 6
dashboard/src/components/porter/InputSlider.tsx

@@ -6,8 +6,6 @@ import styled from 'styled-components';
 import { withStyles } from '@material-ui/core/styles';
 import Text from './Text';
 import Spacer from './Spacer';
-import SmartOptModal from 'main/home/app-dashboard/new-app-flow/tabs/SmartOptModal';
-import NodeInfoModal from 'main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal';
 
 type InputSliderProps = {
   label?: string;
@@ -127,10 +125,6 @@ const InputSlider: React.FC<InputSliderProps> = ({
             >
               help_outline
             </StyledIcon></>}
-          {showNeedHelpModal &&
-            <NodeInfoModal
-              setModalVisible={setShowNeedHelpModal}
-            />}
           {isExceedingLimit &&
             <><Spacer inline x={1} /><Label color="#FFBF00"> Value is not optimal for cost</Label></>}
         </>

+ 29 - 55
dashboard/src/main/home/Home.tsx

@@ -221,7 +221,7 @@ const Home: React.FC<Props> = (props) => {
       } else {
         setHasFinishedOnboarding(true);
       }
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
@@ -460,37 +460,21 @@ const Home: React.FC<Props> = (props) => {
 
             <Switch>
               <Route path="/apps/new/app">
-                {currentProject?.validate_apply_v2 ? (
-                  <ClusterContextProvider
-                    clusterId={currentCluster?.id}
-                    refetchInterval={0}
-                  >
-                    <CreateApp />
-                  </ClusterContextProvider>
-                ) : (
-                  <NewAppFlow />
-                )}
+                <ClusterContextProvider
+                  clusterId={currentCluster?.id}
+                  refetchInterval={0}
+                >
+                  <CreateApp />
+                </ClusterContextProvider>
               </Route>
               <Route path="/apps/:appName/:tab">
-                {currentProject?.validate_apply_v2 ? (
-                  <AppView />
-                ) : (
-                  <ExpandedApp />
-                )}
+                <AppView />
               </Route>
               <Route path="/apps/:appName">
-                {currentProject?.validate_apply_v2 ? (
-                  <AppView />
-                ) : (
-                  <ExpandedApp />
-                )}
+                <AppView />
               </Route>
               <Route path="/apps">
-                {currentProject?.validate_apply_v2 ? (
-                  <Apps />
-                ) : (
-                  <AppDashboard />
-                )}
+                <Apps />
               </Route>
 
               <Route path="/environment-groups/new">
@@ -524,8 +508,8 @@ const Home: React.FC<Props> = (props) => {
 
               <Route path="/addons/new">
                 {currentProject?.capi_provisioner_enabled &&
-                  currentProject?.simplified_view_enabled &&
-                  currentProject?.beta_features_enabled ? (
+                currentProject?.simplified_view_enabled &&
+                currentProject?.beta_features_enabled ? (
                   <AddonTemplates />
                 ) : (
                   <LegacyNewAddOnFlow />
@@ -539,8 +523,8 @@ const Home: React.FC<Props> = (props) => {
               </Route>
               <Route path="/addons">
                 {currentProject?.capi_provisioner_enabled &&
-                  currentProject?.simplified_view_enabled &&
-                  currentProject?.beta_features_enabled ? (
+                currentProject?.simplified_view_enabled &&
+                currentProject?.beta_features_enabled ? (
                   <AddonDashboard />
                 ) : (
                   <LegacyAddOnDashboard />
@@ -590,9 +574,6 @@ const Home: React.FC<Props> = (props) => {
                   "/jobs",
                   "/env-groups",
                   "/datastores",
-                  ...(!currentProject?.validate_apply_v2
-                    ? ["/preview-environments"]
-                    : []),
                   "/stacks",
                 ]}
                 render={() => {
@@ -625,28 +606,21 @@ const Home: React.FC<Props> = (props) => {
                 path={"/project-settings"}
                 render={() => <GuardedProjectSettings />}
               />
-              {currentProject?.validate_apply_v2 && (
-                <>
-                  <Route exact path="/preview-environments/configure">
-                    <SetupApp />
-                  </Route>
-                  <Route
-                    exact
-                    path={`/preview-environments/apps/:appName/:tab`}
-                  >
-                    <AppView preview />
-                  </Route>
-                  <Route exact path="/preview-environments/apps/:appName">
-                    <AppView preview />
-                  </Route>
-                  <Route exact path={`/preview-environments/apps`}>
-                    <Apps />
-                  </Route>
-                  <Route exact path={`/preview-environments`}>
-                    <PreviewEnvs />
-                  </Route>
-                </>
-              )}
+              <Route exact path="/preview-environments/configure">
+                <SetupApp />
+              </Route>
+              <Route exact path={`/preview-environments/apps/:appName/:tab`}>
+                <AppView preview />
+              </Route>
+              <Route exact path="/preview-environments/apps/:appName">
+                <AppView preview />
+              </Route>
+              <Route exact path={`/preview-environments/apps`}>
+                <Apps />
+              </Route>
+              <Route exact path={`/preview-environments`}>
+                <PreviewEnvs />
+              </Route>
               <Route path={"*"} render={() => <LaunchWrapper />} />
             </Switch>
           </ViewWrapper>

+ 0 - 474
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -1,474 +0,0 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import _ from "lodash";
-import { Link, LinkProps } from "react-router-dom";
-import styled from "styled-components";
-
-import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
-import Loading from "components/Loading";
-import Button from "components/porter/Button";
-import Container from "components/porter/Container";
-import DashboardPlaceholder from "components/porter/DashboardPlaceholder";
-import Fieldset from "components/porter/Fieldset";
-import Icon from "components/porter/Icon";
-import PorterLink from "components/porter/Link";
-import SearchBar from "components/porter/SearchBar";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import Toggle from "components/porter/Toggle";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { search } from "shared/search";
-import { readableDate } from "shared/string_utils";
-import applications from "assets/applications.svg";
-import box from "assets/box.png";
-import calendar from "assets/calendar-number.svg";
-import github from "assets/github.png";
-import grid from "assets/grid.png";
-import list from "assets/list.png";
-import notFound from "assets/not-found.png";
-import healthy from "assets/status-healthy.png";
-import time from "assets/time.png";
-import letter from "assets/vector.svg";
-
-import DashboardHeader from "../cluster-dashboard/DashboardHeader";
-
-type Props = {};
-
-const icons = [
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/go/go-original-wordmark.svg",
-  applications,
-];
-
-const namespaceBlacklist = [
-  "cert-manager",
-  "default",
-  "ingress-nginx",
-  "kube-node-lease",
-  "kube-public",
-  "kube-system",
-  "monitoring",
-];
-
-const AppDashboard: React.FC<Props> = ({}) => {
-  const { currentProject, currentCluster, setFeaturePreview } =
-    useContext(Context);
-  const [apps, setApps] = useState([]);
-  const [charts, setCharts] = useState([]);
-  const [error, setError] = useState(null);
-  const [searchValue, setSearchValue] = useState("");
-  const [view, setView] = useState("grid");
-  const [sort, setSort] = useState<"calendar" | "letter">("calendar");
-
-  const [isLoading, setIsLoading] = useState(true);
-  const [shouldLoadTime, setShouldLoadTime] = useState(true);
-
-  const filteredApps = useMemo(() => {
-    const filteredBySearch = search(apps ?? [], searchValue, {
-      keys: ["name"],
-      isCaseSensitive: false,
-    });
-
-    if (sort === "letter") {
-      return _.sortBy(filteredBySearch, ["name"]);
-    } else if (sort === "calendar") {
-      return _.sortBy(filteredBySearch, ["last_deployed"]).reverse(); // Assuming that the latest date should come first.
-    }
-
-    return filteredBySearch; // default
-  }, [apps, searchValue, sort]);
-
-  const getApps = async () => {
-    setIsLoading(true);
-    try {
-      const res = await api.getPorterApps(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-      const apps = res.data;
-      const timeRes = await Promise.all(
-        apps.map(async (app: any) => {
-          return await api.getCharts(
-            "<token>",
-            {
-              limit: 1,
-              skip: 0,
-              byDate: false,
-              statusFilter: [
-                "deployed",
-                "uninstalled",
-                "pending",
-                "pending-install",
-                "pending-upgrade",
-                "pending-rollback",
-                "failed",
-              ],
-            },
-            {
-              id: currentProject.id,
-              cluster_id: currentCluster.id,
-              namespace: `porter-stack-${app.name}`,
-            }
-          );
-        })
-      );
-      apps.forEach((app: any, i: number) => {
-        if (timeRes?.[i]?.data?.[0]?.info?.last_deployed != null) {
-          app.last_deployed = readableDate(
-            timeRes[i].data[0].info.last_deployed
-          );
-        }
-      });
-      setApps(apps.reverse());
-      setIsLoading(false);
-    } catch (err) {
-      setError(err);
-      setIsLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    if (currentProject?.id > 0 && currentCluster?.id > 0) {
-      getApps();
-    }
-  }, [currentCluster, currentProject]);
-
-  const renderSource = (app: any) => {
-    return (
-      <>
-        {app.repo_name ? (
-          <Container row>
-            <SmallIcon opacity="0.6" src={github} />
-            <Text size={13} color="#ffffff44">
-              {app.repo_name}
-            </Text>
-          </Container>
-        ) : (
-          <Container row>
-            <SmallIcon
-              opacity="0.7"
-              height="18px"
-              src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
-            />
-            <Text truncate={true} size={13} color="#ffffff44">
-              {app.image_repo_uri}
-            </Text>
-          </Container>
-        )}
-      </>
-    );
-  };
-
-  const updateStackStartedStep = async () => {
-    try {
-      await api.updateStackStep(
-        "<token>",
-        {
-          step: "stack-launch-start",
-        },
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-        }
-      );
-    } catch (err) {
-      // TODO: handle error
-    }
-  };
-
-  const renderIcon = (b: string, size?: string) => {
-    let src = box;
-    if (b) {
-      const bp = b.split(",")[0]?.split("/")[1];
-      switch (bp) {
-        case "ruby":
-          src = icons[0];
-          break;
-        case "nodejs":
-          src = icons[1];
-          break;
-        case "python":
-          src = icons[2];
-          break;
-        case "go":
-          src = icons[3];
-          break;
-        default:
-          break;
-      }
-    }
-    return (
-      <>
-        {size === "larger" ? (
-          <Icon height="16px" src={src} />
-        ) : (
-          <Icon height="18px" src={src} />
-        )}
-      </>
-    );
-  };
-
-  return (
-    <StyledAppDashboard>
-      <DashboardHeader
-        image={applications}
-        title="Applications"
-        description="Web services, workers, and jobs for this project."
-        disableLineBreak
-      />
-      {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
-        <ClusterProvisioningPlaceholder />
-      ) : apps.length === 0 ? (
-        isLoading ? (
-          <Loading offset="-150px" />
-        ) : (
-          <DashboardPlaceholder>
-            <Text size={16}>No apps have been deployed yet</Text>
-            <Spacer y={0.5} />
-            <Text color={"helper"}>Get started by deploying your app.</Text>
-            <Spacer y={1} />
-            <PorterLink to="/apps/new/app">
-              <Button
-                alt
-                onClick={async () => {
-                  await updateStackStartedStep();
-                }}
-                height="35px"
-              >
-                Deploy app <Spacer inline x={1} />{" "}
-                <i className="material-icons" style={{ fontSize: "18px" }}>
-                  east
-                </i>
-              </Button>
-            </PorterLink>
-          </DashboardPlaceholder>
-        )
-      ) : (
-        <>
-          <Container row spaced>
-            <SearchBar
-              value={searchValue}
-              setValue={(x) => {
-                if (x === "open_sesame") {
-                  setFeaturePreview(true);
-                }
-                setSearchValue(x);
-              }}
-              placeholder="Search applications . . ."
-              width="100%"
-            />
-            <Spacer inline x={2} />
-            <Toggle
-              items={[
-                { label: <ToggleIcon src={calendar} />, value: "calendar" },
-                { label: <ToggleIcon src={letter} />, value: "letter" },
-              ]}
-              active={sort}
-              setActive={setSort}
-            />
-            <Spacer inline x={1} />
-
-            <Toggle
-              items={[
-                { label: <ToggleIcon src={grid} />, value: "grid" },
-                { label: <ToggleIcon src={list} />, value: "list" },
-              ]}
-              active={view}
-              setActive={setView}
-            />
-
-            <Spacer inline x={2} />
-            <PorterLink to="/apps/new/app">
-              <Button
-                onClick={async () => {
-                  await updateStackStartedStep();
-                }}
-                height="30px"
-                width="160px"
-              >
-                <I className="material-icons">add</I> New application
-              </Button>
-            </PorterLink>
-          </Container>
-          <Spacer y={1} />
-
-          {filteredApps.length === 0 ? (
-            <Fieldset>
-              <Container row>
-                <PlaceholderIcon src={notFound} />
-                <Text color="helper">No matching apps were found.</Text>
-              </Container>
-            </Fieldset>
-          ) : isLoading ? (
-            <Loading offset="-150px" />
-          ) : view === "grid" ? (
-            <GridList>
-              {(filteredApps ?? []).map((app: any, i: number) => {
-                if (!namespaceBlacklist.includes(app.name)) {
-                  return (
-                    <Link to={`/apps/${app.name}`} key={i}>
-                      <Block>
-                        <Container row>
-                          {renderIcon(app.buildpacks)}
-                          <Spacer inline width="12px" />
-                          <Text size={14}>{app.name}</Text>
-                          <Spacer inline x={2} />
-                        </Container>
-                        <StatusIcon src={healthy} />
-                        {renderSource(app)}
-                        <Container row>
-                          <SmallIcon opacity="0.4" src={time} />
-                          <Text size={13} color="#ffffff44">
-                            {app.last_deployed}
-                          </Text>
-                        </Container>
-                      </Block>
-                    </Link>
-                  );
-                }
-              })}
-            </GridList>
-          ) : (
-            <List>
-              {(filteredApps ?? []).map((app: any, i: number) => {
-                if (!namespaceBlacklist.includes(app.name)) {
-                  return (
-                    <Link to={`/apps/${app.name}`} key={i}>
-                      <Row>
-                        <Container row>
-                          <Spacer inline width="1px" />
-                          {renderIcon(app.buildpacks, "larger")}
-                          <Spacer inline width="12px" />
-                          <Text size={14}>{app.name}</Text>
-                          <Spacer inline x={1} />
-                          <Icon height="16px" src={healthy} />
-                        </Container>
-                        <Spacer height="15px" />
-                        <Container row>
-                          {renderSource(app)}
-                          <Spacer inline x={1} />
-                          <SmallIcon opacity="0.4" src={time} />
-                          <Text size={13} color="#ffffff44">
-                            {app.last_deployed}
-                          </Text>
-                        </Container>
-                      </Row>
-                    </Link>
-                  );
-                }
-              })}
-            </List>
-          )}
-        </>
-      )}
-      <Spacer y={5} />
-    </StyledAppDashboard>
-  );
-};
-
-export default AppDashboard;
-
-const PlaceholderIcon = styled.img`
-  height: 13px;
-  margin-right: 12px;
-  opacity: 0.65;
-`;
-
-const Row = styled.div<{ isAtBottom?: boolean }>`
-  cursor: pointer;
-  padding: 15px;
-  border-bottom: ${(props) =>
-    props.isAtBottom ? "none" : "1px solid #494b4f"};
-  background: ${(props) => props.theme.clickable.bg};
-  position: relative;
-  border: 1px solid #494b4f;
-  border-radius: 5px;
-  margin-bottom: 15px;
-  animation: fadeIn 0.3s 0s;
-`;
-
-const List = styled.div`
-  overflow: hidden;
-`;
-
-const ToggleIcon = styled.img`
-  height: 12px;
-  margin: 0 5px;
-  min-width: 12px;
-`;
-
-const StatusIcon = styled.img`
-  position: absolute;
-  top: 20px;
-  right: 20px;
-  height: 18px;
-`;
-
-const SmallIcon = styled.img<{ opacity?: string; height?: string }>`
-  margin-left: 2px;
-  height: ${(props) => props.height || "14px"};
-  opacity: ${(props) => props.opacity || 1};
-  filter: grayscale(100%);
-  margin-right: 10px;
-`;
-
-const Block = styled.div`
-  height: 150px;
-  flex-direction: column;
-  display: flex;
-  justify-content: space-between;
-  cursor: pointer;
-  padding: 20px;
-  color: ${(props) => props.theme.text.primary};
-  position: relative;
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const GridList = styled.div`
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
-`;
-
-const I = styled.i`
-  color: white;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-  justify-content: center;
-`;
-
-const StyledAppDashboard = styled.div`
-  width: 100%;
-  height: 100%;
-`;
-
-const CentralContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: left;
-  align-items: left;
-`;

+ 0 - 104
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx

@@ -1,104 +0,0 @@
-import React, { useState } from "react";
-
-import app_event from "assets/app_event.png";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Icon from "components/porter/Icon";
-
-import { StyledEventCard } from "./EventCard";
-import { readableDate } from "shared/string_utils";
-import dayjs from "dayjs";
-import Anser from "anser";
-import api from "shared/api";
-import { PorterAppAppEvent } from "../types";
-import { Direction } from "main/home/app-dashboard/expanded-app/logs/types";
-import AppEventModal from "main/home/app-dashboard/expanded-app/status/AppEventModal";
-
-type Props = {
-  event: PorterAppAppEvent;
-  deploymentTargetId: string;
-  projectId: number;
-  clusterId: number;
-  appName: string;
-};
-
-const AppEventCard: React.FC<Props> = ({ event, deploymentTargetId, projectId, clusterId, appName }) => {
-  const [showModal, setShowModal] = useState<boolean>(false);
-  const [logs, setLogs] = useState([]);
-
-  const getAppLogs = async () => {
-    setShowModal(true);
-    try {
-      const logResp = await api.appLogs(
-        "<token>",
-        {
-          start_range: dayjs(event.created_at).subtract(1, 'minute').toISOString(),
-          end_range: dayjs(event.updated_at).add(1, 'minute').toISOString(),
-          app_id: event.porter_app_id,
-          service_name: event.metadata.service_name,
-          deployment_target_id: deploymentTargetId,
-          limit: 1000,
-          direction: Direction.forward,
-        },
-        {
-          project_id: projectId,
-          cluster_id: clusterId,
-          porter_app_name: appName,
-        }
-      )
-
-      if (logResp.data?.logs != null) {
-        const updatedLogs = logResp.data.logs.map((l: { line: string; timestamp: string; }, index: number) => {
-          try {
-            return {
-              line: JSON.parse(l.line)?.log ?? Anser.ansiToJson(l.line),
-              lineNumber: index + 1,
-              timestamp: l.timestamp,
-            }
-          } catch (err) {
-            return {
-              line: Anser.ansiToJson(l.line),
-              lineNumber: index + 1,
-              timestamp: l.timestamp,
-            }
-          }
-        });
-        setLogs(updatedLogs);
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="16px" src={app_event} />
-          <Spacer inline x={1} />
-          <Text>{event.metadata.summary}</Text>
-        </Container>
-      </Container>
-      <Spacer y={0.5} />
-      <Container row spaced>
-        <Link onClick={getAppLogs} hasunderline>
-          View details
-        </Link>
-      </Container>
-      {showModal && (
-        <AppEventModal
-          setModalVisible={setShowModal}
-          logs={logs}
-          porterAppName={appName}
-          timestamp={readableDate(event.updated_at)}
-          expandedAppEventMessage={event.metadata.detail}
-        />
-      )}
-    </StyledEventCard>
-  );
-};
-
-export default AppEventCard;
-

+ 2 - 2
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx

@@ -8,8 +8,8 @@ import Spacer from "components/porter/Spacer";
 import Tag from "components/porter/Tag";
 import Text from "components/porter/Text";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
-import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes";
 import { isClientServiceNotification } from "lib/porter-apps/notification";
+import { prefixSubdomain } from "lib/porter-apps/services";
 
 import alert from "assets/alert-warning.svg";
 import metrics from "assets/bar-group-03.svg";
@@ -168,7 +168,7 @@ const ServiceStatusDetail: React.FC<Props> = ({
                       <Spacer inline x={0.5} />
                       <Tag>
                         <Link
-                          to={Service.prefixSubdomain(externalUri)}
+                          to={prefixSubdomain(externalUri)}
                           target={"_blank"}
                           showTargetBlankIcon={false}
                         >

+ 0 - 43
dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx

@@ -1,43 +0,0 @@
-import Loading from "components/Loading";
-import Fieldset from "components/porter/Fieldset";
-import Link from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-
-type Props = {
-  repoName: string; 
-  branchName: string;
-};
-
-const AppEvents: React.FC<Props> = ({
-  repoName,
-  branchName,
-}) => {
-  const [isExpanded, setIsExpanded] = useState(false);
-
-  useEffect(() => {
-    // Do something
-  }, []);
-
-  return (
-    <StyledAppEvents>
-      <Fieldset>
-        <Text size={16}>
-          Dream on
-        </Text>
-        <Spacer y={0.5} />
-        <Text color="helper">
-          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-        </Text>
-        <Spacer height="10px" />
-      </Fieldset>
-    </StyledAppEvents>
-  );
-};
-
-export default AppEvents;
-
-const StyledAppEvents = styled.div`
-`;

+ 0 - 195
dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx

@@ -1,195 +0,0 @@
-import React, { FC } from 'react';
-import * as Diff from "deep-diff";
-import styled from 'styled-components';
-import Text from 'components/porter/Text';
-import { flatMapDepth } from 'lodash';
-import Link from 'components/porter/Link';
-
-const createCompareLink = (repoId: string, oldTag: string, newTag: string) => {
-  const baseUrl = 'https://github.com';
-  const link = `${baseUrl}/${repoId}/compare/${oldTag}...${newTag}`;
-  return link;
-}
-
-const getTagsFromChange = (changeString: string) => {
-  const tagPattern = /"global image tag: "([^"]*)" -> "([^"]*)""/;
-  const match = changeString.match(tagPattern);
-  if (match) {
-    return { oldTag: match[1], newTag: match[2] };
-  }
-  return null;
-}
-
-type Props = {
-  oldYaml: any;
-  newYaml: any;
-  appData: any;
-};
-
-const ChangeLogComponent: FC<Props> = ({ oldYaml, newYaml, appData }) => {
-  const diff = Diff.diff(oldYaml, newYaml);
-  const changes: JSX.Element[] = [];
-  // Define the regex pattern to match service creation
-  const servicePattern = /^[a-zA-Z0-9\-]*-[a-zA-Z0-9]*[^\.]$/;
-  diff?.forEach((difference: any) => {
-    let path = difference.path?.join(" ");
-    switch (difference.kind) {
-      case "N":
-        // Check if the added item is a service by testing the path against the regex pattern
-        if (path?.includes('container env normal')) {
-          const appName = path.split(' ')[0];
-          const keyName = path.split(' ')[4];
-          changes.push(
-            <ChangeBox type="N">{`${appName} added env var ${keyName} = ${difference.rhs}`}</ChangeBox>
-          );
-        } else if (servicePattern.test(path)) {
-          changes.push(<ChangeBox type="N">{`${path} created`}</ChangeBox>);
-        } else {
-          // If not, display the full message
-          changes.push(
-            <ChangeBox type="N">{`${path} added: ${JSON.stringify(
-              difference.rhs
-            )}`}</ChangeBox>
-          );
-        }
-        break;
-      case "D":
-        if (servicePattern.test(path)) {
-          // If so, display a simplified message
-          changes.push(<ChangeBox type="D">
-            {`${path} deleted`}
-          </ChangeBox>);
-        } else {
-
-          changes.push(<ChangeBox type="D">
-            {`${path} removed`}
-          </ChangeBox>);
-        }
-        break;
-      case "E":
-        if (path === "global image tag") {
-          const oldCommit = difference.lhs;
-          const newCommit = difference.rhs;
-          if (appData?.app?.repo_name) {
-            const commitDiffLink = `https://github.com/${appData.app.repo_name}/compare/${oldCommit}...${newCommit}`;
-            changes.push(
-              <ChangeBox type="E">
-                {`Tag updated: ${oldCommit} -> ${newCommit}.   `}
-
-                <Link
-                  target="_blank"
-                  hasunderline
-                  to={commitDiffLink}
-                >
-                  View commit diff
-                </Link>
-              </ChangeBox>
-            );
-          } else {
-            <ChangeBox type="E">
-              {`Tag updated: ${oldCommit} -> ${newCommit}.   `}
-            </ChangeBox>
-          }
-        } else {
-          changes.push(
-            <ChangeBox type="E">
-              {`${path}: ${JSON.stringify(difference.lhs)} -> ${JSON.stringify(difference.rhs)}`}
-            </ChangeBox>
-          );
-        }
-        break;
-      case "A":
-        path = path + `[${difference.index}]`;
-        if (difference.item.kind === "N") {
-          if (path.includes('container env synced')) {
-            const appName = path.split(' ')[0];
-            if (path.includes('keys')) {
-              // This is an addition of a key in an existing env group
-              const keyName = difference.item.rhs?.name;
-              changes.push(
-                <ChangeBox type="N">{`${appName} synced env-group key ${keyName} added`}</ChangeBox>
-              );
-            } else {
-              // This is an addition of a whole new env group
-              const groupName = difference.item.rhs?.name;
-              changes.push(
-                <ChangeBox type="N">{`${appName} synced env-group ${groupName} added`}</ChangeBox>
-              );
-            }
-          } else {
-            changes.push(
-              <ChangeBox type="N">{`${path} added: ${JSON.stringify(difference.item.rhs)}`}</ChangeBox>
-            );
-          }
-        }
-        if (difference.item.kind === "D") {
-          if (path.includes('container env synced')) {
-            const appName = path.split(' ')[0];
-            if (path.includes('keys')) {
-              // This is a deletion of a key in an existing env group
-              const keyName = difference.item.lhs?.name;
-              changes.push(
-                <ChangeBox type="D">{`${appName} synced env-group key ${keyName} removed`}</ChangeBox>
-              );
-            } else {
-              // This is a deletion of a whole env group
-              const groupName = difference.item.lhs?.name;
-              changes.push(
-                <ChangeBox type="D">{`${appName} synced env-group ${groupName} removed`}</ChangeBox>
-              );
-            }
-          } else {
-            changes.push(
-              <ChangeBox type="D">{`${path} removed: ${JSON.stringify(difference.item.lhs)}`}</ChangeBox>
-            );
-          }
-        }
-        if (difference.item.kind === "E")
-          changes.push(
-            <ChangeBox type="E">
-              {`${path} updated: ${JSON.stringify(
-                difference.item.lhs
-              )} -> ${JSON.stringify(difference.item.rhs)}`}
-            </ChangeBox>
-          );
-        break;
-    }
-  });
-  if (changes.length === 0) {
-    changes.push(
-      <ChangeBox type="E">
-        {`No changes detected`}
-      </ChangeBox>
-    )
-  }
-
-  return <ChangeLog>{changes}</ChangeLog>
-
-};
-
-export default ChangeLogComponent;
-
-const ChangeLog = styled.div`
-  display: flex;
-  flex-direction: column;
-  border-radius: 8px;
-  overflow: hidden;
-`;
-
-type BoxProps = {
-  type: string,
-  children?: React.ReactNode,
-};
-
-const ChangeBox = styled.div<BoxProps>`
-  padding: 10px;
-  background-color: ${({ type }) =>
-    type === "N"
-      ? "#034a53"
-      : type === "D"
-        ? "#632f34"
-        : type === "E"
-          ? "#272831"
-          : "#fff"};
-  color: "#fff";
-`;

+ 0 - 240
dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx

@@ -1,240 +0,0 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
-import styled from "styled-components";
-import Modal from "components/porter/Modal";
-import Loading from "components/Loading";
-import Text from "components/porter/Text";
-import yaml from "js-yaml";
-import DiffViewer, { DiffMethod } from "react-diff-viewer";
-import Button from "components/porter/Button";
-import ConfirmOverlay from "components/porter/ConfirmOverlay";
-import Spacer from "components/porter/Spacer";
-import Checkbox from "components/porter/Checkbox";
-import { ChartType } from "shared/types";
-import * as Diff from "deep-diff";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import ChangeLogComponent from "./ChangeLogComponent";
-
-type Props = {
-  modalVisible: boolean;
-  setModalVisible: (x: boolean) => void;
-  revision: number;
-  currentChart: ChartType;
-  revertModal?: boolean;
-  appData: any;
-  diffContent: boolean;
-  setDiffContent: (x: boolean) => void;
-};
-
-const ChangeLogModal: React.FC<Props> = ({
-  revision,
-  appData,
-  currentChart,
-  modalVisible,
-  revertModal,
-  setModalVisible,
-}) => {
-  const [values, setValues] = useState("");
-  const [chartEvent, setChartEvent] = useState(null);
-  const [eventValues, setEventValues] = useState("");
-  const [prevChartEvent, setPrevChartEvent] = useState(null);
-  const [prevEventValues, setPrevEventValues] = useState("");
-  const [showRawDiff, setShowRawDiff] = useState(false);
-  const [showOverlay, setShowOverlay] = useState<boolean>(false);
-  const [changesConfig, setChangesConfig] = useState<boolean>(true);
-
-  const [loading, setLoading] = useState(false);
-  const { currentCluster, currentProject, setCurrentError } = useContext(
-    Context
-  );
-  useEffect(() => {
-    let values = "# Nothing here yet";
-    if (currentChart.config) {
-      values = yaml.dump(currentChart.config);
-    }
-    setValues(values);
-  }, [currentChart.config]); // It will run this effect whenever currentChart.config changes
-
-  const getChartData = async (chart: ChartType) => {
-    setLoading(true);
-    const res = await api.getChart(
-      "<token>",
-      {},
-      {
-        name: chart.name,
-        namespace: chart.namespace,
-        cluster_id: currentCluster.id,
-        revision: revision,
-        id: currentProject.id,
-      }
-    );
-    const updatedChart = res.data;
-    setLoading(false);
-    return updatedChart;
-  };
-
-  const revertToRevision = async (revision: number) => {
-    setLoading(true);
-    try {
-      await api
-        .rollbackPorterApp(
-          "<token>",
-          {
-            revision,
-          },
-          {
-            project_id: appData.app.project_id,
-            stack_name: appData.app.name,
-            cluster_id: appData.app.cluster_id,
-          }
-        )
-      window.location.reload();
-    } catch (err) {
-      setLoading(false);
-      console.log(err)
-    }
-  }
-
-  const getPrevChartData = async (chart: ChartType) => {
-    setLoading(true);
-    const prevRevision = revision - 1;
-    const res = await api.getChart(
-      "<token>",
-      {},
-      {
-        name: chart.name,
-        namespace: chart.namespace,
-        cluster_id: currentCluster.id,
-        revision: prevRevision,
-        id: currentProject.id,
-      }
-    );
-    const updatedChart = res.data;
-    setLoading(false);
-    return updatedChart;
-  };
-
-  useEffect(() => {
-    const fetchData = async () => {
-      // Fetch the chart data
-      const updatedChart = await getChartData(currentChart);
-      const prevChart = await getPrevChartData(currentChart);
-
-      // Now that we've waited for getChartData to finish, process the result
-      let eventValues = "# Nothing here yet";
-      if (updatedChart?.config) {
-        eventValues = yaml.dump(updatedChart?.config);
-      }
-      let prevEventValues = "# Nothing here yet";
-      if (prevChart?.config) {
-        prevEventValues = yaml.dump(prevChart?.config);
-      }
-      setEventValues(eventValues);
-      setChartEvent(updatedChart);
-      setPrevEventValues(prevEventValues);
-      setPrevChartEvent(prevChart);
-    };
-
-    fetchData();
-  }, [currentChart.config]);
-
-  return (
-    <>
-      <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-        {revertModal ? <Text size={18}> Revert to version no. {revision} </Text> : <Text size={18}>Changes for version no. {revision}</Text>}
-        <Spacer y={1} />
-        {loading ? (
-          <Loading /> // <-- Render loading state
-        ) : (
-          revertModal ? (<>
-            <div style={{ maxHeight: "400px", overflowY: "auto", borderRadius: "8px" }}>
-              <DiffViewer
-                leftTitle={revertModal ? `Current Revision` : `Revision No. ${revision - 1}`}
-                rightTitle={`Revision No. ${revision}`}
-                oldValue={revertModal ? values : prevEventValues}
-                newValue={eventValues}
-                splitView={true}
-                hideLineNumbers={false}
-                useDarkTheme={true}
-                compareMethod={DiffMethod.TRIMMED_LINES}
-              />
-            </div>
-          </>) :
-            (<>
-              {showRawDiff ? (
-                <>
-                  <div style={{ maxHeight: "400px", overflowY: "auto", borderRadius: "8px" }}>
-                    <DiffViewer
-                      leftTitle={revertModal ? `Current Revision` : `Revision No. ${revision - 1}`}
-                      rightTitle={`Revision No. ${revision}`}
-                      oldValue={revertModal ? values : prevEventValues}
-                      newValue={eventValues}
-                      splitView={true}
-                      hideLineNumbers={false}
-                      useDarkTheme={true}
-                      compareMethod={DiffMethod.TRIMMED_LINES}
-                    />
-                  </div>
-                </>
-              ) : (
-                <div style={{ maxHeight: "400px", overflowY: "auto" }}>
-                  {revertModal ?
-
-                    <ChangeLogComponent
-                      oldYaml={currentChart.config}
-                      newYaml={chartEvent?.config}
-                      appData={appData}
-                    />
-                    : <ChangeLogComponent
-                      oldYaml={prevChartEvent?.config}
-                      newYaml={chartEvent?.config}
-                      appData={appData}
-                    />
-
-                  }
-                </div>
-              )}
-
-              {changesConfig && (<>
-                <Spacer y={1} />
-                <div style={{ display: "flex" }}>
-
-                  <Checkbox
-                    checked={showRawDiff}
-                    toggleChecked={() => setShowRawDiff(!showRawDiff)}
-                  >
-                    <Text>Show raw diff</Text>
-                  </Checkbox>
-                </div></>)}
-            </>))
-        }
-
-        {revertModal && (
-          <>
-            <Spacer y={1} />
-            <Button
-              onClick={() => setShowOverlay(true)}
-              width={"110px"}
-              loadingText={"Submitting..."}
-            >
-              Revert
-            </Button>
-          </>
-        )}
-        {showOverlay && (
-
-          <ConfirmOverlay
-            loading={loading}
-            message={`Are you sure you want to revert to version no. ${revision}?`}
-            onYes={() => revertToRevision(revision)}
-            onNo={() => setShowOverlay(false)}
-          />
-
-        )}
-
-      </Modal>
-    </>
-  );
-};
-
-export default ChangeLogModal;

+ 0 - 308
dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx

@@ -1,308 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import styled from "styled-components";
-import Modal from "components/porter/Modal";
-import TitleSection from "components/TitleSection";
-import Loading from "components/Loading";
-import Text from "components/porter/Text";
-import danger from "assets/danger.svg";
-import Anser, { AnserJsonEntry } from "anser";
-import web from "assets/web-bold.png";
-import settings from "assets/settings-bold.png";
-import sliders from "assets/sliders.svg";
-
-import dayjs from "dayjs";
-import Link from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import Checkbox from "components/porter/Checkbox";
-import { NavLink } from "react-router-dom";
-import SidebarLink from "main/home/sidebar/SidebarLink";
-import { EnvVariablesTab } from "./env-vars/EnvVariablesTab";
-type Props = {
-  modalVisible: boolean;
-  setModalVisible: (x: boolean) => void;
-  serviceChild: any;
-};
-
-const DiffViewModal: React.FC<Props> = ({
-  serviceChild,
-  setModalVisible,
-}) => {
-  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
-  const [currentView, setCurrentView] = useState("overview");
-
-  return (
-    <Modal closeModal={() => setModalVisible(false)} width={"1100px"}>
-      <Text size={18}>Compare Diff</Text>
-
-      <ContentWrapper>
-        <StyledSidebar showSidebar={true}>
-          <SidebarBg />
-          <ScrollWrapper>
-            <NavButton onClick={() => setCurrentView("overview")}>
-              <Img src={web} />
-              Overview
-            </NavButton>
-            <NavButton onClick={() => setCurrentView("environment")}>
-              <Img src={sliders} />
-              Environment
-            </NavButton>
-            <NavButton onClick={() => setCurrentView("buildSettings")}>
-              <Img src={settings} />
-              Build settings
-            </NavButton>
-          </ScrollWrapper>
-        </StyledSidebar>
-
-        <ContentView>
-          {currentView === "overview" && (
-            <ServiceChildContainer>
-              <ServiceChild>
-                <Text> Current </Text>
-                {serviceChild}
-              </ServiceChild>
-              <SidebarBg />
-
-              <ServiceChild>
-                <Text> Revision No.5</Text>
-
-                {serviceChild}
-              </ServiceChild>
-            </ServiceChildContainer>
-          )}
-          {currentView === "environment" && <div></div>}
-          {currentView === "buildSettings" && (
-            <div>
-              <h2>Build Settings</h2>
-              <p>Dummy content for build settings.</p>
-            </div>
-          )}
-        </ContentView>
-      </ContentWrapper>
-    </Modal>
-  );
-};
-
-export default DiffViewModal;
-const ScrollWrapper = styled.div`
-  overflow-y: auto;
-  padding-bottom: 25px;
-  max-height: calc(100vh - 95px);
-`;
-
-const ProjectPlaceholder = styled.div`
-  background: #ffffff11;
-  border-radius: 5px;
-  margin: 0 15px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: calc(100% - 100px);
-  font-size: 13px;
-  color: #aaaabb;
-  padding-bottom: 80px;
-
-  > img {
-    width: 17px;
-    margin-right: 10px;
-  }
-`;
-
-const NavButton = styled(SidebarLink)`
-  display: flex;
-  align-items: center;
-  border-radius: 5px;
-  position: relative;
-  text-decoration: none;
-  height: 34px;
-  margin: 5px 15px;
-  padding: 0 30px 2px 6px;
-  font-size: 13px;
-  color: ${(props) => props.theme.text.primary};
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: any) => (props.active ? "#ffffff11" : "")};
-
-  :hover {
-    background: ${(props: any) => (props.active ? "#ffffff11" : "#ffffff08")};
-  }
-
-  &.active {
-    background: #ffffff11;
-
-    :hover {
-      background: #ffffff11;
-    }
-  }
-
-  :hover {
-    background: #ffffff08;
-  }
-
-  > i {
-    font-size: 18px;
-    border-radius: 3px;
-    margin-left: 2px;
-    margin-right: 10px;
-  }
-`;
-
-const Img = styled.img<{ enlarge?: boolean }>`
-  padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")};
-  height: 22px;
-  padding-top: 4px;
-  border-radius: 3px;
-  margin-right: 8px;
-  opacity: 0.8;
-`;
-
-const SidebarBg = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  background-color: ${(props) => props.theme.bg};
-  height: 100%;
-  z-index: -1;
-  border-right: 1px solid #383a3f;
-`;
-
-const SidebarLabel = styled.div`
-  color: ${(props) => props.theme.text.primary};
-  padding: 5px 23px;
-  margin-bottom: 5px;
-  font-size: 13px;
-  z-index: 1;
-`;
-
-const PullTab = styled.div`
-  position: fixed;
-  width: 30px;
-  height: 50px;
-  background: #7a838f77;
-  top: calc(50vh - 60px);
-  left: 0;
-  z-index: 1;
-  border-top-right-radius: 5px;
-  border-bottom-right-radius: 5px;
-  cursor: pointer;
-
-  :hover {
-    background: #99a5af77;
-  }
-
-  > i {
-    color: #ffffff77;
-    font-size: 18px;
-    position: absolute;
-    top: 15px;
-    left: 4px;
-  }
-`;
-
-const Tooltip = styled.div`
-  position: absolute;
-  right: -60px;
-  top: 34px;
-  min-width: 67px;
-  height: 18px;
-  padding-bottom: 2px;
-  background: #383842dd;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex: 1;
-  color: white;
-  font-size: 12px;
-  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 CollapseButton = styled.div`
-  position: absolute;
-  right: 0;
-  top: 8px;
-  height: 23px;
-  width: 23px;
-  background: #525563aa;
-  border-top-left-radius: 3px;
-  border-bottom-left-radius: 3px;
-  cursor: pointer;
-
-  :hover {
-    background: #636674;
-  }
-
-  > i {
-    color: #ffffff77;
-    font-size: 14px;
-    transform: rotate(180deg);
-    position: absolute;
-    top: 4px;
-    right: 5px;
-  }
-`;
-
-const StyledSidebar = styled.section`
-  width: 240px;
-  position: relative;
-  padding-top: 20px;
-  height: 75vh;
-  z-index: 2;
-  animation: ${(props: { showSidebar: boolean }) =>
-    props.showSidebar ? "showSidebar 0.4s" : "hideSidebar 0.4s"};
-  animation-fill-mode: forwards;
-  @keyframes showSidebar {
-    from {
-      margin-left: -240px;
-    }
-    to {
-      margin-left: 0px;
-    }
-  }
-  @keyframes hideSidebar {
-    from {
-      margin-left: 0px;
-    }
-    to {
-      margin-left: -240px;
-    }
-  }
-`;
-const ContentView = styled.div`
-  flex 1;
-  overflow: auto;
-  padding: 20px;
-`;
-
-const ContentWrapper = styled.div`
-  display: flex;
-  flex-direction: row;
-  height: 75vh;
-`;
-const ServiceChildContainer = styled.div`
-  display: flex;
-  height: 100%;
-  justify-content: space-between;
-  align-items: flex-start; // align top
-`;
-
-const ServiceChild = styled.div`
-  width: calc(50% - 0.5px);
-`;
-
-const Divider = styled.div`
-  width: 8px;
-
-  background-color: white;
-`;

+ 0 - 9
dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts

@@ -1,9 +0,0 @@
-export const DisabledNamespacesForIncidents = [
-  "cert-manager",
-  "ingress-nginx",
-  "kube-node-lease",
-  "kube-public",
-  "kube-system",
-  "monitoring",
-  "porter-agent-system",
-];

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

@@ -1,1090 +0,0 @@
-import React, { useEffect, useState, useContext } from "react";
-import { type RouteComponentProps, useHistory, useLocation, useParams, withRouter } from "react-router";
-import styled from "styled-components";
-import yaml from "js-yaml";
-
-import notFound from "assets/not-found.png";
-import web from "assets/web.png";
-import box from "assets/box.png";
-import github from "assets/github-white.png";
-import pr_icon from "assets/pull_request_icon.svg";
-import loadingImg from "assets/loading.gif";
-import refresh from "assets/refresh.png";
-import save from "assets/save-01.svg";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import Error from "components/porter/Error";
-
-import Banner from "components/porter/Banner";
-import Loading from "components/Loading";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Back from "components/porter/Back";
-import TabSelector from "components/TabSelector";
-import Icon from "components/porter/Icon";
-import { type ChartType, type CreateUpdatePorterAppOptions } from "shared/types";
-import BuildSettingsTab from "../build-settings/BuildSettingsTab";
-import Button from "components/porter/Button";
-import Services from "../new-app-flow/Services";
-import { ImageInfo, Service } from "../new-app-flow/serviceTypes";
-import Fieldset from "components/porter/Fieldset";
-import { type PorterJson, createFinalPorterYaml , PorterYamlSchema } from "../new-app-flow/schema";
-import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import { EnvVariablesTab } from "./env-vars/EnvVariablesTab";
-import GHABanner from "./GHABanner";
-import LogSection from "./logs/LogSection";
-import ActivityFeed from "./activity-feed/ActivityFeed";
-import MetricsSection from "./metrics/MetricsSection";
-import StatusSectionFC from "./status/StatusSection";
-import ExpandedJob from "./expanded-job/ExpandedJob";
-import _ from "lodash";
-import AnimateHeight from "react-animate-height";
-import { type NewPopulatedEnvGroup } from "../../../../components/porter-form/types";
-import { type BuildMethod, PorterApp } from "../types/porterApp";
-import EventFocusView from "./activity-feed/events/focus-views/EventFocusView";
-import HelmValuesTab from "./HelmValuesTab";
-import SettingsTab from "./SettingsTab";
-import PorterAppRevisionSection from "./PorterAppRevisionSection";
-import ImageSettingsTab from "./ImageSettingsTab";
-
-type Props = RouteComponentProps & {};
-
-const icons = [
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-plain.svg",
-  "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/go/go-original-wordmark.svg",
-  web,
-];
-
-const validTabs = [
-  "activity",
-  "events",
-  "overview",
-  "logs",
-  "metrics",
-  "debug",
-  "environment",
-  "build-settings",
-  "image-settings",
-  "settings",
-  "helm-values",
-  "job-history",
-] as const;
-const DEFAULT_TAB = "activity";
-type ValidTab = typeof validTabs[number];
-type Params = {
-  eventId?: string;
-  tab?: ValidTab;
-}
-
-const ExpandedApp: React.FC<Props> = ({ ...props }) => {
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentError,
-    user,
-  } = useContext(Context);
-  const [isLoading, setIsLoading] = useState(true);
-  const [deleting, setDeleting] = useState(false);
-  const [appData, setAppData] = useState(null);
-  const [workflowCheckPassed, setWorkflowCheckPassed] = useState<boolean>(
-    false
-  );
-  const [githubWorkflowFilename, setGithubWorkflowFilename] = useState<string>("");
-  const [hasBuiltImage, setHasBuiltImage] = useState<boolean>(false);
-
-  const [forceRefreshRevisions, setForceRefreshRevisions] = useState<boolean>(
-    false
-  );
-
-  const [showRevisions, setShowRevisions] = useState<boolean>(false);
-
-  // this is what we read from their porter.yaml in github
-  const [porterJson, setPorterJson] = useState<PorterJson | undefined>(undefined);
-  // this is what we use to update the release. the above is a subset of this
-  const [porterYaml, setPorterYaml] = useState<PorterJson>({} as PorterJson);
-  const [showUnsavedChangesBanner, setShowUnsavedChangesBanner] = useState<boolean>(false);
-
-  const [expandedJob, setExpandedJob] = useState(null);
-  const [services, setServices] = useState<Service[]>([]);
-  const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
-  const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
-  const [subdomain, setSubdomain] = useState<string>("");
-  const [syncedEnvGroups, setSyncedEnvGroups] = useState<NewPopulatedEnvGroup[]>([])
-  const [deletedEnvGroups, setDeleteEnvGroups] = useState<NewPopulatedEnvGroup[]>([])
-  const [porterApp, setPorterApp] = useState<PorterApp>();
-
-  // this is the version of the porterApp that is being edited. on save, we set the real porter app to be this version
-  const [tempPorterApp, setTempPorterApp] = useState<PorterApp>(PorterApp.empty());
-  const [buildView, setBuildView] = useState<BuildMethod>("docker");
-
-  const history = useHistory();
-
-  const { tab } = useParams<Params>();
-  const { search } = useLocation();
-  const queryParams = new URLSearchParams(search);
-  const queryParamOpts = {
-    revision: queryParams.get('version'),
-    output_stream: queryParams.get('output_stream'),
-    service: queryParams.get('service'),
-  }
-  const eventId = queryParams.get('event_id');
-  const selectedTab: ValidTab = tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB;
-  useEffect(() => {
-    if (!_.isEqual(_.omitBy(porterApp, _.isEmpty), _.omitBy(tempPorterApp, _.isEmpty))) {
-      setButtonStatus("");
-      setShowUnsavedChangesBanner(true);
-    } else {
-      setShowUnsavedChangesBanner(false);
-    }
-  }, [tempPorterApp, porterApp]);
-
-  useEffect(() => {
-    const { appName } = props.match.params as any;
-    if (currentCluster && appName && currentProject) {
-      getPorterApp({ revision: 0 });
-    }
-  }, [currentCluster]);
-
-  // this method fetches and reconstructs the porter yaml as well as the DB info (stored in PorterApp)
-  const getPorterApp = async ({ revision }: { revision: number }) => {
-    const { appName } = props.match.params as any;
-    try {
-      if (!currentCluster || !currentProject) {
-        return;
-      }
-      const resPorterApp = await api.getPorterApp(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          name: appName,
-        }
-      );
-      const resChartData = await api.getChart(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          namespace: `porter-stack-${appName}`,
-          cluster_id: currentCluster.id,
-          name: appName,
-          revision,
-        }
-      );
-      let preDeployChartData;
-      // get the pre-deploy chart
-      try {
-        preDeployChartData = await api.getChart(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            namespace: `porter-stack-${appName}`,
-            cluster_id: currentCluster.id,
-            name: `${appName}-r`,
-            // this is always latest because we do not tie the pre-deploy chart to the umbrella chart
-            revision: 0,
-          }
-        );
-      } catch (err) {
-        // that's ok if there's an error, just means there is no pre-deploy chart
-      }
-      // update apps and release
-      const newAppData = {
-        app: resPorterApp?.data,
-        chart: resChartData?.data,
-        releaseChart: preDeployChartData?.data,
-      };
-      const porterJson = await fetchPorterYamlContent(
-        resPorterApp?.data?.porter_yaml_path ?? "porter.yaml",
-        newAppData
-      );
-
-      const envGroups: NewPopulatedEnvGroup[] = await api
-        .getAllEnvGroups<any[]>(
-          "<token>",
-          {},
-          {
-            id: currentProject?.id,
-            cluster_id: currentCluster?.id,
-          }
-        )
-        .then((res) => res?.data?.environment_groups)
-        .catch((error) => {
-          console.error("Failed to fetch environment groups:", error);
-          return [];
-        });
-      let filteredEnvGroups: NewPopulatedEnvGroup[] = [];
-
-      if (envGroups) {
-        filteredEnvGroups = envGroups?.filter(envGroup =>
-          envGroup?.linked_applications?.length > 0 && envGroup?.linked_applications?.includes(appName)
-        );
-      }
-
-      setSyncedEnvGroups(filteredEnvGroups || []);
-      setPorterJson(porterJson);
-      setAppData(newAppData);
-      const globalImage = resChartData.data.config?.global?.image
-      const hasBuiltImage = globalImage?.repository != null &&
-        globalImage.tag != null &&
-        !(globalImage.repository === ImageInfo.BASE_IMAGE.repository &&
-          globalImage.tag === ImageInfo.BASE_IMAGE.tag)
-      // annoying that we have to parse buildpacks like this but alas
-      const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] };
-      if (parsedPorterApp.image_repo_uri && hasBuiltImage) {
-        parsedPorterApp.image_info = { repository: globalImage.repository, tag: globalImage.tag };
-      }
-      setPorterApp(parsedPorterApp);
-      setTempPorterApp(parsedPorterApp);
-      setBuildView(!_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks")
-      const [newServices, newEnvVars] = updateServicesAndEnvVariables(
-        resChartData?.data,
-        preDeployChartData?.data,
-        porterJson,
-      );
-      const finalPorterYaml = createFinalPorterYaml(
-        newServices,
-        newEnvVars,
-        porterJson,
-        // if we are using a heroku buildpack, inject a PORT env variable
-        newAppData.app.builder?.includes("heroku")
-      );
-      setPorterYaml(finalPorterYaml);
-      // Only check GHA status if no built image is set
-      if (hasBuiltImage || !resPorterApp.data.repo_name) {
-        setWorkflowCheckPassed(true);
-        setHasBuiltImage(true);
-      } else {
-        try {
-          await api.getBranchContents(
-            "<token>",
-            {
-              dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`,
-            },
-            {
-              project_id: currentProject.id,
-              git_repo_id: resPorterApp.data.git_repo_id,
-              kind: "github",
-              owner: resPorterApp.data.repo_name.split("/")[0],
-              name: resPorterApp.data.repo_name.split("/")[1],
-              branch: resPorterApp.data.git_branch,
-            }
-          );
-          setWorkflowCheckPassed(true);
-          setGithubWorkflowFilename(`porter_stack_${resPorterApp.data.name}.yml`);
-        } catch (err) {
-          // Handle unmerged PR
-          if (err.response?.status === 404) {
-            try {
-              // Check for user-copied porter.yml as fallback
-              await api.getBranchContents(
-                "<token>",
-                { dir: `./.github/workflows/porter.yml` },
-                {
-                  project_id: currentProject.id,
-                  git_repo_id: resPorterApp.data.git_repo_id,
-                  kind: "github",
-                  owner: resPorterApp.data.repo_name.split("/")[0],
-                  name: resPorterApp.data.repo_name.split("/")[1],
-                  branch: resPorterApp.data.git_branch,
-                }
-              );
-              setWorkflowCheckPassed(true);
-              setGithubWorkflowFilename(`porter.yml`);
-            } catch (err) {
-              setWorkflowCheckPassed(false);
-            }
-          }
-        }
-      }
-    } catch (err) {
-      // TODO: handle error
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  const deletePorterApp = async (deleteGHWorkflowFile?: boolean) => {
-    setDeleting(true);
-    const { appName } = props.match.params as any;
-    try {
-      await api.deletePorterApp(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          name: appName,
-        }
-      );
-    } catch (err) {
-      // TODO: handle error
-    }
-    try {
-      await api.deleteNamespace(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          id: currentProject.id,
-          namespace: `porter-stack-${appName}`,
-        }
-      );
-    } catch (err) {
-      // TODO: handle error
-    }
-
-    let deleteWorkflowFile = false;
-
-    if (deleteGHWorkflowFile && githubWorkflowFilename !== "" && appData?.app != null) {
-      try {
-        const res = await api.createSecretAndOpenGitHubPullRequest(
-          "<token>",
-          {
-            github_app_installation_id: appData.app.git_repo_id,
-            github_repo_owner: appData.app.repo_name.split("/")[0],
-            github_repo_name: appData.app.repo_name.split("/")[1],
-            branch: appData.app.git_branch,
-            delete_workflow_filename: githubWorkflowFilename,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            stack_name: appData.app.name,
-          }
-        );
-        if (res.data?.url) {
-          window.open(res.data.url, "_blank", "noreferrer");
-        }
-        deleteWorkflowFile = true;
-      } catch (err) {
-        // TODO: handle error
-      }
-    }
-
-    // intentionally do not await this promise
-    api.updateStackStep(
-      "<token>",
-      {
-        step: "stack-deletion",
-        stack_name: appName,
-        delete_workflow_file: deleteWorkflowFile,
-      },
-      {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-      }
-    );
-
-    props.history.push("/apps");
-  };
-
-  const updatePorterApp = async (options: Partial<CreateUpdatePorterAppOptions>) => {
-    try {
-      setButtonStatus("loading");
-      if (
-        appData != null &&
-        currentCluster != null &&
-        currentProject != null &&
-        appData.app != null &&
-        tempPorterApp != null
-      ) {
-        const finalPorterYaml = createFinalPorterYaml(
-          services,
-          envVars,
-          porterJson,
-          // if we are using a heroku buildpack, inject a PORT env variable
-          appData.app.builder?.includes("heroku")
-        );
-        const yamlString = yaml.dump(finalPorterYaml);
-        const base64Encoded = btoa(yamlString);
-        let updatedPorterApp = {
-          porter_yaml: base64Encoded,
-          override_release: true,
-          ...PorterApp.empty(),
-          build_context: tempPorterApp.build_context,
-          repo_name: tempPorterApp.repo_name,
-          git_branch: tempPorterApp.git_branch,
-          buildpacks: "",
-          environment_groups: syncedEnvGroups?.map((env) => env.name),
-          user_update: true,
-          ...options,
-        }
-        if (buildView === "docker") {
-          updatedPorterApp.dockerfile = tempPorterApp.dockerfile;
-          updatedPorterApp.builder = "null";
-          updatedPorterApp.buildpacks = "null";
-        } else {
-          updatedPorterApp.builder = tempPorterApp.builder;
-          updatedPorterApp.buildpacks = tempPorterApp.buildpacks.join(",");
-          updatedPorterApp.dockerfile = "null";
-        }
-        if (tempPorterApp.image_info?.repository && tempPorterApp.image_info?.tag) {
-          updatedPorterApp = { ...updatedPorterApp, image_info: tempPorterApp.image_info, image_repo_uri: tempPorterApp.image_info.repository }
-        }
-
-        await api.createPorterApp(
-          "<token>",
-          updatedPorterApp,
-          {
-            cluster_id: currentCluster.id,
-            project_id: currentProject.id,
-            stack_name: appData.app.name,
-          }
-        );
-
-
-        setPorterYaml(finalPorterYaml);
-        setPorterApp(tempPorterApp);
-        setButtonStatus("success");
-        setShowUnsavedChangesBanner(false);
-        getPorterApp({ revision: 0 });
-      } else {
-        setButtonStatus(<Error message="Unable to update app" />);
-      }
-      // redirect to the default tab
-      history.push(`/apps/${appData.app.name}/${DEFAULT_TAB}`);
-    } catch (err) {
-      // TODO: better error handling
-      const errMessage =
-        err?.response?.data?.error ??
-        err?.toString() ??
-        "An error occurred while deploying your app. Please try again.";
-      setButtonStatus(<Error message={errMessage} />);
-    }
-  };
-
-  const fetchPorterYamlContent = async (
-    porterYaml: string,
-    appData: any
-  ): Promise<PorterJson | undefined> => {
-    try {
-      const res = await api.getPorterYamlContents(
-        "<token>",
-        {
-          path: porterYaml,
-        },
-        {
-          project_id: appData.app.project_id,
-          git_repo_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          kind: "github",
-          branch: appData.app.git_branch,
-        }
-      );
-      if (res.data == null || res.data == "") {
-        return undefined;
-      }
-      const parsedYaml = yaml.load(atob(res.data));
-      const parsedData = PorterYamlSchema.parse(parsedYaml);
-      const porterYamlToJson = parsedData ;
-      return porterYamlToJson;
-    } catch (err) {
-      // TODO: handle error
-    }
-  };
-
-  const renderIcon = (b: string, size?: string) => {
-    let src = box;
-    if (b) {
-      const bp = b.split(",")[0]?.split("/")[1];
-      switch (bp) {
-        case "ruby":
-          src = icons[0];
-          break;
-        case "nodejs":
-          src = icons[1];
-          break;
-        case "python":
-          src = icons[2];
-          break;
-        case "go":
-          src = icons[3];
-          break;
-        default:
-          break;
-      }
-    }
-    return <Icon src={src} height={"24px"} />;
-  };
-
-  const updateServicesAndEnvVariables = (
-    currentChart?: ChartType,
-    releaseChart?: ChartType,
-    porterJson?: PorterJson,
-  ): [Service[], KeyValueType[]] => {
-    // handle normal chart
-    const helmValues = currentChart?.config;
-    const defaultValues = (currentChart?.chart as any)?.values;
-    let newServices: Service[] = [];
-    let envVars: KeyValueType[] = [];
-
-    if (
-      (defaultValues && Object.keys(defaultValues).length > 0) ||
-      (helmValues && Object.keys(helmValues).length > 0)
-    ) {
-      newServices = Service.deserialize(helmValues, defaultValues, porterJson);
-      const { global, ...helmValuesWithoutGlobal } = helmValues;
-      if (Object.keys(helmValuesWithoutGlobal).length > 0) {
-        envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal);
-        setEnvVars(envVars);
-        const subdomain = Service.retrieveSubdomainFromHelmValues(
-          newServices,
-          helmValuesWithoutGlobal
-        );
-        setSubdomain(subdomain);
-      }
-    }
-
-    // handle release chart
-    if (releaseChart?.config || porterJson?.release) {
-      const release = Service.deserializeRelease(releaseChart?.config, porterJson);
-      newServices.push(release);
-    }
-
-    setServices(newServices);
-
-    return [newServices, envVars];
-  };
-
-  const setRevision = (chart: ChartType, isCurrent?: boolean) => {
-    getPorterApp({ revision: isCurrent ? 0 : chart.version });
-  };
-
-  const getReadableDate = (s: string) => {
-    const ts = new Date(s);
-    const date = ts.toLocaleDateString();
-    const time = ts.toLocaleTimeString([], {
-      hour: "numeric",
-      minute: "2-digit",
-    });
-    return `${time} on ${date}`;
-  };
-
-  const onAppUpdate = (services: Service[], envVars: KeyValueType[]) => {
-    const newPorterYaml = createFinalPorterYaml(
-      services,
-      envVars,
-      porterJson,
-      // if we are using a heroku buildpack, inject a PORT env variable
-      appData.app.builder?.includes("heroku")
-    );
-    if (!_.isEqual(porterYaml, newPorterYaml)) {
-      setButtonStatus("");
-      setShowUnsavedChangesBanner(true);
-    } else {
-      setShowUnsavedChangesBanner(false);
-    }
-  };
-
-  const renderTabContents = () => {
-    switch (selectedTab) {
-      case "activity":
-        return <ActivityFeed
-          chart={appData.chart}
-          stackName={appData?.app?.name}
-          appData={appData}
-        />;
-      case "events":
-        if (eventId != null && eventId !== "") {
-          return <EventFocusView
-            eventId={eventId}
-            appData={appData}
-          />;
-        }
-        return <ActivityFeed
-          chart={appData.chart}
-          stackName={appData?.app?.name}
-          appData={appData}
-        />;
-      case "overview":
-        return (
-          <>
-            {/* pre-deploy stuff - only if this is from github! */}
-            {!isLoading && appData?.app?.git_repo_id != null && (
-              <>
-                <Text size={16}>Pre-deploy job</Text>
-                <Spacer y={0.5} />
-                <Services
-                  setServices={(release: Service[]) => {
-                    if (buttonStatus !== "") {
-                      setButtonStatus("");
-                    }
-                    const nonRelease = services.filter(Service.isNonRelease)
-                    const newServices = [...nonRelease, ...release]
-                    setServices(newServices)
-                    onAppUpdate(newServices, envVars)
-                  }}
-                  chart={appData.releaseChart}
-                  services={services.filter(Service.isRelease)}
-                  limitOne={true}
-                  prePopulateService={Service.default("pre-deploy", "release", porterJson)}
-                  addNewText={"Add a new pre-deploy job"}
-                  defaultExpanded={false}
-                />
-                <Spacer y={0.5} />
-              </>
-            )}
-            <Text size={16}>Application services</Text>
-            <Spacer y={0.5} />
-            {!isLoading && services.length === 0 && (
-              <>
-                <Fieldset>
-                  <Container row>
-                    <PlaceholderIcon src={notFound} />
-                    <Text color="helper">No services were found.</Text>
-                  </Container>
-                </Fieldset>
-                <Spacer y={0.5} />
-              </>
-            )}
-            <Services
-              setServices={(svcs: Service[]) => {
-                if (buttonStatus !== "") {
-                  setButtonStatus("");
-                }
-                const release = services.filter(Service.isRelease)
-                const newServices = [...svcs, ...release]
-                setServices(newServices);
-                onAppUpdate(newServices, envVars);
-              }}
-              services={services.filter(Service.isNonRelease)}
-              chart={appData.chart}
-              addNewText={"Add a new service"}
-              setExpandedJob={(x: string) => { setExpandedJob(x); }}
-              appName={appData.app.name}
-            />
-            <Spacer y={0.75} />
-            <Button
-              onClick={async () => { await updatePorterApp({}); }}
-              status={buttonStatus}
-              loadingText={"Updating..."}
-              disabled={services.length === 0}
-            >
-              Update app
-            </Button>
-          </>
-        );
-      case "build-settings":
-        return (
-          <BuildSettingsTab
-            porterApp={tempPorterApp}
-            setTempPorterApp={(attrs: Partial<PorterApp>) => { setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs)); }}
-            clearStatus={() => { setButtonStatus(""); }}
-            updatePorterApp={updatePorterApp}
-            buildView={buildView}
-            setBuildView={setBuildView}
-          />
-        );
-      case "image-settings":
-        return (
-          <ImageSettingsTab
-            porterApp={tempPorterApp}
-            setTempPorterApp={(attrs: Partial<PorterApp>) => { setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs)); }}
-            updatePorterApp={updatePorterApp}
-          />
-        )
-      case "settings":
-        return <SettingsTab
-          appName={appData.app.name}
-          githubWorkflowFilename={githubWorkflowFilename}
-          deleteApplication={deletePorterApp}
-        />;
-      case "logs":
-        return <LogSection
-          currentChart={appData.chart}
-          services={services.filter(svc => Service.isNonRelease(svc))}
-          appName={appData.app.name}
-          filterOpts={queryParamOpts}
-        />;
-      case "metrics":
-        return <MetricsSection currentChart={appData.chart} appName={appData.app.name} serviceName={queryParamOpts.service} services={services} />;
-      case "debug":
-        return <StatusSectionFC currentChart={appData.chart} />;
-      case "environment":
-        return (
-          <EnvVariablesTab
-            envVars={envVars}
-            setEnvVars={(envVars: KeyValueType[]) => {
-              setEnvVars(envVars);
-              // onAppUpdate(services, envVars.filter((e) => e.key !== "" || e.value !== ""));
-            }}
-            setShowUnsavedChangesBanner={setShowUnsavedChangesBanner}
-            syncedEnvGroups={syncedEnvGroups}
-            status={buttonStatus}
-            updatePorterApp={updatePorterApp}
-            clearStatus={() => { setButtonStatus(""); }}
-            setSyncedEnvGroups={setSyncedEnvGroups}
-            appData={appData}
-            deletedEnvGroups={deletedEnvGroups}
-            setDeletedEnvGroups={setDeleteEnvGroups}
-          />
-        );
-      case "helm-values":
-        return <HelmValuesTab
-          currentChart={appData.chart}
-          updatePorterApp={updatePorterApp}
-          buttonStatus={buttonStatus}
-        />;
-      case "job-history":
-        return <ExpandedJob
-          appName={appData.app.name}
-          jobName={queryParamOpts.service}
-          goBack={() => { setExpandedJob(null); }}
-        />;
-      default:
-        return <ActivityFeed
-          chart={appData.chart}
-          stackName={appData?.app?.name}
-          appData={appData}
-        />;
-    }
-  };
-
-  return (
-    <>
-      {isLoading && <Loading />}
-      {!isLoading && appData == null && (
-        <Placeholder>
-          <Container row>
-            <PlaceholderIcon src={notFound} />
-            <Text color="helper">
-              No application matching "{(props.match.params as any).appName}"
-              was found.
-            </Text>
-          </Container>
-          <Spacer y={1} />
-          <Link to="/apps">Return to dashboard</Link>
-        </Placeholder>
-      )}
-      {!isLoading && appData?.app != null && (
-        <StyledExpandedApp>
-          <Back to="/apps" />
-          <Container row>
-            {renderIcon(appData.app?.buildpacks)}
-            <Spacer inline x={1} />
-            <Text size={21}>{appData.app.name}</Text>
-            {appData.app.repo_name && (
-              <>
-                <Spacer inline x={1} />
-                <Container row>
-                  <A
-                    target="_blank"
-                    href={`https://github.com/${appData.app.repo_name}`}
-                  >
-                    <SmallIcon src={github} />
-                    <Text size={13}>{appData.app.repo_name}</Text>
-                  </A>
-                </Container>
-              </>
-            )}
-            {appData.app.git_branch && (
-              <>
-                <Spacer inline x={1} />
-                <TagWrapper>
-                  Branch
-                  <BranchTag>
-                    <BranchIcon src={pr_icon} />
-                    {appData.app.git_branch}
-                  </BranchTag>
-                </TagWrapper>
-              </>
-            )}
-            {!appData.app.repo_name && appData.app.image_repo_uri && (
-              <>
-                <Spacer inline x={1} />
-                <Container row>
-                  <SmallIcon
-                    height="19px"
-                    src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
-                  />
-                  <Text size={13} color="helper">
-                    {appData.app.image_repo_uri}
-                  </Text>
-                </Container>
-              </>
-            )}
-          </Container>
-          <Spacer y={0.5} />
-          {subdomain && (
-            <>
-              <Container>
-                <Text>
-                  <a href={Service.prefixSubdomain(subdomain)} target="_blank" rel="noreferrer">
-                    {subdomain}
-                  </a>
-                </Text>
-              </Container>
-              <Spacer y={0.5} />
-            </>
-          )}
-          <Text color="#aaaabb66">
-            Last deployed {getReadableDate(appData.chart.info.last_deployed)}
-          </Text>
-          <Spacer y={1} />
-          {deleting ? (
-            <Fieldset>
-              <Text size={16}>
-                <Spinner src={loadingImg} /> Deleting "{appData.app.name}"
-              </Text>
-              <Spacer y={0.5} />
-              <Text color="helper">
-                You will be automatically redirected after deletion is complete.
-              </Text>
-            </Fieldset>
-          ) : (
-            <>
-              {!workflowCheckPassed ? (
-                isLoading ? (
-                  <Banner>
-                    <Loading />
-                  </Banner>
-                ) : (
-                  <GHABanner
-                    repoName={appData.app.repo_name}
-                    branchName={appData.app.git_branch}
-                    pullRequestUrl={appData.app.pull_request_url}
-                    stackName={appData.app.name}
-                    gitRepoId={appData.app.git_repo_id}
-                    porterYamlPath={appData.app.porter_yaml_path}
-                  />
-                )
-              ) : !hasBuiltImage ? (
-                isLoading ? (
-                  <Banner>
-                    <Loading />
-                  </Banner>
-                ) : (
-                  <Banner
-                    suffix={
-                      <>
-                        <RefreshButton
-                          onClick={() => { window.location.reload(); }}
-                        >
-                          <img src={refresh} />
-                          Refresh
-                        </RefreshButton>
-                      </>
-                    }
-                  >
-                    Your GitHub repo has not been built yet.
-                    <Spacer inline width="5px" />
-                    <Link
-                      hasunderline
-                      target="_blank"
-                      to={`https://github.com/${appData.app.repo_name}/actions`}
-                    >
-                      Check status
-                    </Link>
-                  </Banner>
-                )
-              ) : (
-                <>
-                  <DarkMatter />
-                  <PorterAppRevisionSection
-                    showRevisions={showRevisions}
-                    toggleShowRevisions={() => {
-                      setShowRevisions(!showRevisions);
-                    }}
-                    chart={appData.chart}
-                    setRevision={setRevision}
-                    forceRefreshRevisions={forceRefreshRevisions}
-                    refreshRevisionsOff={() => { setForceRefreshRevisions(false); }}
-                    shouldUpdate={
-                      appData.chart.latest_version &&
-                      appData.chart.latest_version !==
-                      appData.chart.chart.metadata.version
-                    }
-                    updatePorterApp={updatePorterApp}
-                    latestVersion={appData.chart.latest_version}
-                    appName={appData.app.name}
-                  />
-                  <DarkMatter antiHeight="-18px" />
-                </>
-              )}
-              <Spacer y={1} />
-              <AnimateHeight height={showUnsavedChangesBanner ? 67 : 0}>
-                <Banner
-                  type="warning"
-                  suffix={
-                    <>
-                      <Button
-                        onClick={async () => { await updatePorterApp({}); }}
-                        status={buttonStatus}
-                        loadingText={"Updating..."}
-                        height={"10px"}
-                      >
-                        <Icon src={save} height={"13px"} />
-                        <Spacer inline x={0.5} />
-                        Save as latest version
-                      </Button>
-                    </>
-                  }
-                >
-                  Changes you are currently previewing have not been saved.
-                  <Spacer inline width="5px" />
-                </Banner>
-              </AnimateHeight>
-              <TabSelector
-                noBuffer
-                options={[
-                  { label: "Activity", value: "activity" },
-                  { label: "Overview", value: "overview" },
-                  hasBuiltImage && { label: "Logs", value: "logs" },
-                  hasBuiltImage && { label: "Metrics", value: "metrics" },
-                  hasBuiltImage && { label: "Debug", value: "debug" },
-                  {
-                    label: "Environment",
-                    value: "environment",
-                  },
-                  appData.app.git_repo_id && {
-                    label: "Build settings",
-                    value: "build-settings",
-                  },
-                  hasBuiltImage && !appData.app.git_repo_id && {
-                    label: "Image settings",
-                    value: "image-settings",
-                  },
-                  { label: "Settings", value: "settings" },
-                  (user.email.endsWith("porter.run") || currentProject.helm_values_enabled) && { label: "Helm values", value: "helm-values" },
-                ].filter((x) => x)}
-                currentTab={selectedTab}
-                setCurrentTab={(tab: string) => {
-                  if (buttonStatus !== "") {
-                    setButtonStatus("");
-                  }
-                  props.history.push(`/apps/${appData.app.name}/${tab}`);
-                }}
-              />
-              <Spacer y={1} />
-              {renderTabContents()}
-              <Spacer y={2} />
-            </>
-          )}
-        </StyledExpandedApp>
-      )}
-    </>
-  );
-};
-
-export default withRouter(ExpandedApp);
-
-const A = styled.a`
-  display: flex;
-  align-items: center;
-`;
-
-const RefreshButton = styled.div`
-  color: #ffffff;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  :hover {
-    color: #ffffff;
-    > img {
-      opacity: 1;
-    }
-  }
-
-  > img {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 11px;
-    margin-right: 10px;
-  }
-`;
-
-const Spinner = styled.img`
-  width: 15px;
-  height: 15px;
-  margin-right: 12px;
-  margin-bottom: -2px;
-`;
-
-const DarkMatter = styled.div<{ antiHeight?: string }>`
-  width: 100%;
-  margin-top: ${(props) => props.antiHeight || "-20px"};
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 6px;
-`;
-
-const BranchTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #ffffff22;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const SmallIcon = styled.img<{ opacity?: string; height?: string }>`
-  height: ${(props) => props.height || "15px"};
-  opacity: ${(props) => props.opacity || 1};
-  margin-right: 10px;
-`;
-
-const BranchIcon = styled.img`
-  height: 14px;
-  opacity: 0.65;
-  margin-right: 5px;
-`;
-
-const PlaceholderIcon = styled.img`
-  height: 13px;
-  margin-right: 12px;
-  opacity: 0.65;
-`;
-
-const Placeholder = styled.div`
-  width: 100%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  font-size: 13px;
-`;
-
-const StyledExpandedApp = styled.div`
-  width: 100%;
-  height: 100%;
-
-  animation: fadeIn 0.5s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;

+ 0 - 84
dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx

@@ -1,84 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-import yaml from "js-yaml";
-import _ from "lodash";
-
-import { ChartType, CreateUpdatePorterAppOptions } from "shared/types";
-
-import YamlEditor from "components/YamlEditor";
-import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-
-type Props = {
-    currentChart: ChartType;
-    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
-    buttonStatus: any;
-};
-
-const HelmValuesTab: React.FC<Props> = ({
-    currentChart,
-    updatePorterApp,
-    buttonStatus,
-}) => {
-    const [values, setValues] = React.useState<string>(yaml.dump(currentChart.config));
-
-    const handleSaveValues = async () => {
-        await updatePorterApp({ full_helm_values: values })
-    };
-
-
-    return (
-        <StyledValuesYaml>
-            <Wrapper>
-                <YamlEditor
-                    value={values}
-                    onChange={setValues}
-                    height="calc(100vh - 412px)"
-                />
-            </Wrapper>
-            <Spacer y={0.5} />
-            <Text color="helper">Note: any unsaved service changes from the Overview tab will be lost.</Text>
-            <Spacer y={0.5} />
-            <Button
-                onClick={handleSaveValues}
-                status={buttonStatus}
-                loadingText={"Updating..."}
-            >
-                Update values
-            </Button>
-        </StyledValuesYaml>
-    );
-
-}
-
-export default HelmValuesTab;
-
-const Wrapper = styled.div`
-  overflow: auto;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-`;
-
-const StyledValuesYaml = styled.div`
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-  height: calc(100vh - 350px);
-  font-size: 13px;
-  overflow: hidden;
-  border-radius: 8px;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;

+ 0 - 119
dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx

@@ -1,119 +0,0 @@
-import React, { useContext, useState } from "react";
-import Spacer from "components/porter/Spacer";
-import Button from "components/porter/Button";
-import Error from "components/porter/Error";
-import styled from "styled-components";
-import copy from "assets/copy-left.svg"
-import CopyToClipboard from "components/CopyToClipboard";
-import Link from "components/porter/Link";
-import Text from "components/porter/Text";
-import ImageSettings from "../image-settings/ImageSettings";
-import { Context } from "shared/Context";
-import { CreateUpdatePorterAppOptions } from "shared/types";
-import { PorterApp } from "../types/porterApp";
-
-type Props = {
-    porterApp: PorterApp;
-    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
-    setTempPorterApp: (app: PorterApp) => void;
-}
-const ImageSettingsTab: React.FC<Props> = ({
-    porterApp,
-    updatePorterApp,
-    setTempPorterApp,
-}) => {
-    const { currentProject } = useContext(Context);
-
-    const [buttonStatus, setButtonStatus] = useState<
-        "loading" | "success" | string
-    >("");
-
-    const saveConfig = async () => {
-        try {
-            await updatePorterApp({});
-        } catch (err) {
-            console.log(err);
-        }
-    };
-
-    const handleSave = async () => {
-        setButtonStatus("loading");
-
-        try {
-            await saveConfig();
-            setButtonStatus("success");
-        } catch (error) {
-            setButtonStatus("Something went wrong");
-            console.log(error);
-        }
-    };
-
-    return (
-        <>
-            <ImageSettings
-                projectId={currentProject?.id ?? 0}
-                imageUri={porterApp.image_info?.repository ?? ""}
-                setImageUri={(uri: string) => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: uri } })}
-                imageTag={porterApp.image_info?.tag ?? ""}
-                setImageTag={(tag: string) => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, tag: tag } })}
-                resetImageInfo={() => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: "", tag: "" } })}
-            />
-            <Spacer y={1} />
-            <Button
-                type="button"
-                status={buttonStatus}
-                disabled={!porterApp.image_info?.repository || !porterApp.image_info?.tag}
-                onClick={handleSave}
-            >
-                Save image settings
-            </Button>
-            <Spacer y={1} />
-            <Text size={16}>Update command</Text>
-            <Spacer y={0.5} />
-            <Text color="helper">If you have the <Link to="https://docs.porter.run/standard/cli/command-reference/porter-update" target="_blank"><Text>Porter CLI</Text></Link> installed, you can update your application image tag by running the following command: </Text>
-            <Spacer y={0.5} />
-            <IdContainer>
-                <Code>{`$ porter app update-tag ${porterApp.name} --tag latest`}</Code>
-                <CopyContainer>
-                    <CopyToClipboard text={`porter app update-tag ${porterApp.name} --tag latest`}>
-                        <CopyIcon src={copy} alt="copy" />
-                    </CopyToClipboard>
-                </CopyContainer>
-            </IdContainer>
-        </>
-    );
-};
-
-export default ImageSettingsTab;
-
-const Code = styled.span`
-  font-family: monospace;
-`;
-
-const IdContainer = styled.div`
-    background: #000000;  
-    border-radius: 5px;
-    padding: 10px;
-    display: flex;
-    width: 100%;
-    border-radius: 5px;
-    border: 1px solid ${({ theme }) => theme.border};
-    align-items: center;
-`;
-
-const CopyContainer = styled.div`
-  display: flex;
-  align-items: center;
-  margin-left: auto;
-`;
-
-const CopyIcon = styled.img`
-  cursor: pointer;
-  margin-left: 5px;
-  margin-right: 5px;
-  width: 15px;
-  height: 15px;
-  :hover {
-    opacity: 0.8;
-  }
-`;

+ 0 - 557
dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx

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

+ 0 - 548
dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx

@@ -1,548 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-import loading from "assets/loading.gif";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { ChartType, CreateUpdatePorterAppOptions, StorageType } from "shared/types";
-
-import ConfirmOverlay from "components/ConfirmOverlay";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-
-import Modal from "main/home/modals/Modal";
-import UpgradeChartModal from "main/home/modals/UpgradeChartModal";
-import { readableDate } from "shared/string_utils";
-import { createPortal } from "react-dom";
-import yaml from "js-yaml";
-
-type PropsType = WithAuthProps & {
-    chart: ChartType;
-    refreshChart: () => void;
-    setRevision: (x: ChartType, isCurrent?: boolean) => void;
-    forceRefreshRevisions: boolean;
-    refreshRevisionsOff: () => void;
-    shouldUpdate: boolean;
-    upgradeVersion: (version: string, cb: () => void) => void;
-    latestVersion: string;
-    showRevisions?: boolean;
-    toggleShowRevisions?: () => void;
-    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
-    appName: string;
-};
-
-type StateType = {
-    revisions: ChartType[];
-    rollbackRevision: number | null;
-    upgradeVersion: string;
-    loading: boolean;
-    maxVersion: number;
-    expandRevisions: boolean;
-};
-
-// TODO: refactor this component it's so gross
-class PorterAppRevisionSection extends Component<PropsType, StateType> {
-    state = {
-        revisions: [] as ChartType[],
-        rollbackRevision: null as number | null,
-        upgradeVersion: "",
-        loading: false,
-        maxVersion: 0, // Track most recent version even when previewing old revisions
-        expandRevisions: false,
-    };
-
-    ws: WebSocket | null = null;
-
-    refreshHistory = () => {
-        let { chart } = this.props;
-        let { currentCluster, currentProject } = this.context;
-
-        return api
-            .getRevisions(
-                "<token>",
-                {},
-                {
-                    id: currentProject.id,
-                    namespace: chart.namespace,
-                    cluster_id: currentCluster.id,
-                    name: chart.name,
-                }
-            )
-            .then((res) => {
-                res.data.sort((a: ChartType, b: ChartType) => {
-                    return -(a.version - b.version);
-                });
-                this.setState({
-                    revisions: res.data,
-                    maxVersion: res.data[0].version,
-                });
-            })
-            .catch(console.log);
-    };
-
-    componentDidMount() {
-        this.refreshHistory();
-        this.connectToLiveUpdates();
-    }
-
-    componentWillUnmount() {
-        if (this.ws) {
-            this.ws.close(); // Close the WebSocket connection
-        }
-    }
-
-    connectToLiveUpdates() {
-        let { chart } = this.props;
-        let { currentCluster, currentProject } = this.context;
-
-        const apiPath = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/helm_release?charts=${chart.name}`;
-        const protocol = window.location.protocol == "https:" ? "wss" : "ws";
-        const url = `${protocol}://${window.location.host}`;
-
-        this.ws = new WebSocket(`${url}${apiPath}`);
-
-        this.ws.onopen = () => {
-            console.log("connected to chart live updates websocket");
-        };
-
-        this.ws.onmessage = (evt: MessageEvent) => {
-            let event = JSON.parse(evt.data);
-
-            if (event.event_type == "UPDATE") {
-                let object = event.Object;
-
-                this.setState(
-                    (prevState) => {
-                        const { revisions: oldRevisions } = prevState;
-                        // Copy old array to clean up references
-                        const prevRevisions = [...oldRevisions];
-
-                        // Check if it's an update of a revision or if it's a new one
-                        const revisionIndex = prevRevisions.findIndex((rev) => {
-                            if (rev.version === object.version) {
-                                return true;
-                            }
-                        });
-
-                        // Place new one at top of the array or update the old one
-                        if (revisionIndex > -1) {
-                            prevRevisions.splice(revisionIndex, 1, object);
-                        } else {
-                            return { ...prevState, revisions: [object, ...prevRevisions] };
-                        }
-
-                        return { ...prevState, revisions: prevRevisions, maxVersion: Math.max(...prevRevisions.map(rev => rev.version)) };
-                    },
-                    () => {
-                        this.props.setRevision(this.state.revisions[0], true);
-                    }
-                );
-            }
-        };
-
-        this.ws.onclose = () => {
-            console.log("closing chart live updates websocket");
-        };
-
-        this.ws.onerror = (err: ErrorEvent) => {
-            console.log(err);
-            this.ws.close();
-        };
-    }
-
-    // Handle update of values.yaml
-    componentDidUpdate(prevProps: PropsType) {
-        if (this.props.forceRefreshRevisions) {
-            this.props.refreshRevisionsOff();
-
-            // Force refresh occurs on submit -> set current to newest
-            this.refreshHistory().then(() => {
-                this.props.setRevision(this.state.revisions[0], true);
-            });
-        } else if (this.props.chart !== prevProps.chart) {
-            this.refreshHistory();
-        }
-    }
-
-    handleRollback = async () => {
-        let { setCurrentError, currentCluster, currentProject } = this.context;
-
-        let revisionNumber = this.state.rollbackRevision;
-        if (revisionNumber == null) {
-            return;
-        }
-        this.setState({ loading: true, rollbackRevision: null });
-
-        try {
-            await api.rollbackPorterApp(
-                "<token>",
-                {
-                    revision: revisionNumber,
-                },
-                {
-                    project_id: currentProject.id,
-                    cluster_id: currentCluster.id,
-                    stack_name: this.props.appName,
-                }
-            );
-        } catch {
-            // TODO: handle error better
-            setCurrentError(err.response.data);
-        } finally {
-            this.setState({ loading: false });
-        }
-    };
-
-    handleClickRevision = (revision: ChartType) => {
-        this.props.setRevision(
-            revision,
-            revision.version === this.state.maxVersion
-        );
-    };
-
-    renderRevisionList = () => {
-        return this.state.revisions.map((revision: ChartType, i: number) => {
-            let isCurrent = revision.version === this.state.maxVersion;
-            const isGithubApp = !!this.props.chart.git_action_config;
-            const imageTag = revision.config?.image?.tag || revision.config?.global?.image?.tag;
-
-            const parsedImageTag = isGithubApp
-                ? String(imageTag).slice(0, 7)
-                : imageTag;
-
-            const isStack = !!this.props.chart.stack_id;
-
-            return (
-                <Tr
-                    key={i}
-                    onClick={() => this.handleClickRevision(revision)}
-                    selected={this.props.chart.version === revision.version}
-                >
-                    <Td>{revision.version}</Td>
-                    <Td>{readableDate(revision.info.last_deployed)}</Td>
-                    <Td>
-                        {!imageTag ? (
-                            "N/A"
-                        ) : isGithubApp && /^[0-9A-Fa-f]{7}$/g.test(imageTag) ? (
-                            <A
-                                href={`https://github.com/${this.props.chart.git_action_config?.git_repo}/commit/${imageTag}`}
-                                target="_blank"
-                                onClick={(e) => {
-                                    e.stopPropagation();
-                                }}
-                            >
-                                {parsedImageTag}
-                            </A>
-                        ) : (
-                            parsedImageTag
-                        )}
-                    </Td>
-                    <Td>v{revision.chart.metadata.version}</Td>
-                    <Td>
-                        <RollbackButton
-                            disabled={
-                                isCurrent ||
-                                !this.props.isAuthorized("application", "", [
-                                    "get",
-                                    "update",
-                                ]) ||
-                                isStack
-                            }
-                            onClick={(e) => {
-                                e.stopPropagation();
-                                this.setState({ rollbackRevision: revision.version })
-                            }
-                            }
-                        >
-                            {isCurrent ? "Current" : "Revert"}
-                        </RollbackButton>
-                    </Td>
-                </Tr >
-            );
-        });
-    };
-
-    renderExpanded = () => {
-        if (this.state.expandRevisions) {
-            return (
-                <TableWrapper>
-                    <RevisionsTable>
-                        <tbody>
-                            <Tr disableHover={true}>
-                                <Th>Revision no.</Th>
-                                <Th>Timestamp</Th>
-                                <Th>
-                                    {this.props.chart.git_action_config ? "Commit" : "Image Tag"}
-                                </Th>
-                                <Th>Template version</Th>
-                                <Th>Rollback</Th>
-                            </Tr>
-                            {this.renderRevisionList()}
-                        </tbody>
-                    </RevisionsTable>
-                </TableWrapper>
-            );
-        }
-    };
-
-    renderContents = () => {
-        if (this.state.loading) {
-            return (
-                <LoadingPlaceholder>
-                    <StatusWrapper>
-                        <LoadingGif src={loading} revision={false} /> Updating . . .
-                    </StatusWrapper>
-                </LoadingPlaceholder>
-            );
-        }
-
-        let isCurrent =
-            this.props.chart.version === this.state.maxVersion ||
-            this.state.maxVersion === 0;
-        return (
-            <div>
-                {this.state.upgradeVersion && (
-                    <Modal
-                        onRequestClose={() => this.setState({ upgradeVersion: "" })}
-                        width="500px"
-                        height="450px"
-                    >
-                        <UpgradeChartModal
-                            currentChart={this.props.chart}
-                            closeModal={() => {
-                                this.setState({ upgradeVersion: "" });
-                            }}
-                            onSubmit={() => {
-                                this.props.upgradeVersion(this.state.upgradeVersion, () => {
-                                    this.setState({ loading: false });
-                                });
-                                this.setState({ upgradeVersion: "", loading: true });
-                            }}
-                        />
-                    </Modal>
-                )}
-                <RevisionHeader
-                    showRevisions={this.props.showRevisions}
-                    isCurrent={isCurrent}
-                    onClick={() => {
-                        if (typeof this.props.toggleShowRevisions === "function") {
-                            this.props.toggleShowRevisions();
-                        }
-                        this.setState((prev) => ({
-                            ...prev,
-                            expandRevisions: !prev.expandRevisions,
-                        }));
-                    }}
-                >
-                    <RevisionPreview>
-                        <i className="material-icons">arrow_drop_down</i>
-                        {isCurrent
-                            ? `Current version`
-                            : `Previewing revision (not deployed)`}{" "}
-                        - <Revision>No. {this.props.chart.version}</Revision>
-                    </RevisionPreview>
-                </RevisionHeader>
-                <RevisionList>{this.renderExpanded()}</RevisionList>
-            </div>
-        );
-    };
-
-    render() {
-        return (
-            <StyledRevisionSection showRevisions={this.state.expandRevisions}>
-                {this.renderContents()}
-                {createPortal(
-                    <ConfirmOverlay
-                        show={this.state.rollbackRevision != null}
-                        message={`Are you sure you want to revert to version ${this.state.rollbackRevision}?`}
-                        onYes={this.handleRollback}
-                        onNo={() => this.setState({ rollbackRevision: null })}
-                    />,
-                    document.body
-                )}
-            </StyledRevisionSection>
-        );
-    }
-}
-
-PorterAppRevisionSection.contextType = Context;
-
-export default withAuth(PorterAppRevisionSection);
-
-const TableWrapper = styled.div`
-  padding-bottom: 20px;
-`;
-
-const LoadingPlaceholder = styled.div`
-  height: 40px;
-  display: flex;
-  align-items: center;
-  padding-left: 20px;
-`;
-
-const LoadingGif = styled.img`
-  width: 15px;
-  height: 15px;
-  margin-right: ${(props: { revision: boolean }) =>
-        props.revision ? "0px" : "9px"};
-  margin-left: ${(props: { revision: boolean }) =>
-        props.revision ? "10px" : "0px"};
-  margin-bottom: ${(props: { revision: boolean }) =>
-        props.revision ? "-2px" : "0px"};
-`;
-
-const StatusWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #ffffff55;
-  margin-right: 25px;
-`;
-
-const RevisionList = styled.div`
-  overflow-y: auto;
-  max-height: 215px;
-`;
-
-const RollbackButton = styled.div`
-  cursor: ${(props: { disabled: boolean }) =>
-        props.disabled ? "not-allowed" : "pointer"};
-  display: flex;
-  border-radius: 3px;
-  align-items: center;
-  justify-content: center;
-  font-weight: 500;
-  height: 21px;
-  font-size: 13px;
-  width: 70px;
-  background: ${(props: { disabled: boolean }) =>
-        props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled: boolean }) =>
-        props.disabled ? "" : "#405eddbb"};
-  }
-`;
-
-const Tr = styled.tr`
-  line-height: 2.2em;
-  cursor: ${(props: { disableHover?: boolean; selected?: boolean }) =>
-        props.disableHover ? "" : "pointer"};
-  background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
-        props.selected ? "#ffffff11" : ""};
-  :hover {
-    background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
-        props.disableHover ? "" : "#ffffff22"};
-  }
-`;
-
-const Td = styled.td`
-  font-size: 13px;
-  color: #ffffff;
-  padding-left: 32px;
-`;
-
-const Th = styled.td`
-  font-size: 13px;
-  font-weight: 500;
-  color: #aaaabb;
-  padding-left: 32px;
-`;
-
-const RevisionsTable = styled.table`
-  width: 100%;
-  margin-top: 5px;
-  padding-left: 32px;
-  padding-bottom: 20px;
-  min-width: 500px;
-  border-collapse: collapse;
-`;
-
-const Revision = styled.div`
-  color: #ffffff;
-  margin-left: 5px;
-`;
-
-const RevisionHeader = styled.div`
-  color: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
-        props.isCurrent ? "#ffffff66" : "#f5cb42"};
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  background: ${({ theme }) => theme.fg};
-  :hover {
-    background: ${(props) => props.showRevisions && props.theme.fg2};
-  }
-
-  > div > i {
-    margin-right: 8px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
-        props.showRevisions ? "" : "rotate(-90deg)"};
-    transition: transform 0.1s ease;
-  }
-`;
-
-const StyledRevisionSection = styled.div`
-  width: 100%;
-  max-height: ${(props: { showRevisions: boolean }) =>
-        props.showRevisions ? "255px" : "40px"};
-  margin: 20px 0px 18px;
-  overflow: hidden;
-  border-radius: 5px;
-  background: ${props => props.theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-  animation: ${(props: { showRevisions: boolean }) =>
-        props.showRevisions ? "expandRevisions 0.3s" : ""};
-  animation-timing-function: ease-out;
-  @keyframes expandRevisions {
-    from {
-      max-height: 40px;
-    }
-    to {
-      max-height: 250px;
-    }
-  }
-`;
-
-const RevisionPreview = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const RevisionUpdateMessage = styled.div`
-  color: white;
-  display: flex;
-  align-items: center;
-  padding: 4px 10px;
-  border-radius: 5px;
-  margin-right: 10px;
-
-  :hover {
-    border: 1px solid white;
-    padding: 3px 9px;
-  }
-
-  > i {
-    margin-right: 6px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: none;
-  }
-`;
-
-const A = styled.a`
-  color: #8590ff;
-  text-decoration: underline;
-  cursor: pointer;
-`;

+ 0 - 57
dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx

@@ -1,57 +0,0 @@
-import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-import DeleteApplicationModal from "./DeleteApplicationModal";
-
-type Props = {
-    appName: string;
-    githubWorkflowFilename: string;
-    deleteApplication: (deleteWorkflowFile?: boolean) => void;
-};
-
-const SettingsTab: React.FC<Props> = ({
-    appName,
-    githubWorkflowFilename,
-    deleteApplication
-}) => {
-    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
-
-    useEffect(() => {
-        // Do something
-    }, []);
-
-    return (
-        <StyledSettingsTab>
-            <Text size={16}>Delete "{appName}"</Text>
-            <Spacer y={1} />
-            <Text color="helper">
-                Delete this application and all of its resources.
-            </Text>
-            <Spacer y={1} />
-            <Button
-                onClick={() => {
-                    setIsDeleteModalOpen(true);
-                }}
-                color="#b91133"
-            >
-                Delete
-            </Button>
-            {isDeleteModalOpen &&
-                <DeleteApplicationModal
-                    closeModal={() => setIsDeleteModalOpen(false)}
-                    githubWorkflowFilename={githubWorkflowFilename}
-                    deleteApplication={deleteApplication}
-                />
-            }
-        </StyledSettingsTab>
-    );
-};
-
-export default SettingsTab;
-
-const StyledSettingsTab = styled.div`
-width: 100%;
-`;

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

@@ -1,508 +0,0 @@
-import React, { useEffect, useState, useContext, useMemo } 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";
-import { pushFiltered } from "shared/routing";
-import { RouteComponentProps, useLocation, withRouter } from "react-router";
-import { timeFormat } from "d3-time-format";
-import AnimateHeight, { Height } from "react-animate-height";
-import { ControllerTabPodType } from "./status/ControllerTab";
-import _ from "lodash";
-import Link from "components/porter/Link";
-
-type Props = RouteComponentProps & {
-  chart: any;
-  service: any;
-  setExpandedJob: any;
-};
-
-interface ErrorMessage {
-  revision: string;
-  message: string;
-}
-
-const StatusFooter: React.FC<Props> = ({
-  chart,
-  service,
-  setExpandedJob,
-  ...props
-}) => {
-  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);
-  const location = useLocation();
-  const [unavailable, setUnavailable] = React.useState<number>(0);
-  const [height, setHeight] = useState<Height>(0);
-  const [expanded, setExpanded] = useState<boolean>(false);
-  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
-
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
-
-  const selectors = useMemo(() => {
-    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;
-  }, [controller]);
-
-  useEffect(() => {
-    updatePods();
-    if (selectors.length > 0) {
-      // updatePods();
-      [controller?.kind, "pod"].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 = async (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 === "deployment") {
-        let [available, total, stale, unavailable] = getAvailabilityStacks(
-          object
-        );
-
-        setAvailable(available);
-        setTotal(total);
-        setStale(stale);
-        setUnavailable(unavailable);
-        return;
-      }
-      await updatePods();
-    };
-
-    options.onclose = () => { };
-
-    options.onerror = (err: ErrorEvent) => {
-      console.log(err);
-      closeWebsocket(kind);
-    };
-
-    newWebsocket(kind, apiEndpoint, options);
-    openWebsocket(kind);
-  };
-
-  const replicaSetArray = useMemo(() => {
-    setExpanded(false);
-    setHeight(0);
-    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;
-  }, [pods]);
-
-  const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
-
-  const updatePods = async () => {
-    try {
-      const res = await api.getMatchingPods(
-        "<token>",
-        {
-          namespace: controller?.metadata?.namespace,
-          selectors: [selectors],
-        },
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-      const data = res?.data as any[];
-      let newPods = data
-        // Parse only data that we need
-        .map((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)
-          );
-
-          const failing = containerStatus?.state?.waiting?.reason === "CrashLoopBackOff" ?? false;
-          const crashLoopReason = containerStatus?.lastState?.terminated?.message ?? "";
-
-          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?.["helm.sh/revision"] || "N/A",
-            crashLoopReason,
-            failing
-          };
-        });
-
-      setPods(newPods);
-    } catch (error) {
-      // TODO: handle error
-    }
-  };
-
-  if (service.type === "job") {
-    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>
-            */}
-            <Link to={`/apps/${chart.name}/job-history?service=${service.name}`}>
-              <Button
-                onClick={() => { }}
-                height="30px"
-                width="87px"
-                color="#ffffff11"
-                withBorder
-              >
-                <I className="material-icons">open_in_new</I>
-                History
-              </Button>
-            </Link>
-          </Container>
-        )}
-      </StyledStatusFooter>
-    );
-  }
-
-  return (
-    <>
-      {replicaSetArray != null &&
-        replicaSetArray.length > 0 &&
-        replicaSetArray.map((replicaSet, i) => {
-          return (
-            <>
-              <StyledStatusFooterTop key={i} expanded={expanded}>
-                <StyledContainer row spaced>
-                  {replicaSet.some((r) => r.crashLoopReason != "") || replicaSet.some((r) => r.failing) ? (
-                    <>
-                      <Running>
-                        <StatusDot color="#ff0000" />
-                        <Text color="helper">
-                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
-                            } ${replicaSet.length === 1 ? "is" : "are"
-                            } failing to run Version ${replicaSet[0].revisionNumber
-                            }`}
-                        </Text>
-                      </Running>
-                      {replicaSet.some((r) => r.crashLoopReason != "") &&
-                        <Button
-                          onClick={() => {
-                            expanded ? setHeight(0) : setHeight(122);
-                            setExpanded(!expanded);
-                          }}
-                          height="20px"
-                          color="#ffffff11"
-                          withBorder
-                        >
-                          {expanded ? (
-                            <I className="material-icons">arrow_drop_up</I>
-                          ) : (
-                            <I className="material-icons">arrow_drop_down</I>
-                          )}
-                          <Text color="helper">See failure reason</Text>
-                        </Button>
-                      }
-                    </>
-                  ) : // check if there are more recent replicasets and if the previous replicaset has a crashloop reason
-                    i > 0 &&
-                      !replicaSetArray[i - 1].some(
-                        (p) => p.crashLoopReason != ""
-                      ) ? (
-                      <Running>
-                        <StatusDot color="#FFA500" />
-                        <Text color="helper">
-                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
-                            } ${replicaSet.length === 1 ? "is" : "are"
-                            } still running at Version ${replicaSet[0].revisionNumber
-                            }. Spinning down...`}
-                        </Text>
-                      </Running>
-                    ) : (
-                      <Running>
-                        {replicaSet.length ? (
-                          <StatusDot />
-                        ) : (
-                          <StatusDot color="#ffffff33" />
-                        )}
-                        <Text color="helper">
-                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
-                            } ${replicaSet.length === 1 ? "is" : "are"
-                            } running at Version ${replicaSet[0].revisionNumber}`}
-                        </Text>
-                      </Running>
-                    )}
-                </StyledContainer>
-              </StyledStatusFooterTop>
-              {replicaSet.some((r) => r.crashLoopReason != "") && (
-                <AnimateHeight height={height}>
-                  <StyledStatusFooter>
-                    <Message>
-                      {
-                        replicaSet.find((r) => r.crashLoopReason != "")
-                          ?.crashLoopReason
-                      }
-                    </Message>
-                  </StyledStatusFooter>
-                </AnimateHeight>
-              )}
-            </>
-          );
-        })}
-    </>
-  );
-};
-
-export default withRouter(StatusFooter);
-
-const StatusDot = styled.div<{ color?: string }>`
-  min-width: 7px;
-  max-width: 7px;
-  height: 7px;
-  border-radius: 50%;
-  margin-right: 10px;
-  background: ${(props) => props.color || "#38a88a"};
-
-  box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
-  transform: scale(1);
-  animation: pulse 2s infinite;
-  @keyframes pulse {
-    0% {
-      transform: scale(0.95);
-      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
-    }
-
-    70% {
-      transform: scale(1);
-      box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
-    }
-
-    100% {
-      transform: scale(0.95);
-      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
-    }
-  }
-`;
-
-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;
-  dashed?: boolean;
-}>`
-  width: 16px;
-  height: 16px;
-  border-radius: 50%;
-  margin-right: 10px;
-  background: conic-gradient(
-    from 0deg,
-    #ffffff33 ${(props) => props.percentage},
-    #ffffffaa 0% ${(props) => props.percentage}
-  );
-  border: ${(props) => (props.dashed ? "1px dashed #ffffff55" : "none")};
-`;
-
-const Running = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-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;
-  overflow: hidden;
-  display: flex;
-  align-items: stretch;
-  flex-direction: row;
-  animation: fadeIn 0.5s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const StyledStatusFooterTop = styled(StyledStatusFooter) <{
-  expanded: boolean;
-}>`
-  height: 40px;
-  border-bottom: ${({ expanded }) => expanded && "0px"};
-  border-bottom-left-radius: ${({ expanded }) => expanded && "0px"};
-  border-bottom-right-radius: ${({ expanded }) => expanded && "0px"};
-`;
-
-const Message = styled.div`
-  padding: 20px;
-  background: #000000;
-  border-radius: 5px;
-  line-height: 1.5em;
-  border: 1px solid #aaaabb33;
-  font-family: monospace;
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  > img {
-    width: 13px;
-    margin-right: 20px;
-  }
-  width: 100%;
-  height: 101px;
-  overflow: hidden;
-`;
-
-const StyledContainer = styled.div<{
-  row: boolean;
-  spaced: boolean;
-}>`
-  display: ${(props) => (props.row ? "flex" : "block")};
-  align-items: center;
-  justify-content: ${(props) =>
-    props.spaced ? "space-between" : "flex-start"};
-  width: 100%;
-`;

+ 0 - 301
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx

@@ -1,301 +0,0 @@
-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 EventCard from "./events/cards/EventCard";
-import Loading from "components/Loading";
-import Spacer from "components/porter/Spacer";
-import Fieldset from "components/porter/Fieldset";
-
-import { feedDate } from "shared/string_utils";
-import Pagination from "components/porter/Pagination";
-import _ from "lodash";
-import Button from "components/porter/Button";
-import { PorterAppEvent, PorterAppEventType } from "./events/types";
-
-type Props = {
-  chart: any;
-  stackName: string;
-  appData: any;
-};
-
-const EVENTS_POLL_INTERVAL = 5000; // poll every 5 seconds
-
-const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-
-  const [events, setEvents] = useState<PorterAppEvent[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-  const [error, setError] = useState<any>(null);
-  const [page, setPage] = useState<number>(1);
-  const [numPages, setNumPages] = useState<number>(0);
-  const [hasPorterAgent, setHasPorterAgent] = useState(false);
-  const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
-  const [shouldAnimate, setShouldAnimate] = useState(true);
-
-  // remove this filter when https://linear.app/porter/issue/POR-1676/disable-porter-agent-code-for-cpu-alerts is resolved
-  const isNotFilteredAppEvent = (event: PorterAppEvent) => {
-    return !(event.type === PorterAppEventType.APP_EVENT &&
-      (
-        event.metadata?.short_summary?.includes("requesting more memory than is available")
-        || event.metadata?.short_summary?.includes("requesting more CPU than is available")
-        || event.metadata?.short_summary?.includes("non-zero exit code")
-      )
-    );
-  }
-
-  const getEvents = async () => {
-    setLoading(true)
-    if (!currentProject || !currentCluster) {
-      setError(true);
-      setLoading(false);
-      return;
-    }
-    try {
-      const res = await api.getFeedEvents(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          stack_name: stackName,
-          page,
-        }
-      );
-
-      setNumPages(res.data.num_pages);
-      setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []);
-    } catch (err) {
-      setError(err);
-    } finally {
-      setLoading(false);
-      setShouldAnimate(false);
-    }
-  };
-
-  const getLatestDeployEventIndex = () => {
-    const deployEvents = events.filter((event) => event.type === PorterAppEventType.DEPLOY);
-    if (deployEvents.length === 0) {
-      return -1;
-    }
-    return events.indexOf(deployEvents[0]);
-  };
-
-  const updateEvents = async () => {
-    if (!currentProject || !currentCluster) {
-      return;
-    }
-    try {
-      const res = await api.getFeedEvents(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          stack_name: stackName,
-          page,
-        }
-      );
-      setError(undefined)
-      setNumPages(res.data.num_pages);
-      setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []);
-    } catch (err) {
-      setError(err);
-    }
-  }
-
-  useEffect(() => {
-    const checkForAgent = async () => {
-      const project_id = currentProject?.id;
-      const cluster_id = currentCluster?.id;
-      if (project_id == null || cluster_id == null) {
-        setError(true);
-        return;
-      }
-      try {
-        const res = await api.detectPorterAgent("<token>", {}, { project_id, cluster_id });
-        const hasAgent = res.data?.version === "v3";
-        setHasPorterAgent(hasAgent);
-      } catch (err) {
-        if (err.response?.status === 404) {
-          setHasPorterAgent(false);
-        }
-      } finally {
-        setLoading(false);
-      }
-    };
-
-    if (!hasPorterAgent) {
-      checkForAgent();
-    } else {
-      const intervalId = setInterval(updateEvents, EVENTS_POLL_INTERVAL);
-      getEvents();
-      return () => clearInterval(intervalId);
-    }
-
-  }, [currentProject, currentCluster, hasPorterAgent, page]);
-
-  const installAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    setIsPorterAgentInstalling(true);
-    try {
-      await api.installPorterAgent("<token>", {}, { project_id, cluster_id });
-      window.location.reload();
-    } catch (err) {
-      setIsPorterAgentInstalling(false);
-      console.log(err);
-    }
-  };
-
-  if (isPorterAgentInstalling) {
-    return (
-      <Fieldset>
-        <Text size={16}>Installing agent...</Text>
-        <Spacer y={0.5} />
-        <Text color="helper">If you are not redirected automatically after a minute, you may need to refresh this page.</Text>
-      </Fieldset>
-    );
-  }
-
-  if (error) {
-    return (
-      <Fieldset>
-        <Text size={16}>Error retrieving events</Text>
-        <Spacer height="15px" />
-        <Text color="helper">An unexpected error occurred.</Text>
-      </Fieldset>
-    );
-  }
-
-  if (loading) {
-    return (
-      <div>
-        <Spacer y={2} />
-        <Loading />
-      </div>
-    );
-  }
-
-  if (!loading && !hasPorterAgent) {
-    return (
-      <Fieldset>
-        <Text size={16}>
-          We couldn't detect the Porter agent on your cluster
-        </Text>
-        <Spacer y={0.5} />
-        <Text color="helper">
-          In order to use the Activity tab, you need to install the Porter agent.
-        </Text>
-        <Spacer y={1} />
-        <Button onClick={() => installAgent()}>
-          <I className="material-icons">add</I> Install Porter agent
-        </Button>
-      </Fieldset>
-    );
-  }
-
-  if (!loading && events?.length === 0) {
-    return (
-      <Fieldset>
-        <Text size={16}>No events found for "{stackName}"</Text>
-        <Spacer height="15px" />
-        <Text color="helper">
-          This application currently has no associated events.
-        </Text>
-      </Fieldset>
-    );
-  }
-
-  return (
-    <StyledActivityFeed shouldAnimate={shouldAnimate}>
-      {events.map((event, i) => {
-        return (
-          <EventWrapper isLast={i === events.length - 1} key={i}>
-            {i !== events.length - 1 && events.length > 1 && <Line shouldAnimate={shouldAnimate} />}
-            <Dot shouldAnimate={shouldAnimate} />
-            <Time shouldAnimate={shouldAnimate}>
-              <Text>{feedDate(event.created_at).split(", ")[0]}</Text>
-              <Spacer x={0.5} />
-              <Text>{feedDate(event.created_at).split(", ")[1]}</Text>
-            </Time>
-            <EventCard appData={appData} event={event} key={i} isLatestDeployEvent={i === getLatestDeployEventIndex()} />
-          </EventWrapper>
-        );
-      })}
-      {numPages > 1 && (
-        <>
-          <Spacer y={1} />
-          <Pagination page={page} setPage={setPage} totalPages={numPages} />
-        </>
-      )}
-    </StyledActivityFeed>
-  );
-};
-
-export default ActivityFeed;
-
-const I = styled.i`
-  font-size: 14px;
-  margin-right: 5px;
-`;
-
-const Time = styled.div<{ shouldAnimate: boolean }>`
-  opacity: ${(props) => props.shouldAnimate ? "0" : "1"};
-  ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"}
-  ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"}
-  width: 90px;
-`;
-
-const Line = styled.div<{ shouldAnimate: boolean }>`
-  width: 1px;
-  height: calc(100% + 30px);
-  background: #414141;
-  position: absolute;
-  left: 3px;
-  top: 36px;
-  opacity: ${(props) => props.shouldAnimate ? "0" : "1"};
-  ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"}
-  ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"}
-`;
-
-const Dot = styled.div<{ shouldAnimate: boolean }>`
-  width: 7px;
-  height: 7px;
-  background: #fff;
-  border-radius: 50%;
-  margin-left: -29px;
-  margin-right: 20px;
-  z-index: 1;
-  opacity: ${(props) => props.shouldAnimate ? "0" : "1"};
-  ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"}
-  ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"}
-`;
-
-const EventWrapper = styled.div<{
-  isLast: boolean;
-}>`
-  padding-left: 30px;
-  display: flex;
-  align-items: center;
-  position: relative;
-  margin-bottom: ${(props) => (props.isLast ? "" : "25px")};
-`;
-
-const StyledActivityFeed = styled.div<{ shouldAnimate: boolean }>`
-  width: 100%;
-  ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0s;"}
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;

+ 0 - 99
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx

@@ -1,99 +0,0 @@
-import React, { useState } from "react";
-
-import app_event from "assets/app_event.png";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Icon from "components/porter/Icon";
-
-import { StyledEventCard } from "./EventCard";
-import AppEventModal from "../../../status/AppEventModal";
-import { readableDate } from "shared/string_utils";
-import dayjs from "dayjs";
-import Anser from "anser";
-import api from "shared/api";
-import { Direction } from "../../../logs/types";
-import { PorterAppEvent } from "../types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const AppEventCard: React.FC<Props> = ({ event, appData }) => {
-  const [showModal, setShowModal] = useState<boolean>(false);
-  const [logs, setLogs] = useState([]);
-
-  const getAppLogs = async () => {
-    setShowModal(true);
-    try {
-      const logResp = await api.getLogsWithinTimeRange(
-        "<token>",
-        {
-          namespace: appData.chart.namespace,
-          start_range: dayjs(event.created_at).subtract(1, 'minute').toISOString(),
-          end_range: dayjs(event.updated_at).add(1, 'minute').toISOString(),
-          pod_selector: event.metadata.pod_name.endsWith(".*") ? event.metadata.pod_name : event.metadata.pod_name + ".*",
-          limit: 1000,
-          direction: Direction.forward,
-        },
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-        }
-      )
-
-      if (logResp.data?.logs != null) {
-        const updatedLogs = logResp.data.logs.map((l: { line: string; timestamp: string; }, index: number) => {
-          try {
-            return {
-              line: JSON.parse(l.line)?.log ?? Anser.ansiToJson(l.line),
-              lineNumber: index + 1,
-              timestamp: l.timestamp,
-            }
-          } catch (err) {
-            return {
-              line: Anser.ansiToJson(l.line),
-              lineNumber: index + 1,
-              timestamp: l.timestamp,
-            }
-          }
-        });
-        setLogs(updatedLogs);
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="16px" src={app_event} />
-          <Spacer inline x={1} />
-          <Text>{event.metadata.summary}</Text>
-        </Container>
-      </Container>
-      <Spacer y={0.5} />
-      <Container row spaced>
-        <Link onClick={getAppLogs} hasunderline>
-          View details
-        </Link>
-      </Container>
-      {showModal && (
-        <AppEventModal
-          setModalVisible={setShowModal}
-          logs={logs}
-          porterAppName={appData.app.name}
-          timestamp={readableDate(event.updated_at)}
-          expandedAppEventMessage={event.metadata.detail}
-        />
-      )}
-    </StyledEventCard>
-  );
-};
-
-export default AppEventCard;
-

+ 0 - 113
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx

@@ -1,113 +0,0 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-
-import build from "assets/build.png";
-
-import run_for from "assets/run_for.png";
-import refresh from "assets/refresh.png";
-
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Icon from "components/porter/Icon";
-import api from "shared/api";
-import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
-import JSZip from "jszip";
-import Anser, { AnserJsonEntry } from "anser";
-import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
-import { StyledEventCard } from "./EventCard";
-import document from "assets/document.svg";
-import { PorterAppEvent } from "../types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
-  const renderStatusText = (event: PorterAppEvent) => {
-    switch (event.status) {
-      case "SUCCESS":
-        return <Text color={getStatusColor(event.status)}>Build succeeded</Text>;
-      case "FAILED":
-        return <Text color={getStatusColor(event.status)}>Build failed</Text>;
-      default:
-        return <Text color={getStatusColor(event.status)}>Build in progress...</Text>;
-    }
-  };
-
-  const renderInfoCta = (event: PorterAppEvent) => {
-    switch (event.status) {
-      case "SUCCESS":
-        return null;
-      case "FAILED":
-        return (
-          <Wrapper>
-            <Link to={`/apps/${appData.app.name}/events?event_id=${event.id}`} hasunderline>
-              <Container row>
-                <Icon src={document} height="10px" />
-                <Spacer inline width="5px" />
-                View details
-              </Container>
-            </Link>
-            <Spacer inline x={1} />
-            <Link hasunderline onClick={() => triggerWorkflow(appData)}>
-              <Container row>
-                <Icon height="10px" src={refresh} />
-                <Spacer inline width="5px" />
-                Retry
-              </Container>
-            </Link>
-          </Wrapper>
-        );
-      default:
-        return (
-          <Wrapper>
-            <Link
-              hasunderline
-              target="_blank"
-              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
-            >
-              View live logs
-            </Link>
-            <Spacer inline x={1} />
-          </Wrapper>
-        );
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="16px" src={build} />
-          <Spacer inline width="10px" />
-          <Text>Application build</Text>
-        </Container>
-        <Container row>
-          <Icon height="14px" src={run_for} />
-          <Spacer inline width="6px" />
-          <Text color="helper">{getDuration(event)}</Text>
-        </Container>
-      </Container>
-      <Spacer y={0.5} />
-      <Container row spaced>
-        <Container row>
-          <Icon height="12px" src={getStatusIcon(event.status)} />
-          <Spacer inline width="10px" />
-          {renderStatusText(event)}
-          <Spacer inline x={1} />
-          {renderInfoCta(event)}
-          <Spacer inline x={1} />
-        </Container>
-      </Container>
-    </StyledEventCard>
-  );
-};
-
-export default BuildEventCard;
-
-const Wrapper = styled.div`
-  margin-top: -3px;
-`;

+ 0 - 229
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx

@@ -1,229 +0,0 @@
-import React, { useState } from "react";
-import deploy from "assets/deploy.png";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Icon from "components/porter/Icon";
-import { getStatusColor, getStatusIcon } from '../utils';
-import { StyledEventCard } from "./EventCard";
-import styled from "styled-components";
-import Link from "components/porter/Link";
-import ChangeLogModal from "../../../ChangeLogModal";
-import { PorterAppDeployEvent } from "../types";
-import AnimateHeight from "react-animate-height";
-import ServiceStatusDetail from "./ServiceStatusDetail";
-
-type Props = {
-  event: PorterAppDeployEvent;
-  appData: any;
-  showServiceStatusDetail?: boolean;
-};
-
-const DeployEventCard: React.FC<Props> = ({ event, appData, showServiceStatusDetail = false }) => {
-  const [diffModalVisible, setDiffModalVisible] = useState(false);
-  const [revertModalVisible, setRevertModalVisible] = useState(false);
-  const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail);
-
-  const renderStatusText = () => {
-    switch (event.status) {
-      case "SUCCESS":
-        return event.metadata.image_tag != null ?
-          event.metadata.service_deployment_metadata != null ?
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Deployed <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))}
-            </StatusTextContainer>
-            :
-            <Text color={getStatusColor(event.status)}>
-              Deployed <Code>{event.metadata.image_tag}</Code>
-            </Text>
-          :
-          <Text color={getStatusColor(event.status)}>
-            Deployment successful
-          </Text>;
-      case "FAILED":
-        if (event.metadata.service_deployment_metadata != null) {
-          let failedServices = 0;
-          for (const key in event.metadata.service_deployment_metadata) {
-            if (event.metadata.service_deployment_metadata[key].status === "FAILED") {
-              failedServices++;
-            }
-          }
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Failed to deploy <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(failedServices, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deployment failed
-            </Text>
-          );
-        }
-      case "CANCELED":
-        if (event.metadata.service_deployment_metadata != null) {
-          let canceledServices = 0;
-          for (const key in event.metadata.service_deployment_metadata) {
-            if (event.metadata.service_deployment_metadata[key].status === "CANCELED") {
-              canceledServices++;
-            }
-          }
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Canceled deploy of <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(canceledServices, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deployment canceled
-            </Text>
-          );
-        }
-      default:
-        if (event.metadata.service_deployment_metadata != null) {
-          return (
-            <StatusTextContainer>
-              <Text color={getStatusColor(event.status)}>
-                Deploying <Code>{event.metadata.image_tag}</Code> to
-              </Text>
-              <Spacer inline x={0.25} />
-              {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))}
-            </StatusTextContainer>
-          );
-        } else {
-          return (
-            <Text color={getStatusColor(event.status)}>
-              Deploying <Code>{event.metadata.image_tag}</Code>...
-            </Text>
-          );
-        }
-    }
-  };
-
-  const renderServiceDropdownCta = (numServices: number, color?: string) => {
-    return (
-      <ServiceStatusDropdownCtaContainer >
-        <Link color={color} onClick={() => setServiceStatusVisible(!serviceStatusVisible)}>
-          <ServiceStatusDropdownIcon className="material-icons" serviceStatusVisible={serviceStatusVisible}>arrow_drop_down</ServiceStatusDropdownIcon>
-          {numServices} service{numServices === 1 ? "" : "s"}
-        </Link>
-      </ServiceStatusDropdownCtaContainer>
-    )
-  }
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="16px" src={deploy} />
-          <Spacer inline width="10px" />
-          <Text>Application version no. {event.metadata?.revision}</Text>
-        </Container>
-      </Container>
-      <Spacer y={0.5} />
-      <Container row spaced>
-        <Container row>
-          <Icon height="12px" src={getStatusIcon(event.status)} />
-          <Spacer inline width="10px" />
-          {renderStatusText()}
-          {appData?.chart?.version !== event.metadata.revision && (
-            <>
-              <Spacer inline x={1} />
-              <TempWrapper>
-                <Link hasunderline onClick={() => setRevertModalVisible(true)}>
-                  Revert to version {event.metadata.revision}
-                </Link>
-
-              </TempWrapper>
-            </>
-          )}
-          <Spacer inline x={1} />
-          <TempWrapper>
-            {event.metadata.revision != 1 && (<Link hasunderline onClick={() => setDiffModalVisible(true)}>
-              View changes
-            </Link>)}
-            {diffModalVisible && (
-              <ChangeLogModal
-                revision={event.metadata.revision}
-                currentChart={appData.chart}
-                modalVisible={diffModalVisible}
-                setModalVisible={setDiffModalVisible}
-                appData={appData}
-              />
-            )}
-            {revertModalVisible && (
-              <ChangeLogModal
-                revision={event.metadata.revision}
-                currentChart={appData.chart}
-                modalVisible={revertModalVisible}
-                setModalVisible={setRevertModalVisible}
-                revertModal={true}
-                appData={appData}
-              />
-            )}
-          </TempWrapper>
-        </Container>
-      </Container>
-      {event.metadata.service_deployment_metadata != null &&
-        <AnimateHeight height={serviceStatusVisible ? "auto" : 0}>
-          <Spacer y={0.5} />
-          <ServiceStatusDetail
-            serviceDeploymentMetadata={event.metadata.service_deployment_metadata}
-            appName={appData.app.name}
-            revision={event.metadata.revision}
-          />
-        </AnimateHeight>
-      }
-    </StyledEventCard>
-  );
-};
-
-export default DeployEventCard;
-
-// TODO: remove after fixing v-align
-const TempWrapper = styled.div`
-  margin-top: -3px;
-`;
-
-const Code = styled.span`
-  font-family: monospace;
-`;
-
-const ServiceStatusDropdownCtaContainer = styled.div`
-  display: flex;
-  justify-content: center;
-  cursor: pointer;
-  padding: 3px 5px;
-  border-radius: 5px;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const ServiceStatusDropdownIcon = styled.i`
-  margin-left: -5px;
-  font-size: 20px;
-  border-radius: 20px;
-  transform: ${(props: { serviceStatusVisible: boolean }) =>
-    props.serviceStatusVisible ? "" : "rotate(-90deg)"};
-  transition: transform 0.1s ease;
-`
-
-const StatusTextContainer = styled.div`
-  display: flex;
-  align-items: center;
-  flex-direction: row;
-`;

+ 0 - 61
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx

@@ -1,61 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-import BuildEventCard from "./BuildEventCard";
-import PreDeployEventCard from "./PreDeployEventCard";
-import AppEventCard from "./AppEventCard";
-import DeployEventCard from "./DeployEventCard";
-import { PorterAppDeployEvent, PorterAppEvent, PorterAppEventType } from "../types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-  isLatestDeployEvent?: boolean;
-};
-
-const EventCard: React.FC<Props> = ({ event, appData, isLatestDeployEvent }) => {
-  const renderEventCard = (event: PorterAppEvent) => {
-    switch (event.type) {
-      case PorterAppEventType.APP_EVENT:
-        return <AppEventCard event={event} appData={appData} />;
-      case PorterAppEventType.BUILD:
-        return <BuildEventCard event={event} appData={appData} />;
-      case PorterAppEventType.DEPLOY:
-        return <DeployEventCard event={event as PorterAppDeployEvent} appData={appData} showServiceStatusDetail={isLatestDeployEvent} />;
-      case PorterAppEventType.PRE_DEPLOY:
-        return <PreDeployEventCard event={event} appData={appData} />;
-      default:
-        return null;
-    };
-  };
-
-  return renderEventCard(event);
-};
-
-export default EventCard;
-
-export const StyledEventCard = styled.div<{ row?: boolean }>`
-  width: 100%;
-  padding: 15px;
-  display: flex;
-  flex-direction: ${({ row }) => row ? "row" : "column"};
-  justify-content: space-between;
-  border-radius: 5px;
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid ${({ theme }) => theme.border};
-  opacity: 0;
-  animation: slideIn 0.5s 0s;
-  animation-fill-mode: forwards;
-  @keyframes slideIn {
-    from {
-      margin-left: -10px;
-      opacity: 0;
-      margin-right: 10px;
-    }
-    to {
-      margin-left: 0;
-      opacity: 1;
-      margin-right: 0;
-    }
-  }
-`;

+ 0 - 90
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx

@@ -1,90 +0,0 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-
-import pre_deploy from "assets/pre_deploy.png";
-
-import run_for from "assets/run_for.png";
-import refresh from "assets/refresh.png";
-
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Icon from "components/porter/Icon";
-
-import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils';
-import { StyledEventCard } from "./EventCard";
-import Link from "components/porter/Link";
-import document from "assets/document.svg";
-import { PorterAppEvent } from "../types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const PreDeployEventCard: React.FC<Props> = ({ event, appData }) => {
-  const renderStatusText = (event: PorterAppEvent) => {
-    switch (event.status) {
-      case "SUCCESS":
-        return <Text color={getStatusColor(event.status)}>Pre-deploy succeeded</Text>;
-      case "FAILED":
-        return <Text color={getStatusColor(event.status)}>Pre-deploy failed</Text>;
-      default:
-        return <Text color={getStatusColor(event.status)}>Pre-deploy in progress...</Text>;
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="16px" src={pre_deploy} />
-          <Spacer inline width="10px" />
-          <Text>Application pre-deploy</Text>
-        </Container>
-        <Container row>
-          <Icon height="14px" src={run_for} />
-          <Spacer inline width="6px" />
-          <Text color="helper">{getDuration(event)}</Text>
-        </Container>
-      </Container>
-      <Spacer y={0.5} />
-      <Container row spaced>
-        <Container row>
-          <Icon height="12px" src={getStatusIcon(event.status)} />
-          <Spacer inline width="10px" />
-          {renderStatusText(event)}
-          {(event.status !== "SUCCESS") &&
-            <>
-              <Spacer inline x={1} />
-              <Wrapper>
-                <Link to={`/apps/${appData.app.name}/events?event_id=${event.id}`} hasunderline>
-                  <Container row>
-                    <Icon src={document} height="10px" />
-                    <Spacer inline width="5px" />
-                    View details
-                  </Container>
-                </Link>
-                <Spacer inline x={1} />
-                <Link hasunderline onClick={() => triggerWorkflow(appData)}>
-                  <Container row>
-                    <Icon height="10px" src={refresh} />
-                    <Spacer inline width="5px" />
-                    Retry
-                  </Container>
-                </Link>
-              </Wrapper>
-            </>
-          }
-          <Spacer inline x={1} />
-        </Container>
-      </Container>
-    </StyledEventCard>
-  );
-};
-
-export default PreDeployEventCard;
-
-const Wrapper = styled.div`
-  margin-top: -3px;
-`;

+ 0 - 126
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx

@@ -1,126 +0,0 @@
-import Icon from 'components/porter/Icon';
-import Spacer from 'components/porter/Spacer';
-import Text from 'components/porter/Text';
-import React from 'react'
-import styled from 'styled-components';
-import { getStatusColor, getStatusIcon } from '../utils';
-import Link from 'components/porter/Link';
-import { PorterAppDeployEvent } from "../types";
-import { Service } from 'main/home/app-dashboard/new-app-flow/serviceTypes';
-
-type Props = {
-    serviceDeploymentMetadata: PorterAppDeployEvent["metadata"]["service_deployment_metadata"];
-    appName: string;
-    revision: number;
-}
-
-const ServiceStatusDetail: React.FC<Props> = ({
-    serviceDeploymentMetadata,
-    appName,
-    revision,
-}) => {
-    const convertEventStatusToCopy = (status: string) => {
-        switch (status) {
-            case "PROGRESSING":
-                return "DEPLOYING";
-            case "SUCCESS":
-                return "DEPLOYED";
-            case "FAILED":
-                return "FAILED";
-            case "CANCELED":
-                return "CANCELED";
-            default:
-                return "UNKNOWN";
-        }
-    };
-
-    return (
-        <ServiceStatusTable>
-            <tbody>
-                {Object.keys(serviceDeploymentMetadata).map((key) => {
-                    const deploymentMetadata = serviceDeploymentMetadata[key];
-                    return (
-                        <ServiceStatusTableRow key={key}>
-                            <ServiceStatusTableData width={"100px"}>
-                                <Text>{key}</Text>
-                            </ServiceStatusTableData>
-                            <ServiceStatusTableData width={"120px"}>
-                                <Icon height="12px" src={getStatusIcon(deploymentMetadata.status)} />
-                                <Spacer inline x={0.5} />
-                                <Text color={getStatusColor(deploymentMetadata.status)}>{convertEventStatusToCopy(serviceDeploymentMetadata[key].status)}</Text>
-                            </ServiceStatusTableData>
-                            <ServiceStatusTableData>
-                                {deploymentMetadata.type !== "job" &&
-                                    <>
-                                        <Link
-                                            to={`/apps/${appName}/logs?version=${revision}&service=${key}`}
-                                            hasunderline
-                                            hoverColor="#949eff"
-                                        >
-                                            Logs
-                                        </Link>
-                                        <Spacer inline x={0.5} />
-                                        <Link
-                                            to={`/apps/${appName}/metrics?service=${key}`}
-                                            hasunderline
-                                            hoverColor="#949eff"
-                                        >
-                                            Metrics
-                                        </Link>
-                                    </>
-                                }
-                                {deploymentMetadata.type === "job" &&
-                                    <>
-                                        <Link
-                                            to={`/apps/${appName}/job-history?service=${key}`}
-                                            hasunderline
-                                            hoverColor="#949eff"
-                                        >
-                                            History
-                                        </Link>
-                                    </>
-                                }
-                                {deploymentMetadata.external_uri !== "" &&
-                                    <>
-                                        <Spacer inline x={0.5} />
-                                        <Link
-                                            to={Service.prefixSubdomain(deploymentMetadata.external_uri)}
-                                            hasunderline
-                                            hoverColor="#949eff"
-                                            target={"_blank"}
-                                        >
-                                            External link
-                                        </Link>
-                                    </>
-                                }
-                            </ServiceStatusTableData>
-                        </ServiceStatusTableRow>
-                    );
-                })}
-            </tbody>
-        </ServiceStatusTable>
-    )
-}
-
-export default ServiceStatusDetail;
-
-const ServiceStatusTable = styled.table`
-  border-collapse: collapse;
-  width: 100%;
-`;
-
-const ServiceStatusTableRow = styled.tr`
-  display: flex;
-  align-items: center;  
-`;
-
-const ServiceStatusTableData = styled.td`
-  padding: 8px;
-  display: flex;
-  align-items: center;
-  ${(props) => props.width && `width: ${props.width};`}
-
-  &:not(:last-child) {
-    border-right: 2px solid #ffffff11;
-  }
-`;

+ 0 - 278
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx

@@ -1,278 +0,0 @@
-import Loading from "components/Loading";
-import Spacer from "components/porter/Spacer";
-import React, { useEffect, useRef, useState } from "react";
-import api from "shared/api";
-import styled from "styled-components";
-import Anser, { AnserJsonEntry } from "anser";
-import JSZip from "jszip";
-import dayjs from "dayjs";
-import Text from "components/porter/Text";
-import { readableDate } from "shared/string_utils";
-import { getDuration } from "../utils";
-import Link from "components/porter/Link";
-import { PorterLog } from "../../../logs/types";
-import { PorterAppEvent } from "../types";
-
-type Props = {
-    event: PorterAppEvent;
-    appData: any;
-};
-
-const BuildFailureEventFocusView: React.FC<Props> = ({
-    event,
-    appData,
-}) => {
-    const [logs, setLogs] = useState<PorterLog[]>([]);
-    const [isLoading, setIsLoading] = useState<boolean>(true);
-    const scrollToBottomRef = useRef<HTMLDivElement>(null);
-
-    useEffect(() => {
-        if (!isLoading && scrollToBottomRef.current) {
-            scrollToBottomRef.current.scrollIntoView({
-                behavior: "smooth",
-                block: "end",
-            });
-        }
-    }, [isLoading, logs, scrollToBottomRef]);
-
-    const getBuildLogs = async () => {
-        if (event == null) {
-            return;
-        }
-        try {
-            setLogs([]);
-
-            const res = await api.getGHWorkflowLogById(
-                "",
-                {},
-                {
-                    project_id: appData.app.project_id,
-                    cluster_id: appData.app.cluster_id,
-                    git_installation_id: appData.app.git_repo_id,
-                    owner: appData.app.repo_name?.split("/")[0],
-                    name: appData.app.repo_name?.split("/")[1],
-                    filename: "porter_stack_" + appData.chart.name + ".yml",
-                    run_id: event.metadata.action_run_id,
-                }
-            );
-            let logs: PorterLog[] = [];
-            if (res.data != null) {
-                // Fetch the logs
-                const logsResponse = await fetch(res.data);
-
-                // Ensure that the response body is only read once
-                const logsBlob = await logsResponse.blob();
-
-                if (logsResponse.headers.get("Content-Type") === "application/zip") {
-                    const zip = await JSZip.loadAsync(logsBlob);
-                    const promises: any[] = [];
-
-                    zip.forEach(function (relativePath, zipEntry) {
-                        promises.push(
-                            (async function () {
-                                const fileData = await zip
-                                    .file(relativePath)
-                                    ?.async("string");
-
-                                if (
-                                    fileData &&
-                                    fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
-                                ) {
-                                    const lines = fileData.split("\n");
-                                    const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
-
-                                    for (let i = 0; i < lines.length; i++) {
-                                        const line = lines[i];
-                                        if (line.includes("Post job cleanup.")) {
-                                            break;
-                                        }
-                                        const lineWithoutTimestamp = line.replace(timestampPattern, "").trimStart();
-                                        const anserLine: AnserJsonEntry[] = Anser.ansiToJson(lineWithoutTimestamp);
-                                        if (lineWithoutTimestamp.toLowerCase().includes("error")) {
-                                            anserLine[0].fg = "238,75,43";
-                                        }
-
-                                        const log: PorterLog = {
-                                            line: anserLine,
-                                            lineNumber: i + 1,
-                                            timestamp: line.match(timestampPattern)?.[0],
-                                        };
-
-                                        logs.push(log);
-                                    }
-                                }
-                            })()
-                        );
-                    });
-
-                    await Promise.all(promises);
-                    setLogs(logs);
-                }
-            }
-        } catch (error) {
-            console.log(error);
-        } finally {
-            setIsLoading(false);
-        }
-    };
-
-    useEffect(() => {
-        getBuildLogs();
-    }, []);
-
-    return (
-        <>
-            <Text size={16} color="#FF6060">Build failed</Text>
-            <Spacer y={0.5} />
-            <Text color="helper">Started {readableDate(event.created_at)} and ran for {getDuration(event)}.</Text>
-            <Spacer y={0.5} />
-            <StyledLogsSection>
-                {isLoading ? (
-                    <Loading message="Waiting for logs..." />
-                ) : logs.length == 0 ? (
-                    <>
-                        <Message>
-                            No logs found.
-                        </Message>
-                    </>
-                ) : (
-                    <>
-                        {logs?.map((log, i) => {
-                            return (
-                                <Log key={[log.lineNumber, i].join(".")}>
-                                    <span className="line-number">{log.lineNumber}.</span>
-                                    <span className="line-timestamp">
-                                        {log.timestamp
-                                            ? dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")
-                                            : "-"}
-                                    </span>
-                                    <LogOuter key={[log.lineNumber, i].join(".")}>
-                                        {log.line?.map((ansi, j) => {
-                                            if (ansi.clearLine) {
-                                                return null;
-                                            }
-
-                                            return (
-                                                <LogInnerSpan
-                                                    key={[log.lineNumber, i, j].join(".")}
-                                                    ansi={ansi}
-                                                >
-                                                    {ansi.content.replace(/ /g, "\u00a0")}
-                                                </LogInnerSpan>
-                                            );
-                                        })}
-                                    </LogOuter>
-                                </Log>
-                            );
-                        })}
-                    </>
-                )}
-                <div ref={scrollToBottomRef} />
-            </StyledLogsSection>
-            <Spacer y={0.5} />
-            <Link
-                hasunderline
-                target="_blank"
-                to={
-                    event.metadata.action_run_id
-                        ? `https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata.action_run_id}`
-                        : `https://github.com/${appData.app.repo_name}/actions`
-                }
-            >
-                View full build logs
-            </Link>
-        </>
-    );
-};
-
-export default BuildFailureEventFocusView;
-
-const StyledLogsSection = styled.div`
-  width: 100%;
-  min-height: 600px;
-  height: calc(100vh - 460px);
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  background: #000000;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const Message = styled.div`
-  display: flex;
-  height: 100%;
-  width: calc(100% - 150px);
-  align-items: center;
-  justify-content: center;
-  margin-left: 75px;
-  text-align: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
-const Log = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-end;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;

+ 0 - 71
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx

@@ -1,71 +0,0 @@
-import Spacer from "components/porter/Spacer";
-import React from "react";
-import dayjs from "dayjs";
-import Text from "components/porter/Text";
-import { readableDate } from "shared/string_utils";
-import { getDuration } from "../utils";
-import LogSection from "../../../logs/LogSection";
-import { AppearingView } from "./EventFocusView";
-import Icon from "components/porter/Icon";
-import loading from "assets/loading.gif";
-import Container from "components/porter/Container";
-import { PorterAppDeployEvent } from "../types";
-import { LogFilterQueryParamOpts } from "../../../logs/types";
-
-type Props = {
-    event: PorterAppDeployEvent;
-    appData: any;
-    filterOpts?: LogFilterQueryParamOpts
-};
-
-const DeployEventFocusView: React.FC<Props> = ({
-    event,
-    appData,
-    filterOpts,
-}) => {
-    const renderHeaderText = () => {
-        switch (event.status) {
-            case "SUCCESS":
-                return <Text color="#68BF8B" size={16}>Deploy succeeded</Text>;
-            case "FAILED":
-                return <Text color="#FF6060" size={16}>Deploy failed</Text>;
-            case "CANCELED":
-                return <Text color="#FFBF00" size={16}>Deploy canceled</Text>;
-            default:
-                return (
-                    <Container row>
-                        <Icon height="16px" src={loading} />
-                        <Spacer inline width="10px" />
-                        <Text size={16}>Deploy in progress...</Text>
-                    </Container>
-                );
-        }
-    };
-
-    const renderDurationText = () => {
-        switch (event.status) {
-            case "PROGRESSING":
-                return <Text color="helper">Started {readableDate(event.created_at)}.</Text>
-            default:
-                return <Text color="helper">Started {readableDate(event.created_at)} and ran for {getDuration(event)}.</Text>;
-        }
-    }
-
-    return (
-        <>
-            <AppearingView>
-                {renderHeaderText()}
-            </AppearingView>
-            <Spacer y={0.5} />
-            {renderDurationText()}
-            <Spacer y={0.5} />
-            <LogSection
-                currentChart={appData.chart}
-                appName={appData.app.name}
-                filterOpts={filterOpts}
-            />
-        </>
-    );
-};
-
-export default DeployEventFocusView;

+ 0 - 129
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx

@@ -1,129 +0,0 @@
-import Loading from "components/Loading";
-import Spacer from "components/porter/Spacer";
-import React, { useContext, useEffect, useState } from "react";
-import { Context } from "shared/Context";
-import api from "shared/api";
-import styled from "styled-components";
-import Link from "components/porter/Link";
-import BuildFailureEventFocusView from "./BuildFailureEventFocusView";
-import PreDeployEventFocusView from "./PredeployEventFocusView";
-import _ from "lodash";
-import { PorterAppDeployEvent, PorterAppEvent } from "../types";
-import DeployEventFocusView from "./DeployEventFocusView";
-import { LogFilterQueryParamOpts } from "../../../logs/types";
-
-type Props = {
-    eventId: string;
-    appData: any;
-    filterOpts?: LogFilterQueryParamOpts;
-};
-
-const EVENT_POLL_INTERVAL = 5000; // poll every 5 seconds
-
-const EventFocusView: React.FC<Props> = ({
-    eventId,
-    appData,
-    filterOpts,
-}) => {
-    const { currentProject, currentCluster } = useContext(Context);
-    const [event, setEvent] = useState<PorterAppEvent | null>(null);
-
-    useEffect(() => {
-        const getEvent = async () => {
-            if (currentProject == null || currentCluster == null) {
-                return;
-            }
-            try {
-                const eventResp = await api.getPorterAppEvent(
-                    "<token>",
-                    {},
-                    {
-                        project_id: currentProject.id,
-                        cluster_id: currentCluster.id,
-                        event_id: eventId,
-                    }
-                )
-                const newEvent = PorterAppEvent.toPorterAppEvent(eventResp.data.event);
-                setEvent(newEvent);
-                if (newEvent.metadata.end_time != null) {
-                    clearInterval(intervalId);
-                }
-            } catch (err) {
-                console.log(err);
-            }
-        }
-        const intervalId = setInterval(getEvent, EVENT_POLL_INTERVAL);
-        getEvent();
-        return () => clearInterval(intervalId);
-    }, []);
-
-    const getEventFocusView = (event: PorterAppEvent, appData: any) => {
-        switch (event.type) {
-            case "BUILD":
-                return <BuildFailureEventFocusView event={event} appData={appData} />
-            case "PRE_DEPLOY":
-                return <PreDeployEventFocusView event={event} appData={appData} />
-            case "DEPLOY":
-                return <DeployEventFocusView
-                    event={event as PorterAppDeployEvent}
-                    appData={appData}
-                    filterOpts={filterOpts}
-                />
-            default:
-                return null
-        }
-    }
-
-    return (
-        <AppearingView>
-            <Link to={`/apps/${appData.app.name}/activity`}>
-                <BackButton>
-                    <i className="material-icons">keyboard_backspace</i>
-                    Activity feed
-                </BackButton>
-            </Link>
-            <Spacer y={0.5} />
-            {event == null && <Loading />}
-            {event != null && getEventFocusView(event, appData)}
-        </AppearingView>
-    );
-};
-
-export default EventFocusView;
-
-export const AppearingView = styled.div`
-    width: 100%;
-    animation: fadeIn 0.3s 0s;
-    @keyframes fadeIn {
-    from {
-        opacity: 0;
-    }
-    to {
-        opacity: 1;
-    }
-    }
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  align-items: center;
-  max-width: fit-content;
-  cursor: pointer;
-  font-size: 11px;
-  max-height: fit-content;
-  padding: 5px 13px;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  color: white;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: white;
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;

+ 0 - 70
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx

@@ -1,70 +0,0 @@
-import Spacer from "components/porter/Spacer";
-import React from "react";
-import dayjs from "dayjs";
-import Text from "components/porter/Text";
-import { readableDate } from "shared/string_utils";
-import { getDuration } from "../utils";
-import LogSection from "../../../logs/LogSection";
-import { AppearingView } from "./EventFocusView";
-import Icon from "components/porter/Icon";
-import loading from "assets/loading.gif";
-import Container from "components/porter/Container";
-import { PorterAppEvent } from "../types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const PreDeployEventFocusView: React.FC<Props> = ({
-  event,
-  appData,
-}) => {
-  const renderHeaderText = () => {
-    switch (event.status) {
-      case "SUCCESS":
-        return <Text color="#68BF8B" size={16}>Pre-deploy succeeded</Text>;
-      case "FAILED":
-        return <Text color="#FF6060" size={16}>Pre-deploy failed</Text>;
-      default:
-        return (
-          <Container row>
-            <Icon height="16px" src={loading} />
-            <Spacer inline width="10px" />
-            <Text size={16}>Pre-deploy in progress...</Text>
-          </Container>
-        );
-    }
-  };
-
-  const renderDurationText = () => {
-    switch (event.status) {
-      case "PROGRESSING":
-        return <Text color="helper">Started {readableDate(event.created_at)}.</Text>
-      default:
-        return <Text color="helper">Started {readableDate(event.created_at)} and ran for {getDuration(event)}.</Text>;
-    }
-  }
-
-  return (
-    <>
-      <AppearingView>
-        {renderHeaderText()}
-      </AppearingView>
-      <Spacer y={0.5} />
-      {renderDurationText()}
-      <Spacer y={0.5} />
-      <LogSection
-        currentChart={appData.releaseChart}
-        timeRange={{
-          startTime: event.metadata.end_time != null ? dayjs(event.metadata.start_time).subtract(1, 'minute') : undefined,
-          endTime: event.metadata.end_time != null ? dayjs(event.metadata.end_time).add(1, 'minute') : undefined,
-        }}
-        showFilter={false}
-        appName={appData.app.name}
-      />
-    </>
-  );
-};
-
-export default PreDeployEventFocusView;

+ 0 - 44
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts

@@ -1,44 +0,0 @@
-export enum PorterAppEventType {
-    BUILD = "BUILD",
-    DEPLOY = "DEPLOY",
-    APP_EVENT = "APP_EVENT",
-    PRE_DEPLOY = "PRE_DEPLOY",
-}
-export interface PorterAppEvent {
-    created_at: string;
-    updated_at: string;
-    id: string;
-    status: string;
-    type: PorterAppEventType;
-    type_source: string;
-    porter_app_id: number;
-    metadata: any;
-}
-export const PorterAppEvent = {
-    toPorterAppEvent: (data: any): PorterAppEvent => {
-        return {
-            created_at: data.created_at ?? "",
-            updated_at: data.updated_at ?? "",
-            id: data.id ?? "",
-            status: data.status ?? "",
-            type: data.type ?? "",
-            type_source: data.type_source ?? "",
-            porter_app_id: data.porter_app_id ?? "",
-            metadata: data.metadata ?? {},
-        };
-    }
-}
-
-interface PorterAppServiceDeploymentMetadata {
-    status: string;
-    external_uri: string;
-    type: string;
-}
-export interface PorterAppDeployEvent extends PorterAppEvent {
-    type: PorterAppEventType.DEPLOY;
-    metadata: {
-        image_tag: string;
-        revision: number;
-        service_deployment_metadata: Record<string, PorterAppServiceDeploymentMetadata>;
-    };
-}

+ 0 - 91
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts

@@ -1,91 +0,0 @@
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import loading from "assets/loading.gif";
-import canceled from "assets/canceled.svg"
-import api from "shared/api";
-import { PorterAppEvent } from "./types";
-
-export const getDuration = (event: PorterAppEvent): string => {
-    const startTimeStamp = new Date(event.metadata.start_time ?? event.created_at).getTime();
-    const endTimeStamp = new Date(event.metadata.end_time ?? event.updated_at).getTime();
-
-    const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp;
-
-    const seconds = Math.floor(timeDifferenceMilliseconds / 1000);
-    const weeks = Math.floor(seconds / 604800);
-    const remainingDays = Math.floor((seconds % 604800) / 86400);
-    const remainingHours = Math.floor((seconds % 86400) / 3600);
-    const remainingMinutes = Math.floor((seconds % 3600) / 60);
-    const remainingSeconds = seconds % 60;
-
-    if (weeks > 0) {
-        return `${weeks}w ${remainingDays}d`;
-    }
-
-    if (remainingDays > 0) {
-        return `${remainingDays}d ${remainingHours}h`;
-    }
-
-    if (remainingHours > 0) {
-        return `${remainingHours}h ${remainingMinutes}m`;
-    }
-
-    if (remainingMinutes > 0) {
-        return `${remainingMinutes}m ${remainingSeconds}s`;
-    }
-
-    return `${remainingSeconds}s`;
-};
-
-export const getStatusIcon = (status: string) => {
-    switch (status) {
-        case "SUCCESS":
-            return healthy;
-        case "FAILED":
-            return failure;
-        case "PROGRESSING":
-            return loading;
-        case "CANCELED":
-            return canceled;
-        default:
-            return loading;
-    }
-};
-
-export const getStatusColor = (status: string) => {
-    switch (status) {
-        case "SUCCESS":
-            return "#68BF8B";
-        case "FAILED":
-            return "#FF6060";
-        case "PROGRESSING":
-            return "#6e9df5";
-        case "CANCELED":
-            return "#FFBF00";
-        default:
-            return "#6e9df5";
-    }
-};
-
-export const triggerWorkflow = async (appData: any) => {
-    try {
-        const res = await api.reRunGHWorkflow(
-            "",
-            {},
-            {
-                project_id: appData.app.project_id,
-                cluster_id: appData.app.cluster_id,
-                git_installation_id: appData.app.git_repo_id,
-                owner: appData.app.repo_name?.split("/")[0],
-                name: appData.app.repo_name?.split("/")[1],
-                branch: appData.app.branch_name,
-                filename: "porter_stack_" + appData.chart.name + ".yml",
-            }
-        );
-        if (res.data != null) {
-            window.open(res.data, "_blank", "noreferrer");
-        }
-    } catch (error) {
-        console.log(error);
-    }
-};

+ 0 - 344
dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx

@@ -1,344 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { isEmpty, isObject } from "lodash";
-import AceEditor from "react-ace";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled, { css } from "styled-components";
-import { set } from "zod";
-
-import Loading from "components/Loading";
-import {
-  type NewPopulatedEnvGroup,
-  type PartialEnvGroup,
-  type PopulatedEnvGroup,
-} from "components/porter-form/types";
-import Button from "components/porter/Button";
-import Checkbox from "components/porter/Checkbox";
-import Container from "components/porter/Container";
-import Error from "components/porter/Error";
-import Modal from "components/porter/Modal";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import YamlEditor from "components/YamlEditor";
-
-import api from "shared/api";
-import sliders from "assets/sliders.svg";
-
-import { Context } from "../../../../../shared/Context";
-import {
-  EnvGroupData,
-  formattedEnvironmentValue,
-} from "../../../cluster-dashboard/env-groups/EnvGroup";
-import { type KeyValueType } from "../../../cluster-dashboard/env-groups/EnvGroupArray";
-import { getGithubAction } from "./utils";
-
-type Props = RouteComponentProps & {
-  closeModal: () => void;
-  availableEnvGroups?: PartialEnvGroup[];
-  setValues: (x: KeyValueType[]) => void;
-  values: KeyValueType[];
-  syncedEnvGroups: NewPopulatedEnvGroup[];
-  setSyncedEnvGroups: (values: NewPopulatedEnvGroup[]) => void;
-  namespace: string;
-  newApp?: boolean;
-};
-
-const EnvGroupModal: React.FC<Props> = ({
-  closeModal,
-  setValues,
-  availableEnvGroups,
-  syncedEnvGroups,
-  setSyncedEnvGroups,
-  values,
-  namespace,
-  newApp,
-}) => {
-  const { currentCluster, currentProject } = useContext(Context);
-  const [envGroups, setEnvGroups] = useState<any>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-  const [error, setError] = useState<any>(null);
-  const [shouldSync, setShouldSync] = useState<boolean>(true);
-  const [selectedEnvGroup, setSelectedEnvGroup] =
-    useState<PopulatedEnvGroup | null>(null);
-  const [cloneSuccess, setCloneSuccess] = useState(false);
-
-  const updateEnvGroups = async () => {
-    let populatedEnvGroups: any[] = [];
-    try {
-      populatedEnvGroups = await api
-        .getAllEnvGroups<any[]>(
-          "<token>",
-          {},
-          {
-            id: currentProject?.id,
-            cluster_id: currentCluster?.id,
-          }
-        )
-        .then((res) => res.data?.environment_groups);
-    } catch (error) {
-      setLoading(false);
-      setError(true);
-      return;
-    }
-
-    try {
-      setEnvGroups(populatedEnvGroups);
-      setLoading(false);
-    } catch (error) {
-      setLoading(false);
-      setError(true);
-    }
-  };
-
-  useEffect(() => {
-    if (!values) {
-      setValues([]);
-    }
-  }, [values]);
-
-  useEffect(() => {
-    setLoading(true);
-    if (Array.isArray(availableEnvGroups)) {
-      setEnvGroups(availableEnvGroups);
-      setLoading(false);
-      return;
-    }
-    updateEnvGroups();
-  }, []);
-
-  const renderEnvGroupList = () => {
-    if (loading) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else {
-      const sortedEnvGroups = envGroups
-        ?.slice()
-        .sort((a, b) => a.name.localeCompare(b.name));
-
-      return sortedEnvGroups
-        ?.filter((envGroup) => {
-          if (!Array.isArray(syncedEnvGroups)) {
-            return true;
-          }
-          return !syncedEnvGroups?.find(
-            (syncedEnvGroup) => syncedEnvGroup?.name === envGroup?.name
-          );
-        })
-        .map((envGroup: any, i: number) => {
-          return (
-            <EnvGroupRow
-              key={i}
-              isSelected={selectedEnvGroup === envGroup}
-              lastItem={i === envGroups?.length - 1}
-              onClick={() => {
-                setSelectedEnvGroup(envGroup);
-              }}
-            >
-              <img src={sliders} />
-              {envGroup?.name}
-            </EnvGroupRow>
-          );
-        });
-    }
-  };
-
-  const onSubmit = () => {
-    if (shouldSync) {
-      syncedEnvGroups.push(selectedEnvGroup);
-      setSyncedEnvGroups(syncedEnvGroups);
-    } else {
-      const _values = [...values];
-
-      Object.entries(selectedEnvGroup?.variables || {}).map(([key, value]) =>
-        _values.push({
-          key,
-          value,
-          hidden: false,
-          locked: false,
-          deleted: false,
-        })
-      );
-      setValues(_values);
-    }
-    closeModal();
-  };
-
-  return (
-    <Modal closeModal={closeModal}>
-      <Text size={16}>Load env group</Text>
-      <Spacer height="15px" />
-      <ColumnContainer>
-        <ScrollableContainer>
-          {syncedEnvGroups?.length != envGroups?.length ? (
-            <>
-              <Text color="helper">
-                Select an Env Group to load into your application.
-              </Text>
-              <Spacer y={0.5} />
-              <GroupModalSections>
-                <SidebarSection $expanded={!selectedEnvGroup}>
-                  <EnvGroupList>{renderEnvGroupList()}</EnvGroupList>
-                </SidebarSection>
-                {selectedEnvGroup && (
-                  <>
-                    <SidebarSection>
-                      <GroupEnvPreview>
-                        {isObject(selectedEnvGroup?.variables) ||
-                        isObject(selectedEnvGroup?.secret_variables) ? (
-                          <>
-                            {[
-                              ...Object.entries(
-                                selectedEnvGroup?.variables || {}
-                              ).map(([key, value]) => ({
-                                source: "variables",
-                                key,
-                                value,
-                              })),
-                              ...Object.entries(
-                                selectedEnvGroup?.secret_variables || {}
-                              ).map(([key, value]) => ({
-                                source: "secret_variables",
-                                key,
-                                value,
-                              })),
-                            ].map(({ key, value, source }, index) => (
-                              <div key={index}>
-                                <span className="key">{key} = </span>
-                                <span className="value">
-                                  {formattedEnvironmentValue(
-                                    source === "secret_variables"
-                                      ? "****"
-                                      : value
-                                  )}
-                                </span>
-                              </div>
-                            ))}
-                          </>
-                        ) : (
-                          <>This environment group has no variables</>
-                        )}
-                      </GroupEnvPreview>
-                    </SidebarSection>
-                  </>
-                )}
-              </GroupModalSections>
-              <Spacer y={1} />
-
-              <Spacer y={1} />
-            </>
-          ) : loading ? (
-            <LoadingWrapper>
-              <Loading />
-            </LoadingWrapper>
-          ) : (
-            <Text>No selectable Env Groups</Text>
-          )}
-        </ScrollableContainer>
-      </ColumnContainer>
-      <SubmitButtonContainer>
-        <Button onClick={onSubmit} disabled={!selectedEnvGroup}>
-          Load Env Group
-        </Button>
-      </SubmitButtonContainer>
-    </Modal>
-  );
-};
-
-export default withRouter(EnvGroupModal);
-
-const LoadingWrapper = styled.div`
-  height: 150px;
-`;
-const Placeholder = styled.div`
-  width: 100%;
-  height: 150px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #aaaabb;
-  font-size: 13px;
-`;
-
-const EnvGroupRow = styled.div<{ lastItem?: boolean; isSelected: boolean }>`
-  display: flex;
-  width: 100%;
-  font-size: 13px;
-  border-bottom: 1px solid
-    ${(props) => (props.lastItem ? "#00000000" : "#606166")};
-  color: #ffffff;
-  user-select: none;
-  align-items: center;
-  padding: 10px 0px;
-  cursor: pointer;
-  background: ${(props) => (props.isSelected ? "#ffffff11" : "")};
-  :hover {
-    background: #ffffff11;
-  }
-
-  > img,
-  i {
-    width: 16px;
-    height: 18px;
-    margin-left: 12px;
-    margin-right: 12px;
-    font-size: 20px;
-  }
-`;
-const EnvGroupList = styled.div`
-  width: 100%;
-  border-radius: 3px;
-  background: #ffffff11;
-  border: 1px solid #ffffff44;
-  overflow-y: auto;
-`;
-
-const SidebarSection = styled.section<{ $expanded?: boolean }>`
-  height: 100%;
-  overflow-y: auto;
-  ${(props) =>
-    props.$expanded &&
-    css`
-      grid-column: span 2;
-    `}
-`;
-
-const GroupEnvPreview = styled.pre`
-  font-family: monospace;
-  margin: 0 0 10px 0;
-  white-space: pre-line;
-  word-break: break-word;
-  user-select: text;
-  .key {
-    color: white;
-  }
-  .value {
-    color: #3a48ca;
-  }
-`;
-const GroupModalSections = styled.div`
-  margin-top: 20px;
-  width: 100%;
-  height: 100%;
-  display: grid;
-  gap: 10px;
-  grid-template-columns: 1fr 1fr;
-  max-height: 365px;
-`;
-const ColumnContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-`;
-
-const ScrollableContainer = styled.div`
-  flex: 1;
-  overflow-y: auto;
-  max-height: 300px;
-`;
-
-const SubmitButtonContainer = styled.div`
-  margin-top: 10px;
-  text-align: right;
-`;

+ 0 - 332
dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx

@@ -1,332 +0,0 @@
-import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import EnvGroupArrayStacks from "main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks";
-import React, { useContext, useEffect, useRef, useState } from "react";
-import styled, { keyframes } from "styled-components";
-import Text from "components/porter/Text";
-import Error from "components/porter/Error";
-import sliders from "assets/sliders.svg";
-import EnvGroupModal from "./EnvGroupModal";
-import ExpandableEnvGroup from "./ExpandableEnvGroup";
-import {
-  PopulatedEnvGroup,
-  PartialEnvGroup,
-  type NewPopulatedEnvGroup,
-} from "../../../../../components/porter-form/types";
-import _, { isObject, differenceBy, omit } from "lodash";
-import api from "../../../../../shared/api";
-import { Context } from "../../../../../shared/Context";
-import yaml from "js-yaml";
-
-type EnvVariablesTabProps = {
-  envVars: any;
-  setEnvVars: (x: any) => void;
-  status: React.ReactNode;
-  updatePorterApp: any;
-  syncedEnvGroups: NewPopulatedEnvGroup[];
-  setSyncedEnvGroups: (values: NewPopulatedEnvGroup[]) => void;
-  clearStatus: () => void;
-  appData: any;
-  deletedEnvGroups: NewPopulatedEnvGroup[];
-  setShowUnsavedChangesBanner: (x: boolean) => void;
-  setDeletedEnvGroups: (values: NewPopulatedEnvGroup[]) => void;
-}
-
-export const EnvVariablesTab: React.FC<EnvVariablesTabProps> = ({
-  envVars,
-  setEnvVars,
-  setShowUnsavedChangesBanner,
-  status,
-  updatePorterApp,
-  syncedEnvGroups,
-  setSyncedEnvGroups,
-  deletedEnvGroups,
-  setDeletedEnvGroups,
-  clearStatus,
-  appData,
-}) => {
-  const [hovered, setHovered] = useState(false);
-
-  const [showEnvModal, setShowEnvModal] = useState(false);
-  const [envGroups, setEnvGroups] = useState<any>([]);
-  const { currentCluster, currentProject } = useContext(Context);
-
-  const [values, setValues] = React.useState<string>(
-    yaml.dump(appData.chart.config)
-  );
-  const initialMount = useRef(true);
-
-  useEffect(() => {
-    if (initialMount.current) {
-      initialMount.current = false;
-    } else {
-      setShowUnsavedChangesBanner(true);
-      setEnvVars(envVars);
-    }
-  }, [envVars]);
-  useEffect(() => {
-    updateEnvGroups();
-  }, []);
-
-  const updateEnvGroups = async () => {
-    let populateEnvGroupsPromises: NewPopulatedEnvGroup[] = [];
-    try {
-      populateEnvGroupsPromises = await api
-        .getAllEnvGroups<NewPopulatedEnvGroup[]>(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        )
-        .then((res) => res?.data?.environment_groups);
-    } catch (error) {
-      return;
-    }
-
-    try {
-      const populatedEnvGroups = await Promise.all(populateEnvGroupsPromises);
-      setEnvGroups(populatedEnvGroups);
-      const filteredEnvGroups = populatedEnvGroups?.filter(
-        (envGroup) =>
-          envGroup.linked_applications &&
-          envGroup.linked_applications.includes(appData.chart.name)
-      );
-      setSyncedEnvGroups(filteredEnvGroups);
-    } catch (error) {
-      
-    }
-  };
-
-  const deleteEnvGroup = (envGroup: NewPopulatedEnvGroup) => {
-    setDeletedEnvGroups([...deletedEnvGroups, envGroup]);
-    setSyncedEnvGroups(
-      syncedEnvGroups?.filter((env) => env.name !== envGroup.name)
-    );
-  };
-  const maxEnvGroupsReached = syncedEnvGroups.length >= 4;
-
-  return (
-    <>
-      <Text size={16}>Environment variables</Text>
-      <Spacer y={0.5} />
-      <Text color="helper">Shared among all services.</Text>
-      <EnvGroupArrayStacks
-        key={envVars.length}
-        values={envVars}
-        setValues={(x: any) => {
-          if (status !== "") {
-            clearStatus();
-          }
-          setEnvVars(x);
-        }}
-        fileUpload={true}
-        syncedEnvGroups={syncedEnvGroups}
-      />
-
-      <>
-        <TooltipWrapper
-          onMouseOver={() => { setHovered(true); }}
-          onMouseOut={() => { setHovered(false); }}
-        >
-          <LoadButton
-            disabled={maxEnvGroupsReached}
-            onClick={() => { !maxEnvGroupsReached && setShowEnvModal(true); }}
-          >
-            <img src={sliders} /> Load from Env Group
-          </LoadButton>
-          <TooltipText visible={maxEnvGroupsReached && hovered}>
-            Max 4 Env Groups allowed
-          </TooltipText>
-        </TooltipWrapper>
-
-        {showEnvModal && (
-          <EnvGroupModal
-            setValues={(x: any) => {
-              if (status !== "") {
-                clearStatus();
-              }
-              setEnvVars(x);
-            }}
-            values={envVars}
-            closeModal={() => { setShowEnvModal(false); }}
-            syncedEnvGroups={syncedEnvGroups}
-            setSyncedEnvGroups={setSyncedEnvGroups}
-            namespace={appData.chart.namespace}
-          />
-        )}
-        {!!syncedEnvGroups?.length && (
-          <>
-            <Spacer y={0.5} />
-            <Text size={16}>Synced environment groups</Text>
-            {syncedEnvGroups?.map((envGroup: any) => {
-              return (
-                <ExpandableEnvGroup
-                  key={envGroup?.name}
-                  envGroup={envGroup}
-                  onDelete={() => {
-                    deleteEnvGroup(envGroup);
-                  }}
-                />
-              );
-            })}
-          </>
-        )}
-      </>
-
-      <Spacer y={0.5} />
-      <Button
-        onClick={() => {
-          updatePorterApp();
-        }}
-        status={status}
-        loadingText={"Updating..."}
-      >
-        Update app
-      </Button>
-      <Spacer y={0.5} />
-    </>
-  );
-};
-
-const AddRowButton = styled.div`
-  display: flex;
-  align-items: center;
-  width: 270px;
-  font-size: 13px;
-  color: #aaaabb;
-  height: 32px;
-  border-radius: 3px;
-  cursor: pointer;
-  background: #ffffff11;
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-`;
-
-const LoadButton = styled(AddRowButton)<{ disabled?: boolean }>`
-  background: ${(props) => (props.disabled ? "#aaaaaa55" : "none")};
-  border: 1px solid ${(props) => (props.disabled ? "#aaaaaa55" : "#ffffff55")};
-  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
-
-  > i {
-    color: ${(props) => (props.disabled ? "#aaaaaa44" : "#ffffff44")};
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  > img {
-    width: 14px;
-    margin-left: 10px;
-    margin-right: 12px;
-    opacity: ${(props) => (props.disabled ? "0.5" : "1")};
-  }
-`;
-
-type InputProps = {
-  disabled?: boolean;
-  width: string;
-  borderColor?: string;
-};
-
-const KeyInput = styled.input<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: 1px solid
-    ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
-  border-radius: 3px;
-  width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
-  padding: 5px 10px;
-  height: 35px;
-`;
-
-export const MultiLineInput = styled.textarea<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: 1px solid
-    ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
-  border-radius: 3px;
-  min-width: ${(props) => (props.width ? props.width : "270px")};
-  max-width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
-  padding: 8px 10px 5px 10px;
-  min-height: 35px;
-  max-height: 100px;
-  white-space: nowrap;
-
-  ::-webkit-scrollbar {
-    width: 8px;
-    :horizontal {
-      height: 8px;
-    }
-  }
-
-  ::-webkit-scrollbar-corner {
-    width: 10px;
-    background: #ffffff11;
-    color: white;
-  }
-
-  ::-webkit-scrollbar-track {
-    width: 10px;
-    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-  }
-
-  ::-webkit-scrollbar-thumb {
-    background-color: darkgrey;
-    outline: 1px solid slategrey;
-  }
-`;
-
-const fadeIn = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-const TooltipWrapper = styled.div`
-  position: relative;
-  display: inline-block;
-`;
-
-const TooltipText = styled.span<{ visible: boolean }>`
-  visibility: ${(props) => (props.visible ? "visible" : "hidden")};
-  width: 240px;
-  color: #fff;
-  text-align: center;
-  padding: 5px 0;
-  border-radius: 6px;
-  position: absolute;
-  z-index: 1;
-  bottom: 100%;
-  left: 50%;
-  margin-left: -120px;
-  opacity: ${(props) => (props.visible ? "1" : "0")};
-  transition: opacity 0.3s;
-  font-size: 12px;
-`;

+ 0 - 262
dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx

@@ -1,262 +0,0 @@
-import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import React, { useEffect, useState } from "react";
-import styled, { keyframes } from "styled-components";
-import { type PopulatedEnvGroup } from "components/porter-form/types";
-import _, { isObject, differenceBy, omit } from "lodash";
-
-
-const ExpandableEnvGroup: React.FC<{
-  envGroup: PopulatedEnvGroup;
-  onDelete: () => void;
-}> = ({ envGroup, onDelete }) => {
-  const [isExpanded, setIsExpanded] = useState(false);
-  return (
-    <>
-      <StyledCard>
-        <Flex>
-          <ContentContainer>
-            <EventInformation>
-              <EventName>{envGroup.name}</EventName>
-            </EventInformation>
-          </ContentContainer>
-          <ActionContainer>
-            <ActionButton onClick={() => { onDelete(); }}>
-              <span className="material-icons">delete</span>
-            </ActionButton>
-            <ActionButton onClick={() => { setIsExpanded((prev) => !prev); }}>
-              <i className="material-icons">
-                {isExpanded ? "arrow_drop_up" : "arrow_drop_down"}
-              </i>
-            </ActionButton>
-          </ActionContainer>
-        </Flex>
-        {isExpanded && (
-          <>
-            {isObject(envGroup.variables) || isObject(envGroup.secret_variables) ? (
-              <>
-                {[
-                  ...Object.entries(envGroup?.variables || {}).map(([key, value]) => ({
-                    key,
-                    value,
-                    source: 'variables',
-                  })),
-                  ...Object.entries(envGroup?.secret_variables || {}).map(([key, value]) => ({
-                    key,
-                    value,
-                    source: 'secret_variables',
-                  })),
-                ].map(({ key, value, source }, i: number) => {
-                  // Preprocess non-string env values set via raw Helm values
-                  if (typeof value === "object") {
-                    value = JSON.stringify(value);
-                  } else {
-                    value = String(value);
-                  }
-
-                  return (
-                    <InputWrapper key={i}>
-                      <KeyInput placeholder="ex: key" width="270px" value={key} disabled />
-                      <Spacer x={0.5} inline />
-                      {source === 'secret_variables' ? (
-                        <KeyInput
-                          placeholder="ex: value"
-                          width="270px"
-                          value={value}
-                          disabled
-                          type="password"
-                        />
-                      ) : (
-                        <MultiLineInput
-                          placeholder="ex: value"
-                          width="270px"
-                          value={value}
-                          disabled
-                          rows={value?.split("\n").length}
-                          spellCheck={false}
-                        ></MultiLineInput>
-                      )}
-                    </InputWrapper>
-                  );
-                })}
-              </>
-            ) : (
-              <NoVariablesTextWrapper>
-                This env group has no variables yet
-              </NoVariablesTextWrapper>
-            )}
-          </>
-        )}
-      </StyledCard>
-    </>
-  );
-};
-
-export default ExpandableEnvGroup;
-const InputWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-top: 5px;
-`;
-
-type InputProps = {
-  disabled?: boolean;
-  width: string;
-  borderColor?: string;
-};
-
-const KeyInput = styled.input<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: 1px solid
-    ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
-  border-radius: 3px;
-  width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
-  padding: 5px 10px;
-  height: 35px;
-`;
-
-export const MultiLineInput = styled.textarea<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: 1px solid
-    ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
-  border-radius: 3px;
-  min-width: ${(props) => (props.width ? props.width : "270px")};
-  max-width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
-  padding: 8px 10px 5px 10px;
-  min-height: 35px;
-  max-height: 100px;
-  white-space: nowrap;
-
-  ::-webkit-scrollbar {
-    width: 8px;
-    :horizontal {
-      height: 8px;
-    }
-  }
-
-  ::-webkit-scrollbar-corner {
-    width: 10px;
-    background: #ffffff11;
-    color: white;
-  }
-
-  ::-webkit-scrollbar-track {
-    width: 10px;
-    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-  }
-
-  ::-webkit-scrollbar-thumb {
-    background-color: darkgrey;
-    outline: 1px solid slategrey;
-  }
-`;
-
-const Label = styled.div`
-  color: #ffffff;
-  margin-bottom: 10px;
-`;
-
-const StyledInputArray = styled.div`
-  margin-bottom: 15px;
-  margin-top: 22px;
-`;
-
-const fadeIn = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-const StyledCard = styled.div`
-  border: 1px solid #ffffff44;
-  background: #ffffff11;
-  margin-bottom: 5px;
-  border-radius: 8px;
-  margin-top: 15px;
-  padding: 10px 14px;
-  overflow: hidden;
-  font-size: 13px;
-  animation: ${fadeIn} 0.5s;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  height: 25px;
-  align-items: center;
-  justify-content: space-between;
-`;
-
-const ContentContainer = styled.div`
-  display: flex;
-  height: 40px;
-  width: 100%;
-  align-items: center;
-`;
-
-const EventInformation = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: space-around;
-  height: 100%;
-`;
-
-const EventName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-`;
-
-const ActionContainer = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  height: 100%;
-`;
-
-const ActionButton = styled.button`
-  position: relative;
-  border: none;
-  background: none;
-  color: white;
-  padding: 5px;
-  width: 30px;
-  height: 30px;
-  margin-left: 5px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 50%;
-  cursor: pointer;
-  color: #aaaabb;
-  border: 1px solid #ffffff00;
-
-  :hover {
-    background: #ffffff11;
-    border: 1px solid #ffffff44;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-`;
-
-const NoVariablesTextWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff99;
-`;

+ 0 - 221
dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx

@@ -1,221 +0,0 @@
-import React, { useEffect, useState, useContext, useCallback } from "react";
-import { RouteComponentProps, useLocation, withRouter } from "react-router";
-import styled from "styled-components";
-
-import history from "assets/history.png";
-import loadingImg from "assets/loading.gif";
-import refresh from "assets/refresh.png";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import Error from "components/porter/Error";
-
-import Banner from "components/porter/Banner";
-import Loading from "components/Loading";
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Back from "components/porter/Back";
-import TabSelector from "components/TabSelector";
-import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
-import ConfirmOverlay from "components/porter/ConfirmOverlay";
-import Fieldset from "components/porter/Fieldset";
-import JobRuns from "../JobRuns";
-import ExpandedJobRun from "./ExpandedJobRun";
-
-type Props = RouteComponentProps & {
-  appName: string;
-  jobName: string;
-  goBack: () => void;
-};
-
-const ExpandedJob: React.FC<Props> = ({
-  appName,
-  jobName,
-  goBack,
-  ...props
-}) => {
-  const { currentCluster, currentProject, setCurrentError } = useContext(
-    Context
-  );
-  const [isLoading, setIsLoading] = useState(false);
-  const [error, setError] = useState(null);
-  const [expandedRun, setExpandedRun] = useState(null);
-
-  return (
-    <>
-      {isLoading && <Loading />}
-      {!isLoading && expandedRun && (
-        <ExpandedJobRun
-          currentChart={null}
-          jobRun={expandedRun}
-          onClose={() => setExpandedRun(null)}
-        />
-      )}
-      {!isLoading && !expandedRun && (
-        <StyledExpandedApp>
-          <Container row>
-            <Icon src={history} />
-            <Text size={21}>Run history for "{jobName}"</Text>
-          </Container>
-          <Spacer y={0.5} />
-          <Text color="#aaaabb66">
-            This job runs under the "{appName}" app.
-          </Text>
-          <Spacer y={1} />
-          {currentCluster?.id && currentProject?.id && (
-            <JobRuns
-              lastRunStatus="all"
-              namespace={`porter-stack-${appName}`}
-              sortType="Newest"
-              jobName={jobName}
-              setExpandedRun={(x: any) => setExpandedRun(x)}
-            />
-          )}
-        </StyledExpandedApp>
-      )}
-    </>
-  );
-};
-
-export default withRouter(ExpandedJob);
-
-const RefreshButton = styled.div`
-  color: #ffffff44;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  :hover {
-    color: #ffffff;
-    > img {
-      opacity: 1;
-    }
-  }
-
-  > img {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 11px;
-    margin-right: 10px;
-    opacity: 0.3;
-  }
-`;
-
-const Spinner = styled.img`
-  width: 15px;
-  height: 15px;
-  margin-right: 12px;
-  margin-bottom: -2px;
-`;
-
-const DarkMatter = styled.div<{ antiHeight?: string }>`
-  width: 100%;
-  margin-top: ${(props) => props.antiHeight || "-20px"};
-`;
-
-const TagWrapper = styled.div`
-  height: 20px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 6px;
-`;
-
-const BranchTag = styled.div`
-  height: 20px;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #ffffff22;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const BranchSection = styled.div`
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-`;
-
-const SmallIcon = styled.img<{ opacity?: string; height?: string }>`
-  height: ${(props) => props.height || "15px"};
-  opacity: ${(props) => props.opacity || 1};
-  margin-right: 10px;
-`;
-
-const BranchIcon = styled.img`
-  height: 14px;
-  opacity: 0.65;
-  margin-right: 5px;
-`;
-
-const Icon = styled.img`
-  height: 24px;
-  margin-right: 15px;
-`;
-
-const PlaceholderIcon = styled.img`
-  height: 13px;
-  margin-right: 12px;
-  opacity: 0.65;
-`;
-
-const Placeholder = styled.div`
-  width: 100%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  font-size: 13px;
-`;
-
-const StyledExpandedApp = styled.div`
-  width: 100%;
-  height: 100%;
-
-  animation: fadeIn 0.5s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-`;
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 8px;
-  margin-top: -1px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-const Dot = styled.div`
-  margin-right: 16px;
-`;
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-left: 3px;
-  margin-top: 22px;
-`;

+ 0 - 559
dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx

@@ -1,559 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import { get, isEmpty } from "lodash";
-import styled from "styled-components";
-
-import job from "assets/job.png";
-import leftArrow from "assets/left-arrow.svg";
-import KeyValueArray from "components/form-components/KeyValueArray";
-import Loading from "components/Loading";
-import TabRegion, { TabOption } from "components/TabRegion";
-import TitleSection from "components/TitleSection";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { ChartType } from "shared/types";
-import DeploymentType from "main/home/cluster-dashboard/expanded-chart/DeploymentType";
-import Logs from "../status/Logs";
-import { useRouting } from "shared/routing";
-import LogsSection, { InitLogData } from "main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection";
-import EventsTab from "main/home/cluster-dashboard/expanded-chart/events/EventsTab";
-import { getPodStatus } from "main/home/cluster-dashboard/expanded-chart/deploy-status-section/util";
-import { capitalize } from "shared/string_utils";
-import { usePods } from "shared/hooks/usePods";
-import Container from "components/porter/Container";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-
-const readableDate = (s: string) => {
-  let ts = new Date(s);
-  let date = ts.toLocaleDateString();
-  let time = ts.toLocaleTimeString([], {
-    hour: "numeric",
-    minute: "2-digit",
-  });
-  return `${time} on ${date}`;
-};
-
-const getLatestPod = (pods: any[]) => {
-  if (!Array.isArray(pods)) {
-    return undefined;
-  }
-
-  return [...pods]
-    .sort((a: any, b: any) => {
-      if (!a?.metadata?.creationTimestamp) {
-        return 1;
-      }
-
-      if (!b?.metadata?.creationTimestamp) {
-        return -1;
-      }
-
-      return (
-        new Date(b?.metadata?.creationTimestamp).getTime() -
-        new Date(a?.metadata?.creationTimestamp).getTime()
-      );
-    })
-    .shift();
-};
-
-export const isRunning = (deleting: boolean, job: any, pod: any) => {
-  if (deleting) {
-    return false;
-  }
-
-  if (job.status?.succeeded >= 1) {
-    return false;
-  }
-
-  if (job.status?.conditions) {
-    if (job.status?.conditions[0]?.reason == "DeadlineExceeded") {
-      return false;
-    }
-  }
-
-  if (job.status?.failed >= 1) {
-    return false;
-  }
-
-  if (job.status?.active >= 1) {
-    // determine the status from the pod
-    return pod ? pod.status.startTime : false;
-  }
-
-  return true;
-};
-
-export const renderStatus = (
-  deleting: boolean,
-  job: any,
-  pod: any,
-  time?: string
-) => {
-  if (deleting) {
-    return <Status color="#cc3d42">Deleting</Status>;
-  }
-
-  if (job.status?.succeeded >= 1) {
-    if (time) {
-      return <Status color="#38a88a">Succeeded at {time}</Status>;
-    }
-
-    return <Status color="#38a88a">Succeeded</Status>;
-  }
-
-  if (job.status?.conditions) {
-    if (job.status?.conditions[0]?.reason == "DeadlineExceeded") {
-      return <Status color="#cc3d42">Timed Out</Status>;
-    }
-  }
-
-  if (job.status?.failed >= 1) {
-    return <Status color="#cc3d42">Failed</Status>;
-  }
-
-  if (job.status?.active >= 1) {
-    // determine the status from the pod
-    return pod ? (
-      <Status color="#ffffff11">{capitalize(getPodStatus(pod?.status))}</Status>
-    ) : (
-      <Status color="#ffffff11">Running</Status>
-    );
-  }
-
-  return <Status color="#ffffff11">Running</Status>;
-};
-
-type ExpandedJobRunTabs = "events" | "logs" | "config" | string;
-
-const ExpandedJobRun = ({
-  currentChart,
-  jobRun,
-  onClose,
-}: {
-  currentChart: ChartType;
-  jobRun: any;
-  onClose: () => void;
-}) => {
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-  const [currentTab, setCurrentTab] = useState<ExpandedJobRunTabs>(
-    currentCluster.agent_integration_enabled ? "events" : "logs"
-  );
-  const { pushQueryParams } = useRouting();
-  const [useDeprecatedLogs, setUseDeprecatedLogs] = useState(false);
-
-  const [pods, isLoading] = usePods({
-    project_id: currentProject.id,
-    cluster_id: currentCluster.id,
-    namespace: jobRun.metadata?.namespace,
-    selectors: [`job-name=${jobRun.metadata?.name}`],
-    controller_kind: "job",
-    controller_name: jobRun.metadata?.name,
-    subscribed: true,
-  });
-
-  let chart = currentChart;
-  let run = jobRun;
-
-  useEffect(() => {
-    return () => {
-      pushQueryParams({}, ["job"]);
-    };
-  }, []);
-
-  const renderConfigSection = (job: any) => {
-    let commandString = job?.spec?.template?.spec?.containers[0]?.command?.join(
-      " "
-    );
-    let envArray = job?.spec?.template?.spec?.containers[0]?.env;
-    let envObject = {} as any;
-    envArray &&
-      envArray.forEach((env: any, i: number) => {
-        const secretName = get(env, "valueFrom.secretKeyRef.name");
-        envObject[env.name] = secretName
-          ? `PORTERSECRET_${secretName}`
-          : env.value;
-      });
-
-    // Handle no config to show
-    if (!commandString && isEmpty(envObject)) {
-      return <Placeholder>No config was found.</Placeholder>;
-    }
-
-    let tag = job.spec.template.spec.containers[0].image.split(":")[1];
-    return (
-      <ConfigSection>
-        {commandString ? (
-          <>
-            Command: <Command>{commandString}</Command>
-          </>
-        ) : (
-          <DarkMatter size="-18px" />
-        )}
-        <Row>
-          Image Tag: <Command>{tag}</Command>
-        </Row>
-        {!isEmpty(envObject) && (
-          <>
-            <KeyValueArray
-              envLoader={true}
-              values={envObject}
-              label="Environment variables:"
-              disabled={true}
-            />
-            <DarkMatter />
-          </>
-        )}
-      </ConfigSection>
-    );
-  };
-
-  const renderEventsSection = () => {
-    return (
-      <EventsTab
-        currentChart={currentChart}
-        overridingJobName={jobRun.metadata?.name}
-        setLogData={() => setCurrentTab("logs")}
-      />
-    );
-  };
-
-  const renderLogsSection = () => {
-    if (useDeprecatedLogs || !currentCluster.agent_integration_enabled) {
-      return (
-        <JobLogsWrapper>
-          <Logs
-            selectedPod={pods[0]}
-            podError={!pods[0] ? "Pod no longer exists." : ""}
-            rawText={true}
-          />
-        </JobLogsWrapper>
-      );
-    }
-
-    let initData: InitLogData = {};
-
-    if (run.status.completionTime) {
-      initData.timestamp = run.status.completionTime;
-    }
-
-    return (
-      <JobLogsWrapper>
-        <DeprecatedWarning>
-          Not seeing your logs? Switch back to{" "}
-          <DeprecatedSelect
-            onClick={() => {
-              setUseDeprecatedLogs(true);
-            }}
-          >
-            {" "}
-            deprecated logging.
-          </DeprecatedSelect>
-        </DeprecatedWarning>
-        <LogsSection
-          isFullscreen={false}
-          setIsFullscreen={() => { }}
-          overridingPodSelector={pods[0]?.metadata?.name || jobRun.metadata?.name}
-          currentChart={currentChart}
-          initData={initData}
-        />
-      </JobLogsWrapper>
-    );
-  };
-
-  if (isLoading) {
-    return <Loading />;
-  }
-
-  let options: TabOption[] = [];
-
-  if (currentCluster.agent_integration_enabled) {
-    options.push({
-      label: "Events",
-      value: "events",
-    });
-  }
-
-  options.push(
-    {
-      label: "Logs",
-      value: "logs",
-    },
-    {
-      label: "Config",
-      value: "config",
-    }
-  );
-
-  return (
-    <StyledExpandedChart>
-      <BreadcrumbRow>
-        <Breadcrumb onClick={onClose}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <Container row>
-          <Icon src={job} />
-          <Text size={21}>
-            {jobRun.metadata?.name.split('-').slice(1, -2).join('-')}
-          </Text>
-          <Spacer inline width="10px" />
-          <Text size={21} color="#aaaabb66">
-            at {run.status.completionTime ? readableDate(run.status.completionTime) : ""}
-          </Text>
-        </Container>
-        <Spacer y={0.5} />
-        <InfoWrapper>
-          <LastDeployed>
-            {renderStatus(
-              false,
-              run,
-              pods[0],
-              run.status.completionTime
-                ? readableDate(run.status.completionTime)
-                : ""
-            )}
-          </LastDeployed>
-        </InfoWrapper>
-      </HeaderWrapper>
-      <Spacer y={1} />
-      <BodyWrapper>
-        <TabRegion
-          currentTab={currentTab}
-          setCurrentTab={(newTab: string) => {
-            setCurrentTab(newTab);
-          }}
-          options={options}
-        >
-          {currentTab === "events" && renderEventsSection()}
-          {currentTab === "logs" && renderLogsSection()}
-          {currentTab === "config" && <>{renderConfigSection(run)}</>}
-        </TabRegion>
-      </BodyWrapper>
-    </StyledExpandedChart>
-  );
-};
-
-export default ExpandedJobRun;
-
-const Icon = styled.img`
-  height: 24px;
-  margin-right: 15px;
-`;
-
-const ArrowIcon = styled.img`
-  width: 15px;
-  margin-right: 8px;
-  opacity: 50%;
-`;
-
-const BreadcrumbRow = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: flex-start;
-`;
-
-const Breadcrumb = styled.div`
-  color: #aaaabb88;
-  font-size: 13px;
-  margin-bottom: 15px;
-  display: flex;
-  align-items: center;
-  margin-top: -10px;
-  z-index: 999;
-  padding: 5px;
-  padding-right: 7px;
-  border-radius: 5px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Wrap = styled.div`
-  z-index: 999;
-`;
-
-const Row = styled.div`
-  margin-top: 20px;
-`;
-
-const DarkMatter = styled.div<{ size?: string }>`
-  width: 100%;
-  margin-bottom: ${(props) => props.size || "-13px"};
-`;
-
-const Command = styled.span`
-  font-family: monospace;
-  color: #aaaabb;
-  margin-left: 7px;
-`;
-
-const ConfigSection = styled.div`
-  padding: 20px 30px 30px;
-  font-size: 13px;
-  font-weight: 500;
-  width: 100%;
-  border-radius: 8px;
-  background: #ffffff08;
-`;
-
-const JobLogsWrapper = styled.div`
-  min-height: 450px;
-  height: fit-content;
-  width: 100%;
-  border-radius: 8px;
-`;
-
-const Status = styled.div<{ color: string }>`
-  padding: 5px 10px;
-  background: ${(props) => props.color};
-  font-size: 13px;
-  border-radius: 3px;
-  height: 25px;
-  color: #ffffff;
-  margin-bottom: -3px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-`;
-
-const Gray = styled.div`
-  color: #ffffff44;
-  margin-left: 15px;
-  font-weight: 400;
-  font-size: 18px;
-`;
-
-const BackButton = styled.div`
-  position: absolute;
-  top: 0px;
-  right: 0px;
-  display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
-`;
-
-const Placeholder = styled.div`
-  min-height: 400px;
-  height: 50vh;
-  padding: 30px;
-  padding-bottom: 70px;
-  font-size: 13px;
-  color: #ffffff44;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-`;
-
-const BodyWrapper = styled.div`
-  position: relative;
-  overflow: hidden;
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  height: 20px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-left: 0;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const TagWrapper = styled.div`
-  height: 25px;
-  font-size: 12px;
-  display: flex;
-  margin-left: 20px;
-  margin-bottom: -3px;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff44;
-  border: 1px solid #ffffff44;
-  border-radius: 3px;
-  padding-left: 5px;
-  background: #26282e;
-`;
-
-const NamespaceTag = styled.div`
-  height: 100%;
-  margin-left: 6px;
-  color: #aaaabb;
-  background: #43454a;
-  border-radius: 3px;
-  font-size: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0px 6px;
-  padding-left: 7px;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
-`;
-
-const StyledExpandedChart = styled.div`
-  width: 100%;
-  z-index: 0;
-  animation: fadeIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  display: flex;
-  overflow-y: auto;
-  padding-bottom: 120px;
-  flex-direction: column;
-  overflow: visible;
-
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const DeprecatedWarning = styled.div`
-  font-size: 12px;
-  color: #ccc;
-  text-align: right;
-  width: 100%;
-  margin-bottom: 20px;
-`;
-
-const DeprecatedSelect = styled.span`
-  cursor: pointer;
-  color: #949effff;
-`;

+ 0 - 45
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx

@@ -1,45 +0,0 @@
-import Text from "components/porter/Text";
-import React from "react";
-import styled from "styled-components";
-import { GenericFilter } from "./types";
-import Spacer from "components/porter/Spacer";
-import Select from "components/porter/Select";
-
-type Props = {
-    filter: GenericFilter;
-    selectedValue: string;
-};
-
-const LogFilterComponent: React.FC<Props> = ({
-    filter,
-    selectedValue,
-}) => {
-    return (
-        <StyledLogFilterComponent>
-            <Text>{filter.displayName}</Text>
-            <Spacer inline x={0.5} />
-            <Select
-                options={[filter.default, ...filter.options]}
-                height={"30px"}
-                value={selectedValue}
-                setValue={filter.setValue}
-            />
-        </StyledLogFilterComponent>
-    );
-};
-
-export default LogFilterComponent;
-
-const StyledLogFilterComponent = styled.div`
-    display: flex;
-    align-items: center;
-    animation: fadeIn 0.3s 0s;
-    @keyframes fadeIn {
-    from {
-        opacity: 0;
-    }
-    to {
-        opacity: 1;
-    }
-    }
-`;

+ 0 - 72
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterContainer.tsx

@@ -1,72 +0,0 @@
-import React from "react";
-
-import styled from "styled-components";
-import filterOutline from "assets/filter-outline-icon.svg";
-import filterOutlineWhite from "assets/filter-outline-white.svg";
-import { GenericFilter, FilterName } from "./types";
-import Icon from "components/porter/Icon";
-import Spacer from "components/porter/Spacer";
-import LogFilterComponent from "./LogFilterComponent";
-
-type Props = {
-    filters: GenericFilter[];
-    selectedFilterValues: Record<FilterName, string>;
-};
-
-const LogFilterContainer: React.FC<Props> = (props) => {
-    const getIcon = () => {
-        if (props.filters.every((filter) => GenericFilter.isDefault(filter, props.selectedFilterValues[filter.name]))) {
-            return filterOutline;
-        }
-        return filterOutlineWhite;
-    }
-
-    const renderFilters = () => {
-        return (
-            <FiltersContainer>
-                {props.filters.map((filter, i) => {
-                    return <LogFilterComponent
-                        key={i}
-                        filter={filter}
-                        selectedValue={props.selectedFilterValues[filter.name]}
-                    />
-                })}
-            </FiltersContainer>
-        )
-    }
-
-    return (
-        <StyledLogFilterContainer>
-            <Icon src={getIcon()} height={"16px"} />
-            <Spacer inline x={1} />
-            <Bar />
-            <Spacer inline x={1} />
-            {renderFilters()}
-        </StyledLogFilterContainer>
-    );
-};
-
-export default LogFilterContainer;
-
-const Bar = styled.div`
-  width: 1px;
-  height: calc(18px);
-  background: #494b4f;
-`;
-
-const StyledLogFilterContainer = styled.div`
-  font-size: 13px;
-  padding: 10px;
-  background: ${(props) => props.theme.fg};
-  border-radius: 5px;
-  display: flex;
-  align-items: center;
-  border: 1px solid #494b4f;
-  width: fit-content;
-`;
-
-const FiltersContainer = styled.div`
-    display: flex;
-    flex-wrap: wrap;
-    gap: 10px;
-`

+ 0 - 572
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx

@@ -1,572 +0,0 @@
-import React, {
-  useCallback,
-  useContext,
-  useEffect,
-  useRef,
-  useState,
-} from "react";
-
-import styled from "styled-components";
-
-import spinner from "assets/loading.gif";
-import { Context } from "shared/Context";
-import api from "shared/api";
-import { getPodSelectorFromServiceName, useLogs } from "./utils";
-import { Direction, GenericFilterOption, GenericFilter, FilterName, LogFilterQueryParamOpts } from "./types";
-import dayjs, { Dayjs } from "dayjs";
-import Loading from "components/Loading";
-import _ from "lodash";
-import { ChartType } from "shared/types";
-import Banner from "components/porter/Banner";
-import LogSearchBar from "components/LogSearchBar";
-import LogQueryModeSelectionToggle from "components/LogQueryModeSelectionToggle";
-import Fieldset from "components/porter/Fieldset";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import Container from "components/porter/Container";
-import Button from "components/porter/Button";
-import { Service } from "../../new-app-flow/serviceTypes";
-import StyledLogs from "./StyledLogs";
-import Filter from "components/porter/Filter";
-
-type Props = {
-  appName: string;
-  currentChart: ChartType;
-  services?: Service[];
-  timeRange?: {
-    startTime?: Dayjs;
-    endTime?: Dayjs;
-  };
-  showFilter?: boolean;
-  filterOpts?: LogFilterQueryParamOpts;
-};
-
-const LogSection: React.FC<Props> = ({
-  currentChart,
-  services,
-  timeRange,
-  appName,
-  filterOpts,
-  showFilter = true,
-}) => {
-  const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
-  const { currentProject, currentCluster } = useContext(Context);
-  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
-  const [enteredSearchText, setEnteredSearchText] = useState("");
-  const [searchText, setSearchText] = useState("");
-  const [selectedDate, setSelectedDate] = useState<Date | undefined>(timeRange?.startTime != null ? timeRange.startTime.toDate() : undefined);
-  const [notification, setNotification] = useState<string>();
-
-  const [hasPorterAgent, setHasPorterAgent] = useState(true);
-  const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
-  const [isLoading, setIsLoading] = useState(true);
-  const [logsError, setLogsError] = useState<string | undefined>(undefined);
-
-  const [selectedFilterValues, setSelectedFilterValues] = useState<Record<FilterName, string>>({
-    revision: filterOpts?.revision ?? GenericFilter.getDefaultOption("revision").value,
-    output_stream: filterOpts?.output_stream ?? GenericFilter.getDefaultOption("output_stream").value,
-    pod_name: getPodSelectorFromServiceName(filterOpts?.service, services) ?? GenericFilter.getDefaultOption("pod_name").value,
-    service_name: filterOpts?.service ?? GenericFilter.getDefaultOption("service_name").value,
-  });
-
-  const createVersionOptions = (number: number) => {
-    return Array.from({ length: number }, (_, index) => {
-      const version = index + 1;
-      const label = version === number ? `Version ${version} (latest)` : `Version ${version}`;
-      const value = version.toString();
-      return GenericFilterOption.of(label, value);
-    }).reverse().slice(0, 3);
-  }
-
-  const isAgentVersionUpdated = (agentImage: string | undefined) => {
-    if (agentImage == null) {
-      return false;
-    }
-    const version = agentImage.split(":").pop();
-    if (version === "dev") {
-      return true;
-    }
-    //make sure version is above v3.1.3
-    if (version == null) {
-      return false;
-    }
-    const versionParts = version.split(".");
-    if (versionParts.length < 3) {
-      return false;
-    }
-    const major = parseInt(versionParts[0]);
-    const minor = parseInt(versionParts[1]);
-    const patch = parseInt(versionParts[2]);
-    if (major < 3) {
-      return false;
-    } else if (major > 3) {
-      return true;
-    }
-    if (minor < 1) {
-      return false;
-    } else if (minor > 1) {
-      return true;
-    }
-    return patch >= 4;
-  }
-
-  const [filters, setFilters] = useState<GenericFilter[]>(showFilter ? [
-    {
-      name: "pod_name",
-      displayName: "Service",
-      default: GenericFilter.getDefaultOption("pod_name"),
-      options: services?.map(s => {
-        return GenericFilterOption.of(s.name, `${s.name}-${s.type == "worker" ? "wkr" : s.type}`)
-      }) ?? [],
-      setValue: (value: string) => {
-        setSelectedFilterValues((s) => ({
-          ...s,
-          pod_name: value,
-        }));
-      }
-    },
-    {
-      name: "revision",
-      displayName: "Version",
-      default: GenericFilter.getDefaultOption("revision"),
-      options: currentChart != null ? createVersionOptions(currentChart.version) : [],
-      setValue: (value: string) => {
-        setSelectedFilterValues((s) => ({
-          ...s,
-          revision: value,
-        }));
-      }
-    },
-    {
-      name: "output_stream",
-      displayName: "Output Stream",
-      default: GenericFilter.getDefaultOption("output_stream"),
-      options: [
-        GenericFilterOption.of('stdout', 'stdout'),
-        GenericFilterOption.of("stderr", "stderr"),
-      ],
-      setValue: (value: string) => {
-        setSelectedFilterValues((s) => ({
-          ...s,
-          output_stream: value,
-        }));
-      }
-    },
-  ] : []);
-
-  const notify = (message: string) => {
-    setNotification(message);
-
-    setTimeout(() => {
-      setNotification(undefined);
-    }, 5000);
-  };
-
-  const { logs, refresh, moveCursor, paginationInfo } = useLogs(
-    selectedFilterValues,
-    appName,
-    currentChart == null ? "" : currentChart.namespace,
-    enteredSearchText,
-    notify,
-    currentChart,
-    setIsLoading,
-    selectedDate,
-    timeRange,
-  );
-
-  useEffect(() => {
-    if (!isLoading && scrollToBottomRef.current && scrollToBottomEnabled) {
-      const scrollPosition = scrollToBottomRef.current.offsetTop + scrollToBottomRef.current.offsetHeight - window.innerHeight;
-      scrollToBottomRef.current.scrollIntoView({
-        behavior: "smooth",
-        top: scrollPosition,
-      });
-    }
-  }, [isLoading, logs, scrollToBottomRef, scrollToBottomEnabled]);
-
-  const onLoadPrevious = useCallback(() => {
-    if (!selectedDate) {
-      setSelectedDate(dayjs(logs[0].timestamp).toDate());
-      return;
-    }
-
-    moveCursor(Direction.backward);
-  }, [logs, selectedDate]);
-
-  const setSelectedDateIfUndefined = () => {
-    if (selectedDate == null) {
-      setSelectedDate(dayjs().toDate());
-    }
-  };
-
-  const renderContents = () => {
-    return (
-      <>
-        <FlexRow>
-          <Flex>
-            <LogSearchBar
-              searchText={searchText}
-              setSearchText={setSearchText}
-              setEnteredSearchText={setEnteredSearchText}
-              setSelectedDate={setSelectedDateIfUndefined}
-            />
-            <Spacer inline width="10px" />
-            <LogQueryModeSelectionToggle
-              selectedDate={selectedDate}
-              setSelectedDate={setSelectedDate}
-            />
-          </Flex>
-          <Flex>
-            {showFilter && (
-              <Filter
-                filters={filters}
-                selectedFilterValues={selectedFilterValues}
-              />
-            )}
-            <Spacer inline width="10px" />
-            <ScrollButton onClick={() => setScrollToBottomEnabled((s) => !s)}>
-              <Checkbox checked={scrollToBottomEnabled}>
-                <i className="material-icons">done</i>
-              </Checkbox>
-              Scroll to bottom
-            </ScrollButton>
-            <Spacer inline width="10px" />
-            <ScrollButton
-              onClick={() => {
-                refresh();
-              }}
-            >
-              <i className="material-icons">autorenew</i>
-              Refresh
-            </ScrollButton>
-          </Flex>
-        </FlexRow>
-        <Spacer y={0.5} />
-        <LogsSectionWrapper>
-          <StyledLogsSection>
-            {isLoading && <Loading message="Waiting for logs..." />}
-            {!isLoading && logs.length !== 0 && (
-              <>
-                <LoadMoreButton
-                  active={
-                    logs.length !== 0 && paginationInfo.previousCursor !== null
-                  }
-                  role="button"
-                  onClick={onLoadPrevious}
-                >
-                  Load Previous
-                </LoadMoreButton>
-                <StyledLogs
-                  logs={logs}
-                  appName={appName}
-                  filters={filters}
-                  services={services}
-                />
-                <LoadMoreButton
-                  active={selectedDate && logs.length !== 0}
-                  role="button"
-                  onClick={() => moveCursor(Direction.forward)}
-                >
-                  Load more
-                </LoadMoreButton>
-              </>
-            )}
-            {!isLoading && logs.length === 0 && selectedDate != null && (
-              <Message>
-                No logs found for this time range.
-                <Highlight onClick={() => setSelectedDate(undefined)}>
-                  <i className="material-icons">autorenew</i>
-                  Reset
-                </Highlight>
-              </Message>
-            )}
-            {!isLoading && logs.length === 0 && selectedDate == null && (
-              <Loading message="Waiting for logs..." />
-            )}
-            <div ref={scrollToBottomRef} />
-          </StyledLogsSection>
-          <NotificationWrapper
-            key={JSON.stringify(logs)}
-            active={!!notification}
-          >
-            <Banner>{notification}</Banner>
-          </NotificationWrapper>
-        </LogsSectionWrapper>
-      </>
-    );
-  };
-
-  useEffect(() => {
-    // determine if the agent is installed properly - if not, start by render upgrade screen
-    checkForAgent();
-  }, []);
-
-  useEffect(() => {
-    if (!isPorterAgentInstalling) {
-      return;
-    }
-
-    const checkForAgentInterval = setInterval(checkForAgent, 3000);
-
-    return () => clearInterval(checkForAgentInterval);
-  }, [isPorterAgentInstalling]);
-
-  const checkForAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    if (!project_id || !cluster_id) {
-      return;
-    }
-
-    try {
-      const res = await api.detectPorterAgent("<token>", {}, { project_id, cluster_id });
-
-      setHasPorterAgent(true);
-
-      const agentImage = res.data?.image;
-      if (!isAgentVersionUpdated(agentImage)) {
-        setFilters([
-          {
-            name: "pod_name",
-            displayName: "Service",
-            default: GenericFilter.getDefaultOption("pod_name"),
-            options: services?.map(s => {
-              return GenericFilterOption.of(s.name, `${s.name}-${s.type == "worker" ? "wkr" : s.type}`)
-            }) ?? [],
-            setValue: (value: string) => {
-              setSelectedFilterValues((s) => ({
-                ...s,
-                pod_name: value,
-              }));
-            }
-          },
-        ])
-      }
-    } catch (err) {
-      if (err.response?.status === 404) {
-        setHasPorterAgent(false);
-      }
-    }
-  };
-
-  const installAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    setIsPorterAgentInstalling(true);
-
-    api
-      .installPorterAgent("<token>", {}, { project_id, cluster_id })
-      .then()
-      .catch((err) => {
-        setIsPorterAgentInstalling(false);
-        console.log(err);
-      });
-  };
-
-  const triggerInstall = () => {
-    installAgent();
-  };
-
-  return isPorterAgentInstalling ? (
-    <Fieldset>
-      <Container row>
-        <Spinner src={spinner} />
-        <Spacer inline x={1} />
-        <Text color="helper">The Porter agent is being installed . . .</Text>
-      </Container>
-    </Fieldset>
-  ) : !hasPorterAgent ? (
-    <Fieldset>
-      <Text size={16}>We couldn't detect the Porter agent on your cluster</Text>
-      <Spacer y={0.5} />
-      <Text color="helper">
-        In order to use the Logs tab, you need to install the Porter agent.
-      </Text>
-      <Spacer y={1} />
-      <Button onClick={() => triggerInstall()}>
-        <I className="material-icons">add</I> Install Porter agent
-      </Button>
-    </Fieldset>
-  ) : logsError ? (
-    <Fieldset>
-      <Container row>
-        <WarnI className="material-icons">warning</WarnI>
-        <Text color="helper">
-          Porter encountered an error retrieving logs for this application.
-        </Text>
-      </Container>
-    </Fieldset>
-  ) : (
-    renderContents()
-  );
-};
-
-export default LogSection;
-
-const I = styled.i`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-  justify-content: center;
-`;
-
-const WarnI = styled.i`
-  font-size: 18px;
-  display: flex;
-  align-items: center;
-  margin-right: 10px;
-  justify-content: center;
-  opacity: 0.6;
-`;
-
-const Spinner = styled.img`
-  width: 15px;
-  height: 15px;
-`;
-
-const Checkbox = styled.div<{ checked: boolean }>`
-  width: 16px;
-  height: 16px;
-  border: 1px solid #ffffff55;
-  margin: 1px 10px 0px 1px;
-  border-radius: 3px;
-  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  > i {
-    font-size: 12px;
-    padding-left: 0px;
-    display: ${(props) => (props.checked ? "" : "none")};
-  }
-`;
-
-const ScrollButton = styled.div`
-  background: #181B20;
-  border-radius: 5px;
-  height: 30px;
-  font-size: 13px;
-  display: flex;
-  cursor: pointer;
-  align-items: center;
-  padding: 10px;
-  padding-left: 8px;
-  > i {
-    font-size: 16px;
-    margin-right: 5px;
-  }
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const Message = styled.div`
-  display: flex;
-  height: 100%;
-  width: calc(100% - 150px);
-  align-items: center;
-  justify-content: center;
-  margin-left: 75px;
-  text-align: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
-const Highlight = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-left: 8px;
-  color: #8590ff;
-  cursor: pointer;
-
-  > i {
-    font-size: 16px;
-    margin-right: 3px;
-  }
-`;
-
-const FlexRow = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  flex-wrap: wrap;
-`;
-
-const StyledLogsSection = styled.div`
-  width: 100%;
-  height: 600px;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  background: #000000;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const LoadMoreButton = styled.div<{ active: boolean }>`
-  width: 100%;
-  display: ${(props) => (props.active ? "flex" : "none")};
-  justify-content: center;
-  align-items: center;
-  padding-block: 10px;
-  background: #1f2023;
-  cursor: pointer;
-  font-family: monospace;
-`;
-
-const NotificationWrapper = styled.div<{ active?: boolean }>`
-  position: absolute;
-  bottom: 10px;
-  display: ${(props) => (props.active ? "flex" : "none")};
-  justify-content: center;
-  align-items: center;
-  left: 50%;
-  transform: translateX(-50%);
-  width: fit-content;
-  background: #101420;
-  z-index: 9999;
-
-  @keyframes bounceIn {
-    0% {
-      transform: translateZ(-1400px);
-      opacity: 0;
-    }
-    100% {
-      transform: translateZ(0);
-      opacity: 1;
-    }
-  }
-`;
-
-const LogsSectionWrapper = styled.div`
-  position: relative;
-  margin-top: 10px;
-`;

+ 0 - 186
dashboard/src/main/home/app-dashboard/expanded-app/logs/StyledLogs.tsx

@@ -1,186 +0,0 @@
-import React from "react";
-import { GenericFilter, PorterLog } from "./types";
-import styled from "styled-components";
-import Anser from "anser";
-import dayjs from "dayjs";
-import { getPodSelectorFromServiceName, getServiceNameFromPodNameAndAppName, getVersionTagColor } from "./utils";
-import { Service } from "../../new-app-flow/serviceTypes";
-
-
-type Props = {
-    logs: PorterLog[];
-    appName: string;
-    filters: GenericFilter[];
-    services?: Service[];
-};
-
-const StyledLogs: React.FC<Props> = ({
-    logs,
-    appName,
-    filters,
-    services,
-}) => {
-    const renderFilterTagForLog = (filter: GenericFilter, log: PorterLog, index: number) => {
-        if (log.metadata == null) {
-            return null;
-        }
-        switch (filter.name) {
-            case "revision":
-                if (log.metadata.revision == null || log.metadata.revision === "") {
-                    return null;
-                }
-                return (
-                    <StyledLogsTableData width={"100px"} key={index}>
-                        <LogInnerPill
-                            color={getVersionTagColor(log.metadata.revision)}
-                            key={index}
-                            onClick={() => filter.setValue(log.metadata.revision)}
-                        >
-                            {`Version: ${log.metadata.revision}`}
-                        </LogInnerPill>
-                    </StyledLogsTableData>
-                )
-            case "pod_name":
-                if (log.metadata.pod_name == null || log.metadata.pod_name === "") {
-                    return null;
-                }
-                return (
-                    <StyledLogsTableData width={"100px"} key={index}>
-                        <LogInnerPill
-                            color={"white"}
-                            key={index}
-                            onClick={() => filter.setValue(getPodSelectorFromServiceName(getServiceNameFromPodNameAndAppName(log.metadata.pod_name, appName), services) ?? GenericFilter.getDefaultOption("pod_name").value)}
-                        >
-                            {getServiceNameFromPodNameAndAppName(log.metadata.pod_name, appName)}
-                        </LogInnerPill>
-                    </StyledLogsTableData>
-                )
-            case "service_name":
-                if (log.metadata?.raw_labels?.porter_run_service_name == null || log.metadata?.raw_labels?.porter_run_service_name === "") {
-                    return null;
-                }
-                return (
-                    <StyledLogsTableData width={"100px"} key={index}>
-                        <LogInnerPill
-                            color={"white"}
-                            key={index}
-                            onClick={() => filter.setValue(log.metadata?.raw_labels?.porter_run_service_name ?? GenericFilter.getDefaultOption("service_name").value)}
-                        >
-                            {log.metadata.raw_labels?.porter_run_service_name}
-                        </LogInnerPill>
-                    </StyledLogsTableData>
-                )
-            default:
-                return null;
-        }
-    }
-
-    return (
-        <StyledLogsTable>
-            <StyledLogsTableBody>
-                {logs.map((log, i) => {
-                    return (
-                        <StyledLogsTableRow key={[log.lineNumber, i].join(".")}>
-                            <StyledLogsTableData width={"100px"}>
-                                <LineTimestamp className="line-timestamp">
-                                    {log.timestamp
-                                        ? dayjs(log.timestamp).format("MM/DD HH:mm:ss")
-                                        : "-"}
-                                </LineTimestamp>
-                            </StyledLogsTableData>
-                            {filters.map((filter, j) => {
-                                return renderFilterTagForLog(filter, log, j)
-                            })}
-                            <StyledLogsTableData>
-                                <LogOuter key={[log.lineNumber, i].join(".")}>
-                                    {log.line?.map((ansi, j) => {
-                                        if (ansi.clearLine) {
-                                            return null;
-                                        }
-
-                                        return (
-                                            <LogInnerSpan
-                                                key={[log.lineNumber, i, j].join(".")}
-                                                ansi={ansi}
-                                            >
-                                                {ansi.content.replace(/ /g, "\u00a0")}
-                                            </LogInnerSpan>
-                                        );
-                                    })}
-                                </LogOuter>
-                            </StyledLogsTableData>
-                        </StyledLogsTableRow>
-                    )
-                })}
-            </StyledLogsTableBody>
-        </StyledLogsTable>
-    );
-};
-
-export default StyledLogs;
-
-const StyledLogsTable = styled.table`
-    border-collapse: collapse;
-`;
-
-const StyledLogsTableBody = styled.tbody`
-`;
-
-const StyledLogsTableRow = styled.tr`
-    
-`;
-
-const StyledLogsTableData = styled.td<{ width?: string }>`
-    padding: 2px;
-    vertical-align: top;
-    ${(props) => props.width && `width: ${props.width};`}
-`;
-
-const LineTimestamp = styled.div`
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    white-space: nowrap;
-`
-
-const LogInnerPill = styled.div<{ color: string }>`
-    display: inline-block;
-    vertical-align: middle;
-    width: 120px;
-    padding: 0px 5px;
-    height: 20px;
-    color: black;
-    background-color: ${(props) => props.color};
-    border-radius: 5px;
-    opacity: 1;
-    font-family: monospace;
-    cursor: pointer;
-    hover: {
-        border: 1px solid #949effff;
-    }
-    overflow: hidden;
-    white-space: nowrap;    
-    text-overflow: ellipsis;
-`
-
-const LogOuter = styled.div`
-  user-select: text;
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  user-select: text;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-        props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;

+ 0 - 536
dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts

@@ -1,536 +0,0 @@
-import dayjs, { Dayjs } from "dayjs";
-import _ from "lodash";
-import { useContext, useEffect, useRef, useState } from "react";
-import api from "shared/api";
-import Anser from "anser";
-import { Context } from "shared/Context";
-import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
-import { ChartType } from "shared/types";
-import { AgentLog, agentLogValidator, Direction, PorterLog, PaginationInfo, GenericFilter, FilterName } from "./types";
-import { Service } from "../../new-app-flow/serviceTypes";
-
-const MAX_LOGS = 5000;
-const MAX_BUFFER_LOGS = 1000;
-const QUERY_LIMIT = 1000;
-
-export const parseLogs = (logs: any[] = []): PorterLog[] => {
-  return logs.map((log: any, idx) => {
-    try {
-      const parsed: AgentLog = agentLogValidator.parse(log);
-
-      // TODO Move log parsing to the render method
-      const ansiLog = Anser.ansiToJson(parsed.line);
-      return {
-        line: ansiLog,
-        lineNumber: idx + 1,
-        timestamp: parsed.timestamp,
-        metadata: parsed.metadata,
-      };
-    } catch (err) {
-      console.log(err);
-      try {
-        return {
-          line: Anser.ansiToJson(log.toString()),
-          lineNumber: idx + 1,
-          timestamp: undefined,
-        }
-      } catch (error) {
-        console.log(error);
-        return {
-          line: [],
-          lineNumber: idx + 1,
-          timestamp: undefined,
-        }
-      }
-    }
-  });
-};
-
-export const useLogs = (
-  selectedFilterValues: Record<FilterName, string>,
-  appName: string,
-  namespace: string,
-  searchParam: string,
-  notify: (message: string) => void,
-  currentChart: ChartType | undefined,
-  setLoading: (isLoading: boolean) => void,
-  // if setDate is set, results are not live
-  setDate?: Date,
-  timeRange?: {
-    startTime?: Dayjs,
-    endTime?: Dayjs,
-  },
-) => {
-  const isLive = !setDate;
-  const logsBufferRef = useRef<PorterLog[]>([]);
-  const { currentCluster, currentProject } = useContext(
-    Context
-  );
-  const [logs, setLogs] = useState<PorterLog[]>([]);
-  const [paginationInfo, setPaginationInfo] = useState<PaginationInfo>({
-    previousCursor: null,
-    nextCursor: null,
-  });
-
-  // if currentPodName is default value we are looking at all chart pod logs
-  const currentPodSelector = selectedFilterValues.pod_name === GenericFilter.getDefaultOption("pod_name").value
-    ? `${currentChart?.name ?? ''}-.*` : `${currentChart?.name}-${selectedFilterValues.pod_name}-.*`;
-
-  // if we are live:
-  // - start date is initially set to 2 weeks ago
-  // - the query has an end date set to current date
-  // - moving the cursor forward does nothing
-
-  // if we are not live:
-  // - end date is set to the setDate
-  // - start date is initially set to 2 weeks ago, but then gets set to the
-  //   result of the initial query
-  // - moving the cursor both forward and backward changes the start and end dates
-
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-  } = useWebsockets();
-
-  const updateLogs = (
-    newLogs: PorterLog[],
-    direction: Direction = Direction.forward
-  ) => {
-    // Nothing to update here
-    if (!newLogs.length) {
-      return;
-    }
-
-    setLogs((logs) => {
-      let updatedLogs = _.cloneDeep(logs);
-      /**
-       * If direction = Direction.forward, we want to append the new logs
-       * at the end of the current logs, else we want to append before the current logs
-       *
-       */
-      if (direction === Direction.forward) {
-        const lastLineNumber = updatedLogs.at(-1)?.lineNumber ?? 0;
-
-        updatedLogs.push(
-          ...newLogs.map((log, idx) => ({
-            ...log,
-            lineNumber: lastLineNumber + idx + 1,
-          }))
-        );
-
-        // For direction = Direction.forward, remove logs from the front
-        if (updatedLogs.length > MAX_LOGS) {
-          const logsToBeRemoved =
-            newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS;
-          updatedLogs = updatedLogs.slice(logsToBeRemoved);
-        }
-      } else {
-        updatedLogs = newLogs.concat(
-          updatedLogs.map((log) => ({
-            ...log,
-            lineNumber: log.lineNumber + newLogs.length,
-          }))
-        );
-
-        // For direction = Direction.backward, remove logs from the back
-        if (updatedLogs.length > MAX_LOGS) {
-          const logsToBeRemoved =
-            newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS;
-
-          updatedLogs = updatedLogs.slice(0, logsToBeRemoved);
-        }
-      }
-
-      return filterLogs(updatedLogs);
-    });
-  };
-
-  /**
-   * Flushes the logs buffer. If `discard` is true,
-   * it will update `current logs` before executing
-   * the flush operation
-   */
-  const flushLogsBuffer = (discard: boolean = false) => {
-    if (!discard) {
-      updateLogs(logsBufferRef.current ?? []);
-    }
-
-    logsBufferRef.current = [];
-  };
-
-  const pushLogs = (newLogs: PorterLog[]) => {
-    logsBufferRef.current.push(...newLogs);
-
-    if (logsBufferRef.current.length >= MAX_BUFFER_LOGS) {
-      flushLogsBuffer();
-    }
-  };
-
-  const setupWebsocket = (websocketKey: string) => {
-    if (namespace == "" || currentCluster == null || currentProject == null || currentChart == null) {
-      return;
-    }
-
-    const websocketBaseURL = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki`;
-
-    const searchParams = {
-      pod_selector: currentPodSelector,
-      namespace,
-      search_param: searchParam,
-      revision: currentChart.version.toString(),
-    }
-
-    const q = new URLSearchParams(searchParams).toString();
-
-    const endpoint = `${websocketBaseURL}?${q}`;
-
-    const config: NewWebsocketOptions = {
-      onopen: () => {
-        console.log("Opened websocket:", websocketKey);
-      },
-      onmessage: (evt: MessageEvent) => {
-        // Nothing to do here
-        if (evt.data == null) {
-          return;
-        }
-        const jsonData = evt.data.trim().split("\n")
-        const newLogs: any[] = [];
-        jsonData.forEach((data: string) => {
-          try {
-            const jsonLog = JSON.parse(data);
-            newLogs.push(jsonLog)
-          } catch (err) {
-            // TODO: better error handling
-            console.log(err)
-          }
-        });
-        const newLogsParsed = parseLogs(newLogs);
-        const newLogsFiltered = filterLogs(newLogsParsed);
-        pushLogs(newLogsFiltered);
-      },
-      onclose: () => {
-        console.log("Closed websocket:", websocketKey);
-      },
-    };
-
-    newWebsocket(websocketKey, endpoint, config);
-    openWebsocket(websocketKey);
-  };
-
-  const filterLogs = (logs: PorterLog[]) => {
-    return logs.filter(log => {
-      if (log.metadata == null) {
-        return true;
-      }
-
-      // TODO: refactor this extremely hacky way to filter out pre-deploy logs
-      if (!currentChart?.name.endsWith("-r") && log.metadata.pod_name.startsWith(`${appName}-r-`)) {
-        return false;
-      }
-
-      if (selectedFilterValues.output_stream !== GenericFilter.getDefaultOption("output_stream").value &&
-        log.metadata.output_stream !== selectedFilterValues.output_stream) {
-        return false;
-      }
-
-      if (selectedFilterValues.revision !== GenericFilter.getDefaultOption("revision").value &&
-        log.metadata.revision !== selectedFilterValues.revision) {
-        return false;
-      }
-
-      return true;
-    });
-  };
-
-  const queryLogs = async (
-    startDate: string,
-    endDate: string,
-    direction: Direction,
-    limit: number = QUERY_LIMIT
-  ): Promise<{
-    logs: PorterLog[];
-    previousCursor: string | null;
-    nextCursor: string | null;
-  }> => {
-    if (currentCluster == null || currentProject == null) {
-      return {
-        logs: [],
-        previousCursor: null,
-        nextCursor: null,
-      };
-    }
-
-    const getLogsReq = {
-      namespace,
-      search_param: searchParam,
-      start_range: startDate,
-      end_range: endDate,
-      limit,
-      chart_name: "",
-      pod_selector: currentPodSelector,
-      direction,
-    };
-
-    if (currentChart == null) {
-      return {
-        logs: [],
-        previousCursor: null,
-        nextCursor: null,
-      };
-    }
-
-    // special casing for pre-deploy logs - see get_logs_within_time_range.go
-    if (currentChart.name.endsWith("-r")) {
-      getLogsReq.chart_name = currentChart.name;
-      getLogsReq.pod_selector = "";
-    }
-
-    try {
-      const logsResp = await api.getLogsWithinTimeRange(
-        "<token>",
-        getLogsReq,
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-        }
-      )
-
-      if (logsResp.data == null) {
-        return {
-          logs: [],
-          previousCursor: null,
-          nextCursor: null,
-        };
-      }
-
-      const newLogs = parseLogs(logsResp.data.logs);
-      if (direction === Direction.backward) {
-        newLogs.reverse();
-      }
-      return {
-        logs: newLogs,
-        previousCursor:
-          // There are no more historical logs so don't set the previous cursor
-          newLogs.length < QUERY_LIMIT && direction == Direction.backward
-            ? null
-            : logsResp.data.backward_continue_time,
-        nextCursor: logsResp.data.forward_continue_time,
-      };
-    } catch {
-      return {
-        logs: [],
-        previousCursor: null,
-        nextCursor: null,
-      };
-    }
-  };
-
-  const refresh = async () => {
-    if (!currentPodSelector) {
-      return;
-    }
-
-    setLoading(true);
-    setLogs([]);
-    flushLogsBuffer(true);
-    const endDate = timeRange?.endTime != null ? timeRange.endTime : dayjs(setDate);
-    const oneDayAgo = timeRange?.startTime != null ? timeRange.startTime : endDate.subtract(1, "day");
-
-    const { logs: initialLogs, previousCursor, nextCursor } = await queryLogs(
-      oneDayAgo.toISOString(),
-      endDate.toISOString(),
-      Direction.backward
-    );
-
-    setPaginationInfo({
-      previousCursor,
-      nextCursor,
-    });
-
-    updateLogs(initialLogs);
-
-    if (!isLive && !initialLogs.length) {
-      notify(
-        "You have no logs for this time period. Try with a different time range."
-      );
-    }
-
-    closeAllWebsockets();
-    const suffix = Math.random().toString(36).substring(2, 15);
-    const websocketKey = `${currentPodSelector}-${namespace}-websocket-${suffix}`;
-
-    setLoading(false);
-
-    if (isLive) {
-      setupWebsocket(websocketKey);
-    }
-  };
-
-  const moveCursor = async (direction: Direction) => {
-    if (direction === Direction.backward) {
-      // we query by setting the endDate equal to the previous startDate, and setting the direction
-      // to "backward"
-      const refDate = paginationInfo.previousCursor ?? dayjs().toISOString();
-      const oneDayAgo = dayjs(refDate).subtract(1, "day");
-
-      const { logs: newLogs, previousCursor } = await queryLogs(
-        oneDayAgo.toISOString(),
-        refDate,
-        Direction.backward
-      );
-
-      const logsToUpdate = paginationInfo.previousCursor
-        ? newLogs.slice(0, -1)
-        : newLogs;
-
-      updateLogs(logsToUpdate, direction);
-
-      if (!logsToUpdate.length) {
-        notify("You have reached the beginning of the logs");
-      }
-
-      setPaginationInfo((paginationInfo) => ({
-        ...paginationInfo,
-        previousCursor,
-      }));
-    } else {
-      if (isLive) {
-        return;
-      }
-
-      // we query by setting the startDate equal to the previous endDate, setting the endDate equal to the
-      // current time, and setting the direction to "forward"
-      const refDate = paginationInfo.nextCursor ?? dayjs(setDate).toISOString();
-      const currDate = dayjs();
-
-      const { logs: newLogs, nextCursor } = await queryLogs(
-        refDate,
-        currDate.toISOString(),
-        Direction.forward
-      );
-
-      const logsToUpdate = paginationInfo.nextCursor
-        ? newLogs.slice(1)
-        : newLogs;
-
-      // If previously we had next cursor set, it is likely that the log might have a duplicate entry so we ignore the first line
-      updateLogs(logsToUpdate);
-
-      if (!logsToUpdate.length) {
-        notify("You are already at the latest logs");
-      }
-
-      setPaginationInfo((paginationInfo) => ({
-        ...paginationInfo,
-        nextCursor,
-      }));
-    }
-  };
-
-  useEffect(() => {
-    setLogs([]);
-    flushLogsBuffer(true);
-  }, []);
-
-  /**
-   * In some situations, we might never hit the limit for the max buffer size.
-   * An example is if the total logs for the pod < MAX_BUFFER_LOGS.
-   *
-   * For handling situations like this, we would want to force a flush operation
-   * on the buffer so that we dont have any stale logs
-   */
-  useEffect(() => {
-    /**
-     * We don't want users to wait for too long for the initial
-     * logs to appear. So we use a setTimeout for 1s to force-flush
-     * logs after 1s of load
-     */
-    setTimeout(flushLogsBuffer, 500);
-
-    const flushLogsBufferInterval = setInterval(flushLogsBuffer, 3000);
-
-    return () => clearInterval(flushLogsBufferInterval);
-  }, []);
-
-  useEffect(() => {
-    refresh();
-  }, [currentPodSelector, namespace, searchParam, setDate, selectedFilterValues]);
-
-  useEffect(() => {
-    // if the streaming is no longer live, close all websockets
-    if (!isLive) {
-      closeAllWebsockets();
-    }
-  }, [isLive]);
-
-  useEffect(() => {
-    return () => {
-      closeAllWebsockets();
-    };
-  }, []);
-
-  return {
-    logs,
-    refresh,
-    moveCursor,
-    paginationInfo,
-  };
-};
-
-export const getVersionTagColor = (version: string) => {
-  const colors = [
-    "#7B61FF",
-    "#FF7B61",
-    "#61FF7B",
-  ];
-
-  const versionInt = parseInt(version);
-  if (isNaN(versionInt)) {
-    return colors[0];
-  }
-  return colors[versionInt % colors.length];
-};
-
-export const getServiceNameFromPodNameAndAppName = (podName: string, porterAppName: string) => {
-  const prefix: string = porterAppName + "-";
-  if (!podName.startsWith(prefix)) {
-    return "";
-  }
-
-  podName = podName.replace(prefix, "");
-  const suffixes: string[] = ["-web", "-wkr", "-job"];
-  let index: number = -1;
-
-  for (const suffix of suffixes) {
-    const newIndex: number = podName.lastIndexOf(suffix);
-    if (newIndex > index) {
-      index = newIndex;
-    }
-  }
-
-  if (index !== -1) {
-    return podName.substring(0, index);
-  }
-
-  // if the suffix wasn't found, it's possible that the service name was too long to keep the entire suffix. example: postgres-snowflake-connector-postgres-snowflake-service-wk8gnst
-  // if this is the case, find the service name by removing everything after the last dash
-  // This is only to fix current pods; new pods will be named correctly because we imposed service name limits in https://github.com/porter-dev/porter/pull/3439
-  index = podName.lastIndexOf("-");
-  if (index !== -1) {
-    return podName.substring(0, index)
-  }
-
-  return "";
-}
-
-export const getPodSelectorFromServiceName = (serviceName: string | null | undefined, services?: Service[]): string | undefined => {
-  if (serviceName == null) {
-    return undefined;
-  }
-  const match = services?.find(s => s.name === serviceName);
-  if (match == null) {
-    return undefined;
-  }
-  return `${match.name}-${match.type == "worker" ? "wkr" : match.type}`;
-}

+ 0 - 370
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsSection.tsx

@@ -1,370 +0,0 @@
-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 { Service } from "../../new-app-flow/serviceTypes";
-
-import TabSelector from "components/TabSelector";
-import SelectRow from "components/form-components/SelectRow";
-import { getServiceNameFromControllerName, MetricNormalizer, resolutions, secondsBeforeNow } from "./utils";
-import { Metric, MetricType, NginxStatusMetric } from "./types";
-import { match } from "ts-pattern";
-import { AvailableMetrics, NormalizedMetricsData } from "main/home/cluster-dashboard/expanded-chart/metrics/types";
-import MetricsChart from "./MetricsChart";
-import { useQuery } from "@tanstack/react-query";
-import Loading from "components/Loading";
-import CheckboxRow from "components/CheckboxRow";
-import Spacer from "components/porter/Spacer";
-
-type PropsType = {
-  currentChart: ChartType;
-  appName: string;
-  serviceName?: string;
-  services: Service[];
-};
-
-const MetricsSection: React.FunctionComponent<PropsType> = ({
-  currentChart,
-  appName,
-  serviceName,
-  services,
-}) => {
-  const [selectedController, setSelectedController] = useState<any>(null);
-  const [selectedRange, setSelectedRange] = useState("1H");
-  const [showAutoscalingThresholds, setShowAutoscalingThresholds] = useState(false);
-
-  const { currentCluster, currentProject } = useContext(
-    Context
-  );
-
-  const { data: controllerOptions, isLoading: isControllerListLoading } = useQuery(
-    [
-      "getChartControllers",
-      currentProject?.id,
-      currentChart.name,
-      currentChart.namespace,
-      currentCluster?.id,
-      currentChart.version,
-    ],
-    async () => {
-      if (currentProject?.id == null || currentCluster?.id == null) {
-        return;
-      }
-      const res = await api.getChartControllers(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          name: currentChart.name,
-          namespace: currentChart.namespace,
-          cluster_id: currentCluster.id,
-          revision: currentChart.version,
-        }
-      );
-
-      const controllerOptions = res.data.map((controller: any) => {
-        return { value: controller, label: getServiceNameFromControllerName(controller?.metadata?.name, appName) };
-      });
-
-      return controllerOptions;
-    },
-    {
-      refetchOnWindowFocus: false,
-    }
-  );
-
-  const { data: metricsData, isLoading: isMetricsDataLoading, refetch } = useQuery(
-    [
-      "getMetrics",
-      currentProject?.id,
-      currentCluster?.id,
-      selectedController?.metadata?.name,
-      selectedRange,
-    ],
-    async () => {
-      if (currentProject?.id == null || currentCluster?.id == null) {
-        return;
-      }
-      const metrics: Metric[] = [];
-      const metricTypes: MetricType[] = ["cpu", "memory"];
-
-      const serviceName: string = selectedController?.metadata.labels["app.kubernetes.io/name"]
-      const isHpaEnabled: boolean = currentChart?.config?.[serviceName]?.autoscaling?.enabled
-
-      const shortServiceName: string = getServiceNameFromControllerName(selectedController?.metadata?.name, appName)
-      if (services.some(svc => svc.name === shortServiceName && svc.type === "web")) {
-         metricTypes.push("network");
-      }
-
-      if (isHpaEnabled) {
-        metricTypes.push("hpa_replicas");
-      }
-
-      if (currentChart?.chart?.metadata?.name == "ingress-nginx") {
-        metricTypes.push("nginx:errors");
-      }
-
-      if (currentChart?.config?.[serviceName]?.ingress?.enabled) {
-        metricTypes.push("nginx:status")
-      }
-
-      const d = new Date();
-      const end = Math.round(d.getTime() / 1000);
-      const start = end - secondsBeforeNow[selectedRange];
-
-      for (const metricType of metricTypes) {
-        const kind = metricType === "nginx:status" ? "Ingress" : selectedController?.kind
-
-        const aggregatedMetricsResponse = await api.getMetrics(
-          "<token>",
-          {
-            metric: metricType,
-            shouldsum: false,
-            kind: kind,
-            name: selectedController?.metadata.name,
-            namespace: currentChart.namespace,
-            startrange: start,
-            endrange: end,
-            resolution: resolutions[selectedRange],
-            pods: [],
-          },
-          {
-            id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        );
-        // TODO: type the response to this
-        const metricsNormalizer = new MetricNormalizer(
-          [{ results: (aggregatedMetricsResponse.data ?? []).flatMap((d: any) => d.results) }],
-          metricType,
-        );
-        if (metricType === "nginx:status") {
-          const nginxMetric: NginxStatusMetric = {
-            type: metricType,
-            label: "Throughput",
-            areaData: metricsNormalizer.getNginxStatusData(),
-          }
-          metrics.push(nginxMetric)
-        } else {
-          const [data, allPodsAggregatedData] = metricsNormalizer.getAggregatedData();
-          const hpaData: NormalizedMetricsData[] = [];
-
-          if (isHpaEnabled && ["cpu", "memory"].includes(metricType)) {
-            let hpaMetricType = "cpu_hpa_threshold"
-            if (metricType === "memory") {
-              hpaMetricType = "memory_hpa_threshold"
-            }
-
-            const hpaRes = await api.getMetrics(
-              "<token>",
-              {
-                metric: hpaMetricType,
-                shouldsum: false,
-                kind: kind,
-                name: selectedController?.metadata.name,
-                namespace: currentChart.namespace,
-                startrange: start,
-                endrange: end,
-                resolution: resolutions[selectedRange],
-                pods: [],
-              },
-              {
-                id: currentProject.id,
-                cluster_id: currentCluster.id,
-              }
-            );
-
-            const autoscalingMetrics = new MetricNormalizer(hpaRes.data, hpaMetricType as AvailableMetrics);
-            hpaData.push(...autoscalingMetrics.getParsedData());
-          }
-
-          const metric: Metric = match(metricType)
-            .with("cpu", () => ({
-              type: metricType,
-              label: "CPU Utilization (vCPUs)",
-              data: data,
-              aggregatedData: allPodsAggregatedData,
-              hpaData,
-            }))
-            .with("memory", () => ({
-              type: metricType,
-              label: "RAM Utilization (Mi)",
-              data: data,
-              aggregatedData: allPodsAggregatedData,
-              hpaData,
-            }))
-            .with("network", () => ({
-              type: metricType,
-              label: "Network Received Bytes (Ki)",
-              data: data,
-              aggregatedData: allPodsAggregatedData,
-              hpaData,
-            }))
-            .with("hpa_replicas", () => ({
-              type: metricType,
-              label: "Number of replicas",
-              data: data,
-              aggregatedData: allPodsAggregatedData,
-              hpaData,
-            }))
-            .with("nginx:errors", () => ({
-              type: metricType,
-              label: "5XX Error Percentage",
-              data: data,
-              aggregatedData: allPodsAggregatedData,
-              hpaData,
-            }))
-            .exhaustive();
-          metrics.push(metric);
-        }
-      };
-      return metrics;
-    },
-    {
-      enabled: selectedController != null,
-      refetchOnWindowFocus: false,
-      refetchInterval: 10000, // refresh metrics every 10 seconds
-    }
-  );
-
-  useEffect(() => {
-    if (controllerOptions == null) {
-      return;
-    }
-    const controllerOption = controllerOptions.find(
-      (option: any) => option.label === serviceName
-    );
-    if (controllerOption) {
-      setSelectedController(controllerOption.value);
-    } else {
-      setSelectedController(controllerOptions[0]?.value);
-    }
-  }, [controllerOptions]);
-
-  const renderMetrics = () => {
-    if (metricsData == null || isMetricsDataLoading) {
-      return <Loading />;
-    }
-    return metricsData.map((metric: Metric, i: number) => {
-      return (
-        <MetricsChart
-          key={metric.type}
-          metric={metric}
-          selectedRange={selectedRange}
-          isLoading={isMetricsDataLoading}
-          showAutoscalingLine={showAutoscalingThresholds}
-        />
-      );
-    })
-  }
-
-  const renderShowAutoscalingThresholdsCheckbox = () => {
-    const serviceName: string = selectedController?.metadata.labels["app.kubernetes.io/name"]
-    const isHpaEnabled: boolean = currentChart?.config?.[serviceName]?.autoscaling?.enabled
-
-    if (!isHpaEnabled) {
-      return null;
-    }
-    return (
-      <>
-        <Spacer inline x={1} />
-        <CheckboxRow
-          toggle={() => setShowAutoscalingThresholds(!showAutoscalingThresholds)}
-          checked={showAutoscalingThresholds}
-          label="Show autoscaling thresholds"
-        />
-      </>
-    )
-  }
-
-  return (
-    <StyledMetricsSection>
-      <MetricsHeader>
-        <Flex>
-          <SelectRow
-            displayFlex={true}
-            label="Service"
-            value={selectedController}
-            setActiveValue={(x: any) => setSelectedController(x)}
-            options={controllerOptions}
-            width="200px"
-            isLoading={isControllerListLoading}
-          />
-          <Highlight color={"#7d7d81"} onClick={() => refetch()}>
-            <i className="material-icons">autorenew</i>
-          </Highlight>
-          {renderShowAutoscalingThresholdsCheckbox()}
-        </Flex>
-        <RangeWrapper>
-          <Relative>
-          </Relative>
-          <TabSelector
-            noBuffer={true}
-            options={[
-              { value: "1H", label: "1H" },
-              { value: "6H", label: "6H" },
-              { value: "1D", label: "1D" },
-              { value: "1M", label: "1M" },
-            ]}
-            currentTab={selectedRange}
-            setCurrentTab={(x: string) => setSelectedRange(x)}
-          />
-        </RangeWrapper>
-      </MetricsHeader>
-      {renderMetrics()}
-    </StyledMetricsSection>
-  );
-};
-
-export default MetricsSection;
-
-const Relative = styled.div`
-  position: relative;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const MetricsHeader = styled.div`
-  width: 100%;
-  display: flex;
-  align-items: center;
-  margin-bottom: 10px;
-  overflow: visible;
-  justify-content: space-between;
-`;
-
-const RangeWrapper = styled.div`
-  float: right;
-  font-weight: bold;
-  width: 158px;
-  margin-top: -8px;
-`;
-
-const StyledMetricsSection = styled.div`
-  width: 100%;
-  display: flex;
-  margin-top: -20px;
-  flex-direction: column;
-  position: relative;
-`;
-
-const Highlight = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-top: 20px;
-  margin-left: 8px;
-  margin-bottom: 15px;
-  color: ${(props: { color: string }) => props.color};
-  cursor: pointer;
-
-  > i {
-    font-size: 20px;
-    margin-right: 3px;
-  }
-`;

+ 0 - 81
dashboard/src/main/home/app-dashboard/expanded-app/status/AppEventModal.tsx

@@ -1,81 +0,0 @@
-import React, { useEffect, useRef } from "react";
-import Modal from "components/porter/Modal";
-import TitleSection from "components/TitleSection";
-import Text from "components/porter/Text";
-import danger from "assets/danger.svg";
-
-import ExpandedIncidentLogs from "./ExpandedIncidentLogs";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import document from "assets/document.svg";
-import styled from "styled-components";
-import time from "assets/time.svg";
-import { PorterLog } from "../logs/types";
-
-interface AppEventModalProps {
-    logs: PorterLog[];
-    setModalVisible: (x: boolean) => void;
-    porterAppName: string;
-    timestamp: string;
-    expandedAppEventMessage: string;
-}
-const AppEventModal: React.FC<AppEventModalProps> = ({ logs, porterAppName, setModalVisible, timestamp, expandedAppEventMessage }) => {
-    const scrollToBottomRef = useRef<HTMLDivElement>(null);
-    const scrollToBottom = () => {
-        if (scrollToBottomRef.current) {
-            scrollToBottomRef.current.scrollIntoView({
-                behavior: "smooth",
-                block: "end",
-            });
-        }
-    }
-    useEffect(() => {
-        scrollToBottom();
-    }, [scrollToBottomRef]);
-
-
-    return (
-        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-            <TitleSection icon={danger}>
-                <Text size={16}>Details for {porterAppName}</Text>
-            </TitleSection>
-            <Spacer y={0.5} />
-            <Container row>
-                <Img src={time} />
-                <Spacer inline x={0.5} />
-                <Text color="helper">
-                    Last updated {timestamp}
-                </Text>
-            </Container>
-            <Spacer y={0.5} />
-            <Message>
-                <img src={document} />
-                {expandedAppEventMessage}
-            </Message>
-            {logs.length > 0 &&
-                <ExpandedIncidentLogs logs={logs} />
-            }
-        </Modal>
-    );
-};
-
-export default AppEventModal;
-
-const Message = styled.div`
-  padding: 20px;
-  background: #26292e;
-  border-radius: 5px;
-  line-height: 1.5em;
-  border: 1px solid #aaaabb33;
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  > img {
-    width: 13px;
-    margin-right: 20px;
-  }
-`;
-
-const Img = styled.img`
-    width: 13px;
-`;

+ 0 - 52
dashboard/src/main/home/app-dashboard/expanded-app/status/ConnectToLogsInstructionModal.tsx

@@ -1,52 +0,0 @@
-import Modal from "main/home/modals/Modal";
-import React from "react";
-import styled from "styled-components";
-
-const ConnectToLogsInstructionModal: React.FC<{
-  show: boolean;
-  onClose: () => void;
-  chartName: string;
-  namespace: string;
-}> = ({ show, chartName, namespace, onClose }) => {
-  if (!show) {
-    return null;
-  }
-
-  return (
-    <Modal
-      onRequestClose={() => onClose()}
-      width="700px"
-      height="300px"
-      title="Shell Access Instructions"
-    >
-      To get shell live logs for this pod, make sure you have the Porter CLI
-      installed (installation instructions&nbsp;
-      <a href={"https://docs.porter.run/cli/installation"} target="_blank">
-        here
-      </a>
-      ).
-      <br />
-      <br />
-      Run the following line of code:
-      <Code>
-        porter logs {chartName || "[APP-NAME]"} --follow --namespace{" "}
-        {namespace || "[NAMESPACE]"}
-      </Code>
-    </Modal>
-  );
-};
-
-export default ConnectToLogsInstructionModal;
-
-const Code = styled.div`
-  background: #181b21;
-  padding: 10px 15px;
-  border: 1px solid #ffffff44;
-  border-radius: 5px;
-  margin: 10px 0px 15px;
-  color: #ffffff;
-  font-size: 13px;
-  user-select: text;
-  line-height: 1em;
-  font-family: monospace;
-`;

+ 0 - 448
dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx

@@ -1,448 +0,0 @@
-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 "components/ResourceTab";
-import ConfirmOverlay from "components/ConfirmOverlay";
-import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
-import PodRow from "./PodRow";
-import { timeFormat } from "d3-time-format";
-
-type Props = {
-  controller: any;
-  selectedPod: any;
-  selectPod: (newPod: any) => unknown;
-  selectors: any;
-  isLast?: boolean;
-  isFirst?: boolean;
-  setPodError: (x: string) => 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;
-  crashLoopReason?: string;
-  failing?: boolean;
-};
-
-const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
-
-const ControllerTabFC: React.FunctionComponent<Props> = ({
-  controller,
-  selectPod,
-  isFirst,
-  isLast,
-  selectors,
-  setPodError,
-  selectedPod,
-}) => {
-  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]);
-
-  useEffect(() => {
-    return () => closeAllWebsockets();
-  }, [])
-
-  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 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) {
-          collatedStatus = "failed";
-        }
-      });
-      return collatedStatus;
-    }
-  };
-
-  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 = pods.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;
-    }, []);
-
-    if (podsDividedByReplicaSet.length === 1) {
-      return [];
-    } else {
-      return podsDividedByReplicaSet;
-    }
-  }, [pods]);
-
-  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];
-    }
-  };
-
-  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") {
-        const availability = getAvailability(object.metadata.kind, object);
-        if (availability != null) {
-          let [available, total] = availability;
-          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)}
-        />
-      );
-    });
-  };
-
-  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 RevisionLabel = styled.div`
-  font-size: 12px;
-  color: #ffffff33;
-  width: 78px;
-  text-align: right;
-  padding-top: 7px;
-  margin-right: 10px;
-  margin-left: 10px;
-  overflow-wrap: anywhere;
-`;
-
-const ReplicaSetContainer = styled.div`
-  padding: 10px 5px;
-  display: flex;
-  overflow-wrap: anywhere;
-  justify-content: space-between;
-  border-top: 2px solid #ffffff11;
-`;
-
-const ReplicaSetName = styled.span`
-  padding-left: 10px;
-  overflow-wrap: anywhere;
-  max-width: calc(100% - 45px);
-  line-height: 1.5em;
-  color: #ffffff33;
-`;

+ 0 - 157
dashboard/src/main/home/app-dashboard/expanded-app/status/ExpandedIncidentLogs.tsx

@@ -1,157 +0,0 @@
-import { useEffect, useRef } from "react";
-import React from "react";
-import styled from "styled-components";
-import Loading from "components/Loading";
-import dayjs from "dayjs";
-import Anser from "anser";
-import { PorterLog } from "../logs/types";
-
-interface ExpandedIncidentLogsProps {
-  logs: PorterLog[];
-}
-
-const ExpandedIncidentLogs: React.FC<ExpandedIncidentLogsProps> = ({ logs }: ExpandedIncidentLogsProps) => {
-  const scrollToBottomRef = useRef<HTMLDivElement>(null);
-
-  useEffect(() => {
-    if (scrollToBottomRef.current) {
-      scrollToBottomRef.current.scrollIntoView({
-        behavior: "smooth",
-        block: "end",
-      });
-    }
-  }, [logs, scrollToBottomRef]);
-
-  return logs.length ?
-    (<LogsSectionWrapper>
-      <StyledLogsSection>
-        {logs?.map((log, i) => {
-          return (
-            <LogSpan key={[log.lineNumber, i].join(".")}>
-              <span className="line-number">{log.lineNumber}.</span>
-              {log.timestamp && <span className="line-timestamp">
-                {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
-              </span>}
-              <LogOuter key={[log.lineNumber, i].join(".")}>
-                {Array.isArray(log.line) ? log.line?.map((ansi, j) => {
-                  if (ansi.clearLine) {
-                    return null;
-                  }
-
-                  return (
-                    <LogInnerSpan
-                      key={[log.lineNumber, i, j].join(".")}
-                      ansi={ansi}
-                    >
-                      {ansi.content.replace(/ /g, "\u00a0")}
-                    </LogInnerSpan>
-                  );
-                }) : (
-                  log.line
-                )}
-              </LogOuter>
-            </LogSpan>
-          );
-        })}
-        <div ref={scrollToBottomRef} />
-      </StyledLogsSection>
-    </LogsSectionWrapper>)
-    :
-    (<LogsLoadWrapper>
-      <Loading />
-    </LogsLoadWrapper >)
-};
-
-export default ExpandedIncidentLogs;
-
-
-const LogsSectionWrapper = styled.div`
-  position: relative;
-`;
-
-const StyledLogsSection = styled.div`
-  margin-top: 20px;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  max-height: 400px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  border-top: none;
-  background: #101420;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const LogSpan = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-start;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;
-
-export const ViewLogsWrapper = styled.div`
-  margin-bottom: -15px;
-  margin-top: 15px;
-`;
-const LogsLoadWrapper = styled.div`
-  height: 50px;
-`;

+ 0 - 231
dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx

@@ -1,231 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import styled from "styled-components";
-import Modal from "components/porter/Modal";
-import TitleSection from "components/TitleSection";
-import Loading from "components/Loading";
-import Text from "components/porter/Text";
-import danger from "assets/danger.svg";
-import Anser from "anser";
-
-import dayjs from "dayjs";
-import Link from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import { PorterLog } from "../logs/types";
-type Props = {
-  appData: any;
-  logs: PorterLog[];
-  modalVisible: boolean;
-  setModalVisible: (x: boolean) => void;
-  actionRunId?: string;
-};
-
-interface ExpandedIncidentLogsProps {
-  logs: PorterLog[];
-}
-
-const GHALogsModal: React.FC<Props> = ({
-  appData,
-  logs,
-  modalVisible,
-  setModalVisible,
-  actionRunId,
-}) => {
-  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
-  const scrollToBottomRef = useRef<HTMLDivElement>(null);
-  const ExpandedIncidentLogs = ({ logs }: ExpandedIncidentLogsProps) => {
-    if (!logs.length) {
-      return (
-        <LogsLoadWrapper>
-          <Loading />
-        </LogsLoadWrapper>
-      );
-    }
-
-    return (
-      <LogsSectionWrapper>
-        <StyledLogsSection>
-          {logs?.map((log, i) => {
-            return (
-              <LogSpan key={[log.lineNumber, i].join(".")}>
-                <span className="line-number">{log.lineNumber}.</span>
-                <span className="line-timestamp">
-                  {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
-                </span>
-                <LogOuter key={[log.lineNumber, i].join(".")}>
-                  {log.line?.map((ansi, j) => {
-                    if (ansi.clearLine) {
-                      return null;
-                    }
-
-                    return (
-                      <LogInnerSpan
-                        key={[log.lineNumber, i, j].join(".")}
-                        ansi={ansi}
-                      >
-                        {ansi.content.replace(/ /g, "\u00a0")}
-                      </LogInnerSpan>
-                    );
-                  })}
-                </LogOuter>
-              </LogSpan>
-            );
-          })}
-          <div ref={scrollToBottomRef} />
-        </StyledLogsSection>
-      </LogsSectionWrapper>
-    );
-  };
-  useEffect(() => {
-    if (scrollToBottomRef.current && scrollToBottomEnabled) {
-      scrollToBottomRef.current.scrollIntoView({
-        behavior: "smooth",
-        block: "end",
-      });
-    }
-  }, [logs, scrollToBottomRef, scrollToBottomEnabled]);
-  const renderExpandedEventMessage = () => {
-    if (!logs) {
-      return <Loading />;
-    }
-    return (
-      <>
-        <ExpandedIncidentLogs logs={logs} />
-      </>
-    );
-  };
-
-  return (
-    <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-      <TitleSection icon={danger}>
-        <Text size={16}>Logs for {appData.app.name}</Text>
-      </TitleSection>
-
-      {renderExpandedEventMessage()}
-      <Spacer y={0.5} />
-      <Link
-        hasunderline
-        target="_blank"
-        to={
-          actionRunId
-            ? `https://github.com/${appData.app.repo_name}/actions/runs/${actionRunId}`
-            : `https://github.com/${appData.app.repo_name}/actions`
-        }
-      >
-        View full build logs
-      </Link>
-    </Modal>
-  );
-};
-
-export default GHALogsModal;
-
-const LogsSectionWrapper = styled.div`
-  position: relative;
-`;
-
-const StyledLogsSection = styled.div`
-  margin-top: 20px;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  max-height: 400px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  border-top: none;
-  background: #101420;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const LogSpan = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-start;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;
-
-export const ViewLogsWrapper = styled.div`
-  margin-bottom: -15px;
-  margin-top: 15px;
-`;
-const LogsLoadWrapper = styled.div`
-  height: 50px;
-`;
-
-const ScrollButton = styled.div`
-  background: #26292e;
-  border-radius: 5px;
-  height: 30px;
-  font-size: 13px;
-  display: flex;
-  cursor: pointer;
-  align-items: center;
-  padding: 10px;
-  padding-left: 8px;
-  > i {
-    font-size: 16px;
-    margin-right: 5px;
-  }
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;

+ 0 - 398
dashboard/src/main/home/app-dashboard/expanded-app/status/Logs.tsx

@@ -1,398 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import styled from "styled-components";
-import Anser from "anser";
-import CommandLineIcon from "assets/command-line-icon";
-import { SelectedPodType } from "./types";
-import { useLogs } from "./useLogs";
-
-const LogsFC: React.FC<{
-  selectedPod: SelectedPodType;
-  podError: string;
-  rawText?: boolean;
-}> = ({ selectedPod, podError, rawText }) => {
-  const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(true);
-
-  const [showConnectionModal, setShowConnectionModal] = useState(false);
-
-  const shouldScroll = useRef<boolean>(true);
-  const wrapperRef = useRef<HTMLDivElement>();
-
-  const scrollToBottom = (smooth: boolean) => {
-    if (!wrapperRef.current || !shouldScroll.current) {
-      return;
-    }
-
-    if (smooth) {
-      wrapperRef.current.lastElementChild.scrollIntoView({
-        behavior: "smooth",
-        block: "nearest",
-        inline: "start",
-      });
-    } else {
-      wrapperRef.current.lastElementChild.scrollIntoView({
-        behavior: "auto",
-        block: "nearest",
-        inline: "start",
-      });
-    }
-  };
-
-  const {
-    logs,
-    previousLogs,
-    containers,
-    currentContainer,
-    setCurrentContainer,
-    refresh,
-  } = useLogs(selectedPod, scrollToBottom);
-
-  const [showPreviousLogs, setShowPreviousLogs] = useState<boolean>(false);
-
-  useEffect(() => {
-    shouldScroll.current = isScrollToBottomEnabled;
-  }, [isScrollToBottomEnabled]);
-
-  const renderLogs = () => {
-    if (podError && podError != "") {
-      return <Message>{podError}</Message>;
-    }
-
-    if (!selectedPod?.metadata?.name) {
-      return <Message>Please select a pod to view its logs.</Message>;
-    }
-
-    if (selectedPod?.status.phase === "Succeeded" && !rawText) {
-      return (
-        <Message>
-          ⌛ This job has been completed. You can now delete this job.
-        </Message>
-      );
-    }
-
-    if (
-      showPreviousLogs &&
-      Array.isArray(previousLogs) &&
-      previousLogs.length
-    ) {
-      return previousLogs?.map((log, i) => {
-        return (
-          <Log key={i}>
-            {log.map((ansi, j) => {
-              if (ansi.clearLine) {
-                return null;
-              }
-
-              return (
-                <LogSpan key={i + "." + j} ansi={ansi}>
-                  {ansi.content.replace(/ /g, "\u00a0")}
-                </LogSpan>
-              );
-            })}
-          </Log>
-        );
-      });
-    }
-
-    if (!Array.isArray(logs) || logs?.length === 0) {
-      return (
-        <Message>
-          No logs to display from this pod.
-          <Highlight onClick={refresh}>
-            <i className="material-icons">autorenew</i>
-            Refresh
-          </Highlight>
-        </Message>
-      );
-    }
-
-    return logs?.map((log, i) => {
-      return (
-        <Log key={i}>
-          {log.map((ansi, j) => {
-            if (ansi.clearLine) {
-              return null;
-            }
-
-            return (
-              <LogSpan key={i + "." + j} ansi={ansi}>
-                {ansi.content.replace(/ /g, "\u00a0")}
-              </LogSpan>
-            );
-          })}
-        </Log>
-      );
-    });
-  };
-
-  const renderContent = () => (
-    <>
-      {/* <ConnectToLogsInstructionModal
-        show={showConnectionModal}
-        onClose={() => setShowConnectionModal(false)}
-        chartName={selectedPod?.metadata?.labels["app.kubernetes.io/instance"]}
-        namespace={selectedPod?.metadata?.namespace}
-      />
-      <CLIModalIconWrapper
-        onClick={(e) => {
-          e.preventDefault();
-          setShowConnectionModal(true);
-        }}
-      >
-        <CLIModalIcon />
-        CLI Logs Instructions
-      </CLIModalIconWrapper> */}
-      <Wrapper ref={wrapperRef}>{renderLogs()}</Wrapper>
-      <LogTabs>
-        {containers.map((containerName, _i, arr) => {
-          return (
-            <Tab
-              key={containerName}
-              onClick={() => {
-                setCurrentContainer(containerName);
-              }}
-              clicked={currentContainer === containerName}
-            >
-              {arr.length > 1 ? containerName : "Application"}
-            </Tab>
-          );
-        })}
-        <Tab
-          onClick={() => {
-            setCurrentContainer("system");
-          }}
-          clicked={currentContainer == "system"}
-        >
-          System
-        </Tab>
-      </LogTabs>
-      <Options>
-        <Scroll
-          onClick={() => {
-            setIsScrollToBottomEnabled(!isScrollToBottomEnabled);
-            if (isScrollToBottomEnabled) {
-              scrollToBottom(true);
-            }
-          }}
-        >
-          <input
-            type="checkbox"
-            checked={isScrollToBottomEnabled}
-            onChange={() => {}}
-          />
-          Scroll to bottom
-        </Scroll>
-        {Array.isArray(previousLogs) && previousLogs.length > 0 && (
-          <Scroll
-            onClick={() => {
-              setShowPreviousLogs(!showPreviousLogs);
-            }}
-          >
-            <input
-              type="checkbox"
-              checked={showPreviousLogs}
-              onChange={() => {}}
-            />
-            Show previous logs
-          </Scroll>
-        )}
-        <Refresh onClick={() => refresh()}>
-          <i className="material-icons">autorenew</i>
-          Refresh
-        </Refresh>
-      </Options>
-    </>
-  );
-
-  if (!containers?.length) {
-    return null;
-  }
-
-  if (rawText) {
-    return <LogStreamAlt>{renderContent()}</LogStreamAlt>;
-  }
-
-  return <LogStream>{renderContent()}</LogStream>;
-};
-
-export default LogsFC;
-
-const Highlight = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-left: 8px;
-  color: #8590ff;
-  cursor: pointer;
-
-  > i {
-    font-size: 16px;
-    margin-right: 3px;
-  }
-`;
-
-const Scroll = styled.div`
-  align-items: center;
-  display: flex;
-  cursor: pointer;
-  width: max-content;
-  height: 100%;
-
-  :hover {
-    background: #2468d6;
-  }
-
-  > input {
-    width: 18px;
-    margin-left: 10px;
-    margin-right: 6px;
-    pointer-events: none;
-  }
-`;
-
-const Tab = styled.div`
-  background: ${(props: { clicked: boolean }) =>
-    props.clicked ? "#503559" : "#7c548a"};
-  padding: 0px 10px;
-  margin: 0px 7px 0px 0px;
-  align-items: center;
-  display: flex;
-  cursor: pointer;
-  height: 100%;
-  border-radius: 8px 8px 0px 0px;
-
-  :hover {
-    background: #503559;
-  }
-`;
-
-const Refresh = styled.div`
-  display: flex;
-  align-items: center;
-  width: 87px;
-  user-select: none;
-  cursor: pointer;
-  height: 100%;
-
-  > i {
-    margin-left: 6px;
-    font-size: 17px;
-    margin-right: 6px;
-  }
-
-  :hover {
-    background: #2468d6;
-  }
-`;
-
-const LogTabs = styled.div`
-  width: 100%;
-  height: 25px;
-  margin-top: -25px;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: flex-end;
-`;
-
-const Options = styled.div`
-  width: 100%;
-  height: 25px;
-  background: #397ae3;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-`;
-
-const Wrapper = styled.div`
-  width: 100%;
-  height: 100%;
-  overflow: auto;
-  padding: 25px 30px;
-`;
-
-const LogStream = styled.div`
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  float: right;
-  height: 100%;
-  font-size: 13px;
-  background: #000000;
-  user-select: text;
-  max-width: 65%;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-`;
-
-const LogStreamAlt = styled(LogStream)`
-  width: 100%;
-  max-width: 100%;
-`;
-
-const Message = styled.div`
-  display: flex;
-  height: 100%;
-  width: calc(100% - 150px);
-  align-items: center;
-  justify-content: center;
-  margin-left: 75px;
-  text-align: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
-const Log = styled.div`
-  font-family: monospace;
-`;
-
-const LogSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;
-
-const CLIModalIconWrapper = styled.div`
-  max-width: 200px;
-  height: 35px;
-  margin: 10px;
-  font-size: 13px;
-  font-weight: 500;
-  font-family: "Work Sans", sans-serif;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 6px 20px 6px 10px;
-  text-align: left;
-  border: 1px solid #ffffff55;
-  border-radius: 8px;
-  background: #ffffff11;
-  color: #ffffffdd;
-  cursor: pointer;
-  :hover {
-    cursor: pointer;
-    background: #ffffff22;
-    > path {
-      fill: #ffffff77;
-    }
-  }
-
-  > path {
-    fill: #ffffff99;
-  }
-`;
-
-const CLIModalIcon = styled(CommandLineIcon)`
-  width: 32px;
-  height: 32px;
-  padding: 8px;
-
-  > path {
-    fill: #ffffff99;
-  }
-`;

+ 0 - 50
dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx

@@ -1,50 +0,0 @@
-import React, { useEffect, useRef } from "react";
-
-import Modal from "components/porter/Modal";
-import Text from "components/porter/Text";
-import TitleSection from "components/TitleSection";
-
-import danger from "assets/danger.svg";
-
-import { type PorterLog } from "../logs/types";
-import ExpandedIncidentLogs from "./ExpandedIncidentLogs";
-
-type LogsModalProps = {
-  logs: PorterLog[];
-  setModalVisible: (x: boolean) => void;
-  logsName: string;
-};
-const LogsModal: React.FC<LogsModalProps> = ({
-  logs,
-  logsName,
-  setModalVisible,
-}) => {
-  const scrollToBottomRef = useRef<HTMLDivElement>(null);
-  const scrollToBottom = () => {
-    if (scrollToBottomRef.current) {
-      scrollToBottomRef.current.scrollIntoView({
-        behavior: "smooth",
-        block: "end",
-      });
-    }
-  };
-  useEffect(() => {
-    scrollToBottom();
-  }, [scrollToBottomRef]);
-
-  return (
-    <Modal
-      closeModal={() => {
-        setModalVisible(false);
-      }}
-      width={"800px"}
-    >
-      <TitleSection icon={danger}>
-        <Text size={16}>Logs for {logsName}</Text>
-      </TitleSection>
-      <ExpandedIncidentLogs logs={logs} />
-    </Modal>
-  );
-};
-
-export default LogsModal;

+ 0 - 234
dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx

@@ -1,234 +0,0 @@
-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} selected={isSelected} onClick={onTabClick}>
-      <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>
-        <StatusColor status={podStatus} />
-        {podStatus}
-        {podStatus === "failed" && (
-          <CloseIcon
-            className="material-icons-outlined"
-            onClick={onDeleteClick}
-          >
-            close
-          </CloseIcon>
-        )}
-      </Status>
-    </Tab>
-  );
-};
-
-export default PodRow;
-
-const InfoIcon = styled.div`
-  width: 22px;
-`;
-
-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;
-  color: ${(props: { selected: boolean }) =>
-    props.selected ? "white" : "#ffffff66"};
-  background: ${(props: { selected: boolean }) =>
-    props.selected ? "#ffffff18" : ""};
-  font-size: 13px;
-  padding: 20px 19px 20px 42px;
-  text-shadow: 0px 0px 8px none;
-  overflow: visible;
-  cursor: pointer;
-  :hover {
-    color: white;
-    background: #ffffff18;
-  }
-`;
-
-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: 5px;
-  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-right: 7px;
-  width: 7px;
-  min-width: 7px;
-  height: 7px;
-  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;
-`;

+ 0 - 278
dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx

@@ -1,278 +0,0 @@
-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 Logs from "./Logs";
-import ControllerTab from "./ControllerTab";
-import Banner from "components/porter/Banner";
-import Spacer from "components/porter/Spacer";
-
-type Props = {
-  selectors?: string[];
-  currentChart: ChartType;
-  fullscreen?: boolean;
-  setFullScreenLogs?: any;
-};
-
-const StatusSectionFC: React.FunctionComponent<Props> = ({
-  currentChart,
-  fullscreen,
-  setFullScreenLogs,
-  selectors,
-}) => {
-  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 renderLogs = () => {
-    return (
-      <Logs
-        podError={podError}
-        key={selectedPod?.metadata?.name}
-        selectedPod={selectedPod}
-      />
-    );
-  };
-
-  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)}
-        />
-      );
-    });
-  };
-
-  const renderStatusSection = () => {
-    if (isLoading) {
-      return (
-        <NoControllers>
-          <Loading />
-        </NoControllers>
-      );
-    }
-    if (controllers?.length > 0) {
-      return (
-        <Wrapper>
-          <TabWrapper>{renderTabs()}</TabWrapper>
-          {renderLogs()}
-        </Wrapper>
-      );
-    }
-
-    if (currentChart?.chart?.metadata?.name === "job") {
-      return (
-        <NoControllers>
-          <i className="material-icons">category</i>
-          There are no jobs currently running.
-        </NoControllers>
-      );
-    }
-
-    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 StatusSectionFC;
-
-const MyLink = styled.a`
-  cursor: pointer;
-  color: #ffffff;
-  text-decoration: underline;
-`;
-
-const FullScreenButton = styled.div<{ top?: string }>`
-  position: absolute;
-  top: ${(props) => props.top || "10px"};
-  right: 10px;
-  width: 24px;
-  height: 24px;
-  cursor: pointer;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 5px;
-  background: #ffffff11;
-  border: 1px solid #aaaabb;
-
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    font-size: 14px;
-  }
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  width: 30px;
-  z-index: 999;
-  cursor: pointer;
-  height: 30px;
-  align-items: center;
-  margin-right: 15px;
-  justify-content: center;
-  cursor: pointer;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  > i {
-    font-size: 18px;
-  }
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const AbsoluteTitle = styled.div`
-  position: absolute;
-  top: 0px;
-  left: 0px;
-  width: 100%;
-  height: 60px;
-  display: flex;
-  align-items: center;
-  padding-left: 20px;
-  font-size: 18px;
-  font-weight: 500;
-  user-select: text;
-`;
-
-const TabWrapper = styled.div`
-  width: 35%;
-  min-width: 250px;
-  height: 100%;
-  overflow-y: auto;
-`;
-
-const StyledStatusSection = styled.div`
-  padding: 0px;
-  user-select: text;
-  overflow: hidden;
-  width: 100%;
-  min-height: 400px;
-  height: calc(100vh - 400px);
-  font-size: 13px;
-  overflow: hidden;
-  border-radius: 8px;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const FullScreen = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  padding-top: 60px;
-`;
-
-const Wrapper = styled.div`
-  width: 100%;
-  height: 100%;
-  display: flex;
-`;
-
-const NoControllers = styled.div`
-  padding-top: 20%;
-  position: relative;
-  width: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  color: #ffffff44;
-  font-size: 14px;
-
-  > i {
-    font-size: 18px;
-    margin-right: 12px;
-  }
-`;

+ 0 - 19
dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts

@@ -1,19 +0,0 @@
-export type SelectedPodType = {
-  spec: {
-    [key: string]: any;
-    containers: {
-      [key: string]: any;
-      name: string;
-    }[];
-  };
-  metadata: {
-    name: string;
-    namespace: string;
-    labels: {
-      [key: string]: string;
-    };
-  };
-  status: {
-    phase: string;
-  };
-};

+ 0 - 218
dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts

@@ -1,218 +0,0 @@
-import Anser from "anser";
-import { useContext, useEffect, useMemo, useRef, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
-import { SelectedPodType } from "./types";
-
-const MAX_LOGS = 250;
-
-export const useLogs = (
-  currentPod: SelectedPodType,
-  scroll?: (smooth: boolean) => void
-) => {
-  const currentPodName = useRef<string>();
-
-  const { currentCluster, currentProject } = useContext(Context);
-  const [containers, setContainers] = useState<string[]>([]);
-  const [currentContainer, setCurrentContainer] = useState<string>("");
-  const [logs, setLogs] = useState<{
-    [key: string]: Anser.AnserJsonEntry[][];
-  }>({});
-
-  const [prevLogs, setPrevLogs] = useState<{
-    [key: string]: Anser.AnserJsonEntry[][];
-  }>({});
-
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    getWebsocket,
-    closeWebsocket,
-  } = useWebsockets();
-
-  const getSystemLogs = async () => {
-    const events = await api
-      .getPodEvents(
-        "<token>",
-        {},
-        {
-          name: currentPod?.metadata?.name,
-          namespace: currentPod?.metadata?.namespace,
-          cluster_id: currentCluster?.id,
-          id: currentProject?.id,
-        }
-      )
-      .then((res) => res.data);
-
-    let processedLogs = [] as Anser.AnserJsonEntry[][];
-
-    events.items.forEach((evt: any) => {
-      let ansiEvtType = evt.type == "Warning" ? "\u001b[31m" : "\u001b[32m";
-      let ansiLog = Anser.ansiToJson(
-        `${ansiEvtType}${evt.type}\u001b[0m \t \u001b[43m\u001b[34m\t${evt.reason} \u001b[0m \t ${evt.message}`
-      );
-      processedLogs.push(ansiLog);
-    });
-
-    // SET LOGS FOR SYSTEM
-    setLogs((prevState) => ({
-      ...prevState,
-      system: processedLogs,
-    }));
-  };
-
-  const getContainerPreviousLogs = async (containerName: string) => {
-    try {
-      const logs = await api
-        .getPreviousLogsForContainer<{ previous_logs: string[] }>(
-          "<token>",
-          {
-            container_name: containerName,
-          },
-          {
-            pod_name: currentPod?.metadata?.name,
-            namespace: currentPod?.metadata?.namespace,
-            cluster_id: currentCluster?.id,
-            project_id: currentProject?.id,
-          }
-        )
-        .then((res) => res.data);
-      // Process logs
-      const processedLogs: Anser.AnserJsonEntry[][] = logs.previous_logs.map(
-        (currentLog) => {
-          let ansiLog = Anser.ansiToJson(currentLog);
-          return ansiLog;
-        }
-      );
-
-      setPrevLogs((pl) => ({
-        ...pl,
-        [containerName]: processedLogs,
-      }));
-    } catch (error) { }
-  };
-
-  const setupWebsocket = (containerName: string, websocketKey: string) => {
-    if (!currentPod?.metadata?.name) return;
-
-    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${currentPod?.metadata?.namespace}/pod/${currentPod?.metadata?.name}/logs?container_name=${containerName}`;
-
-    const config: NewWebsocketOptions = {
-      onopen: () => {
-        console.log("Opened websocket:", websocketKey);
-      },
-      onmessage: (evt: MessageEvent) => {
-        let ansiLog = Anser.ansiToJson(evt.data);
-        setLogs((logs) => {
-          const tmpLogs = { ...logs };
-          let containerLogs = tmpLogs[containerName] || [];
-
-          containerLogs.push(ansiLog);
-          // this is technically not as efficient as things could be
-          // if there are performance issues, a deque can be used in place of a list
-          // for storing logs
-          if (containerLogs.length > MAX_LOGS) {
-            containerLogs.shift();
-          }
-          if (typeof scroll === "function") {
-            scroll(true);
-          }
-          return {
-            ...logs,
-            [containerName]: containerLogs,
-          };
-        });
-      },
-      onclose: () => {
-        console.log("Closed websocket:", websocketKey);
-      },
-    };
-
-    newWebsocket(websocketKey, endpoint, config);
-    openWebsocket(websocketKey);
-  };
-
-  const refresh = () => {
-    const websocketKey = `${currentPodName.current}-${currentContainer}-websocket`;
-    closeWebsocket(websocketKey);
-
-    setPrevLogs((prev) => ({ ...prev, [currentContainer]: [] }));
-    setLogs((prev) => ({ ...prev, [currentContainer]: [] }));
-
-    if (!Array.isArray(containers)) {
-      return;
-    }
-
-    if (currentContainer === "system") {
-      getSystemLogs();
-    } else {
-      getContainerPreviousLogs(currentContainer);
-      setupWebsocket(currentContainer, websocketKey);
-    }
-  };
-
-  useEffect(() => {
-    // console.log("Selected pod updated");
-    if (currentPod?.metadata?.name === currentPodName.current) {
-      return () => { };
-    }
-    currentPodName.current = currentPod?.metadata?.name;
-    const currentContainers =
-      currentPod?.spec?.containers?.map((container) => container?.name) || [];
-
-    setContainers(currentContainers);
-    setCurrentContainer(currentContainers[0]);
-  }, [currentPod]);
-
-  // Retrieve all previous logs for containers
-  useEffect(() => {
-    if (!Array.isArray(containers)) {
-      return;
-    }
-
-    closeAllWebsockets();
-
-    setPrevLogs({});
-    setLogs({});
-
-    getSystemLogs();
-    containers.forEach((containerName) => {
-      const websocketKey = `${currentPodName.current}-${containerName}-websocket`;
-
-      getContainerPreviousLogs(containerName);
-
-      if (!getWebsocket(websocketKey)) {
-        setupWebsocket(containerName, websocketKey);
-      }
-    });
-
-    return () => {
-      closeAllWebsockets();
-    };
-  }, [containers]);
-
-  useEffect(() => {
-    return () => {
-      closeAllWebsockets();
-    };
-  }, []);
-
-  const currentLogs = useMemo(() => {
-    return logs[currentContainer] || [];
-  }, [currentContainer, logs]);
-
-  const currentPreviousLogs = useMemo(() => {
-    return prevLogs[currentContainer] || [];
-  }, [currentContainer, prevLogs]);
-
-  return {
-    containers,
-    currentContainer,
-    setCurrentContainer,
-    logs: currentLogs,
-    previousLogs: currentPreviousLogs,
-    refresh,
-  };
-};

+ 0 - 781
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -1,781 +0,0 @@
-import React, { useState, useContext, useEffect } from "react";
-import styled from "styled-components";
-import { type RouteComponentProps, withRouter } from "react-router";
-import _ from "lodash";
-import yaml from "js-yaml";
-
-import { Context } from "shared/Context";
-import api from "shared/api";
-import web from "assets/web.png";
-import sliders from "assets/sliders.svg";
-
-import Back from "components/porter/Back";
-import DashboardHeader from "../../cluster-dashboard/DashboardHeader";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import Input from "components/porter/Input";
-import VerticalSteps from "components/porter/VerticalSteps";
-import Button from "components/porter/Button";
-import SourceSelector, { type SourceType } from "./SourceSelector";
-import Container from "components/porter/Container";
-
-import SourceSettings from "./SourceSettings";
-import Services from "./Services";
-import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import GithubActionModal from "./GithubActionModal";
-import Error from "components/porter/Error";
-import { type PorterJson, PorterYamlSchema, createFinalPorterYaml } from "./schema";
-import { ImageInfo, Service } from "./serviceTypes";
-import GithubConnectModal from "./GithubConnectModal";
-import Link from "components/porter/Link";
-import { type BuildMethod, PorterApp } from "../types/porterApp";
-import { type NewPopulatedEnvGroup, PartialEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types";
-import EnvGroupArrayStacks from "main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks";
-import EnvGroupModal from "../expanded-app/env-vars/EnvGroupModal";
-import ExpandableEnvGroup from "../expanded-app/env-vars/ExpandableEnvGroup";
-
-type Props = RouteComponentProps & {};
-
-type FormState = {
-  applicationName: string;
-  selectedSourceType: SourceType | undefined;
-  serviceList: Service[];
-  envVariables: KeyValueType[];
-}
-
-const INITIAL_STATE: FormState = {
-  applicationName: "",
-  selectedSourceType: undefined,
-  serviceList: [],
-  envVariables: [],
-};
-
-const Validators: {
-  [key in keyof FormState]: (value: FormState[key]) => boolean;
-} = {
-  applicationName: (value: string) => value.trim().length > 0,
-  selectedSourceType: (value: SourceType | undefined) => value !== undefined,
-  serviceList: (value: Service[]) => value.length > 0,
-  envVariables: (value: KeyValueType[]) => true,
-};
-
-type Detected = {
-  detected: boolean;
-  message: string;
-};
-type GithubAppAccessData = {
-  username?: string;
-  accounts?: string[];
-}
-
-type PorterJsonWithPath = {
-  porterYamlPath: string;
-  porterJson: PorterJson;
-}
-
-const NewAppFlow: React.FC<Props> = ({ ...props }) => {
-  const [porterApp, setPorterApp] = useState<PorterApp>(PorterApp.empty());
-  const [hovered, setHovered] = useState(false);
-
-  const [imageTag, setImageTag] = useState("");
-  const { currentCluster, currentProject } = useContext(Context);
-  const [deploying, setDeploying] = useState<boolean>(false);
-  const [deploymentError, setDeploymentError] = useState<string | undefined>(undefined);
-  const [currentStep, setCurrentStep] = useState<number>(0);
-  const [existingStep, setExistingStep] = useState<number>(0);
-  const [formState, setFormState] = useState<FormState>(INITIAL_STATE);
-  const [porterYaml, setPorterYaml] = useState("");
-  const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
-  const [showGithubConnectModal, setShowGithubConnectModal] = useState<boolean>(
-    false
-  );
-
-  const [showConnectModal, setConnectModal] = useState<boolean>(true);
-  const [hasClickedDoNotConnect, setHasClickedDoNotConnect] = useState(() =>
-    JSON.parse(localStorage.getItem("hasClickedDoNotConnect") || "false")
-  );
-  const [accessLoading, setAccessLoading] = useState(true);
-  const [accessError, setAccessError] = useState(false);
-  const [accessData, setAccessData] = useState<GithubAppAccessData>({});
-  const [hasProviders, setHasProviders] = useState(true);
-
-  const [porterJsonWithPath, setPorterJsonWithPath] = useState<PorterJsonWithPath | undefined>(undefined);
-  const [detected, setDetected] = useState<Detected | undefined>(undefined);
-  const [buildView, setBuildView] = useState<BuildMethod>("buildpacks");
-
-  const [existingApps, setExistingApps] = useState<string[]>([]);
-  const [appNameInputError, setAppNameInputError] = useState<string | undefined>(undefined);
-
-  const [syncedEnvGroups, setSyncedEnvGroups] = useState<NewPopulatedEnvGroup[]>([]);
-  const [showEnvModal, setShowEnvModal] = useState(false);
-  const [deletedEnvGroups, setDeleteEnvGroups] = useState<NewPopulatedEnvGroup[]>([])
-
-  // this advances the step in the case that a user chooses a repo that doesn't have a porter.yaml
-  useEffect(() => {
-    if (porterApp.git_branch !== "") {
-      setCurrentStep(Math.max(currentStep, 2));
-    }
-  }, [porterApp.git_branch]);
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    if (currentProject == null) {
-      return;
-    }
-
-    api
-      .getGitProviders("<token>", {}, { project_id: currentProject?.id })
-      .then((res) => {
-        const data = res.data;
-        if (!isSubscribed) {
-          return;
-        }
-
-        if (!Array.isArray(data)) {
-          setHasProviders(false);
-          
-        }
-      })
-      .catch((err) => {
-        setHasProviders(false);
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [currentProject]);
-
-  useEffect(() => {
-    const getApps = async () => {
-      try {
-        const res = await api.getPorterApps(
-          "<token>",
-          {},
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        );
-        if (res?.data != null) {
-          setExistingApps(res.data.map((app: PorterApp) => app.name));
-        }
-      } catch (err) {
-      }
-    };
-    getApps();
-  }, [])
-
-  useEffect(() => {
-    setFormState({ ...formState, serviceList: [] });
-    setDetected(undefined);
-  }, [porterApp.git_branch]);
-
-  const handleSetAccessData = (data: GithubAppAccessData) => {
-    setAccessData(data);
-    setShowGithubConnectModal(
-      !hasClickedDoNotConnect &&
-      (accessError || !data.accounts || data.accounts?.length === 0)
-    );
-  };
-
-  const handleSetAccessError = (error: boolean) => {
-    setAccessError(error);
-    setShowGithubConnectModal(
-      !hasClickedDoNotConnect &&
-      (error || !accessData.accounts || accessData.accounts?.length === 0)
-    );
-  };
-
-  const updateStackStep = async (step: string, errorMessage: string = "") => {
-    try {
-      if (currentCluster?.id == null || currentProject?.id == null) {
-        throw "Unable to capture analytics, project or cluster not found";
-      }
-      await api.updateStackStep(
-        "<token>",
-        {
-          step,
-          stack_name: porterApp.name,
-          error_message: errorMessage,
-        },
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-        }
-      );
-    } catch (err) {
-      // TODO: handle analytics error
-    }
-  };
-
-  const validateAndSetPorterYaml = (yamlString: string, filename: string) => {
-    let parsedYaml;
-    try {
-      parsedYaml = yaml.load(yamlString);
-      const parsedData = PorterYamlSchema.parse(parsedYaml);
-      const porterYamlToJson = parsedData ;
-      setPorterJsonWithPath({ porterJson: porterYamlToJson, porterYamlPath: filename });
-      const newServices = [];
-      const existingServices = formState.serviceList.map((s) => s.name);
-      for (const [name, app] of Object.entries(porterYamlToJson.apps)) {
-        if (!existingServices.includes(name)) {
-          if (app.type) {
-            newServices.push(Service.default(name, app.type, porterYamlToJson));
-          } else if (name.includes("web")) {
-            newServices.push(Service.default(name, "web", porterYamlToJson));
-          } else {
-            newServices.push(Service.default(name, "worker", porterYamlToJson));
-          }
-        }
-      }
-      if (porterYamlToJson.release != null && !existingServices.includes("pre-deploy")) {
-        newServices.push(Service.default("pre-deploy", "release", porterYamlToJson));
-      }
-      const newServiceList = [...formState.serviceList, ...newServices];
-      if (Validators.serviceList(newServiceList)) {
-        setCurrentStep(Math.max(currentStep, 5));
-      }
-      setFormState({
-        ...formState,
-        serviceList: newServiceList,
-      });
-      if (
-        porterYamlToJson &&
-        porterYamlToJson.apps &&
-        Object.keys(porterYamlToJson.apps).length > 0
-      ) {
-        setDetected({
-          detected: true,
-          message: `Detected ${Object.keys(porterYamlToJson.apps).length
-            } service${Object.keys(porterYamlToJson.apps).length === 1 ? "" : "s"} from porter.yaml`,
-        });
-      } else {
-        setDetected({
-          detected: false,
-          message:
-            "Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.",
-        });
-      }
-    } catch (error) {
-      console.log("Error converting porter yaml file to input: " + error);
-    }
-  };
-
-  const handleAppNameChange = (name: string) => {
-    setPorterApp(PorterApp.setAttribute(porterApp, "name", name));
-    const appNameInputError = getAppNameInputError(name);
-    if (appNameInputError == null) {
-      setCurrentStep(Math.max(Math.max(currentStep, 1), existingStep));
-    } else {
-      setExistingStep(Math.max(currentStep, existingStep));
-      setCurrentStep(0);
-    }
-    setAppNameInputError(appNameInputError);
-  };
-
-  const handleDoNotConnect = () => {
-    setHasClickedDoNotConnect(true);
-    localStorage.setItem("hasClickedDoNotConnect", "true");
-  };
-
-  const getAppNameInputError = (name: string) => {
-    const regex = /^[a-z0-9-]{1,61}$/;
-    if (name === "") {
-      return undefined;
-    } else if (!regex.test(name)) {
-      return 'Lowercase letters, numbers, and "-" only.';
-    } else if (name.length > 30) {
-      return "Maximum 30 characters allowed.";
-    } else if (existingApps.includes(name)) {
-      return "An app with this name already exists.";
-    }
-    return undefined;
-  };
-
-  const deleteEnvGroup = (envGroup: PopulatedEnvGroup) => {
-    setDeleteEnvGroups([...deletedEnvGroups, envGroup]);
-    setSyncedEnvGroups(syncedEnvGroups?.filter(
-      (env) => env.name !== envGroup.name
-    ))
-  }
-
-  const deployPorterApp = async () => {
-    try {
-      setDeploying(true);
-      setDeploymentError(undefined);
-
-      // log analytics event that we started form submission
-      updateStackStep("stack-launch-complete");
-
-      if (currentProject?.id == null || currentCluster?.id == null) {
-        throw "Project or cluster not found";
-      }
-
-      // validate form data
-      const finalPorterYaml = createFinalPorterYaml(
-        formState.serviceList,
-        formState.envVariables,
-        porterJsonWithPath?.porterJson,
-        // if we are using a heroku buildpack, inject a PORT env variable
-        porterApp.builder.includes("heroku")
-      );
-
-      const yamlString = yaml.dump(finalPorterYaml);
-      const base64Encoded = btoa(yamlString);
-      const imageInfo: ImageInfo = ImageInfo.BASE_IMAGE;
-
-      const porterAppRequest = {
-        porter_yaml: base64Encoded,
-        override_release: true,
-        ...PorterApp.empty(),
-        image_info: imageInfo,
-        buildpacks: "",
-        // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/
-        porter_yaml_path: porterJsonWithPath?.porterYamlPath,
-        repo_name: porterApp.repo_name,
-        git_branch: porterApp.git_branch,
-        git_repo_id: porterApp.git_repo_id,
-        build_context: porterApp.build_context,
-        image_repo_uri: porterApp.image_repo_uri,
-        environment_groups: syncedEnvGroups?.map((env: NewPopulatedEnvGroup) => env.name),
-        user_update: true,
-      }
-      if (porterApp.image_repo_uri && imageTag) {
-        porterAppRequest.image_info = {
-          repository: porterApp.image_repo_uri,
-          tag: imageTag,
-        };
-        porterAppRequest.repo_name = "";
-        porterAppRequest.git_branch = "";
-        porterAppRequest.git_repo_id = 0;
-      } else if (buildView === "docker") {
-        if (porterApp.dockerfile === "") {
-          porterAppRequest.dockerfile = "./Dockerfile";
-        } else {
-          if (!porterApp.dockerfile.startsWith("./") && !porterApp.dockerfile.startsWith("/")) {
-            porterAppRequest.dockerfile = `./${porterApp.dockerfile}`;
-          } else {
-            porterAppRequest.dockerfile = porterApp.dockerfile;
-          }
-        }
-      } else {
-        porterAppRequest.builder = porterApp.builder;
-        porterAppRequest.buildpacks = porterApp.buildpacks.join(",");
-      }
-
-      await api.createPorterApp(
-        "<token>",
-        porterAppRequest,
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          stack_name: porterApp.name,
-        }
-      );
-
-      if (porterAppRequest.repo_name === "") {
-        props.history.push(`/apps/${porterApp.name}`);
-      }
-
-      // log analytics event that we successfully deployed
-      updateStackStep("stack-launch-success");
-
-      return true;
-    } catch (err: any) {
-      // TODO: better error handling
-      const errMessage =
-        err?.response?.data?.error ??
-        err?.toString() ??
-        "An error occurred while deploying your app. Please try again.";
-      setDeploymentError(errMessage);
-      updateStackStep("stack-launch-failure", errMessage);
-      return false;
-    } finally {
-      setDeploying(false);
-    }
-  };
-  const maxEnvGroupsReached = syncedEnvGroups.length >= 4;
-
-
-  return (
-    <CenterWrapper>
-      <Div>
-        {showConnectModal && !hasProviders && (
-          <GithubConnectModal
-            closeModal={() => { setConnectModal(false); }}
-            hasClickedDoNotConnect={hasClickedDoNotConnect}
-            handleDoNotConnect={handleDoNotConnect}
-            accessData={accessData}
-            setAccessLoading={setAccessLoading}
-            accessError={accessError}
-            setAccessData={handleSetAccessData}
-            setAccessError={handleSetAccessError}
-          />
-        )}
-        <StyledConfigureTemplate>
-          <Back to="/apps" />
-          <DashboardHeader
-            prefix={<Icon src={web} />}
-            title="Deploy a new application"
-            capitalize={false}
-            disableLineBreak
-          />
-          <DarkMatter />
-          <VerticalSteps
-            currentStep={currentStep}
-            steps={[
-              <>
-                <Text size={16}>Application name</Text>
-                <Spacer y={0.5} />
-                <Text color="helper">
-                  Lowercase letters, numbers, and "-" only.
-                </Text>
-                <Spacer y={0.5} />
-                <Input
-                  placeholder="ex: academic-sophon"
-                  value={porterApp.name}
-                  width="300px"
-                  error={appNameInputError}
-                  setValue={handleAppNameChange}
-                />
-              </>,
-              <>
-                <Text size={16}>Deployment method</Text>
-                <Spacer y={0.5} />
-                <Text color="helper">
-                  Deploy from a Git repository or a Docker registry.
-                  <Spacer inline width="5px" />
-                  <Link
-                    hasunderline
-                    to="https://docs.porter.run/standard/deploying-applications/overview"
-                    target="_blank"
-                  >
-                    Learn more
-                  </Link>
-                </Text>
-                <Spacer y={0.5} />
-                <SourceSelector
-                  selectedSourceType={formState.selectedSourceType}
-                  setSourceType={(type) => {
-                    setPorterYaml("");
-                    setFormState({ ...formState, selectedSourceType: type });
-                  }}
-                />
-                <SourceSettings
-                  source={formState.selectedSourceType}
-                  setPorterYaml={(newYaml: string, filename: string) => {
-                    validateAndSetPorterYaml(newYaml, filename);
-                  }}
-                  porterApp={porterApp}
-                  setPorterApp={setPorterApp}
-                  imageUrl={porterApp.image_repo_uri}
-                  setImageUrl={(url: string) => {
-                    setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", url));
-                    setCurrentStep(Math.max(currentStep, 2));
-                  }}
-                  imageTag={imageTag}
-                  setImageTag={setImageTag}
-                  buildView={buildView}
-                  setBuildView={setBuildView}
-                  projectId={currentProject?.id ?? 0}
-                  resetImageInfo={() => {
-                    setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", ""));
-                    setImageTag("");
-                  }}
-                />
-              </>,
-              <>
-                <Container row>
-                  <Text size={16}>
-                    Application services{" "}
-                  </Text>
-                  {detected && formState.serviceList.length > 0 && (
-                    <AppearingDiv color={detected.detected ? "#8590ff" : "#fcba03"}>
-                      {detected.detected ? (
-                        <I className="material-icons">check</I>
-                      ) : (
-                        <I className="material-icons">error</I>
-                      )}
-                      <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
-                        {detected.message}
-                      </Text>
-                    </AppearingDiv>
-                  )}
-                </Container>
-                <Spacer y={0.5} />
-                <Services
-                  setServices={(services: Service[]) => {
-                    const release = formState.serviceList.filter(Service.isRelease)
-                    setFormState({ ...formState, serviceList: [...services, ...release] });
-                    if (Validators.serviceList(services)) {
-                      setCurrentStep(Math.max(currentStep, 5));
-                    }
-                  }}
-                  services={formState.serviceList.filter(Service.isNonRelease)}
-                  defaultExpanded={true}
-                  addNewText={"Add a new service"}
-                  appName={porterApp.name}
-                />
-              </>,
-              <>
-                <Text size={16}>Environment variables (optional)</Text>
-                <Spacer y={0.5} />
-                <Text color="helper">
-                  Specify environment variables shared among all services.
-                </Text>
-                <EnvGroupArrayStacks
-                  key={formState.envVariables.length}
-                  values={formState.envVariables}
-                  setValues={(x: any) => {
-                    setFormState({ ...formState, envVariables: x });
-                  }}
-                  fileUpload={true}
-                  syncedEnvGroups={syncedEnvGroups}
-                />
-
-                <>
-                  <TooltipWrapper
-                    onMouseOver={() => { setHovered(true); }}
-                    onMouseOut={() => { setHovered(false); }}>
-                    <LoadButton
-                      disabled={maxEnvGroupsReached}
-                      onClick={() => { !maxEnvGroupsReached && setShowEnvModal(true); }}
-                    >
-                      <img src={sliders} /> Load from Env Group
-                    </LoadButton>
-                    <TooltipText visible={maxEnvGroupsReached && hovered}>Max 4 Env Groups allowed</TooltipText>
-                  </TooltipWrapper>
-
-                  {showEnvModal && <EnvGroupModal
-                    setValues={(x: any) => {
-                      setFormState({ ...formState, envVariables: x });
-                    }}
-                    values={formState.envVariables}
-                    closeModal={() => { setShowEnvModal(false); }}
-                    syncedEnvGroups={syncedEnvGroups}
-                    setSyncedEnvGroups={setSyncedEnvGroups}
-                    namespace={"porter-stack-" + porterApp.name}
-                    newApp={true}
-                  />}
-                  {!!syncedEnvGroups?.length && (
-                    <>
-                      <Spacer y={0.5} />
-                      <Text size={16}>Synced environment groups</Text >
-                      {syncedEnvGroups?.map((envGroup: any) => {
-                        return (
-                          <ExpandableEnvGroup
-                            key={envGroup?.name}
-                            envGroup={envGroup}
-                            onDelete={() => {
-                              deleteEnvGroup(envGroup);
-                            }}
-                          />
-                        );
-                      })}
-                    </>
-                  )}
-                </>
-
-              </>,
-              formState.selectedSourceType == "github" &&
-              <>
-                <Text size={16}>Pre-deploy job (optional)</Text>
-                <Spacer y={0.5} />
-                <Text color="helper">
-                  After your application is built each time, your pre-deploy command will run before your services
-                  are deployed. Use this for operations like a database migration.
-                </Text>
-                <Spacer y={0.5} />
-                <Services
-                  setServices={(release: Service[]) => {
-                    const nonRelease = formState.serviceList.filter(Service.isNonRelease)
-                    setFormState({ ...formState, serviceList: [...nonRelease, ...release] });
-                  }}
-                  services={formState.serviceList.filter(Service.isRelease)}
-                  limitOne={true}
-                  addNewText={"Add a new pre-deploy job"}
-                  prePopulateService={Service.default("pre-deploy", "release", porterJsonWithPath?.porterJson)}
-                  appName={porterApp.name}
-                />
-              </>,
-              <Button
-                onClick={() => {
-                  if (porterApp.image_repo_uri) {
-                    deployPorterApp();
-                  } else {
-                    setDeploymentError(undefined);
-                    setShowGHAModal(true);
-                  }
-                }}
-                disabled={formState.serviceList.length === 0 || deploying}
-                status={
-                  deploying ? (
-                    "loading"
-                  ) : deploymentError ? (
-                    <Error message={deploymentError} />
-                  ) : undefined
-                }
-                loadingText={"Deploying..."}
-                width={"120px"}
-              >
-                Deploy app
-              </Button>,
-            ].filter((x) => x)}
-          />
-          <Spacer y={3} />
-        </StyledConfigureTemplate>
-      </Div>
-      {showGHAModal && currentCluster != null && currentProject != null && (
-        <GithubActionModal
-          closeModal={() => { setShowGHAModal(false); }}
-          githubAppInstallationID={porterApp.git_repo_id}
-          githubRepoOwner={porterApp.repo_name.split("/")[0]}
-          githubRepoName={porterApp.repo_name.split("/")[1]}
-          branch={porterApp.git_branch}
-          stackName={porterApp.name}
-          projectId={currentProject.id}
-          clusterId={currentCluster.id}
-          deployPorterApp={deployPorterApp}
-          deploymentError={deploymentError}
-          porterYamlPath={porterJsonWithPath?.porterYamlPath}
-        />
-      )}
-    </CenterWrapper>
-  );
-};
-
-export default withRouter(NewAppFlow);
-
-const I = styled.i`
-  font-size: 18px;
-  margin-right: 5px;
-`;
-
-const Div = styled.div`
-  width: 100%;
-  max-width: 900px;
-`;
-
-const CenterWrapper = styled.div`
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-`;
-
-const DarkMatter = styled.div`
-  width: 100%;
-  margin-top: -5px;
-`;
-
-const Icon = styled.img`
-  margin-right: 15px;
-  height: 28px;
-  animation: floatIn 0.5s;
-  animation-fill-mode: forwards;
-
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(20px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const AppearingDiv = styled.div<{ color?: string }>`
-  animation: floatIn 0.5s;
-  animation-fill-mode: forwards;
-  display: flex;
-  align-items: center;
-  color: ${(props) => props.color || "#ffffff44"};
-  margin-left: 10px;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(20px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const StyledConfigureTemplate = styled.div`
-  height: 100%;
-`;
-
-
-const AddRowButton = styled.div`
-  display: flex;
-  align-items: center;
-  width: 270px;
-  font-size: 13px;
-  color: #aaaabb;
-  height: 32px;
-  border-radius: 3px;
-  cursor: pointer;
-  background: #ffffff11;
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-`;
-const LoadButton = styled(AddRowButton) <{ disabled?: boolean }>`
-  background: ${(props) => (props.disabled ? "#aaaaaa55" : "none")};
-  border: 1px solid ${(props) => (props.disabled ? "#aaaaaa55" : "#ffffff55")};
-  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
-
-  > i {
-    color: ${(props) => (props.disabled ? "#aaaaaa44" : "#ffffff44")};
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  > img {
-    width: 14px;
-    margin-left: 10px;
-    margin-right: 12px;
-    opacity: ${(props) => (props.disabled ? "0.5" : "1")};
-  }
-`;
-
-const TooltipWrapper = styled.div`
-  position: relative;
-  display: inline-block;
-`;
-
-const TooltipText = styled.span`
-  visibility: ${(props) => (props.visible ? 'visible' : 'hidden')};
-  width: 240px;
-  color: #fff;
-  text-align: center;
-  padding: 5px 0;
-  border-radius: 6px;
-  position: absolute;
-  z-index: 1;
-  bottom: 100%;
-  left: 50%;
-  margin-left: -120px;
-  opacity: ${(props) => (props.visible ? '1' : '0')};
-  transition: opacity 0.3s;
-  font-size: 12px;
-`;
-

+ 0 - 325
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -1,325 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import AnimateHeight, { Height } from "react-animate-height";
-import styled from "styled-components";
-import _, { set } from "lodash";
-
-import web from "assets/web.png";
-import worker from "assets/worker.png";
-import job from "assets/job.png";
-
-import Spacer from "components/porter/Spacer";
-import WebTabs from "./tabs/WebTabs";
-import WorkerTabs from "./tabs/WorkerTabs";
-import JobTabs from "./tabs/JobTabs";
-import { Service } from "./serviceTypes";
-import StatusFooter from "../expanded-app/StatusFooter";
-import ReleaseTabs from "./tabs/ReleaseTabs";
-import { Context } from "shared/Context";
-import { AWS_INSTANCE_LIMITS } from "./tabs/utils";
-import api from "shared/api";
-
-interface ServiceProps {
-  service: Service;
-  chart?: any;
-  editService: (service: Service) => void;
-  deleteService: () => void;
-  setExpandedJob?: (x: string) => void;
-}
-
-const ServiceContainer: React.FC<ServiceProps> = ({
-  service,
-  chart,
-  deleteService,
-  editService,
-  setExpandedJob,
-}) => {
-  const [height, setHeight] = React.useState<Height>("auto");
-  const [applicationNodeCount, setApplicationNodeCount] = useState<number>(1);
-  const [maxCPU, setMaxCPU] = useState(2); //default is set to a t3 medium 
-  const [maxRAM, setMaxRAM] = useState(4); //default is set to a t3 medium
-  const context = useContext(Context);
-
-  useEffect(() => {
-    const { currentCluster, currentProject } = context;
-    if (!currentCluster || !currentProject) {
-      return;
-    }
-    var instanceType = "";
-
-
-    if (service) {
-      const serviceName = service.name;
-
-      //first check if there is a nodeSelector for the given application (Can be null)
-      if (chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"]) {
-        instanceType = chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"]
-        const [instanceClass, instanceSize] = instanceType.split('.');
-        const currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
-        setMaxCPU(currentInstance.vCPU);
-        setMaxRAM(currentInstance.RAM);
-      }
-    }
-    //Query the given nodes if no instance type is specified
-    if (instanceType == "") {
-
-      api
-        .getClusterNodes(
-          "<token>",
-          {},
-          {
-            cluster_id: currentCluster.id,
-            project_id: currentProject.id,
-          }
-        )
-        .then(({ data }) => {
-          if (data) {
-            var nodeCount = 0
-            let largestInstanceType = {
-              vCPUs: 2,
-              RAM: 4,
-            };
-
-            // TODO: type this response
-            data.forEach((node: any) => {
-              if (node.labels['porter.run/workload-kind'] == "application") {
-                nodeCount += 1
-                var instanceType: string = node.labels['beta.kubernetes.io/instance-type'];
-                const [instanceClass, instanceSize] = instanceType.split('.');
-                if (instanceClass && instanceSize) {
-                  if (AWS_INSTANCE_LIMITS[instanceClass] && AWS_INSTANCE_LIMITS[instanceClass][instanceSize]) {
-                    let currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
-                    largestInstanceType.vCPUs = currentInstance.vCPU;
-                    largestInstanceType.RAM = currentInstance.RAM;
-                  }
-                }
-              }
-            });
-            setApplicationNodeCount(nodeCount);
-            setMaxCPU(largestInstanceType.vCPUs);
-            setMaxRAM(largestInstanceType.RAM);
-          }
-        }).catch((error) => {
-
-        });
-    }
-  }, []);
-  // TODO: calculate heights instead of hardcoding them
-  const renderTabs = (service: Service) => {
-    switch (service.type) {
-      case "web":
-        return (
-          <WebTabs
-            service={service}
-            editService={editService}
-            setHeight={setHeight}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            nodeCount={applicationNodeCount}
-          />
-        );
-      case "worker":
-        return (
-          <WorkerTabs
-            service={service}
-            editService={editService}
-            setHeight={setHeight}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            nodeCount={applicationNodeCount}
-          />
-        );
-      case "job":
-        return (
-          <JobTabs
-            service={service}
-            editService={editService}
-            setHeight={setHeight}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            nodeCount={applicationNodeCount}
-          />
-        );
-      case "release":
-        return (
-          <ReleaseTabs
-            service={service}
-            editService={editService}
-            setHeight={setHeight}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            nodeCount={applicationNodeCount}
-          />
-        );
-    }
-  };
-
-  const renderIcon = (service: Service) => {
-    switch (service.type) {
-      case "web":
-        return <Icon src={web} />;
-      case "worker":
-        return <Icon src={worker} />;
-      case "job":
-        return <Icon src={job} />;
-      case "release":
-        return <Icon src={job} />;
-    }
-  };
-
-  const getHasBuiltImage = () => {
-    if (chart?.chart?.values == null) {
-      return false;
-    }
-    return !_.isEmpty((Object.values(chart.chart.values)[0] as any)?.global);
-  };
-
-  return (
-    <>
-      <ServiceHeader
-        showExpanded={service.expanded}
-        onClick={() => editService({ ...service, expanded: !service.expanded })}
-        chart={chart}
-        bordersRounded={!getHasBuiltImage() && !service.expanded}
-      >
-        <ServiceTitle>
-          <ActionButton>
-            <span className="material-icons dropdown">arrow_drop_down</span>
-          </ActionButton>
-          {renderIcon(service)}
-          {service.name.trim().length > 0 ? service.name : "New Service"}
-        </ServiceTitle>
-        {service.canDelete && (
-          <ActionButton onClick={(e) => {
-            e.stopPropagation();
-            deleteService();
-          }}>
-            <span className="material-icons">delete</span>
-          </ActionButton>
-        )}
-      </ServiceHeader>
-      <AnimateHeight height={service.expanded ? height : 0}>
-        <StyledSourceBox
-          showExpanded={service.expanded}
-          chart={chart}
-          hasFooter={chart && service && getHasBuiltImage()}
-        >
-          {renderTabs(service)}
-        </StyledSourceBox>
-      </AnimateHeight>
-      {chart &&
-        service &&
-        // Check if has built image
-        getHasBuiltImage() && (
-          <StatusFooter
-            setExpandedJob={setExpandedJob}
-            chart={chart}
-            service={service}
-          />
-        )}
-      <Spacer y={0.5} />
-    </>
-  );
-};
-
-export default ServiceContainer;
-
-const ServiceTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSourceBox = styled.div<{
-  showExpanded: boolean;
-  chart: any;
-  hasFooter?: boolean;
-}>`
-  width: 100%;
-  color: #ffffff;
-  padding: 14px 25px 30px;
-  position: relative;
-  font-size: 13px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  border-top: 0;
-  border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
-  border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
-`;
-
-const ActionButton = styled.button`
-  position: relative;
-  border: none;
-  background: none;
-  padding: 5px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 50%;
-  cursor: pointer;
-  color: #aaaabb;
-  :hover {
-    color: white;
-  }
-
-  > span {
-    font-size: 20px;
-  }
-  margin-right: 5px;
-`;
-
-const ServiceHeader = styled.div<{
-  showExpanded: boolean;
-  chart: any;
-  bordersRounded?: boolean;
-}>`
-  flex-direction: row;
-  display: flex;
-  height: 60px;
-  justify-content: space-between;
-  cursor: pointer;
-  padding: 20px;
-  color: ${(props) => props.theme.text.primary};
-  position: relative;
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-
-  border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")};
-  border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")};
-
-  .dropdown {
-    font-size: 30px;
-    cursor: pointer;
-    border-radius: 20px;
-    margin-left: -10px;
-    transform: ${(props: { showExpanded: boolean; chart: any }) =>
-    props.showExpanded ? "" : "rotate(-90deg)"};
-  }
-
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const Icon = styled.img`
-  height: 18px;
-  margin-right: 15px;
-
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;

+ 0 - 248
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -1,248 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-import ServiceContainer from "./ServiceContainer";
-import styled from "styled-components";
-import Spacer from "components/porter/Spacer";
-import Modal from "components/porter/Modal";
-import Text from "components/porter/Text";
-import Select from "components/porter/Select";
-import Input from "components/porter/Input";
-import Container from "components/porter/Container";
-import Button from "components/porter/Button";
-
-import web from "assets/web.png";
-import worker from "assets/worker.png";
-import job from "assets/job.png";
-import { Service, ServiceType } from "./serviceTypes";
-
-interface ServicesProps {
-  services: Service[];
-  appName: string;
-  setServices: (services: Service[]) => void;
-  addNewText: string;
-  defaultExpanded?: boolean;
-  chart?: any;
-  limitOne?: boolean;
-  prePopulateService?: Service;
-  setExpandedJob?: (x: string) => void;
-}
-
-const Services: React.FC<ServicesProps> = ({
-  appName,
-  services,
-  setServices,
-  addNewText,
-  chart,
-  limitOne = false,
-  setExpandedJob,
-  prePopulateService,
-}) => {
-  const [showAddServiceModal, setShowAddServiceModal] = useState<boolean>(
-    false
-  );
-  const [serviceName, setServiceName] = useState<string>("");
-  const [serviceType, setServiceType] = useState<ServiceType>("web");
-  const isServiceNameValid = (name: string) => {
-    const regex = /^[a-z0-9-]+$/;
-
-    return regex.test(name);
-  };
-  const isServiceNameDuplicate = (name: string) => {
-    const serviceNames = services.map((service) => service.name);
-    return serviceNames.includes(name);
-  };
-  const isServiceNameTooLong = (name: string) => {
-    // k8s pod name limit is 63 characters: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
-    // the pod name is the appName-serviceName-web/wkr/job-random_4_char_string, so the max limit is 53
-    return name.length + appName.length > 53;
-  };
-
-  const maybeGetError = (): string | undefined => {
-    if (serviceName.length > 30) {
-      return "Must be 30 characters or less.";
-    } else if (serviceName != "" && !isServiceNameValid(serviceName)) {
-      return "Lowercase letters, numbers, and '-' only.";
-    } else if (isServiceNameDuplicate(serviceName)) {
-      return "Service name is duplicate!";
-    } else if (isServiceNameTooLong(serviceName)) {
-      return "Service name is too long!";
-    } else {
-      return undefined;
-    }
-  };
-
-  const maybeRenderAddServicesButton = () => {
-    if (limitOne && services.length > 0) {
-      return null;
-    }
-    return (
-      <>
-        <AddServiceButton
-          onClick={() => {
-            if (prePopulateService == null) {
-              setShowAddServiceModal(true);
-              setServiceType("web");
-            } else {
-              const newServices = [
-                ...services,
-                prePopulateService,
-              ]
-              setServices(newServices);
-            }
-          }}
-        >
-          <i className="material-icons add-icon">add_icon</i>
-          {addNewText}
-        </AddServiceButton>
-        <Spacer y={0.5} />
-      </>
-    );
-  };
-
-  return (
-    <>
-      {services.length > 0 && (
-        <ServicesContainer>
-          {services.map((service, index) => {
-            return (
-              <ServiceContainer
-                key={service.name}
-                setExpandedJob={setExpandedJob}
-                service={service}
-                chart={chart}
-                editService={(newService: Service) => {
-                  const newServices = services.map((s, i) => (i === index ? newService : s));
-                  setServices(newServices);
-                }}
-                deleteService={() => {
-                  const newServices = services.filter((_, i) => i !== index);
-                  setServices(newServices);
-                }}
-              />
-            );
-          })}
-        </ServicesContainer>
-      )}
-      {maybeRenderAddServicesButton()}
-      {showAddServiceModal && (
-        <Modal
-          closeModal={() => {
-            setShowAddServiceModal(false)
-            setServiceName("")
-            setServiceType("web")
-          }}
-          width="500px"
-        >
-          <Text size={16}>{addNewText}</Text>
-          <Spacer y={1} />
-          <Text color="helper">Select a service type:</Text>
-          <Spacer y={0.5} />
-          <Container row>
-            <ServiceIcon>
-              {serviceType === "web" && <img src={web} />}
-              {serviceType === "worker" && <img src={worker} />}
-              {serviceType === "job" && <img src={job} />}
-            </ServiceIcon>
-            <Select
-              value={serviceType}
-              width="100%"
-              setValue={(value: string) => setServiceType(value as ServiceType)}
-              options={[
-                { label: "Web", value: "web" },
-                { label: "Worker", value: "worker" },
-                { label: "Cron Job", value: "job" },
-              ]}
-            />
-          </Container>
-          <Spacer y={1} />
-          <Text color="helper">Name this service:</Text>
-          <Spacer y={0.5} />
-          <Input
-            placeholder="ex: my-service"
-            width="100%"
-            value={serviceName}
-            error={maybeGetError()}
-            setValue={setServiceName}
-          />
-          <Spacer y={1} />
-          <Button
-            onClick={() => {
-              const newServices = [
-                ...services,
-                Service.default(serviceName, serviceType),
-              ]
-              setServices(newServices);
-              setShowAddServiceModal(false);
-              setServiceName("");
-              setServiceType("web");
-            }}
-            disabled={maybeGetError() != null || serviceName == ""}
-          >
-            <I className="material-icons">add</I> Add service
-          </Button>
-        </Modal>
-      )}
-    </>
-  );
-};
-
-export default Services;
-
-const ServiceIcon = styled.div`
-  border: 1px solid #494b4f;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 35px;
-  width: 35px;
-  min-width: 35px;
-  margin-right: 10px;
-  overflow: hidden;
-  border-radius: 5px;
-  > img {
-    height: 18px;
-    animation: floatIn 0.5s 0s;
-    @keyframes floatIn {
-      from {
-        opacity: 0;
-        transform: translateY(7px);
-      }
-      to {
-        opacity: 1;
-        transform: translateY(0px);
-      }
-    }
-  }
-`;
-
-const I = styled.i`
-  color: white;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 7px;
-  justify-content: center;
-`;
-
-const ServicesContainer = styled.div``;
-
-const AddServiceButton = styled.div`
-  color: #aaaabb;
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  align-items: center;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  .add-icon {
-    width: 30px;
-    font-size: 20px;
-  }
-`;

+ 0 - 695
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -1,695 +0,0 @@
-import _ from "lodash";
-import { overrideObjectValues } from "./utils";
-import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import { PorterJson } from "./schema";
-
-export type ImageInfo = {
-    repository: string;
-    tag: string;
-}
-export const ImageInfo = {
-    BASE_IMAGE: {
-        repository: "ghcr.io/porter-dev/porter/hello-porter",
-        tag: "latest",
-    } as const,
-}
-
-
-export type Service = WorkerService | WebService | JobService | ReleaseService;
-export type ServiceType = 'web' | 'worker' | 'job' | 'release';
-
-export type ServiceString = {
-    readOnly: boolean;
-    value: string;
-}
-type ServiceBoolean = {
-    readOnly: boolean;
-    value: boolean;
-}
-export type ServiceArray<T extends ServiceString | ServiceBoolean> = T[];
-const ServiceArray = {
-    serialize: <T extends ServiceString | ServiceBoolean>(serviceArray: ServiceArray<T>) => {
-        return serviceArray.map((service) => service.value).filter((val) => val !== '');
-    }
-}
-export type ServiceKeyValueArray<T extends ServiceString | ServiceBoolean> = {
-    key: string;
-    value: T;
-}[];
-const ServiceKeyValueArray = {
-    serialize: <T extends ServiceString | ServiceBoolean>(serviceKeyValueArray: ServiceKeyValueArray<T>) => {
-        const map: Record<string, string> = {};
-        serviceKeyValueArray.map(({ key, value }: {
-            key: string;
-            value: T;
-        }) => {
-            if (key != '') {
-                map[key] = value.value.toString();
-            }
-        });
-        return map;
-    }
-}
-
-type Ingress = {
-    enabled: ServiceBoolean;
-    customDomains: ServiceArray<ServiceString>;
-    hosts: ServiceArray<ServiceString>;
-    porterHosts: ServiceString;
-    annotations: ServiceKeyValueArray<ServiceString>;
-}
-type Autoscaling = {
-    enabled: ServiceBoolean,
-    minReplicas: ServiceString,
-    maxReplicas: ServiceString,
-    targetCPUUtilizationPercentage: ServiceString,
-    targetMemoryUtilizationPercentage: ServiceString,
-}
-type LivenessProbe = {
-    enabled: ServiceBoolean,
-    failureThreshold: ServiceString,
-    path: ServiceString,
-    periodSeconds: ServiceString,
-}
-type ReadinessProbe = {
-    enabled: ServiceBoolean,
-    failureThreshold: ServiceString,
-    path: ServiceString,
-    initialDelaySeconds: ServiceString,
-}
-type StartUpProbe = {
-    enabled: ServiceBoolean,
-    failureThreshold: ServiceString,
-    path: ServiceString,
-    periodSeconds: ServiceString,
-}
-type Health = {
-    livenessProbe: LivenessProbe,
-    startupProbe: StartUpProbe,
-    readinessProbe: ReadinessProbe,
-}
-type CloudSql = {
-    enabled: ServiceBoolean,
-    connectionName: ServiceString,
-    dbPort: ServiceString,
-    serviceAccountJSON: ServiceString,
-}
-
-
-const ServiceField = {
-    string: (defaultValue: string, overrideValue?: string): ServiceString => {
-        return {
-            readOnly: overrideValue != null,
-            value: overrideValue ?? defaultValue,
-        }
-    },
-    boolean: (defaultValue: boolean, overrideValue?: boolean): ServiceBoolean => {
-        return {
-            readOnly: overrideValue != null,
-            value: overrideValue ?? defaultValue,
-        }
-    },
-    array: (defaultValues: string[], overrideValues?: string[]): ServiceArray<ServiceString> => {
-        const serviceMap: Record<string, ServiceString> = {};
-        for (const val of defaultValues) {
-            serviceMap[val] = ServiceField.string(val);
-        }
-        for (const val of overrideValues ?? []) {
-            serviceMap[val] = ServiceField.string('', val);
-        }
-        if (Object.keys(serviceMap).length == 0) {
-            return [];
-        }
-        return Object.values(serviceMap);
-    },
-    keyValueArray: (defaultMap: Record<string, string>, overrideMap?: Record<string, string>): ServiceKeyValueArray<ServiceString> => {
-        const serviceMap: Record<string, ServiceString> = {};
-        for (const key in defaultMap) {
-            serviceMap[key] = ServiceField.string(defaultMap[key]);
-        }
-        for (const key in overrideMap) {
-            serviceMap[key] = ServiceField.string('', overrideMap[key]);
-        }
-        if (Object.keys(serviceMap).length == 0) {
-            return [];
-        }
-        return Object.keys(serviceMap).map((key) => ({
-            key,
-            value: serviceMap[key],
-        }));
-    }
-}
-
-type SharedServiceParams = {
-    name: string;
-    cpu: ServiceString;
-    ram: ServiceString;
-    startCommand: ServiceString;
-    type: ServiceType;
-    canDelete: boolean;
-    expanded: boolean;
-    smartOptimization: boolean;
-    cloudsql: CloudSql;
-}
-
-export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
-    type: 'web';
-    port: ServiceString;
-    ingress: Ingress;
-    health: Health;
-}
-const WebService = {
-    default: (name: string, porterJson?: PorterJson): WebService => ({
-        name,
-        smartOptimization: true,
-        expanded: true,
-        cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
-        type: 'web',
-        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
-        autoscaling: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-            minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-            maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-            targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-            targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-        },
-        ingress: {
-            enabled: ServiceField.boolean(true, porterJson?.apps?.[name]?.config?.ingress?.enabled),
-            customDomains: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts),
-            hosts: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts),
-            porterHosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
-            annotations: ServiceField.keyValueArray({}, porterJson?.apps?.[name]?.config?.ingress?.annotations)
-        },
-        port: ServiceField.string('3000', porterJson?.apps?.[name]?.config?.container?.port),
-        canDelete: porterJson?.apps?.[name] == null,
-        health: {
-            startupProbe: {
-                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled),
-                failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold),
-                path: ServiceField.string('/startupz', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path),
-                periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds),
-            },
-            readinessProbe: {
-                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled),
-                failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold),
-                path: ServiceField.string('/readyz', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path),
-                initialDelaySeconds: ServiceField.string('0', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
-            },
-            livenessProbe: {
-                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
-                failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold),
-                path: ServiceField.string('/livez', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
-                periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
-            },
-        },
-        cloudsql: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-            serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-        },
-    }),
-    serialize: (service: WebService) => {
-        return {
-            smartOptimization: service.smartOptimization,
-            replicaCount: service.replicas.value,
-            resources: {
-                requests: {
-                    cpu: service.cpu.value + 'm',
-                    memory: service.ram.value + 'Mi',
-                }
-            },
-            container: {
-                command: service.startCommand.value,
-                port: service.port.value,
-            },
-            autoscaling: {
-                enabled: service.autoscaling.enabled.value,
-                minReplicas: service.autoscaling.minReplicas.value,
-                maxReplicas: service.autoscaling.maxReplicas.value,
-                targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value,
-                targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value,
-            },
-            ingress: {
-                enabled: service.ingress.enabled.value,
-                custom_domain: service.ingress.customDomains.length ? true : false,
-                hosts: ServiceArray.serialize(service.ingress.customDomains),
-                porter_hosts: service.ingress.porterHosts.value ? [service.ingress.porterHosts.value] : [],
-                annotations: ServiceKeyValueArray.serialize(service.ingress.annotations),
-            },
-            service: {
-                port: service.port.value,
-            },
-            health: {
-                startupProbe: {
-                    enabled: service.health.startupProbe.enabled.value,
-                    failureThreshold: service.health.startupProbe.failureThreshold.value,
-                    path: service.health.startupProbe.path.value,
-                    periodSeconds: service.health.startupProbe.periodSeconds.value,
-                },
-                readinessProbe: {
-                    enabled: service.health.readinessProbe.enabled.value,
-                    failureThreshold: service.health.readinessProbe.failureThreshold.value,
-                    path: service.health.readinessProbe.path.value,
-                    initialDelaySeconds: service.health.readinessProbe.initialDelaySeconds.value,
-                },
-                livenessProbe: {
-                    enabled: service.health.livenessProbe.enabled.value,
-                    failureThreshold: service.health.livenessProbe.failureThreshold.value,
-                    path: service.health.livenessProbe.path.value,
-                    periodSeconds: service.health.livenessProbe.periodSeconds.value,
-                },
-            },
-            cloudsql: {
-                enabled: service.cloudsql.enabled.value,
-                connectionName: service.cloudsql.connectionName.value,
-                dbPort: service.cloudsql.dbPort.value,
-                serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
-            },
-        }
-    },
-    deserialize: (name: string, values: any, porterJson?: PorterJson): WebService => {
-        return {
-            name,
-            expanded: false,
-            smartOptimization: values.smartOptimization,
-            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
-            type: 'web',
-            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
-            autoscaling: {
-                enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-                minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-                maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-                targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-                targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-            },
-            ingress: {
-                enabled: ServiceField.boolean(values.ingress?.enabled ?? false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
-                customDomains: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts),
-                hosts: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts),
-                porterHosts: ServiceField.string(values.ingress?.porter_hosts?.length ? values.ingress.porter_hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
-                annotations: ServiceField.keyValueArray(values.ingress?.annotations ?? {}, porterJson?.apps?.[name]?.config?.ingress?.annotations),
-            },
-            port: ServiceField.string(values.container?.port ?? '', porterJson?.apps?.[name]?.config?.container?.port),
-            canDelete: porterJson?.apps?.[name] == null,
-            health: {
-                startupProbe: {
-                    enabled: ServiceField.boolean(values.health?.startupProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health?.startupProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold),
-                    path: ServiceField.string(values.health?.startupProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path),
-                    periodSeconds: ServiceField.string(values.health?.startupProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds),
-                },
-                readinessProbe: {
-                    enabled: ServiceField.boolean(values.health?.readinessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health?.readinessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold),
-                    path: ServiceField.string(values.health?.readinessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path),
-                    initialDelaySeconds: ServiceField.string(values.health?.readinessProbe?.initialDelaySeconds ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
-                },
-                livenessProbe: {
-                    enabled: ServiceField.boolean(values.health?.livenessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health?.livenessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold),
-                    path: ServiceField.string(values.health?.livenessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
-                    periodSeconds: ServiceField.string(values.health?.livenessProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
-                },
-            },
-            cloudsql: {
-                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-                serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-            },
-        }
-    },
-}
-
-export type WorkerService = SharedServiceParams & {
-    type: 'worker';
-    replicas: ServiceString;
-    autoscaling: Autoscaling;
-}
-const WorkerService = {
-    default: (name: string, porterJson?: PorterJson): WorkerService => ({
-        name,
-        expanded: true,
-        smartOptimization: true,
-        cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
-        type: 'worker',
-        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
-        autoscaling: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-            minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-            maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-            targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-            targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-        },
-        canDelete: porterJson?.apps?.[name] == null,
-        cloudsql: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-            serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-        },
-    }),
-    serialize: (service: WorkerService) => {
-        return {
-            replicaCount: service.replicas.value,
-            container: {
-                command: service.startCommand.value,
-            },
-            resources: {
-                requests: {
-                    cpu: service.cpu.value + 'm',
-                    memory: service.ram.value + 'Mi',
-                }
-            },
-            autoscaling: {
-                enabled: service.autoscaling.enabled.value,
-                minReplicas: service.autoscaling.minReplicas.value,
-                maxReplicas: service.autoscaling.maxReplicas.value,
-                targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value,
-                targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value,
-            },
-            cloudsql: {
-                enabled: service.cloudsql.enabled.value,
-                connectionName: service.cloudsql.connectionName.value,
-                dbPort: service.cloudsql.dbPort.value,
-                serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
-            },
-            smartOptimization: service.smartOptimization,
-        }
-    },
-    deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
-        return {
-            name,
-            expanded: false,
-            smartOptimization: values.smartOptimization,
-            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
-            type: 'worker',
-            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
-            autoscaling: {
-                enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
-                minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
-                maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
-                targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
-                targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-            },
-            canDelete: porterJson?.apps?.[name] == null,
-            cloudsql: {
-                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-                serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-            },
-        }
-    },
-}
-
-export type JobService = SharedServiceParams & {
-    type: 'job';
-    jobsExecuteConcurrently: ServiceBoolean;
-    cronSchedule: ServiceString;
-}
-const JobService = {
-    default: (name: string, porterJson?: PorterJson): JobService => ({
-        name,
-        expanded: true,
-        smartOptimization: true,
-        cpu: ServiceField.string('187.', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
-        type: 'job',
-        jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent),
-        cronSchedule: ServiceField.string('*/10 * * * *', porterJson?.apps?.[name]?.config?.schedule?.value),
-        canDelete: porterJson?.apps?.[name] == null,
-        cloudsql: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-            serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-        },
-    }),
-    serialize: (service: JobService) => {
-        return {
-            smartOptimization: service.smartOptimization,
-            allowConcurrent: service.jobsExecuteConcurrently.value,
-            container: {
-                command: service.startCommand.value,
-            },
-            resources: {
-                requests: {
-                    cpu: service.cpu.value + 'm',
-                    memory: service.ram.value + 'Mi',
-                }
-            },
-            schedule: {
-                enabled: service.cronSchedule.value ? true : false,
-                value: service.cronSchedule.value,
-            },
-            paused: true,
-            cloudsql: {
-                enabled: service.cloudsql.enabled.value,
-                connectionName: service.cloudsql.connectionName.value,
-                dbPort: service.cloudsql.dbPort.value,
-                serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
-            },
-        }
-    },
-    deserialize: (name: string, values: any, porterJson?: PorterJson): JobService => {
-        return {
-            name,
-            expanded: false,
-            smartOptimization: values.smartOptimization,
-            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
-            type: 'job',
-            jobsExecuteConcurrently: ServiceField.boolean(values.allowConcurrent ?? false, porterJson?.apps?.[name]?.config?.allowConcurrent),
-            cronSchedule: ServiceField.string(values.schedule?.value ?? '', porterJson?.apps?.[name]?.config?.schedule?.value),
-            canDelete: porterJson?.apps?.[name] == null,
-            cloudsql: {
-                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-                serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-            },
-        }
-    },
-}
-
-export type ReleaseService = SharedServiceParams & {
-    type: 'release';
-};
-const ReleaseService = {
-    default: (name: string, porterJson?: PorterJson): ReleaseService => ({
-        name,
-        expanded: true,
-        smartOptimization: true,
-        cpu: ServiceField.string('187.5', porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('384', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-        startCommand: ServiceField.string('', porterJson?.release?.run),
-        type: 'release',
-        canDelete: porterJson?.release == null,
-        cloudsql: {
-            enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-            connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-            dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-            serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-        },
-    }),
-
-    serialize: (service: ReleaseService) => {
-        return {
-            smartOptimization: service.smartOptimization,
-            container: {
-                command: service.startCommand.value,
-            },
-            resources: {
-                requests: {
-                    cpu: service.cpu.value + 'm',
-                    memory: service.ram.value + 'Mi',
-                }
-            },
-            paused: true, // this makes sure the release isn't run immediately. it is flipped when the porter apply runs the release in the GHA
-            cloudsql: {
-                enabled: service.cloudsql.enabled.value,
-                connectionName: service.cloudsql.connectionName.value,
-                dbPort: service.cloudsql.dbPort.value,
-                serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
-            },
-        }
-    },
-
-    deserialize: (name: string, values: any, porterJson?: PorterJson): ReleaseService => {
-        return {
-            name,
-            expanded: false,
-            smartOptimization: values.smartOptimization,
-            cpu: ServiceField.string(values?.resources?.requests?.cpu?.replace('m', ''), porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-            ram: ServiceField.string(values?.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
-            startCommand: ServiceField.string(values?.container?.command ?? '', porterJson?.release?.run),
-            type: 'release',
-            canDelete: porterJson?.release == null,
-            cloudsql: {
-                enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled),
-                connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName),
-                dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort),
-                serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON),
-            },
-        }
-    },
-}
-
-
-const TYPE_TO_SUFFIX: Record<ServiceType, string> = {
-    'web': '-web',
-    'worker': '-wkr',
-    'job': '-job',
-    'release': '',
-}
-const SUFFIX_TO_TYPE: Record<string, ServiceType> = {
-    '-web': 'web',
-    '-wkr': 'worker',
-    '-job': 'job',
-}
-
-export const Service = {
-    // populates an empty service
-    default: (name: string, type: ServiceType, porterJson?: PorterJson) => {
-        switch (type) {
-            case 'web':
-                return WebService.default(name, porterJson);
-            case 'worker':
-                return WorkerService.default(name, porterJson);
-            case 'job':
-                return JobService.default(name, porterJson);
-            case 'release':
-                return ReleaseService.default(name, porterJson);
-        }
-    },
-
-    // converts a service to a helm values object
-    serialize: (service: Service) => {
-        switch (service.type) {
-            case 'web':
-                return WebService.serialize(service);
-            case 'worker':
-                return WorkerService.serialize(service);
-            case 'job':
-                return JobService.serialize(service);
-            case 'release':
-                return ReleaseService.serialize(service);
-        }
-    },
-
-    // converts a helm values object and porter json (from their repo) to a service
-    deserialize: (helmValues: any, defaultValues: any, porterJson?: PorterJson): Service[] => {
-        if (defaultValues == null) {
-            return [];
-        }
-        return Object.keys(defaultValues).map((name: string) => {
-            const suffix = name.slice(-4);
-            if (suffix in SUFFIX_TO_TYPE) {
-                const type = SUFFIX_TO_TYPE[suffix];
-                const appName = name.slice(0, -4);
-                const coalescedValues = overrideObjectValues(
-                    defaultValues[name],
-                    helmValues?.[name] ?? {}
-                );
-                switch (type) {
-                    case 'web':
-                        return WebService.deserialize(appName, coalescedValues, porterJson);
-                    case 'worker':
-                        return WorkerService.deserialize(appName, coalescedValues, porterJson);
-                    case 'job':
-                        return JobService.deserialize(appName, coalescedValues, porterJson);
-                }
-            }
-        }).filter((service: Service | undefined): service is Service => service != null) as Service[];
-    },
-    // TODO: consolidate these
-    deserializeRelease: (helmValues: any, porterJson?: PorterJson): ReleaseService => {
-        return ReleaseService.deserialize('pre-deploy', helmValues, porterJson);
-    },
-
-    // standard typeguards
-    isWeb: (service: Service): service is WebService => service.type === 'web',
-    isWorker: (service: Service): service is WorkerService => service.type === 'worker',
-    isJob: (service: Service): service is JobService => service.type === 'job',
-    isRelease: (service: Service): service is ReleaseService => service.type === 'release',
-    isNonRelease: (service: Service): service is Exclude<Service, ReleaseService> => service.type !== 'release',
-
-    // required because of https://github.com/helm/helm/issues/9214
-    toHelmName: (service: Service): string => {
-        return service.name + TYPE_TO_SUFFIX[service.type]
-    },
-
-    retrieveEnvFromHelmValues: (helmValues: any): KeyValueType[] => {
-        const firstService = Object.keys(helmValues)[0];
-        const env = helmValues[firstService]?.container?.env?.normal;
-        if (env == null) {
-            return [];
-        }
-        try {
-            return Object.keys(env).map((key: string) => ({
-                key,
-                value: env[key],
-                hidden: false,
-                locked: false,
-                deleted: false,
-            }));
-        } catch (err) {
-            // TODO: handle error
-            return [];
-        }
-    },
-
-    retrieveSubdomainFromHelmValues: (services: Service[], helmValues: any): string => {
-        const webServices = services.filter(Service.isWeb);
-        if (webServices.length == 0) {
-            return "";
-        }
-
-        let matchedWebCount = 0;
-        let matchedWebHost = "";
-
-        for (const web of webServices) {
-            const values = helmValues[Service.toHelmName(web)];
-            if (values == null || values.ingress == null || !values.ingress.enabled) {
-                continue;
-            }
-            if (values.ingress.porter_hosts?.length > 0 || (values.ingress.custom_domain && values.ingress.hosts?.length > 0)) {
-                if (values.ingress.custom_domain && values.ingress.hosts?.length === 1) {
-                    // if they have a single custom domain, use that
-                    matchedWebHost = values.ingress.hosts[0];
-                } else {
-                    // otherwise, use their porter domain
-                    matchedWebHost = values.ingress.porter_hosts[0];
-                }
-                matchedWebCount++;
-            }
-        }
-
-        // if multiple web services have a subdomain, return nothing
-        if (matchedWebCount > 1) {
-            return "";
-        }
-
-        return matchedWebHost;
-    },
-
-    prefixSubdomain: (subdomain: string) => {
-        if (subdomain.startsWith('https://') || subdomain.startsWith('http://')) {
-            return subdomain;
-        }
-        return 'https://' + subdomain;
-    },
-}
-

+ 0 - 102
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx

@@ -1,102 +0,0 @@
-import React from 'react';
-import { ServiceArray, ServiceString } from '../serviceTypes';
-import Button from 'components/porter/Button';
-import styled from 'styled-components';
-import Input from 'components/porter/Input';
-import Spacer from 'components/porter/Spacer';
-
-interface Props {
-    customDomains: ServiceArray<ServiceString>;
-    onChange: (customDomains: ServiceArray<ServiceString>) => void;
-}
-
-const CustomDomains: React.FC<Props> = ({ customDomains, onChange }) => {
-    const renderInputs = () => {
-        return customDomains.map((customDomain, i) => {
-            return (
-                <>
-                    <AnnotationContainer key={i}>
-                        <Input
-                            placeholder="ex: my-app.my-domain.com"
-                            value={customDomain.value}
-                            setValue={(e) => {
-                                const newCustomDomains = [...customDomains];
-                                newCustomDomains[i] = { readOnly: false, value: e };
-                                onChange(newCustomDomains);
-                            }}
-                            disabled={customDomain.readOnly}
-                            width="275px"
-                            disabledTooltip={
-                                "You may only edit this field in your porter.yaml."
-                            }
-                        />
-                        <DeleteButton
-                            onClick={() => {
-                                //remove customDomain at the index
-                                const newCustomDomains = [...customDomains];
-                                newCustomDomains.splice(i, 1);
-                                onChange(newCustomDomains);
-                            }}
-                        >
-                            <i className="material-icons">cancel</i>
-                        </DeleteButton>
-                    </AnnotationContainer>
-                    <Spacer y={0.25} />
-                </>
-            );
-        });
-    };
-
-    return (
-        <CustomDomainsContainer>
-            {customDomains.length !== 0 &&
-                <>
-                    {renderInputs()}
-                    <Spacer y={0.5} />
-                </>
-            }
-            <Button
-                onClick={() => {
-                    const newCustomDomains = [...customDomains];
-                    newCustomDomains.push({ readOnly: false, value: "" });
-                    onChange(newCustomDomains);
-                }}
-            >
-                + Add Custom Domain
-            </Button>
-        </CustomDomainsContainer >
-    )
-};
-
-export default CustomDomains;
-
-const CustomDomainsContainer = styled.div`
-`;
-
-const AnnotationContainer = styled.div`
-    display: flex;
-    align-items: center;
-    gap: 5px;
-`
-
-const DeleteButton = styled.div`
-  width: 15px;
-  height: 15px;
-  display: flex;
-  align-items: center;
-  margin-left: 8px;
-  margin-top: -3px;
-  justify-content: center;
-
-  > i {
-    font-size: 17px;
-    color: #ffffff44;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    cursor: pointer;
-    :hover {
-      color: #ffffff88;
-    }
-  }
-`;

+ 0 - 116
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx

@@ -1,116 +0,0 @@
-import React from 'react';
-import { ServiceKeyValueArray, ServiceString } from '../serviceTypes';
-import Button from 'components/porter/Button';
-import styled from 'styled-components';
-import Input from 'components/porter/Input';
-import Spacer from 'components/porter/Spacer';
-
-interface Props {
-    annotations: ServiceKeyValueArray<ServiceString>;
-    onChange: (annotations: ServiceKeyValueArray<ServiceString>) => void;
-}
-
-const IngressCustomAnnotations: React.FC<Props> = ({ annotations, onChange }) => {
-    const renderInputs = () => {
-        return annotations.map(({ key: annotationKey, value: annotationValue }, i) => {
-            return (
-                <>
-                    <AnnotationContainer key={i}>
-                        <Input
-                            placeholder="kubernetes.io/ingress.class"
-                            value={annotationKey}
-                            setValue={(e) => {
-                                const newAnnotations = [...annotations];
-                                newAnnotations[i].key = e;
-                                onChange(newAnnotations);
-                            }}
-                            disabled={annotationValue.readOnly}
-                            width="275px"
-                            disabledTooltip={
-                                "You may only edit this field in your porter.yaml."
-                            }
-                        />
-                        <Input
-                            placeholder="nginx"
-                            value={annotationValue.value}
-                            setValue={(e) => {
-                                const newAnnotations = [...annotations];
-                                newAnnotations[i].value = { readOnly: false, value: e };
-                                onChange(newAnnotations);
-                            }}
-                            disabled={annotationValue.readOnly}
-                            width="275px"
-                            disabledTooltip={
-                                "You may only edit this field in your porter.yaml."
-                            }
-                        />
-                        <DeleteButton
-                            onClick={() => {
-                                //remove annotation at the index
-                                const newAnnotations = [...annotations];
-                                newAnnotations.splice(i, 1);
-                                onChange(newAnnotations);
-                            }}
-                        >
-                            <i className="material-icons">cancel</i>
-                        </DeleteButton>
-                    </AnnotationContainer>
-                    <Spacer y={0.25} />
-                </>
-            );
-        });
-    };
-
-    return (
-        <IngressCustomAnnotationsContainer>
-            {annotations.length !== 0 &&
-                <>
-                    {renderInputs()}
-                    <Spacer y={0.5} />
-                </>
-            }
-            <Button
-                onClick={() => {
-                    const newAnnotations = [...annotations];
-                    newAnnotations.push({ key: "", value: { readOnly: false, value: "" } });
-                    onChange(newAnnotations);
-                }}
-            >
-                + Add Annotation
-            </Button>
-        </IngressCustomAnnotationsContainer >
-    )
-};
-
-export default IngressCustomAnnotations;
-
-const IngressCustomAnnotationsContainer = styled.div`
-`;
-
-const AnnotationContainer = styled.div`
-    display: flex;
-    align-items: center;
-    gap: 5px;
-`
-
-const DeleteButton = styled.div`
-  width: 15px;
-  height: 15px;
-  display: flex;
-  align-items: center;
-  margin-left: 8px;
-  margin-top: -3px;
-  justify-content: center;
-
-  > i {
-    font-size: 17px;
-    color: #ffffff44;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    cursor: pointer;
-    :hover {
-      color: #ffffff88;
-    }
-  }
-`;

+ 0 - 325
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx

@@ -1,325 +0,0 @@
-import Input from "components/porter/Input";
-import React, { useContext, useState } from "react"
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import Checkbox from "components/porter/Checkbox";
-import { JobService } from "../serviceTypes";
-import AnimateHeight, { Height } from "react-animate-height";
-import cronstrue from 'cronstrue';
-import Link from "components/porter/Link";
-import { Context } from "shared/Context";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils";
-import InputSlider from "components/porter/InputSlider";
-import SmartOptModal from "./SmartOptModal";
-import { Switch } from "@material-ui/core";
-import styled from "styled-components";
-
-interface Props {
-  service: JobService;
-  editService: (service: JobService) => void;
-  setHeight: (height: Height) => void;
-  maxRAM: number;
-  maxCPU: number;
-  nodeCount: number;
-}
-
-const JobTabs: React.FC<Props> = ({
-  service,
-  editService,
-  setHeight,
-  maxRAM,
-  maxCPU,
-  nodeCount,
-}) => {
-  const [currentTab, setCurrentTab] = React.useState<string>('main');
-  const { currentCluster } = useContext(Context);
-  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
-  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
-  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
-  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
-    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
-
-      editService({
-        ...service,
-        cpu: {
-          readOnly: false,
-          value: (smartLimitCPU * MILI_TO_CORE).toString()
-        },
-        ram: {
-          readOnly: false,
-          value: (smartLimitRAM * MIB_TO_GIB).toString()
-        },
-        smartOptimization: !service.smartOptimization
-      })
-    }
-    else {
-      editService({
-        ...service,
-        smartOptimization: !service.smartOptimization
-      })
-    }
-
-  };
-  const getScheduleDescription = () => {
-    try {
-      return <Text color="helper">This job runs: {cronstrue.toString(service.cronSchedule.value)}</Text>;
-    } catch (err) {
-      return <Text color="helper">
-        Invalid cron schedule.{" "}
-        <Link
-          to={"https://crontab.cronhub.io/"}
-          hasunderline
-          target="_blank"
-        >
-          Need help?
-        </Link>
-      </Text>;
-    }
-  }
-
-  const renderMain = () => {
-    setHeight(276);
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Start command"
-          placeholder="ex: sh start.sh"
-          disabled={service.startCommand.readOnly}
-          value={service.startCommand.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Input
-          label="Cron schedule"
-          placeholder="ex: */5 * * * *"
-          value={service.cronSchedule.value}
-          disabled={service.cronSchedule.readOnly}
-          width="300px"
-          setValue={(e) => { editService({ ...service, cronSchedule: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={0.5} />
-        {getScheduleDescription()}
-      </>
-    )
-  };
-
-  const renderResources = () => {
-    setHeight(316);
-    return (
-      <>
-        <Spacer y={1} />
-        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
-          <StyledIcon
-            className="material-icons"
-            onClick={() => {
-              setShowNeedHelpModal(true)
-            }}
-          >
-            help_outline
-          </StyledIcon>
-          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
-          <Switch
-            size="small"
-            color="primary"
-            checked={service.smartOptimization}
-            onChange={handleSwitch}
-            inputProps={{ 'aria-label': 'controlled' }}
-          />
-        </div>
-        {showNeedHelpModal &&
-          <SmartOptModal
-            setModalVisible={setShowNeedHelpModal}
-          />}
-        <>
-          <InputSlider
-            label="CPUs: "
-            unit="Cores"
-            override={!service.smartOptimization}
-            min={0}
-            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
-            nodeCount={nodeCount}
-            color={"#3f51b5"}
-            smartLimit={smartLimitCPU}
-            value={(service.cpu.value / MILI_TO_CORE).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
-                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-            }}
-            step={0.1}
-            disabled={false}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-
-          <Spacer y={1} />
-
-          <InputSlider
-            label="RAM: "
-            unit="GiB"
-            min={0}
-            override={!service.smartOptimization}
-            nodeCount={nodeCount}
-            smartLimit={smartLimitRAM}
-            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
-            color={"#3f51b5"}
-            value={(service.ram.value / MIB_TO_GIB).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
-                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-            }}
-
-            disabled={service.ram.readOnly}
-            step={0.1}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-        </>
-      </>
-    )
-  };
-
-  const renderDatabase = () => {
-    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
-    return (
-      <>
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.cloudsql.enabled.value}
-          disabled={service.cloudsql.enabled.readOnly}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              cloudsql: {
-                ...service.cloudsql,
-                enabled: {
-                  readOnly: false,
-                  value: !service.cloudsql.enabled.value,
-                },
-              },
-            });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Securely connect to Google Cloud SQL</Text>
-        </Checkbox>
-        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label={"Instance Connection Name"}
-            placeholder="ex: project-123:us-east1:pachyderm"
-            value={service.cloudsql.connectionName.value}
-            disabled={service.cloudsql.connectionName.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  connectionName: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"DB Port"}
-            placeholder="5432"
-            value={service.cloudsql.dbPort.value}
-            disabled={service.cloudsql.dbPort.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  dbPort: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"Service Account JSON"}
-            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
-            value={service.cloudsql.serviceAccountJSON.value}
-            disabled={service.cloudsql.serviceAccountJSON.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  serviceAccountJSON: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-        </AnimateHeight>
-      </>
-    );
-  }
-
-  const renderAdvanced = () => {
-    setHeight(118);
-    return (
-      <>
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.jobsExecuteConcurrently.value}
-          toggleChecked={() => { editService({ ...service, jobsExecuteConcurrently: { readOnly: false, value: !service.jobsExecuteConcurrently.value } }) }}
-          disabled={service.jobsExecuteConcurrently.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Allow jobs to execute concurrently</Text>
-        </Checkbox>
-      </>
-    );
-  };
-
-  return (
-    <>
-      <TabSelector
-        options={currentCluster?.cloud_provider === "GCP" || (currentCluster?.service === "gke") ?
-          [
-            { label: 'Main', value: 'main' },
-            { label: 'Resources', value: 'resources' },
-            { label: "Database", value: "database" },
-            { label: 'Advanced', value: 'advanced' },
-          ] :
-          [
-            { label: 'Main', value: 'main' },
-            { label: 'Resources', value: 'resources' },
-            { label: 'Advanced', value: 'advanced' },
-          ]
-        }
-        currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
-      />
-      {currentTab === 'main' && renderMain()}
-      {currentTab === 'resources' && renderResources()}
-      {currentTab === 'advanced' && renderAdvanced()}
-      {currentTab === "database" && renderDatabase()}
-    </>
-  )
-}
-
-export default JobTabs;
-
-const StyledIcon = styled.i`
-  cursor: pointer;
-  font-size: 16px; 
-  margin-right : 5px;
-  &:hover {
-    color: #666;  
-  }
-`;

+ 0 - 35
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx

@@ -1,35 +0,0 @@
-
-import React, { useEffect, useRef, useState } from "react";
-import Modal from "components/porter/Modal";
-
-import Text from "components/porter/Text";
-
-import Spacer from "components/porter/Spacer";
-import Step from "components/porter/Step";
-import Link from "components/porter/Link";
-
-type Props = {
-    setModalVisible: (x: boolean) => void;
-};
-
-const NodeInfoModal: React.FC<Props> = ({
-    setModalVisible,
-}) => {
-    return (
-        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-            <Text size={16}>Resource Optimization on Porter</Text>
-            <Spacer y={1} />
-            <Text color="helper">
-                Using the recommended marks ensures that your service runs cost-efficiently on Porter.
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper">
-                <Link to="https://docs.porter.run/other/kubernetes-101" target="_blank">
-                    For more information about Kubernetes resource management, visit our docs.
-                </Link>
-            </Text>
-
-        </Modal>
-    );
-};
-export default NodeInfoModal;

+ 0 - 275
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx

@@ -1,275 +0,0 @@
-import Input from "components/porter/Input";
-import React, { useContext, useState } from "react"
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import { ReleaseService } from "../serviceTypes";
-import AnimateHeight, { Height } from "react-animate-height";
-import { Context } from "shared/Context";
-import Checkbox from "components/porter/Checkbox";
-import Text from "components/porter/Text";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils";
-import InputSlider from "components/porter/InputSlider";
-import SmartOptModal from "./SmartOptModal";
-import { Switch } from "@material-ui/core";
-import styled from "styled-components";
-
-interface Props {
-    service: ReleaseService;
-    editService: (service: ReleaseService) => void;
-    setHeight: (height: Height) => void;
-    maxRAM: number;
-    maxCPU: number;
-    nodeCount?: number;
-}
-
-const ReleaseTabs: React.FC<Props> = ({
-    service,
-    editService,
-    setHeight,
-    maxRAM,
-    maxCPU,
-    nodeCount,
-}) => {
-    const [currentTab, setCurrentTab] = React.useState<string>('main');
-    const { currentCluster } = useContext(Context);
-    const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
-    const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
-    const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
-    const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
-        if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
-
-            editService({
-                ...service,
-                cpu: {
-                    readOnly: false,
-                    value: (smartLimitCPU * MILI_TO_CORE).toString()
-                },
-                ram: {
-                    readOnly: false,
-                    value: (smartLimitRAM * MIB_TO_GIB).toString()
-                },
-                smartOptimization: !service.smartOptimization
-            })
-        }
-        else {
-            editService({
-                ...service,
-                smartOptimization: !service.smartOptimization
-            })
-        }
-
-    };
-    const renderMain = () => {
-        setHeight(159);
-        return (
-            <>
-                <Spacer y={1} />
-                <Input
-                    label="Start command"
-                    placeholder="ex: sh start.sh"
-                    disabled={service.startCommand.readOnly}
-                    value={service.startCommand.value}
-                    width="300px"
-                    setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
-            </>
-        )
-    };
-
-    const renderResources = () => {
-        setHeight(316);
-        return (
-            <>
-                <Spacer y={1} />
-                <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
-                    <StyledIcon
-                        className="material-icons"
-                        onClick={() => {
-                            setShowNeedHelpModal(true)
-                        }}
-                    >
-                        help_outline
-                    </StyledIcon>
-                    <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
-                    <Switch
-                        size="small"
-                        color="primary"
-                        checked={service.smartOptimization}
-                        onChange={handleSwitch}
-                        inputProps={{ 'aria-label': 'controlled' }}
-                    />
-                </div>
-                {showNeedHelpModal &&
-                    <SmartOptModal
-                        setModalVisible={setShowNeedHelpModal}
-                    />}
-                <>
-                    <InputSlider
-                        label="CPUs: "
-                        unit="Cores"
-                        override={!service.smartOptimization}
-                        min={0}
-                        max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
-                        nodeCount={nodeCount}
-                        color={"#3f51b5"}
-                        smartLimit={smartLimitCPU}
-                        value={(service.cpu.value / MILI_TO_CORE).toString()}
-                        setValue={(e) => {
-                            service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
-                                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-                        }}
-                        step={0.1}
-                        disabled={false}
-                        disabledTooltip={"You may only edit this field in your porter.yaml."} />
-
-                    <Spacer y={1} />
-
-                    <InputSlider
-                        label="RAM: "
-                        unit="GiB"
-                        min={0}
-                        override={!service.smartOptimization}
-                        nodeCount={nodeCount}
-                        smartLimit={smartLimitRAM}
-                        max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
-                        color={"#3f51b5"}
-                        value={(service.ram.value / MIB_TO_GIB).toString()}
-                        setValue={(e) => {
-                            service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
-                                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-                        }}
-
-                        disabled={service.ram.readOnly}
-                        step={0.1}
-                        disabledTooltip={"You may only edit this field in your porter.yaml."} />
-                </>
-            </>
-        )
-    };
-
-    const renderDatabase = () => {
-        setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
-        return (
-            <>
-                <Spacer y={1} />
-                <Checkbox
-                    checked={service.cloudsql.enabled.value}
-                    disabled={service.cloudsql.enabled.readOnly}
-                    toggleChecked={() => {
-                        editService({
-                            ...service,
-                            cloudsql: {
-                                ...service.cloudsql,
-                                enabled: {
-                                    readOnly: false,
-                                    value: !service.cloudsql.enabled.value,
-                                },
-                            },
-                        });
-                    }}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                >
-                    <Text color="helper">Securely connect to Google Cloud SQL</Text>
-                </Checkbox>
-                <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
-                    <Spacer y={1} />
-                    <Input
-                        label={"Instance Connection Name"}
-                        placeholder="ex: project-123:us-east1:pachyderm"
-                        value={service.cloudsql.connectionName.value}
-                        disabled={service.cloudsql.connectionName.readOnly}
-                        width="300px"
-                        setValue={(e) => {
-                            editService({
-                                ...service,
-                                cloudsql: {
-                                    ...service.cloudsql,
-                                    connectionName: { readOnly: false, value: e },
-                                },
-                            });
-                        }}
-                        disabledTooltip={
-                            "You may only edit this field in your porter.yaml."
-                        }
-                    />
-                    <Spacer y={1} />
-                    <Input
-                        label={"DB Port"}
-                        placeholder="5432"
-                        value={service.cloudsql.dbPort.value}
-                        disabled={service.cloudsql.dbPort.readOnly}
-                        width="300px"
-                        setValue={(e) => {
-                            editService({
-                                ...service,
-                                cloudsql: {
-                                    ...service.cloudsql,
-                                    dbPort: { readOnly: false, value: e },
-                                },
-                            });
-                        }}
-                        disabledTooltip={
-                            "You may only edit this field in your porter.yaml."
-                        }
-                    />
-                    <Spacer y={1} />
-                    <Input
-                        label={"Service Account JSON"}
-                        placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
-                        value={service.cloudsql.serviceAccountJSON.value}
-                        disabled={service.cloudsql.serviceAccountJSON.readOnly}
-                        width="300px"
-                        setValue={(e) => {
-                            editService({
-                                ...service,
-                                cloudsql: {
-                                    ...service.cloudsql,
-                                    serviceAccountJSON: { readOnly: false, value: e },
-                                },
-                            });
-                        }}
-                        disabledTooltip={
-                            "You may only edit this field in your porter.yaml."
-                        }
-                    />
-                </AnimateHeight>
-            </>
-        );
-    }
-
-
-    return (
-        <>
-            <TabSelector
-                options={currentCluster?.cloud_provider === "GCP" || (currentCluster?.service === "gke") ?
-                    [
-                        { label: 'Main', value: 'main' },
-                        { label: 'Resources', value: 'resources' },
-                        { label: "Database", value: "database" },
-                    ] :
-                    [
-                        { label: 'Main', value: 'main' },
-                        { label: 'Resources', value: 'resources' },
-                    ]
-                }
-                currentTab={currentTab}
-                setCurrentTab={setCurrentTab}
-            />
-            {currentTab === 'main' && renderMain()}
-            {currentTab === 'resources' && renderResources()}
-            {currentTab === "database" && renderDatabase()}
-        </>
-    )
-}
-
-export default ReleaseTabs;
-
-const StyledIcon = styled.i`
-  cursor: pointer;
-  font-size: 16px; 
-  margin-right : 5px;
-  &:hover {
-    color: #666;  
-  }
-`;

+ 0 - 50
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx

@@ -1,50 +0,0 @@
-
-import React from "react";
-import Modal from "components/porter/Modal";
-
-import Text from "components/porter/Text";
-
-import Spacer from "components/porter/Spacer";
-import Step from "components/porter/Step";
-import Link from "components/porter/Link";
-
-type Props = {
-    setModalVisible: (x: boolean) => void;
-};
-
-const SmartOptModal: React.FC<Props> = ({
-    setModalVisible,
-}) => {
-    return (
-        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-            <Text size={16}>Resource Optimization on Porter</Text>
-            <Spacer y={1} />
-            <Text color="helper">
-                Smart Optimization ensures that your app runs smoothly while minimizing costs. Smart Optimization performs the following:
-            </Text>
-            <Spacer y={1} />
-            <Step number={1}>
-                Maintains a consistent ratio between RAM and CPU based on the instance type.
-            </Step>
-            <Spacer y={1} />
-            <Step number={2}>
-                Enforces limits so that your app does not consume resources beyond the instance type's limits.
-            </Step>
-            <Spacer y={1} />
-            <Step number={3}> Determines an optimal resource threshold to save cost.</Step>
-            <Spacer y={1} />
-
-            <Text color="helper">
-                Turning off Smart Optimization will allow you to specify your own resource values. This is not recommended unless you are familiar with Kubernetes resource management.
-            </Text>
-            <Spacer y={1} />
-            <Text color="helper">
-                <Link to="https://docs.porter.run/other/kubernetes-101" target="_blank">
-                    For more information about Kubernetes resource management, visit our docs.
-                </Link>
-            </Text>
-
-        </Modal>
-    );
-};
-export default SmartOptModal;

+ 0 - 914
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx

@@ -1,914 +0,0 @@
-import Input from "components/porter/Input";
-import React, { useContext, useEffect, useState } from "react";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import Checkbox from "components/porter/Checkbox";
-import { Service, WebService } from "../serviceTypes";
-import AnimateHeight, { Height } from "react-animate-height";
-import { Context } from "shared/Context";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, AWS_INSTANCE_LIMITS, MILI_TO_CORE, MIB_TO_GIB, UPPER_BOUND_SMART, UPPER_BOUND_REG, RESOURCE_ALLOCATION, RESOURCE_ALLOCATION_CPU, RESOURCE_ALLOCATION_RAM } from "./utils";
-import IngressCustomAnnotations from "./IngressCustomAnnotations";
-import CustomDomains from "./CustomDomains";
-import InputSlider from "components/porter/InputSlider";
-import api from "shared/api";
-import Toggle from "components/porter/Toggle";
-import Container from "components/porter/Container";
-import { FormControlLabel, Switch } from "@material-ui/core";
-import DialToggle from "components/porter/DialToggle";
-import { max } from "lodash";
-import styled from "styled-components";
-import Step from "components/porter/Step";
-import Link from "components/porter/Link";
-import Modal from "components/porter/Modal";
-import SmartOptModal from "./SmartOptModal";
-interface Props {
-  service: WebService;
-  editService: (service: WebService) => void;
-  setHeight: (height: Height) => void;
-  chart?: any;
-  maxRAM: number;
-  maxCPU: number;
-  nodeCount: number;
-}
-
-const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
-const NETWORKING_HEIGHT_WITH_INGRESS = 395;
-const ADVANCED_BASE_HEIGHT = 215;
-const PROBE_INPUTS_HEIGHT = 230;
-const CUSTOM_ANNOTATION_HEIGHT = 44;
-
-const WebTabs: React.FC<Props> = ({
-  service,
-  editService,
-  setHeight,
-  maxRAM,
-  maxCPU,
-  nodeCount,
-}) => {
-  const [currentTab, setCurrentTab] = React.useState<string>("main");
-  const { currentCluster } = useContext(Context);
-  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
-  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
-  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
-
-  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
-    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
-
-      editService({
-        ...service,
-        cpu: {
-          readOnly: false,
-          value: (smartLimitCPU * MILI_TO_CORE).toString()
-        },
-        ram: {
-          readOnly: false,
-          value: (smartLimitRAM * MIB_TO_GIB).toString()
-        },
-        smartOptimization: !service.smartOptimization
-      })
-    }
-    else {
-      editService({
-        ...service,
-        smartOptimization: !service.smartOptimization
-      })
-    }
-
-  };
-  const renderMain = () => {
-    setHeight(159);
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Start command"
-          // TODO: uncomment the below once we have docs on what /cnb/lifecycle/launcher is
-          // label={
-          //   <>
-          //     <span>Start command</span>
-          //     {!service.startCommand.readOnly && service.startCommand.value.includes("/cnb/lifecycle/launcher") &&
-          //       <a
-          //         href="https://docs.porter.run/deploying-applications/https-and-domains/custom-domains"
-          //         target="_blank"
-          //       >
-          //         &nbsp;(?)
-          //       </a>
-          //     }
-          //   </>}
-          placeholder="ex: sh start.sh"
-          value={service.startCommand.value}
-          width="300px"
-          disabled={service.startCommand.readOnly}
-          setValue={(e) => {
-            editService({
-              ...service,
-              startCommand: { readOnly: false, value: e },
-            });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-      </>
-    );
-  };
-
-  const renderNetworking = () => {
-    setHeight(service.ingress.enabled.value ? calculateNetworkingHeight() : NETWORKING_HEIGHT_WITHOUT_INGRESS)
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Container port"
-          placeholder="ex: 3000"
-          value={service.port.value}
-          disabled={service.port.readOnly}
-          width="300px"
-          setValue={(e) => {
-            editService({ ...service, port: { readOnly: false, value: e } });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.ingress.enabled.value}
-          disabled={service.ingress.enabled.readOnly}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              ingress: {
-                ...service.ingress,
-                enabled: {
-                  readOnly: false,
-                  value: !service.ingress.enabled.value,
-                },
-              },
-            });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Expose to external traffic</Text>
-        </Checkbox>
-        <AnimateHeight height={service.ingress.enabled.value ? 'auto' : 0}>
-          <Spacer y={0.5} />
-          {getApplicationURLText()}
-          <Spacer y={0.5} />
-          <Text color="helper">
-            Custom domains
-            <a
-              href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
-              target="_blank"
-            >
-              &nbsp;(?)
-            </a>
-          </Text>
-          <Spacer y={0.5} />
-          <CustomDomains
-            customDomains={service.ingress.customDomains}
-            onChange={(customDomains) => {
-              editService({ ...service, ingress: { ...service.ingress, customDomains: customDomains } });
-              setHeight(calculateNetworkingHeight());
-            }}
-          />
-          <Spacer y={0.5} />
-          <Text color="helper">
-            Ingress Custom Annotations
-            <a
-              href="https://docs.porter.run/standard/deploying-applications/runtime-configuration-options/web-applications#ingress-custom-annotations"
-              target="_blank"
-            >
-              &nbsp;(?)
-            </a>
-          </Text>
-          <Spacer y={0.5} />
-          <IngressCustomAnnotations
-            annotations={service.ingress.annotations}
-            onChange={(annotations) => {
-              editService({ ...service, ingress: { ...service.ingress, annotations: annotations } });
-              setHeight(calculateNetworkingHeight());
-            }}
-          />
-        </AnimateHeight>
-      </>
-    );
-  }
-
-  const renderDatabase = () => {
-    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
-    return (
-      <>
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.cloudsql.enabled.value}
-          disabled={service.cloudsql.enabled.readOnly}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              cloudsql: {
-                ...service.cloudsql,
-                enabled: {
-                  readOnly: false,
-                  value: !service.cloudsql.enabled.value,
-                },
-              },
-            });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Securely connect to Google Cloud SQL</Text>
-        </Checkbox>
-        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label={"Instance Connection Name"}
-            placeholder="ex: project-123:us-east1:pachyderm"
-            value={service.cloudsql.connectionName.value}
-            disabled={service.cloudsql.connectionName.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  connectionName: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"DB Port"}
-            placeholder="5432"
-            value={service.cloudsql.dbPort.value}
-            disabled={service.cloudsql.dbPort.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  dbPort: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"Service Account JSON"}
-            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
-            value={service.cloudsql.serviceAccountJSON.value}
-            disabled={service.cloudsql.serviceAccountJSON.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  serviceAccountJSON: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-        </AnimateHeight>
-      </>
-    );
-  }
-
-  const renderResources = () => {
-    setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING)
-    return (
-      <>
-        <Spacer y={1} />
-        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
-          <StyledIcon
-            className="material-icons"
-            onClick={() => {
-              setShowNeedHelpModal(true)
-            }}
-          >
-            help_outline
-          </StyledIcon>
-          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
-          <Switch
-            size="small"
-            color="primary"
-            checked={service.smartOptimization}
-            onChange={handleSwitch}
-            inputProps={{ 'aria-label': 'controlled' }}
-          />
-        </div>
-        {showNeedHelpModal &&
-          <SmartOptModal
-            setModalVisible={setShowNeedHelpModal}
-          />}
-        <>
-          <InputSlider
-            label="CPUs: "
-            unit="Cores"
-            override={!service.smartOptimization}
-            min={0}
-            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
-            nodeCount={nodeCount}
-            color={"#3f51b5"}
-            smartLimit={smartLimitCPU}
-            value={(service.cpu.value / MILI_TO_CORE).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
-                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-            }}
-            step={0.1}
-            disabled={false}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-
-          <Spacer y={1} />
-
-          <InputSlider
-            label="RAM: "
-            unit="GiB"
-            min={0}
-            override={!service.smartOptimization}
-            nodeCount={nodeCount}
-            smartLimit={smartLimitRAM}
-            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
-            color={"#3f51b5"}
-            value={(service.ram.value / MIB_TO_GIB).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
-                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-            }}
-
-            disabled={service.ram.readOnly}
-            step={0.1}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-        </>
-        <Spacer y={1} />
-
-        <Input
-          label="Replicas"
-          placeholder="ex: 1"
-          value={service.replicas.value}
-          disabled={service.replicas.readOnly || service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => {
-            editService({
-              ...service,
-              replicas: { readOnly: false, value: e },
-            });
-          }}
-          disabledTooltip={service.replicas.readOnly
-            ? "You may only edit this field in your porter.yaml."
-            : "Disable autoscaling to specify replicas."} /><Spacer y={1} /><Checkbox
-              checked={service.autoscaling.enabled.value}
-              toggleChecked={() => {
-                editService({
-                  ...service,
-                  autoscaling: {
-                    ...service.autoscaling,
-                    enabled: {
-                      readOnly: false,
-                      value: !service.autoscaling.enabled.value,
-                    },
-                  },
-                });
-                setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITHOUT_AUTOSCALING : RESOURCE_HEIGHT_WITH_AUTOSCALING);
-              }}
-              disabled={service.autoscaling.enabled.readOnly}
-              disabledTooltip={"You may only edit this field in your porter.yaml."}
-            >
-          <Text color="helper">Enable autoscaling (overrides replicas)</Text>
-        </Checkbox><AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label="Min replicas"
-            placeholder="ex: 1"
-            value={service.autoscaling.minReplicas.value}
-            disabled={service.autoscaling.minReplicas.readOnly ||
-              !service.autoscaling.enabled.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  minReplicas: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={service.autoscaling.minReplicas.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify min replicas."} />
-          <Spacer y={1} />
-          <Input
-            label="Max replicas"
-            placeholder="ex: 10"
-            value={service.autoscaling.maxReplicas.value}
-            disabled={service.autoscaling.maxReplicas.readOnly ||
-              !service.autoscaling.enabled.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  maxReplicas: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={service.autoscaling.maxReplicas.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify max replicas."} />
-          <Spacer y={1} />
-          <InputSlider
-            label="Target CPU utilization: "
-            unit="%"
-            min={0}
-            max={100}
-            value={service.autoscaling.targetCPUUtilizationPercentage.value}
-            disabled={service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  targetCPUUtilizationPercentage: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={service.autoscaling.targetCPUUtilizationPercentage.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify target CPU utilization."} />
-          <Spacer y={1} />
-          <InputSlider
-            label="Target RAM utilization: "
-            unit="%"
-            min={0}
-            max={100}
-            value={service.autoscaling.targetMemoryUtilizationPercentage.value}
-            disabled={service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  targetMemoryUtilizationPercentage: {
-                    readOnly: false,
-                    value: e,
-                  },
-                },
-              });
-            }}
-            disabledTooltip={service.autoscaling.targetMemoryUtilizationPercentage.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify target RAM utilization."} />
-        </AnimateHeight></>
-    );
-  };
-
-  const calculateHealthHeight = () => {
-    let height = ADVANCED_BASE_HEIGHT;
-    if (service.health.livenessProbe.enabled.value) {
-      height += PROBE_INPUTS_HEIGHT;
-    }
-    if (service.health.startupProbe.enabled.value) {
-      height += PROBE_INPUTS_HEIGHT;
-    }
-    if (service.health.readinessProbe.enabled.value) {
-      height += PROBE_INPUTS_HEIGHT;
-    }
-    return height;
-  };
-
-  const calculateNetworkingHeight = () => {
-    return NETWORKING_HEIGHT_WITH_INGRESS + (service.ingress.annotations.length * CUSTOM_ANNOTATION_HEIGHT) + (service.ingress.customDomains.length * CUSTOM_ANNOTATION_HEIGHT);
-  }
-
-  const renderAdvanced = () => {
-    setHeight(calculateHealthHeight());
-    return (
-      <>
-        <Spacer y={1} />
-        <Text color="helper">
-          <>
-            <span>Health checks</span>
-            <a
-              href="https://docs.porter.run/enterprise/deploying-applications/zero-downtime-deployments#health-checks"
-              target="_blank"
-            >
-              &nbsp;(?)
-            </a>
-          </>
-        </Text>
-        <Spacer y={0.5} />
-        <Checkbox
-          checked={service.health.livenessProbe.enabled.value}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              health: {
-                ...service.health,
-                livenessProbe: {
-                  ...service.health.livenessProbe,
-                  enabled: {
-                    readOnly: false,
-                    value: !service.health.livenessProbe.enabled.value,
-                  },
-                },
-              },
-            });
-            setHeight(calculateHealthHeight() + (service.health.livenessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
-          }}
-          disabled={service.health.livenessProbe.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Enable Liveness Probe</Text>
-        </Checkbox>
-        <AnimateHeight height={service.health.livenessProbe.enabled.value ? 'auto' : 0}>
-          <Spacer y={0.5} />
-          <Input
-            label="Liveness Check Endpoint "
-            placeholder="ex: /liveness"
-            value={service.health.livenessProbe.path.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  livenessProbe: {
-                    ...service.health.livenessProbe,
-                    path: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabled={service.health.livenessProbe.path.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Failure Threshold"
-            placeholder="ex: 10"
-            value={service.health.livenessProbe.failureThreshold.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  livenessProbe: {
-                    ...service.health.livenessProbe,
-                    failureThreshold: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabled={
-              service.health.livenessProbe.failureThreshold.readOnly
-            }
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Retry Interval"
-            placeholder="ex: 5"
-            value={service.health.livenessProbe.periodSeconds.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  livenessProbe: {
-                    ...service.health.livenessProbe,
-                    periodSeconds: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabled={service.health.livenessProbe.periodSeconds.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-        </AnimateHeight>
-        <Spacer y={0.5} />
-        <Checkbox
-          checked={service.health.startupProbe.enabled.value}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              health: {
-                ...service.health,
-                startupProbe: {
-                  ...service.health.startupProbe,
-                  enabled: {
-                    readOnly: false,
-                    value: !service.health.startupProbe.enabled.value,
-                  },
-                },
-              },
-            });
-            setHeight(calculateHealthHeight() + (service.health.startupProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
-          }}
-          disabled={service.health.startupProbe.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Enable Start Up Probe</Text>
-        </Checkbox>
-        <AnimateHeight height={service.health.startupProbe.enabled.value ? 'auto' : 0}>
-          <Spacer y={0.5} />
-          <Input
-            label="Start Up Check Endpoint "
-            placeholder="ex: /startupz"
-            value={service.health.startupProbe.path.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  startupProbe: {
-                    ...service.health.startupProbe,
-                    path: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabled={service.health.startupProbe.path.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Failure Threshold"
-            placeholder="ex: 5"
-            value={service.health.startupProbe.failureThreshold.value}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  startupProbe: {
-                    ...service.health.startupProbe,
-                    failureThreshold: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabled={service.health.startupProbe.failureThreshold.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Retry Interval"
-            placeholder="ex: 5"
-            value={service.health.startupProbe.periodSeconds.value}
-            disabled={service.health.startupProbe.periodSeconds.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  startupProbe: {
-                    ...service.health.startupProbe,
-                    periodSeconds: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-        </AnimateHeight>
-        <Spacer y={0.5} />
-        <Checkbox
-          checked={service.health.readinessProbe.enabled.value}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              health: {
-                ...service.health,
-                readinessProbe: {
-                  ...service.health.readinessProbe,
-                  enabled: {
-                    readOnly: false,
-                    value: !service.health.readinessProbe.enabled.value,
-                  },
-                },
-              },
-            });
-            setHeight(calculateHealthHeight() + (service.health.readinessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
-          }}
-          disabled={service.health.readinessProbe.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Enable Readiness Probe</Text>
-        </Checkbox>
-        <AnimateHeight height={service.health.readinessProbe?.enabled.value ? 'auto' : 0}>
-          <Spacer y={0.5} />
-          <Input
-            label="Readiness Check Endpoint "
-            placeholder="ex: /readiness"
-            value={service.health.readinessProbe.path.value}
-            disabled={service.health.readinessProbe.path.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  readinessProbe: {
-                    ...service.health.readinessProbe,
-                    path: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Failure Threshold"
-            placeholder="ex: 5"
-            value={service.health.readinessProbe.failureThreshold.value}
-            disabled={
-              service.health.readinessProbe.failureThreshold.readOnly
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  readinessProbe: {
-                    ...service.health.readinessProbe,
-                    failureThreshold: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-          <Input
-            label="Initial Delay Threshold"
-            placeholder="ex: 100"
-            value={service.health.readinessProbe.initialDelaySeconds.value}
-            disabled={
-              service.health.readinessProbe.initialDelaySeconds.readOnly
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                health: {
-                  ...service.health,
-                  readinessProbe: {
-                    ...service.health.readinessProbe,
-                    initialDelaySeconds: {
-                      readOnly: false,
-                      value: e,
-                    },
-                  },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={0.5} />
-        </AnimateHeight>
-      </>
-    );
-  };
-
-  const getApplicationURLText = () => {
-    if (service.ingress.hosts.length !== 0) {
-      return (
-        <Text>{`Application URL${service.ingress.hosts.length === 1 ? "" : "s"}: `}
-          {service.ingress.hosts.map((host, i) => {
-            return (
-              <a href={Service.prefixSubdomain(host.value)} target="_blank">
-                {host.value}
-                {i !== service.ingress.hosts.length - 1 && ", "}
-              </a>
-            )
-          })}
-        </Text>
-      )
-    } else if (service.ingress.porterHosts.value !== "") {
-      return (
-        <Text>Application URL:{" "}
-          <a href={Service.prefixSubdomain(service.ingress.porterHosts.value)} target="_blank">
-            {service.ingress.porterHosts.value}
-          </a>
-        </Text>
-      )
-    } else if (service.ingress.customDomains.length !== 0) {
-      return (
-        <Text color="helper">
-          {`Application URL${service.ingress.customDomains.length === 1 ? "" : "s"}: Your application will be available at the specified custom domain${service.ingress.customDomains.length === 1 ? "" : "s"} on next deploy.`}
-        </Text>
-      )
-    } else {
-      return (
-        <Text color="helper">
-          Application URL: Not generated yet. Porter will generate a URL for you on next deploy.
-        </Text>
-      )
-    }
-  }
-
-  return (
-    <>
-      <TabSelector
-        options={currentCluster?.cloud_provider === "GCP" || (currentCluster?.service === "gke") ?
-          [
-            { label: "Main", value: "main" },
-            { label: "Resources", value: "resources" },
-            { label: "Networking", value: "networking" },
-            { label: "Database", value: "database" },
-            { label: "Advanced", value: "advanced" },
-          ] :
-          [
-            { label: "Main", value: "main" },
-            { label: "Resources", value: "resources" },
-            { label: "Networking", value: "networking" },
-            { label: "Advanced", value: "advanced" },
-          ]
-        }
-        currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
-      />
-      {currentTab === "main" && renderMain()}
-      {currentTab === "resources" && renderResources()}
-      {currentTab === "networking" && renderNetworking()}
-      {currentTab === "database" && renderDatabase()}
-      {currentTab === "advanced" && renderAdvanced()}
-    </>
-  );
-};
-
-export default WebTabs;
-
-const StyledIcon = styled.i`
-  cursor: pointer;
-  font-size: 16px; 
-  margin-right : 5px;
-  &:hover {
-    color: #666;  
-  }
-`;

+ 0 - 402
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx

@@ -1,402 +0,0 @@
-import Input from "components/porter/Input";
-import React, { useContext, useState } from "react"
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import TabSelector from "components/TabSelector";
-import Checkbox from "components/porter/Checkbox";
-import { WorkerService } from "../serviceTypes";
-import AnimateHeight, { Height } from "react-animate-height";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, UPPER_BOUND_SMART } from "./utils";
-import { Context } from "shared/Context";
-import InputSlider from "components/porter/InputSlider";
-import styled from "styled-components";
-import { Switch } from "@material-ui/core";
-import SmartOptModal from "./SmartOptModal";
-
-interface Props {
-  service: WorkerService;
-  editService: (service: WorkerService) => void;
-  setHeight: (height: Height) => void;
-  maxRAM: number;
-  maxCPU: number;
-  nodeCount: number;
-}
-
-const WorkerTabs: React.FC<Props> = ({
-  service,
-  editService,
-  setHeight,
-  maxCPU,
-  maxRAM,
-  nodeCount,
-}) => {
-  const [currentTab, setCurrentTab] = React.useState<string>('main');
-  const { currentCluster } = useContext(Context);
-  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
-  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
-  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
-  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
-    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
-
-      editService({
-        ...service,
-        cpu: {
-          readOnly: false,
-          value: (smartLimitCPU * MILI_TO_CORE).toString()
-        },
-        ram: {
-          readOnly: false,
-          value: (smartLimitRAM * MIB_TO_GIB).toString()
-        },
-        smartOptimization: !service.smartOptimization
-      })
-    }
-    else {
-      editService({
-        ...service,
-        smartOptimization: !service.smartOptimization
-      })
-    }
-
-  };
-  const renderMain = () => {
-    setHeight(159);
-    return (
-      <>
-        <Spacer y={1} />
-        <Input
-          label="Start command"
-          placeholder="ex: sh start.sh"
-          disabled={service.startCommand.readOnly}
-          value={service.startCommand.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, startCommand: { readOnly: false, value: e } }) }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-      </>
-    )
-  };
-
-  const renderResources = () => {
-    setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING)
-    return (
-      <>
-        <Spacer y={1} />
-        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
-          <StyledIcon
-            className="material-icons"
-            onClick={() => {
-              setShowNeedHelpModal(true)
-            }}
-          >
-            help_outline
-          </StyledIcon>
-          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
-          <Switch
-            size="small"
-            color="primary"
-            checked={service.smartOptimization}
-            onChange={handleSwitch}
-            inputProps={{ 'aria-label': 'controlled' }}
-          />
-        </div>
-        {showNeedHelpModal &&
-          <SmartOptModal
-            setModalVisible={setShowNeedHelpModal}
-          />}
-        <>
-          <InputSlider
-            label="CPUs: "
-            unit="Cores"
-            override={!service.smartOptimization}
-            min={0}
-            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
-            nodeCount={nodeCount}
-            color={"#3f51b5"}
-            smartLimit={smartLimitCPU}
-            value={(service.cpu.value / MILI_TO_CORE).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
-                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-            }}
-            step={0.1}
-            disabled={false}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-
-          <Spacer y={1} />
-
-          <InputSlider
-            label="RAM: "
-            unit="GiB"
-            min={0}
-            override={!service.smartOptimization}
-            nodeCount={nodeCount}
-            smartLimit={smartLimitRAM}
-            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
-            color={"#3f51b5"}
-            value={(service.ram.value / MIB_TO_GIB).toString()}
-            setValue={(e) => {
-              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
-                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-            }}
-
-            disabled={service.ram.readOnly}
-            step={0.1}
-            disabledTooltip={"You may only edit this field in your porter.yaml."} />
-        </>
-        <Spacer y={1} />
-        <Input
-          label="Replicas"
-          placeholder="ex: 1"
-          value={service.replicas.value}
-          disabled={service.replicas.readOnly || service.autoscaling.enabled.value}
-          width="300px"
-          setValue={(e) => { editService({ ...service, replicas: { readOnly: false, value: e } }) }}
-          disabledTooltip={service.replicas.readOnly ? "You may only edit this field in your porter.yaml." : "Disable autoscaling to specify replicas."}
-        />
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.autoscaling.enabled.value}
-          toggleChecked={() => { editService({ ...service, autoscaling: { ...service.autoscaling, enabled: { readOnly: false, value: !service.autoscaling.enabled.value } } }) }}
-          disabled={service.autoscaling.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Enable autoscaling (overrides replicas)</Text>
-        </Checkbox>
-        <AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label="Min replicas"
-            placeholder="ex: 1"
-            value={service.autoscaling.minReplicas.value}
-            disabled={
-              service.autoscaling.minReplicas.readOnly ||
-              !service.autoscaling.enabled.value
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  minReplicas: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              service.autoscaling.minReplicas.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify min replicas."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label="Max replicas"
-            placeholder="ex: 10"
-            value={service.autoscaling.maxReplicas.value}
-            disabled={
-              service.autoscaling.maxReplicas.readOnly ||
-              !service.autoscaling.enabled.value
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  maxReplicas: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              service.autoscaling.maxReplicas.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify max replicas."
-            }
-          />
-          <Spacer y={1} />
-          <InputSlider
-            label="Target CPU utilization: "
-            unit="%"
-            min={0}
-            max={100}
-            value={service.autoscaling.targetCPUUtilizationPercentage.value}
-            disabled={
-              service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  targetCPUUtilizationPercentage: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              service.autoscaling.targetCPUUtilizationPercentage.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify target CPU utilization."
-            }
-          />
-          <Spacer y={1} />
-          <InputSlider
-            label="Target RAM utilization: "
-            unit="%"
-            min={0}
-            max={100}
-            value={service.autoscaling.targetMemoryUtilizationPercentage.value}
-            disabled={
-              service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value
-            }
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                autoscaling: {
-                  ...service.autoscaling,
-                  targetMemoryUtilizationPercentage: {
-                    readOnly: false,
-                    value: e,
-                  },
-                },
-              });
-            }}
-            disabledTooltip={
-              service.autoscaling.targetMemoryUtilizationPercentage.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify target RAM utilization."
-            }
-          />
-        </AnimateHeight>
-      </>
-    )
-  };
-
-  const renderDatabase = () => {
-    setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED)
-    return (
-      <>
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.cloudsql.enabled.value}
-          disabled={service.cloudsql.enabled.readOnly}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              cloudsql: {
-                ...service.cloudsql,
-                enabled: {
-                  readOnly: false,
-                  value: !service.cloudsql.enabled.value,
-                },
-              },
-            });
-          }}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
-          <Text color="helper">Securely connect to Google Cloud SQL</Text>
-        </Checkbox>
-        <AnimateHeight height={service.cloudsql.enabled.value ? 'auto' : 0}>
-          <Spacer y={1} />
-          <Input
-            label={"Instance Connection Name"}
-            placeholder="ex: project-123:us-east1:pachyderm"
-            value={service.cloudsql.connectionName.value}
-            disabled={service.cloudsql.connectionName.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  connectionName: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"DB Port"}
-            placeholder="5432"
-            value={service.cloudsql.dbPort.value}
-            disabled={service.cloudsql.dbPort.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  dbPort: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          <Spacer y={1} />
-          <Input
-            label={"Service Account JSON"}
-            placeholder="ex: { <SERVICE_ACCOUNT_JSON> }"
-            value={service.cloudsql.serviceAccountJSON.value}
-            disabled={service.cloudsql.serviceAccountJSON.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                cloudsql: {
-                  ...service.cloudsql,
-                  serviceAccountJSON: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-        </AnimateHeight>
-      </>
-    );
-  }
-
-  return (
-    <>
-      <TabSelector
-        options={currentCluster?.cloud_provider === "GCP" || (currentCluster?.service === "gke") ?
-          [
-            { label: 'Main', value: 'main' },
-            { label: 'Resources', value: 'resources' },
-            { label: "Database", value: "database" },
-          ] :
-          [
-            { label: 'Main', value: 'main' },
-            { label: 'Resources', value: 'resources' },
-          ]
-        }
-        currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
-      />
-      {currentTab === 'main' && renderMain()}
-      {currentTab === 'resources' && renderResources()}
-      {currentTab === 'database' && renderDatabase()}
-    </>
-  )
-}
-
-export default WorkerTabs;
-
-const StyledIcon = styled.i`
-  cursor: pointer;
-  font-size: 16px; 
-  margin-right : 5px;
-  &:hover {
-    color: #666;  
-  }
-`;

+ 0 - 88
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts

@@ -1,88 +0,0 @@
-export const DATABASE_HEIGHT_ENABLED = 374;
-export const DATABASE_HEIGHT_DISABLED = 119;
-export const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 446;
-export const RESOURCE_HEIGHT_WITH_AUTOSCALING = 833;
-export const MIB_TO_GIB = 1024;
-export const MILI_TO_CORE = 1000;
-interface InstanceDetails {
-    vCPU: number;
-    "RAM": number;
-}
-
-interface InstanceTypes {
-    [key: string]: {
-        [size: string]: InstanceDetails;
-    };
-}
-
-export const AWS_INSTANCE_LIMITS: InstanceTypes = {
-    "t3a": {
-        "nano": { "vCPU": 2, "RAM": 0.5 },
-        "micro": { "vCPU": 2, "RAM": 1 },
-        "small": { "vCPU": 2, "RAM": 2 },
-        "medium": { "vCPU": 2, "RAM": 4 },
-        "large": { "vCPU": 2, "RAM": 8 },
-        "xlarge": { "vCPU": 4, "RAM": 16 },
-        "2xlarge": { "vCPU": 8, "RAM": 32 }
-    },
-    "t3": {
-        "nano": { "vCPU": 2, "RAM": 0.5 },
-        "micro": { "vCPU": 2, "RAM": 1 },
-        "small": { "vCPU": 2, "RAM": 2 },
-        "medium": { "vCPU": 2, "RAM": 4 },
-        "large": { "vCPU": 2, "RAM": 8 },
-        "xlarge": { "vCPU": 4, "RAM": 16 },
-        "2xlarge": { "vCPU": 8, "RAM": 32 }
-    },
-    "t2": {
-        "nano": { "vCPU": 1, "RAM": 0.5 },
-        "micro": { "vCPU": 1, "RAM": 1 },
-        "small": { "vCPU": 1, "RAM": 2 },
-        "medium": { "vCPU": 2, "RAM": 4 },
-        "large": { "vCPU": 2, "RAM": 8 },
-        "xlarge": { "vCPU": 4, "RAM": 16 },
-        "2xlarge": { "vCPU": 8, "RAM": 32 }
-    },
-    "c6i": {
-        "large": { "vCPU": 2, "RAM": 4 },
-        "xlarge": { "vCPU": 4, "RAM": 8 },
-        "2xlarge": { "vCPU": 8, "RAM": 16 },
-        "4xlarge": { "vCPU": 16, "RAM": 32 },
-        "8xlarge": { "vCPU": 32, "RAM": 64 },
-        "12xlarge": { "vCPU": 48, "RAM": 96 },
-    },
-    "g4dn": {
-        "xlarge": { "vCPU": 4, "RAM": 16 },
-        "2xlarge": { "vCPU": 8, "RAM": 32 },
-        "4xlarge": { "vCPU": 16, "RAM": 64 },
-        "8xlarge": { "vCPU": 32, "RAM": 128 },
-    },
-    "r6a": {
-        "large": { "vCPU": 2, "RAM": 16 },
-        "xlarge": { "vCPU": 4, "RAM": 32 },
-        "2xlarge": { "vCPU": 8, "RAM": 64 },
-        "4xlarge": { "vCPU": 16, "RAM": 128 },
-        "8xlarge": { "vCPU": 32, "RAM": 256 },
-    },
-    "c5": {
-        "large": { "vCPU": 2, "RAM": 4 },
-        "xlarge": { "vCPU": 4, "RAM": 8 },
-        "2xlarge": { "vCPU": 8, "RAM": 16 },
-        "4xlarge": { "vCPU": 16, "RAM": 32 },
-    },
-    "m5": {
-        "large": { "vCPU": 2, "RAM": 8 },
-        "xlarge": { "vCPU": 4, "RAM": 16 },
-        "2xlarge": { "vCPU": 8, "RAM": 32 },
-        "4xlarge": { "vCPU": 16, "RAM": 64 },
-    },
-    "x2gd": {
-        "medium": { "vCPU": 1, "RAM": 16 },
-        "large": { "vCPU": 2, "RAM": 32 },
-        "xlarge": { "vCPU": 4, "RAM": 64 },
-    }
-}
-
-
-export const UPPER_BOUND_SMART = .5;
-export const RESOURCE_ALLOCATION_RAM = 1;

+ 0 - 4
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx

@@ -5,7 +5,6 @@ import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
 
 import Spacer from "components/porter/Spacer";
-import NodeInfoModal from "main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal";
 
 const SMART_LIMIT_FRACTION = 0.5;
 
@@ -151,9 +150,6 @@ const IntelligentSlider: React.FC<IntelligentSliderProps> = ({
             </StyledIcon>
           </>
         )}
-        {showNeedHelpModal && (
-          <NodeInfoModal setModalVisible={setShowNeedHelpModal} />
-        )}
         {isExceedingLimit && (
           <>
             <Spacer inline x={1} />

+ 1 - 6
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useMemo, useState } from "react";
+import React, { useContext, useMemo } from "react";
 import { Controller, useFormContext } from "react-hook-form";
 import { match } from "ts-pattern";
 
@@ -7,7 +7,6 @@ import { ControlledInput } from "components/porter/ControlledInput";
 import InputSlider from "components/porter/InputSlider";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import SmartOptModal from "main/home/app-dashboard/new-app-flow/tabs/SmartOptModal";
 import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type PorterAppFormData } from "lib/porter-apps";
 import {
@@ -33,7 +32,6 @@ const Resources: React.FC<ResourcesProps> = ({
 }) => {
   const { control, register, watch } = useFormContext<PorterAppFormData>();
   const { currentProject } = useContext(Context);
-  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
   const { nodes } = useClusterContext();
   const { maxRamMegabytes, maxCpuCores } = useMemo(() => {
     return getServiceResourceAllowances(nodes, currentProject?.sandbox_enabled);
@@ -64,9 +62,6 @@ const Resources: React.FC<ResourcesProps> = ({
   return (
     <>
       <Spacer y={1} />
-      {showNeedHelpModal && (
-        <SmartOptModal setModalVisible={setShowNeedHelpModal} />
-      )}
       <Controller
         name={
           lifecycleJobType === "predeploy"

+ 0 - 404
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx

@@ -1,404 +0,0 @@
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-import Modal from "main/home/modals/Modal";
-import EnvEditorModal from "main/home/modals/EnvEditorModal";
-
-import upload from "assets/upload.svg";
-import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
-import { dotenv_parse } from "shared/string_utils";
-import { NewPopulatedEnvGroup, PopulatedEnvGroup } from "components/porter-form/types";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-export type KeyValueType = {
-  key: string;
-  value: string;
-  hidden: boolean;
-  locked: boolean;
-  deleted: boolean;
-};
-
-type PropsType = {
-  label?: string;
-  values: KeyValueType[];
-  setValues: (x: KeyValueType[]) => void;
-  disabled?: boolean;
-  fileUpload?: boolean;
-  secretOption?: boolean;
-  syncedEnvGroups?: NewPopulatedEnvGroup[];
-};
-
-const EnvGroupArray = ({
-  label,
-  values,
-  setValues,
-  disabled,
-  fileUpload,
-  secretOption,
-  syncedEnvGroups
-}: PropsType) => {
-  const [showEditorModal, setShowEditorModal] = useState(false);
-
-  useEffect(() => {
-    if (!values) {
-      setValues([]);
-    }
-  }, [values]);
-  const isKeyOverriding = (key: string) => {
-    if (!syncedEnvGroups || !values) return false;
-    return syncedEnvGroups?.some(envGroup => {
-      if (!envGroup || !envGroup.variables) return false;
-      return key in envGroup.variables || (envGroup.secret_variables && key in envGroup.secret_variables);
-    });
-  };
-
-  const readFile = (env: string) => {
-    const envObj = dotenv_parse(env);
-    const _values = [...values];
-
-    for (const key in envObj) {
-      let push = true;
-
-      for (let i = 0; i < values.length; i++) {
-        const existingKey = values[i]["key"];
-        const isExistingKeyDeleted = values[i]["deleted"];
-        if (key === existingKey && !isExistingKeyDeleted) {
-          _values[i]["value"] = envObj[key];
-          push = false;
-        }
-      }
-
-      if (push) {
-        _values.push({
-          key,
-          value: envObj[key],
-          hidden: false,
-          locked: false,
-          deleted: false,
-        });
-      }
-    }
-
-    setValues(_values);
-  };
-
-  if (!values) {
-    return null;
-  }
-
-  return (
-    <>
-      <StyledInputArray>
-        <Label>{label}</Label>
-        {!!values?.length &&
-          values.map((entry: KeyValueType, i: number) => {
-            if (!entry.deleted) {
-              return (
-                <InputWrapper key={i}>
-                  <Input
-                    placeholder="ex: key"
-                    width="270px"
-                    value={entry.key}
-                    onChange={(e: any) => {
-                      const _values = [...values];
-                      _values[i].key = e.target.value;
-                      setValues(_values);
-                    }}
-                    disabled={disabled || entry.locked}
-                    spellCheck={false}
-                    override={isKeyOverriding(entry.key)}
-                  />
-                  < Spacer x={.5} inline />
-                  {entry.hidden ? (
-                    entry.value?.includes("PORTERSECRET") ? (
-                      <Input
-                        placeholder="ex: value"
-                        width="270px"
-                        value={entry.value}
-                        disabled
-                        type={"password"}
-                        spellCheck={false}
-                        override={isKeyOverriding(entry.key)}
-                      />) : (
-                      <Input
-                        placeholder="ex: value"
-                        width="270px"
-                        value={entry.value}
-                        onChange={(e: any) => {
-                          const _values = [...values];
-                          _values[i].value = e.target.value;
-                          setValues(_values);
-                        }}
-                        disabled={disabled || entry.locked}
-                        type={entry.hidden ? "password" : "text"}
-                        spellCheck={false}
-                        override={isKeyOverriding(entry.key)}
-
-                      />)
-                  ) : (
-                    entry.value?.includes("PORTERSECRET") ? (
-                      <Input
-                        placeholder="ex: value"
-                        width="270px"
-                        value={entry.value}
-                        disabled
-                        type={"password"}
-                        spellCheck={false}
-                        override={isKeyOverriding(entry.key)}
-                      />) : (
-                      <MultiLineInputer
-                        placeholder="ex: value"
-                        width="270px"
-                        value={entry.value}
-                        onChange={(e: any) => {
-                          const _values = [...values];
-                          _values[i].value = e.target.value;
-                          setValues(_values);
-                        }}
-                        rows={entry.value?.split("\n").length}
-                        disabled={disabled || entry.locked}
-                        spellCheck={false}
-                        override={isKeyOverriding(entry.key)}
-                      />
-                    ))
-                  }
-                  {secretOption && (
-                    <HideButton
-                      onClick={() => {
-                        if (!entry.locked) {
-                          const _values = [...values];
-                          _values[i].hidden = !_values[i].hidden;
-                          setValues(_values);
-                        }
-                      }}
-                      disabled={entry.locked}
-                    >
-                      {entry.hidden ? (
-                        <i className="material-icons">lock</i>
-                      ) : (
-                        <i className="material-icons">lock_open</i>
-                      )}
-                    </HideButton>
-                  )}
-
-                  {!disabled && (
-                    <DeleteButton
-                      onClick={() => {
-                        setValues(values.filter((val, index) => index !== i));
-                      }}
-                    >
-                      <i className="material-icons">cancel</i>
-                    </DeleteButton>
-                  )}
-
-                  {isKeyOverriding(entry.key) && <><Spacer x={1} inline /> <Text color={'#6b74d6'} >Key is overriding value in a environment group</Text></>}
-                </InputWrapper>
-              );
-            }
-          })}
-        {!disabled && (
-          <InputWrapper>
-            <AddRowButton
-              onClick={() => {
-                const _values = [
-                  ...values,
-                  {
-                    key: "",
-                    value: "",
-                    hidden: false,
-                    locked: false,
-                    deleted: false,
-                  },
-                ];
-                setValues(_values);
-              }}
-            >
-              <i className="material-icons">add</i> Add Row
-            </AddRowButton>
-            <Spacer x={.5} inline />
-            {fileUpload && (
-              <UploadButton
-                onClick={() => {
-                  setShowEditorModal(true);
-                }}
-              >
-                <img src={upload} /> Copy from File
-              </UploadButton>
-            )}
-          </InputWrapper>
-        )}
-      </StyledInputArray>
-      {showEditorModal && (
-        <Modal
-          onRequestClose={() => setShowEditorModal(false)}
-          width="60%"
-          height="650px"
-        >
-          <EnvEditorModal
-            closeModal={() => setShowEditorModal(false)}
-            setEnvVariables={(envFile: string) => readFile(envFile)}
-          />
-        </Modal>
-      )}
-    </>
-  );
-};
-
-export default EnvGroupArray;
-
-
-const AddRowButton = styled.div`
-  display: flex;
-  align-items: center;
-  width: 270px;
-  font-size: 13px;
-  color: #aaaabb;
-  height: 32px;
-  border-radius: 3px;
-  cursor: pointer;
-  background: #ffffff11;
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-`;
-
-const UploadButton = styled(AddRowButton)`
-  background: none;
-  position: relative;
-  border: 1px solid #ffffff55;
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  > img {
-    width: 14px;
-    margin-left: 10px;
-    margin-right: 12px;
-  }
-`;
-
-const DeleteButton = styled.div`
-  width: 15px;
-  height: 15px;
-  display: flex;
-  align-items: center;
-  margin-left: 8px;
-  margin-top: -3px;
-  justify-content: center;
-
-  > i {
-    font-size: 17px;
-    color: #ffffff44;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    cursor: pointer;
-    :hover {
-      color: #ffffff88;
-    }
-  }
-`;
-
-const HideButton = styled(DeleteButton)`
-  margin-top: -5px;
-  > i {
-    font-size: 19px;
-    cursor: ${(props: { disabled: boolean }) =>
-    props.disabled ? "default" : "pointer"};
-    :hover {
-      color: ${(props: { disabled: boolean }) =>
-    props.disabled ? "#ffffff44" : "#ffffff88"};
-    }
-  }
-`;
-
-const InputWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-top: 5px;
-
-`;
-
-type InputProps = {
-  disabled?: boolean;
-  width: string;
-  override?: boolean;
-};
-
-const Input = styled.input<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: ${(props) => (props.override ? '2px solid #6b74d6' : ' 1px solid #ffffff55')};
-  border-radius: 3px;
-  width: ${(props) => props.width ? props.width : "270px"};
-  color: ${(props) => props.disabled ? "#ffffff44" : "white"};
-  padding: 5px 10px;
-  height: 35px;
-`;
-const Label = styled.div`
-  color: #ffffff;
-  margin-bottom: 10px;
-`;
-
-const StyledInputArray = styled.div`
-  margin-bottom: 15px;
-  margin-top: 22px;
-`;
-
-export const MultiLineInputer = styled.textarea<InputProps>`
-  outline: none;
-  border: none;
-  margin-bottom: 5px;
-  font-size: 13px;
-  background: #ffffff11;
-  border: ${(props) => (props.override ? '2px solid #6b74d6' : ' 1px solid #ffffff55')};
-  border-radius: 3px;
-  min-width: ${(props) => (props.width ? props.width : "270px")};
-  max-width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
-  padding: 8px 10px 5px 10px;
-  min-height: 35px;
-  max-height: 100px;
-  white-space: nowrap;
-
-  ::-webkit-scrollbar {
-    width: 8px;
-    :horizontal {
-      height: 8px;
-    }
-  }
-
-  ::-webkit-scrollbar-corner {
-    width: 10px;
-    background: #ffffff11;
-    color: white;
-  }
-
-  ::-webkit-scrollbar-track {
-    width: 10px;
-    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-  }
-
-  ::-webkit-scrollbar-thumb {
-    background-color: darkgrey;
-    outline: 1px solid slategrey;
-  }
-`;

+ 202 - 460
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -1,44 +1,32 @@
-import React, {
-  Component,
-  useContext,
-  useEffect,
-  useMemo,
-  useState,
-} from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import yaml from "js-yaml";
-import { createFinalPorterYaml, PorterYamlSchema } from "../../app-dashboard/new-app-flow/schema"
+import _, { remove } from "lodash";
 import styled, { keyframes } from "styled-components";
-import backArrow from "assets/back_arrow.png";
-import key from "assets/key.svg";
-import loading from "assets/loading.gif";
-import leftArrow from "assets/left-arrow.svg";
-
-import { type ChartType, type ClusterType, CreateUpdatePorterAppOptions } from "shared/types";
-import { Context } from "shared/Context";
-import { isAlphanumeric } from "shared/common";
-import api from "shared/api";
 
-import TitleSection from "components/TitleSection";
-import SaveButton from "components/SaveButton";
-import TabRegion from "components/TabRegion";
-import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray";
+import DocsHelper from "components/DocsHelper";
+import DynamicLink from "components/DynamicLink";
 import Heading from "components/form-components/Heading";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
-import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
-import _, { flatMapDepth, remove, update } from "lodash";
-import { type NewPopulatedEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types";
-import { isAuthorized } from "shared/auth/authorization-helpers";
-import useAuth from "shared/auth/useAuth";
-import { fillWithDeletedVariables } from "components/porter-form/utils";
-import DynamicLink from "components/DynamicLink";
-import DocsHelper from "components/DocsHelper";
+import {
+  type NewPopulatedEnvGroup,
+  type PopulatedEnvGroup,
+} from "components/porter-form/types";
 import Spacer from "components/porter/Spacer";
-import EnvGroups from "../stacks/ExpandedStack/components/EnvGroups";
-import { type PorterJson } from "main/home/app-dashboard/new-app-flow/schema";
-import { BuildMethod, PorterApp } from "main/home/app-dashboard/types/porterApp";
-import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes";
-import { consoleSandbox } from "@sentry/utils";
+import SaveButton from "components/SaveButton";
+import TabRegion from "components/TabRegion";
+import TitleSection from "components/TitleSection";
+
+import api from "shared/api";
+import { type WithAuthProps } from "shared/auth/AuthorizationHoc";
+import useAuth from "shared/auth/useAuth";
+import { Context } from "shared/Context";
+import { type ClusterType } from "shared/types";
+import key from "assets/key.svg";
+import leftArrow from "assets/left-arrow.svg";
+import loading from "assets/loading.gif";
+
+import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray";
 
 type PropsType = WithAuthProps & {
   namespace: string;
@@ -79,32 +67,16 @@ export const ExpandedEnvGroupFC = ({
   closeExpanded,
   allEnvGroups,
 }: PropsType) => {
-  const {
-    currentProject,
-    currentCluster,
-    setCurrentOverlay,
-    setCurrentError,
-  } = useContext(Context);
+  const { currentProject, currentCluster, setCurrentOverlay, setCurrentError } =
+    useContext(Context);
   const [isAuthorized] = useAuth();
 
-  const [workflowCheckPassed, setWorkflowCheckPassed] = useState<boolean>(
-    false
-  );
-  const [isLoading, setIsLoading] = useState(true);
-
   const [currentTab, setCurrentTab] = useState("variables-editor");
   const [isDeleting, setIsDeleting] = useState(false);
   const [buttonStatus, setButtonStatus] = useState("");
-  const [services, setServices] = useState<Service[]>([]);
-  const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
-  const [subdomain, setSubdomain] = useState<string>("");
-
-
-  const [currentEnvGroup, setCurrentEnvGroup] = useState<EditableEnvGroup>(
-    null
-  );
-  const [hasBuiltImage, setHasBuiltImage] = useState<boolean>(false);
 
+  const [currentEnvGroup, setCurrentEnvGroup] =
+    useState<EditableEnvGroup>(null);
   const [originalEnvVars, setOriginalEnvVars] = useState<
     Array<{
       key: string;
@@ -112,48 +84,15 @@ export const ExpandedEnvGroupFC = ({
     }>
   >();
 
-
-  const fetchPorterYamlContent = async (
-    porterYaml: string,
-    appData: any
-  ) => {
-    try {
-      if (porterYaml && appData?.app?.git_repo_id) {
-        const res = await api.getPorterYamlContents(
-          "<token>",
-          {
-            path: porterYaml,
-          },
-          {
-            project_id: appData.app.project_id,
-            git_repo_id: appData.app.git_repo_id,
-            owner: appData.app.repo_name?.split("/")[0],
-            name: appData.app.repo_name?.split("/")[1],
-            kind: "github",
-            branch: appData.app.git_branch,
-          }
-        );
-        if (res.data == null || res.data == "") {
-          return undefined;
-        }
-        const parsedYaml = yaml.load(atob(res.data));
-
-        return parsedYaml
-      }
-    } catch (err) {
-      // TODO: handle error
-      console.log("No Porter Yaml")
-
-    }
-  };
-
   const tabOptions = useMemo(() => {
     if (!isAuthorized("env_group", "", ["get", "delete"])) {
       return [{ value: "variables-editor", label: "Environment variables" }];
     }
     if (
       !isAuthorized("env_group", "", ["get", "delete"]) &&
-      (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length)
+      (currentProject?.simplified_view_enabled
+        ? currentEnvGroup?.linked_applications?.length
+        : currentEnvGroup?.applications?.length)
     ) {
       return [
         { value: "variables-editor", label: "Environment variables" },
@@ -161,7 +100,11 @@ export const ExpandedEnvGroupFC = ({
       ];
     }
 
-    if (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) {
+    if (
+      currentProject?.simplified_view_enabled
+        ? currentEnvGroup?.linked_applications?.length
+        : currentEnvGroup?.applications?.length
+    ) {
       return [
         { value: "variables-editor", label: "Environment variables" },
         { value: "applications", label: "Linked applications" },
@@ -175,11 +118,7 @@ export const ExpandedEnvGroupFC = ({
     ];
   }, [currentEnvGroup]);
   const populateEnvGroup = async () => {
-
-    // apply v2 already supplies the full env group
-    if (currentProject?.validate_apply_v2) {
-      updateEnvGroup(envGroup);
-    } else if (currentProject?.simplified_view_enabled) {
+    if (currentProject?.simplified_view_enabled) {
       try {
         const populatedEnvGroup = await api
           .getAllEnvGroups(
@@ -191,7 +130,9 @@ export const ExpandedEnvGroupFC = ({
             }
           )
           .then((res) => res.data.environment_groups);
-        updateEnvGroup(populatedEnvGroup.find((i: any) => i.name === envGroup.name));
+        updateEnvGroup(
+          populatedEnvGroup.find((i: any) => i.name === envGroup.name)
+        );
       } catch (error) {
         console.log(error);
       }
@@ -217,8 +158,6 @@ export const ExpandedEnvGroupFC = ({
   };
 
   const updateEnvGroup = (populatedEnvGroup: NewPopulatedEnvGroup) => {
-
-
     if (currentProject?.simplified_view_enabled) {
       const normal_variables: KeyValueType[] = Object.entries(
         populatedEnvGroup.variables || {}
@@ -240,7 +179,6 @@ export const ExpandedEnvGroupFC = ({
       }));
       const variables = [...normal_variables, ...secret_variables];
 
-
       setOriginalEnvVars(
         Object.entries({
           ...(populatedEnvGroup?.variables || {}),
@@ -255,7 +193,6 @@ export const ExpandedEnvGroupFC = ({
         ...populatedEnvGroup,
         variables,
       });
-
     } else {
       const variables: KeyValueType[] = Object.entries(
         populatedEnvGroup.variables || {}
@@ -268,17 +205,18 @@ export const ExpandedEnvGroupFC = ({
       }));
 
       setOriginalEnvVars(
-        Object.entries(populatedEnvGroup?.variables || {}).map(([key, value]) => ({
-          key,
-          value,
-        }))
+        Object.entries(populatedEnvGroup?.variables || {}).map(
+          ([key, value]) => ({
+            key,
+            value,
+          })
+        )
       );
 
       setCurrentEnvGroup({
         ...populatedEnvGroup,
         variables,
       });
-
     }
   };
 
@@ -312,7 +250,6 @@ export const ExpandedEnvGroupFC = ({
       );
     }
 
-
     return await api.deleteEnvGroup(
       "<token>",
       {
@@ -340,228 +277,14 @@ export const ExpandedEnvGroupFC = ({
       });
   };
 
-  const getPorterApp = async ({ appName }: { appName: string }) => {
-    try {
-      if (!currentCluster || !currentProject) {
-        return;
-      }
-      const resPorterApp = await api.getPorterApp(
-        "<token>",
-        {},
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-          name: appName,
-        }
-      );
-      const resChartData = await api.getChart(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          namespace: `porter-stack-${appName}`,
-          cluster_id: currentCluster.id,
-          name: appName,
-          revision: 0,
-        }
-      );
-
-      let preDeployChartData;
-      // get the pre-deploy chart
-      try {
-        preDeployChartData = await api.getChart(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            namespace: `porter-stack-${appName}`,
-            cluster_id: currentCluster.id,
-            name: `${appName}-r`,
-            // this is always latest because we do not tie the pre-deploy chart to the umbrella chart
-            revision: 0,
-          }
-        );
-      } catch (err) {
-        // that's ok if there's an error, just means there is no pre-deploy chart
-      }
-
-      // update apps and release
-      const newAppData = {
-        app: resPorterApp?.data,
-        chart: resChartData?.data,
-        releaseChart: preDeployChartData?.data,
-      };
-      const porterJson = await fetchPorterYamlContent(
-        resPorterApp?.data?.porter_yaml_path ?? "porter.yaml",
-        newAppData
-      );
-
-      let filteredEnvGroups: NewPopulatedEnvGroup[] = []
-      filteredEnvGroups = allEnvGroups?.filter(envGroup =>
-        envGroup.linked_applications && envGroup.linked_applications.includes(appName)
-      );
-
-      const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] };
-      const buildView = !_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks"
-
-      const [newServices, newEnvVars] = updateServicesAndEnvVariables(
-        resChartData?.data,
-        preDeployChartData?.data,
-        porterJson,
-      );
-      const finalPorterYaml = createFinalPorterYaml(
-        newServices,
-        newEnvVars,
-        porterJson,
-        // if we are using a heroku buildpack, inject a PORT env variable
-        newAppData.app.builder?.includes("heroku")
-      );
-
-
-      // Only check GHA status if no built image is set
-      const hasBuiltImage = !!resChartData.data.config?.global?.image
-        ?.repository;
-      if (hasBuiltImage || !resPorterApp.data.repo_name) {
-        setWorkflowCheckPassed(true);
-        setHasBuiltImage(true);
-      } else {
-        try {
-          await api.getBranchContents(
-            "<token>",
-            {
-              dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`,
-            },
-            {
-              project_id: currentProject.id,
-              git_repo_id: resPorterApp.data.git_repo_id,
-              kind: "github",
-              owner: resPorterApp.data.repo_name.split("/")[0],
-              name: resPorterApp.data.repo_name.split("/")[1],
-              branch: resPorterApp.data.git_branch,
-            }
-          );
-          setWorkflowCheckPassed(true);
-
-        } catch (err) {
-          // Handle unmerged PR
-          if (err.response?.status === 404) {
-            try {
-              // Check for user-copied porter.yml as fallback
-              const resPorterYml = await api.getBranchContents(
-                "<token>",
-                { dir: `./.github/workflows/porter.yml` },
-                {
-                  project_id: currentProject.id,
-                  git_repo_id: resPorterApp.data.git_repo_id,
-                  kind: "github",
-                  owner: resPorterApp.data.repo_name.split("/")[0],
-                  name: resPorterApp.data.repo_name.split("/")[1],
-                  branch: resPorterApp.data.git_branch,
-                }
-              );
-              setWorkflowCheckPassed(true);
-            } catch (err) {
-              setWorkflowCheckPassed(false);
-            }
-          }
-        }
-      }
-
-      if (
-        currentCluster != null &&
-        currentProject != null
-      ) {
-
-        const yamlString = yaml.dump(finalPorterYaml);
-        const base64Encoded = btoa(yamlString);
-
-        const updatedPorterApp = {
-          porter_yaml: base64Encoded,
-          override_release: true,
-          ...PorterApp.empty(),
-          build_context: newAppData?.build_context,
-          repo_name: newAppData?.repo_name,
-          git_branch: newAppData?.git_branch,
-          buildpacks: "",
-          // full_helm_values: yaml.dump(values),
-          environment_groups: filteredEnvGroups?.map((env) => env.name),
-          user_update: true,
-        }
-
-        if (buildView === "docker") {
-          updatedPorterApp.dockerfile = newAppData?.dockerfile;
-          updatedPorterApp.builder = "null";
-          updatedPorterApp.buildpacks = "null";
-        } else {
-          updatedPorterApp.builder = newAppData?.builder;
-          updatedPorterApp.buildpacks = newAppData?.buildpacks?.join(",");
-          updatedPorterApp.dockerfile = "null";
-        }
-
-        await api.createPorterApp(
-          "<token>",
-          updatedPorterApp,
-          {
-            cluster_id: currentCluster.id,
-            project_id: currentProject.id,
-            stack_name: appName,
-          }
-        );
-      } else {
-        setButtonStatus("error");
-      }
-    } catch (err) {
-      // TODO: handle error
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
-  const updateServicesAndEnvVariables = (
-    currentChart?: ChartType,
-    releaseChart?: ChartType,
-    porterJson?: PorterJson,
-  ): [Service[], KeyValueType[]] => {
-    // handle normal chart
-    const helmValues = currentChart?.config;
-    const defaultValues = (currentChart?.chart as any)?.values;
-    let newServices: Service[] = [];
-    let envVars: KeyValueType[] = [];
-
-    if (
-      (defaultValues && Object.keys(defaultValues).length > 0) ||
-      (helmValues && Object.keys(helmValues).length > 0)
-    ) {
-      newServices = Service.deserialize(helmValues, defaultValues, porterJson);
-      const { global, ...helmValuesWithoutGlobal } = helmValues;
-      if (Object.keys(helmValuesWithoutGlobal).length > 0) {
-        envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal);
-        setEnvVars(envVars);
-        const subdomain = Service.retrieveSubdomainFromHelmValues(
-          newServices,
-          helmValuesWithoutGlobal
-        );
-        setSubdomain(subdomain);
-      }
-    }
-
-    // handle release chart
-    if (releaseChart?.config || porterJson?.release) {
-      const release = Service.deserializeRelease(releaseChart?.config, porterJson);
-      newServices.push(release);
-    }
-
-    setServices(newServices);
-
-    return [newServices, envVars];
-  };
-
   const handleUpdateValues = async () => {
     setButtonStatus("loading");
     const name = currentEnvGroup.name;
     const variables = currentEnvGroup?.variables;
-    if (currentEnvGroup.meta_version === 2 || currentProject?.simplified_view_enabled) {
-
+    if (
+      currentEnvGroup.meta_version === 2 ||
+      currentProject?.simplified_view_enabled
+    ) {
       const secretVariables = remove(variables, (envVar) => {
         return !envVar.value.includes("PORTERSECRET") && envVar.hidden;
       }).reduce(
@@ -582,7 +305,6 @@ export const ExpandedEnvGroupFC = ({
 
       if (currentProject?.simplified_view_enabled) {
         try {
-
           const normal_variables: KeyValueType[] = Object.entries(
             normalVariables || {}
           ).map(([key, value]) => ({
@@ -604,68 +326,69 @@ export const ExpandedEnvGroupFC = ({
           }));
           const variables = [...normal_variables, ...secret_variables];
 
-
           setCurrentEnvGroup({
             ...currentEnvGroup,
             variables,
           });
 
-
           const linkedApp: string[] = currentEnvGroup?.linked_applications;
           // doppler env groups update themselves, and we don't want to increment the version
-          if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") {
+          if (
+            currentEnvGroup?.type !== "doppler" &&
+            currentEnvGroup.type !== "infisical"
+          ) {
             await api.createEnvironmentGroups(
-                "<token>",
-                {
-                  name,
-                  variables: normalVariables,
-                  secret_variables: secretVariables,
-                },
-                {
-                  id: currentProject.id,
-                  cluster_id: currentCluster.id,
-                }
+              "<token>",
+              {
+                name,
+                variables: normalVariables,
+                secret_variables: secretVariables,
+              },
+              {
+                id: currentProject.id,
+                cluster_id: currentCluster.id,
+              }
             );
           }
-          if (!currentProject.validate_apply_v2) {
-            if (linkedApp) {
-              const promises = linkedApp.map(async appName => {
-                if (!currentProject.validate_apply_v2) {
-                  await getPorterApp({ appName });
-                }
-              });
-              await Promise.all(promises);
-            }
-          } else {
-            try {
-              const res = await api.updateAppsLinkedToEnvironmentGroup(
-                "<token>",
-                {
-                  name: currentEnvGroup?.name,
-                },
-                {
-                  id: currentProject.id,
-                  cluster_id: currentCluster.id,
-                }
-              )
-            } catch (error) {
-              setCurrentError(error);
-            }
+
+          try {
+            const res = await api.updateAppsLinkedToEnvironmentGroup(
+              "<token>",
+              {
+                name: currentEnvGroup?.name,
+              },
+              {
+                id: currentProject.id,
+                cluster_id: currentCluster.id,
+              }
+            );
+          } catch (error) {
+            setCurrentError(error);
           }
 
-          const populatedEnvGroup = await api.getAllEnvGroups("<token>", {}, {
-            id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }).then(res => res.data.environment_groups);
+          const populatedEnvGroup = await api
+            .getAllEnvGroups(
+              "<token>",
+              {},
+              {
+                id: currentProject.id,
+                cluster_id: currentCluster.id,
+              }
+            )
+            .then((res) => res.data.environment_groups);
 
-          const newEnvGroup = populatedEnvGroup.find((i: any) => i.name === name);
+          const newEnvGroup = populatedEnvGroup.find(
+            (i: any) => i.name === name
+          );
 
           updateEnvGroup(newEnvGroup);
           setButtonStatus("successful");
         } catch (error) {
           setButtonStatus("Couldn't update successfully");
           setCurrentError(error);
-          setTimeout(() => { setButtonStatus(""); }, 1000);
+          setTimeout(() => {
+            setButtonStatus("");
+          }, 1000);
         }
       } else {
         try {
@@ -689,16 +412,18 @@ export const ExpandedEnvGroupFC = ({
           }
           updateEnvGroup(updatedEnvGroup);
 
-          setTimeout(() => { setButtonStatus(""); }, 1000);
-        }
-        catch (error) {
+          setTimeout(() => {
+            setButtonStatus("");
+          }, 1000);
+        } catch (error) {
           setButtonStatus("Couldn't update successfully");
           setCurrentError(error);
-          setTimeout(() => { setButtonStatus(""); }, 1000);
+          setTimeout(() => {
+            setButtonStatus("");
+          }, 1000);
         }
       }
-    }
-    else {
+    } else {
       // SEPARATE THE TWO KINDS OF VARIABLES
       let secret = variables.filter(
         (variable) =>
@@ -815,11 +540,15 @@ export const ExpandedEnvGroupFC = ({
           .then((res) => res.data);
         setButtonStatus("successful");
         updateEnvGroup(updatedEnvGroup);
-        setTimeout(() => { setButtonStatus(""); }, 1000);
+        setTimeout(() => {
+          setButtonStatus("");
+        }, 1000);
       } catch (error) {
         setButtonStatus("Couldn't update successfully");
         setCurrentError(error);
-        setTimeout(() => { setButtonStatus(""); }, 1000);
+        setTimeout(() => {
+          setButtonStatus("");
+        }, 1000);
       }
     }
   };
@@ -833,8 +562,9 @@ export const ExpandedEnvGroupFC = ({
       case "variables-editor":
         return (
           <EnvGroupVariablesEditor
-            onChange={(x) => { setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); }
-            }
+            onChange={(x) => {
+              setCurrentEnvGroup((prev) => ({ ...prev, variables: x }));
+            }}
             handleUpdateValues={handleUpdateValues}
             variables={variables}
             buttonStatus={buttonStatus}
@@ -873,9 +603,17 @@ export const ExpandedEnvGroupFC = ({
       <HeaderWrapper>
         <TitleSection icon={key} iconWidth="33px">
           {envGroup.name}
-          {!currentProject?.simplified_view_enabled && <TagWrapper>
-            Namespace <NamespaceTag>{currentProject?.capi_provisioner_enabled && namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace}</NamespaceTag>
-          </TagWrapper>}
+          {!currentProject?.simplified_view_enabled && (
+            <TagWrapper>
+              Namespace{" "}
+              <NamespaceTag>
+                {currentProject?.capi_provisioner_enabled &&
+                namespace.startsWith("porter-stack-")
+                  ? namespace.replace("porter-stack-", "")
+                  : namespace}
+              </NamespaceTag>
+            </TagWrapper>
+          )}
         </TitleSection>
       </HeaderWrapper>
 
@@ -896,7 +634,9 @@ export const ExpandedEnvGroupFC = ({
       ) : (
         <TabRegion
           currentTab={currentTab}
-          setCurrentTab={(x: string) => { setCurrentTab(x); }}
+          setCurrentTab={(x: string) => {
+            setCurrentTab(x);
+          }}
           options={tabOptions}
           color={null}
         >
@@ -923,7 +663,7 @@ const EnvGroupVariablesEditor = ({
   setButtonStatus: (status: string) => void;
 }) => {
   const [isAuthorized] = useAuth();
-  const [buttonDisabled, setButtonDisabled] = useState(false)
+  const [buttonDisabled, setButtonDisabled] = useState(false);
 
   return (
     <TabWrapper>
@@ -954,7 +694,9 @@ const EnvGroupVariablesEditor = ({
       {isAuthorized("env_group", "", ["get", "update"]) && (
         <SaveButton
           text="Update"
-          onClick={() => { handleUpdateValues(); }}
+          onClick={() => {
+            handleUpdateValues();
+          }}
           status={buttonStatus}
           disabled={buttonStatus == "loading" || buttonDisabled}
           makeFlush={true}
@@ -975,19 +717,13 @@ const EnvGroupSettings = ({
   handleDeleteEnvGroup: () => void;
   namespace?: string;
 }) => {
-  const {
-    setCurrentOverlay,
-    currentProject,
-    currentCluster,
-    setCurrentError,
-  } = useContext(Context);
+  const { setCurrentOverlay, currentProject, currentCluster, setCurrentError } =
+    useContext(Context);
   const [isAuthorized] = useAuth();
 
   // When cloning an env group, append "-2" for the default name
   // (i.e. my-env-group-2)
-  const [name, setName] = useState<string>(
-    envGroup.name + "-2"
-  );
+  const [name, setName] = useState<string>(envGroup.name + "-2");
   const [cloneNamespace, setCloneNamespace] = useState<string>("default");
   const [cloneSuccess, setCloneSuccess] = useState(false);
 
@@ -1053,7 +789,9 @@ const EnvGroupSettings = ({
               setCurrentOverlay({
                 message: `Are you sure you want to delete ${envGroup.name}?`,
                 onYes: handleDeleteEnvGroup,
-                onNo: () => { setCurrentOverlay(null); },
+                onNo: () => {
+                  setCurrentOverlay(null);
+                },
               });
             }}
             disabled={!canDelete}
@@ -1070,14 +808,18 @@ const EnvGroupSettings = ({
               <InputRow
                 type="string"
                 value={name}
-                setValue={(x: string) => { setName(x); }}
+                setValue={(x: string) => {
+                  setName(x);
+                }}
                 label="New env group name"
                 placeholder="ex: my-cloned-env-group"
               />
               <InputRow
                 type="string"
                 value={cloneNamespace}
-                setValue={(x: string) => { setCloneNamespace(x); }}
+                setValue={(x: string) => {
+                  setCloneNamespace(x);
+                }}
                 label="New env group namespace"
                 placeholder="ex: default"
               />
@@ -1112,69 +854,69 @@ const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => {
           disableMargin
         />
       </HeadingWrapper>
-      {currentProject?.simplified_view_enabled ? (
-        envGroup.linked_applications.map((appName) => {
-          return (
-            <StyledCard>
-              <Flex>
-                <ContentContainer>
-                  <EventInformation>
-                    <EventName>{appName}</EventName>
-                  </EventInformation>
-                </ContentContainer>
-                <ActionContainer>
-                  {currentProject?.simplified_view_enabled ? (
-                    <ActionButton
-                      to={`/apps/${appName}`}
-                      target="_blank"
-                    >
-                      <span className="material-icons-outlined">open_in_new</span>
-                    </ActionButton>
-                  ) : (
-                    <ActionButton
-                      to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
-                      target="_blank"
-                    >
-                      <span className="material-icons-outlined">open_in_new</span>
-                    </ActionButton>
-                  )}
-                </ActionContainer>
-              </Flex>
-            </StyledCard>
-          );
-        })
-      ) : (
-        envGroup.applications.map((appName) => {
-          return (
-            <StyledCard>
-              <Flex>
-                <ContentContainer>
-                  <EventInformation>
-                    <EventName>{appName}</EventName>
-                  </EventInformation>
-                </ContentContainer>
-                <ActionContainer>
-                  {currentProject?.simplified_view_enabled ? (
-                    <ActionButton
-                      to={`/apps/${appName}`}
-                      target="_blank"
-                    >
-                      <span className="material-icons-outlined">open_in_new</span>
-                    </ActionButton>
-                  ) : (
-                    <ActionButton
-                      to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
-                      target="_blank"
-                    >
-                      <span className="material-icons-outlined">open_in_new</span>
-                    </ActionButton>
-                  )}
-                </ActionContainer>
-              </Flex>
-            </StyledCard>
-          );
-        })
-      )}
+      {currentProject?.simplified_view_enabled
+        ? envGroup.linked_applications.map((appName) => {
+            return (
+              <StyledCard>
+                <Flex>
+                  <ContentContainer>
+                    <EventInformation>
+                      <EventName>{appName}</EventName>
+                    </EventInformation>
+                  </ContentContainer>
+                  <ActionContainer>
+                    {currentProject?.simplified_view_enabled ? (
+                      <ActionButton to={`/apps/${appName}`} target="_blank">
+                        <span className="material-icons-outlined">
+                          open_in_new
+                        </span>
+                      </ActionButton>
+                    ) : (
+                      <ActionButton
+                        to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
+                        target="_blank"
+                      >
+                        <span className="material-icons-outlined">
+                          open_in_new
+                        </span>
+                      </ActionButton>
+                    )}
+                  </ActionContainer>
+                </Flex>
+              </StyledCard>
+            );
+          })
+        : envGroup.applications.map((appName) => {
+            return (
+              <StyledCard>
+                <Flex>
+                  <ContentContainer>
+                    <EventInformation>
+                      <EventName>{appName}</EventName>
+                    </EventInformation>
+                  </ContentContainer>
+                  <ActionContainer>
+                    {currentProject?.simplified_view_enabled ? (
+                      <ActionButton to={`/apps/${appName}`} target="_blank">
+                        <span className="material-icons-outlined">
+                          open_in_new
+                        </span>
+                      </ActionButton>
+                    ) : (
+                      <ActionButton
+                        to={`/applications/${currentCluster.name}/${envGroup.namespace}/${appName}`}
+                        target="_blank"
+                      >
+                        <span className="material-icons-outlined">
+                          open_in_new
+                        </span>
+                      </ActionButton>
+                    )}
+                  </ActionContainer>
+                </Flex>
+              </StyledCard>
+            );
+          })}
     </>
   );
 };

+ 1 - 4
dashboard/src/main/home/modals/LoadEnvGroupModal.tsx

@@ -9,10 +9,6 @@ import { Context } from "shared/Context";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import { KeyValue } from "components/form-components/KeyValueArray";
-import {
-  EnvGroupData,
-  formattedEnvironmentValue,
-} from "../cluster-dashboard/env-groups/EnvGroup";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import {
   PartialEnvGroup,
@@ -21,6 +17,7 @@ import {
 import Helper from "components/form-components/Helper";
 import DocsHelper from "components/DocsHelper";
 import { isEmpty, isObject } from "lodash";
+import { formattedEnvironmentValue } from "../env-dashboard/EnvGroup";
 
 type PropsType = {
   namespace: string;

+ 13 - 21
dashboard/src/main/home/sidebar/ProjectButton.tsx

@@ -3,7 +3,6 @@ import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 
 import Spacer from "components/porter/Spacer";
-import Tooltip from "components/porter/Tooltip";
 
 import { Context } from "shared/Context";
 import { pushFiltered } from "shared/routing";
@@ -59,27 +58,20 @@ const ProjectButton: React.FC<PropsType> = (props) => {
         )}
 
         {user.isPorterUser && currentProject.simplified_view_enabled ? (
-          <Tooltip
-            content={`Porter Apps ${
-              currentProject.validate_apply_v2 ? "V2" : "V1"
-            }`}
-            position="right"
+          <MainSelector
+            projectsLength={props.projects.length}
+            isPorterUser={user.isPorterUser}
+            onClick={() => {
+              (props.projects.length > 1 || user.isPorterUser) &&
+                setShowModal(true);
+            }}
           >
-            <MainSelector
-              projectsLength={props.projects.length}
-              isPorterUser={user.isPorterUser}
-              onClick={() => {
-                (props.projects.length > 1 || user.isPorterUser) &&
-                  setShowModal(true);
-              }}
-            >
-              <ProjectIcon>
-                <ProjectImage src={gradient} />
-                <Letter>{currentProject.name[0].toUpperCase()}</Letter>
-              </ProjectIcon>
-              <ProjectName>{currentProject.name}</ProjectName>
-            </MainSelector>
-          </Tooltip>
+            <ProjectIcon>
+              <ProjectImage src={gradient} />
+              <Letter>{currentProject.name[0].toUpperCase()}</Letter>
+            </ProjectIcon>
+            <ProjectName>{currentProject.name}</ProjectName>
+          </MainSelector>
         ) : (
           <MainSelector
             projectsLength={props.projects.length}

+ 1 - 3
dashboard/src/shared/util.ts

@@ -19,9 +19,7 @@ export function envGroupPath(
   project: ProjectType | undefined,
   subPath: string
 ): string {
-  return project?.simplified_view_enabled && !project?.validate_apply_v2
-    ? `/env-groups${subPath}`
-    : `/environment-groups${subPath}`;
+  return `/environment-groups${subPath}`;
 }
 
 export const PREFLIGHT_MESSAGE_CONST = {