jusrhee пре 3 година
родитељ
комит
d31742446c
26 измењених фајлова са 1031 додато и 469 уклоњено
  1. 43 1
      api/server/handlers/stacks/create_porter_app.go
  2. 5 0
      api/server/handlers/stacks/parse.go
  3. 0 117
      api/server/handlers/stacks/update.go
  4. 1 1
      api/types/incident.go
  5. 2 0
      dashboard/src/components/repo-selector/ActionConfBranchSelector.tsx
  6. 3 1
      dashboard/src/components/repo-selector/ActionConfEditorStack.tsx
  7. 3 7
      dashboard/src/components/repo-selector/BuildpackStack.tsx
  8. 6 3
      dashboard/src/components/repo-selector/DetectContentsList.tsx
  9. 39 55
      dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx
  10. 23 11
      dashboard/src/main/home/app-dashboard/expanded-app/EventsTab.tsx
  11. 6 5
      dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx
  12. 295 80
      dashboard/src/main/home/app-dashboard/expanded-app/LogSection.tsx
  13. 8 0
      dashboard/src/main/home/app-dashboard/expanded-app/SharedBuildSettings.tsx
  14. 4 3
      dashboard/src/main/home/app-dashboard/expanded-app/useAgentLogs.ts
  15. 14 17
      dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx
  16. 24 4
      dashboard/src/main/home/app-dashboard/new-app-flow/GithubConnectModal.tsx
  17. 117 54
      dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx
  18. 333 41
      dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx
  19. 7 3
      dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx
  20. 6 0
      dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx
  21. 4 20
      dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx
  22. 38 44
      dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts
  23. 26 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts
  24. 1 1
      dashboard/src/shared/api.tsx
  25. 18 0
      dashboard/src/shared/themes/opal.ts
  26. 5 1
      internal/kubernetes/porter_agent/v2/agent_server.go

+ 43 - 1
api/server/handlers/stacks/create_porter_app.go

@@ -115,6 +115,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			Registries: registries,
 		}
 
+		// create the chart
 		_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
@@ -153,6 +154,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			PullRequestURL: request.PullRequestURL,
 		}
 
+		// create the db entry
 		porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing app to DB: %s", err.Error())))
@@ -190,6 +192,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			Registries: registries,
 		}
 
+		// update the chart
 		_, err = helmAgent.UpgradeInstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
@@ -197,12 +200,51 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 		}
 
+		// update the DB entry
 		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
 
-		c.WriteResult(w, r, app.ToPorterAppType())
+		if request.RepoName != "" {
+			app.RepoName = request.RepoName
+		}
+		if request.GitBranch != "" {
+			app.GitBranch = request.GitBranch
+		}
+		if request.BuildContext != "" {
+			app.BuildContext = request.BuildContext
+		}
+		if request.Builder != "" {
+			app.Builder = request.Builder
+		}
+		if request.Buildpacks != "" {
+			if request.Buildpacks == "null" {
+				app.Buildpacks = ""
+			} else {
+				app.Buildpacks = request.Buildpacks
+			}
+		}
+		if request.Dockerfile != "" {
+			if request.Dockerfile == "null" {
+				app.Dockerfile = ""
+			} else {
+				app.Dockerfile = request.Dockerfile
+			}
+		}
+		if request.ImageRepoURI != "" {
+			app.ImageRepoURI = request.ImageRepoURI
+		}
+		if request.PullRequestURL != "" {
+			app.PullRequestURL = request.PullRequestURL
+		}
+
+		updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
+		if err != nil {
+			return
+		}
+
+		c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
 	}
 }

+ 5 - 0
api/server/handlers/stacks/parse.go

@@ -328,6 +328,11 @@ func createSubdomainIfRequired(
 			customDomain, cOK := customDomVal.(bool)
 
 			if eOK && cOK && enabled && !customDomain {
+				// subdomain already exists, no need to create one
+				if porterHosts, ok := ingressMap["porter_hosts"].([]interface{}); ok && len(porterHosts) > 0 {
+					return nil
+				}
+
 				// in the case of ingress enabled but no custom domain, create subdomain
 				dnsRecord, err := createDNSRecord(opts)
 				if err != nil {

+ 0 - 117
api/server/handlers/stacks/update.go

@@ -1,117 +0,0 @@
-package stacks
-
-import (
-	"encoding/base64"
-	"fmt"
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/helm"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type UpdateStackHandler struct {
-	handlers.PorterHandlerReadWriter
-	authz.KubernetesAgentGetter
-}
-
-func NewUpdateStackHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *UpdateStackHandler {
-	return &UpdateStackHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
-	}
-}
-
-func (c *UpdateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
-	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-
-	request := &types.CreateStackReleaseRequest{}
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
-		return
-	}
-
-	stackName := request.StackName
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
-
-	helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
-		return
-	}
-
-	helmRelease, _ := helmAgent.GetRelease(stackName, 0, false)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting latest release: %w", err)))
-		return
-	}
-
-	k8sAgent, err := c.GetAgent(r, cluster, namespace)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
-		return
-	}
-
-	porterYamlBase64 := request.PorterYAMLBase64
-	porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding porter yaml: %w", err)))
-		return
-	}
-	imageInfo := request.ImageInfo
-	chart, values, err := parse(
-		porterYaml,
-		imageInfo,
-		c.Config(),
-		cluster.ProjectID,
-		helmRelease.Config,
-		helmRelease.Chart.Metadata.Dependencies,
-		SubdomainCreateOpts{
-			k8sAgent:       k8sAgent,
-			dnsRepo:        c.Repo().DNSRecord(),
-			powerDnsClient: c.Config().PowerDNSClient,
-			appRootDomain:  c.Config().ServerConf.AppRootDomain,
-			stackName:      stackName,
-		})
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter yaml into chart and values: %w", err)))
-		return
-	}
-
-	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
-		return
-	}
-
-	conf := &helm.InstallChartConfig{
-		Chart:      chart,
-		Name:       stackName,
-		Namespace:  namespace,
-		Values:     values,
-		Cluster:    cluster,
-		Repo:       c.Repo(),
-		Registries: registries,
-	}
-
-	_, err = helmAgent.UpgradeInstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error updating a chart: %s", err.Error()),
-			http.StatusBadRequest,
-		))
-
-		return
-	}
-	w.WriteHeader(http.StatusCreated)
-}

+ 1 - 1
api/types/incident.go

@@ -107,7 +107,7 @@ type GetLogRequest struct {
 	SearchParam string     `schema:"search_param"`
 	Revision    string     `schema:"revision"`
 	PodSelector string     `schema:"pod_selector" form:"required"`
-	Namespace   string     `schema:"namespace" form:"required"`
+	Namespace   string     `schema:"namespace"`
 	Direction   string     `schema:"direction"`
 }
 

+ 2 - 0
dashboard/src/components/repo-selector/ActionConfBranchSelector.tsx

@@ -18,6 +18,7 @@ type Props = {
   setDockerfilePath: (x: string) => void;
 
   setFolderPath: (x: string) => void;
+  setBuildView?: (x: string) => void;
 };
 
 const ActionConfEditorStack: React.FC<Props> = (props) => {
@@ -63,6 +64,7 @@ const ActionConfEditorStack: React.FC<Props> = (props) => {
           props.setFolderPath("");
           props.setDockerfilePath("");
           props.setActionConfig(actionConfig);
+          props.setBuildView("buildpacks");
         }}
       >
         <i className="material-icons">keyboard_backspace</i>

+ 3 - 1
dashboard/src/components/repo-selector/ActionConfEditorStack.tsx

@@ -12,6 +12,7 @@ type Props = {
   setBranch: (x: string) => void;
   setDockerfilePath: (x: string) => void;
   setFolderPath: (x: string) => void;
+  setBuildView?: (x: string) => void;
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -28,8 +29,8 @@ const ActionConfEditorStack: React.FC<Props> = ({
   setActionConfig,
   setFolderPath,
   setDockerfilePath,
+  setBuildView,
 }) => {
-
   if (!actionConfig.git_repo) {
     return (
       <ExpandedWrapperAlt>
@@ -57,6 +58,7 @@ const ActionConfEditorStack: React.FC<Props> = ({
             setBranch("");
             setFolderPath("");
             setDockerfilePath("");
+            setBuildView("buildpacks");
           }}
         >
           <i className="material-icons">keyboard_backspace</i>

+ 3 - 7
dashboard/src/components/repo-selector/BuildpackStack.tsx

@@ -115,7 +115,6 @@ export const BuildpackStack: React.FC<{
   };
   useEffect(() => {
     let buildConfig: BuildConfig = {} as BuildConfig;
-
     buildConfig.builder = selectedStack;
     buildConfig.buildpacks = selectedBuildpacks?.map((buildpack) => {
       return buildpack.buildpack;
@@ -177,7 +176,7 @@ export const BuildpackStack: React.FC<{
         var detectedBuildpacks = defaultBuilder.detected;
         var availableBuildpacks = defaultBuilder.others;
         var defaultStack = "";
-        if (currentBuildConfig) {
+        if (currentBuildConfig && currentBuildConfig.buildpacks.length != 0) {
           if (!detectedBuildpacks) {
             detectedBuildpacks = [];
           }
@@ -268,8 +267,6 @@ export const BuildpackStack: React.FC<{
       }));
     });
   }, [builders]);
-
-  // const handleSelectBuilder = (builderName: string) => {
   //   const builder = builders.find(
   //     (b) => b.name.toLowerCase() === builderName.toLowerCase()
   //   );
@@ -487,7 +484,7 @@ const Shade = styled.div`
   background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
 `;
 
-const Footer = styled.div` 
+const Footer = styled.div`
   position: relative;
   width: calc(100% + 50px);
   margin-left: -25px;
@@ -496,8 +493,7 @@ const Footer = styled.div`
   border-bottom-right-radius: 10px;
   background: ${({ theme }) => theme.fg};
   margin-bottom: -30px;
-  padding-bottom: 30px
-
+  padding-bottom: 30px;
 `;
 
 const I = styled.i`

+ 6 - 3
dashboard/src/components/repo-selector/DetectContentsList.tsx

@@ -13,7 +13,6 @@ import Loading from "../Loading";
 import Spacer from "components/porter/Spacer";
 import AdvancedBuildSettings from "main/home/app-dashboard/new-app-flow/AdvancedBuildSettings";
 import { render } from "react-dom";
-import BuildpackConfigSection from "main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection";
 
 interface AutoBuildpack {
   name?: string;
@@ -31,6 +30,8 @@ type PropsType = {
   setFolderPath: (x: string) => void;
   setBuildConfig: (x: any) => void;
   setPorterYaml: (x: any) => void;
+  buildView: string;
+  setBuildView: (x: string) => void;
 };
 
 const DetectContentsList: React.FC<PropsType> = (props) => {
@@ -38,6 +39,7 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
   const [error, setError] = useState(false);
   const [contents, setContents] = useState<FileType[]>([]);
   const [currentDir, setCurrentDir] = useState("");
+
   const [autoBuildpack, setAutoBuildpack] = useState<AutoBuildpack>({
     valid: false,
     name: "",
@@ -75,7 +77,7 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
 
     if (dockerFileItem) {
       props.setDockerfilePath(dockerFileItem.path);
-      setShowingBuildContextPrompt("docker");
+      props.setBuildView("docker");
     }
   }, [contents]);
 
@@ -231,10 +233,11 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
             setBuildConfig={props.setBuildConfig}
             autoBuildPack={autoBuildpack}
             showSettings={false}
-            buildView={props.dockerfilePath ? "dockerfile" : "buildpacks"}
             actionConfig={props.actionConfig}
             branch={props.branch}
             folderPath={props.folderPath}
+            buildView={props.buildView}
+            setBuildView={props.setBuildView}
           />
         </>
       )}

+ 39 - 55
dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx

@@ -46,6 +46,10 @@ type Props = {
   onTabSwitch: () => void;
   updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
 };
+interface AutoBuildpack {
+  name?: string;
+  valid: boolean;
+}
 
 const BuildSettingsTabStack: React.FC<Props> = ({
   appData,
@@ -57,9 +61,11 @@ const BuildSettingsTabStack: React.FC<Props> = ({
   const [updated, setUpdated] = useState(null);
   const [branch, setBranch] = useState(appData.app.git_branch);
   const [showSettings, setShowSettings] = useState(false);
-  const [dockerfilePath, setDockerfilePath] = useState(
-    appData.app.dockerfilePath
+  const [dockerfilePath, setDockerfilePath] = useState(appData.app.dockerfile);
+  const [buildView, setBuildView] = useState<string>(
+    appData.app.dockerfile ? "docker" : "buildpacks"
   );
+
   const [folderPath, setFolderPath] = useState("./");
   const defaultActionConfig: ActionConfigType = {
     git_repo: appData.app.repo_name,
@@ -69,8 +75,12 @@ const BuildSettingsTabStack: React.FC<Props> = ({
     kind: "github",
   };
   const defaultBuildConfig: BuildConfig = {
-    builder: appData.app.builder,
-    buildpacks: appData.app.build_packs?.split(","),
+    builder: appData.app.builder
+      ? appData.app.builder
+      : "paketobuildpacks/builder:full",
+    buildpacks: appData.app.build_packs
+      ? appData.app.build_packs.split(",")
+      : [],
     config: appData.chart.config,
   };
   const [buildConfig, setBuildConfig] = useState<BuildConfig>({
@@ -78,6 +88,10 @@ const BuildSettingsTabStack: React.FC<Props> = ({
   });
   const [redeployOnSave, setRedeployOnSave] = useState(true);
   const [runningWorkflowURL, setRunningWorkflowURL] = useState("");
+  const [autoBuildpack, setAutoBuildpack] = useState<AutoBuildpack>({
+    valid: false,
+    name: "",
+  });
 
   const [actionConfig, setActionConfig] = useState<ActionConfigType>({
     ...defaultActionConfig,
@@ -165,14 +179,19 @@ const BuildSettingsTabStack: React.FC<Props> = ({
   };
   const saveConfig = async () => {
     console.log(appData);
+    console.log(appData.app.dockerfile);
+    console.log(buildView);
     try {
       await updatePorterApp({
         repo_name: appData.app.repo_name,
         git_branch: branch,
         build_context: appData.app.build_context,
         builder: buildConfig.builder,
-        buildpacks: buildConfig.buildpacks?.join(","),
-        dockerfile: appData.app.dockerfile,
+        buildpacks:
+          buildView === "buildpacks"
+            ? buildConfig?.buildpacks?.join(",")
+            : "null",
+        dockerfile: buildView === "buildpacks" ? "null" : dockerfilePath,
         image_repo_uri: appData.chart.image_repo_uri,
       });
       onTabSwitch();
@@ -184,8 +203,6 @@ const BuildSettingsTabStack: React.FC<Props> = ({
     setButtonStatus("loading");
 
     try {
-      console.log(buildConfig.builder);
-
       await saveConfig();
       setAppData(appData);
 
@@ -200,8 +217,6 @@ const BuildSettingsTabStack: React.FC<Props> = ({
     setButtonStatus("loading");
 
     try {
-      console.log(buildConfig.builder);
-
       await saveConfig();
       setAppData(appData);
 
@@ -217,19 +232,6 @@ const BuildSettingsTabStack: React.FC<Props> = ({
   return (
     <>
       <Text size={16}>Build settings</Text>
-      {/* <ActionConfEditorStack
-        actionConfig={actionConfig}
-        setActionConfig={(actionConfig: ActionConfigType) => {
-          setActionConfig((currentActionConfig: ActionConfigType) => ({
-            ...currentActionConfig,
-            ...actionConfig,
-          }));
-          setImageUrl(actionConfig.image_repo_uri);
-        }}
-        setBranch={setBranch}
-        setDockerfilePath={setDockerfilePath}
-        setFolderPath={setFolderPath}
-      /> */}
       <InputRow
         disabled={true}
         label="Git repository"
@@ -253,6 +255,7 @@ const BuildSettingsTabStack: React.FC<Props> = ({
             setBranch={setBranch}
             setDockerfilePath={setDockerfilePath}
             setFolderPath={setFolderPath}
+            setBuildView={setBuildView}
           />
         </>
       )}
@@ -271,38 +274,19 @@ const BuildSettingsTabStack: React.FC<Props> = ({
           />
         </>
       )}
-      <StyledAdvancedBuildSettings
-        showSettings={showSettings}
-        isCurrent={true}
-        onClick={() => {
-          setShowSettings(!showSettings);
-        }}
-      >
-        <AdvancedBuildTitle>
-          <i className="material-icons dropdown">arrow_drop_down</i>
-          Configure buildpack settings
-        </AdvancedBuildTitle>
-      </StyledAdvancedBuildSettings>
-      <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
-        <StyledSourceBox>
-          <Spacer y={0.5} />
-          {actionConfig && (
-            <BuildpackStack
-              actionConfig={actionConfig}
-              branch={branch}
-              folderPath={folderPath}
-              onChange={(config) => {
-                setBuildConfig(config);
-                setDockerfilePath("");
-              }}
-              hide={!showSettings}
-              currentBuildConfig={buildConfig}
-              setBuildConfig={setBuildConfig}
-            />
-          )}
-          <Spacer y={0.5} />
-        </StyledSourceBox>
-      </AnimateHeight>
+      <AdvancedBuildSettings
+        dockerfilePath={dockerfilePath}
+        setDockerfilePath={setDockerfilePath}
+        setBuildConfig={setBuildConfig}
+        autoBuildPack={autoBuildpack}
+        showSettings={false}
+        buildView={buildView}
+        setBuildView={setBuildView}
+        actionConfig={actionConfig}
+        branch={branch}
+        folderPath={folderPath}
+        currentBuildConfig={buildConfig}
+      />
       <Spacer y={1} />
       <Checkbox
         checked={redeployOnSave}

+ 23 - 11
dashboard/src/main/home/app-dashboard/expanded-app/EventsTab.tsx

@@ -4,6 +4,10 @@ import styled from "styled-components";
 import EventList from "./EventList";
 import Loading from "components/Loading";
 import { Context } from "shared/Context";
+import Fieldset from "components/porter/Fieldset";
+import Button from "components/porter/Button";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
 
 type Props = {
   currentChart: any;
@@ -108,23 +112,23 @@ const EventsTab: React.FC<Props> = ({ currentChart }) => {
 
   if (isLoading) {
     return (
-      <Placeholder>
+      <Fieldset>
         <Loading />
-      </Placeholder>
+      </Fieldset>
     );
   }
 
   if (!hasPorterAgent) {
     return (
-      <Placeholder>
-        <div>
-          <Header>We couldn't detect the Porter agent on your cluster</Header>
-          In order to use the events tab, you need to install the Porter agent.
-          <InstallPorterAgentButton onClick={() => triggerInstall()}>
-            <i className="material-icons">add</i> Install Porter agent
-          </InstallPorterAgentButton>
-        </div>
-      </Placeholder>
+      <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 Events 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>
     );
   }
 
@@ -222,3 +226,11 @@ const Header = styled.div`
   font-size: 16px;
   margin-bottom: 15px;
 `;
+
+const I = styled.i`
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-right: 5px;
+  justify-content: center;
+`;

+ 6 - 5
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -173,6 +173,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         newAppData
       );
       setPorterJson(porterJson);
+      console.log(newAppData)
       setAppData(newAppData);
       updateServicesAndEnvVariables(resChartData?.data, porterJson);
     } catch (err) {
@@ -227,9 +228,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           services,
           envVars,
           porterJson,
-          appData.app.name,
-          currentProject.id,
-          currentCluster.id
         );
         const yamlString = yaml.dump(finalPorterYaml);
         const base64Encoded = btoa(yamlString);
@@ -507,6 +505,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                 }
                 setServices(x);
               }}
+              chart={appData.chart}
               services={services} />
             <Spacer y={1} />
             <Button
@@ -707,8 +706,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   appData.app.git_repo_id
                     ? hasBuiltImage
                       ? [
-                        { label: "Logs", value: "logs" },
                         { label: "Overview", value: "overview" },
+                        { label: "Events", value: "events" },
+                        { label: "Logs", value: "logs" },
                         {
                           label: "Environment variables",
                           value: "environment-variables",
@@ -726,8 +726,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                         { label: "Settings", value: "settings" },
                       ]
                     : [
-                      { label: "Logs", value: "logs" },
                       { label: "Overview", value: "overview" },
+                      { label: "Events", value: "events" },
+                      { label: "Logs", value: "logs" },
                       {
                         label: "Environment variables",
                         value: "environment-variables",

+ 295 - 80
dashboard/src/main/home/app-dashboard/expanded-app/LogSection.tsx

@@ -9,6 +9,7 @@ import React, {
 import styled from "styled-components";
 import RadioFilter from "components/RadioFilter";
 
+import spinner from "assets/loading.gif";
 import filterOutline from "assets/filter-outline.svg";
 import time from "assets/time.svg";
 import { Context } from "shared/Context";
@@ -23,6 +24,11 @@ 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";
 
 type Props = {
   currentChart?: ChartType;
@@ -46,6 +52,11 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
   const [selectedDate, setSelectedDate] = useState<Date | undefined>(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 notify = (message: string) => {
     setNotification(message);
 
@@ -54,6 +65,8 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
     }, 5000);
   };
 
+  console.log(podFilter);
+
   const { loading, logs, refresh, moveCursor, paginationInfo } = useLogs(
     podFilter.podName,
     podFilter.podNamespace,
@@ -64,70 +77,70 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
   );
 
   const refreshPodLogsValues = async () => {
-    // const filters = {
-    //   namespace: currentChart.namespace,
-    //   revision: currentChart.version.toString(),
-    //   match_prefix: currentChart.name,
-    // };
-
-    // const logPodValuesResp = await api.getLogPodValues("<TOKEN>", filters, {
-    //   project_id: currentProject.id,
-    //   cluster_id: currentCluster.id,
-    // });
-
-    // if (logPodValuesResp.data?.length != 0) {
-    //   setPodFilterOpts(
-    //     _.uniq(logPodValuesResp.data ?? []).map((podName: any) => {
-    //       return { podName: podName, podNamespace: currentChart.namespace };
-    //     })
-    //   );
-
-    //   // only set pod filter if the current pod is not found in the resulting data
-    //   if (!podFilter || !logPodValuesResp.data?.includes(podFilter)) {
-    //     setPodFilter({
-    //       podName: logPodValuesResp.data[0],
-    //       podNamespace: currentChart.namespace,
-    //     });
-    //   }
-    //   console.log("pod values set chart namespace", podFilter, podFilterOpts);
-    //   return;
-    // }
-
-    // // check if pods are in default namespace
-    // const filters_default = {
-    //   namespace: "default",
-    //   revision: currentChart.version.toString(),
-    //   match_prefix: currentChart.name,
-    // };
-
-    // const logPodValuesResp_default = await api.getLogPodValues(
-    //   "<TOKEN>",
-    //   filters_default,
-    //   {
-    //     project_id: currentProject.id,
-    //     cluster_id: currentCluster.id,
-    //   }
-    // );
-
-    // if (logPodValuesResp_default.data?.length != 0) {
-    //   setPodFilterOpts(
-    //     _.uniq(logPodValuesResp_default.data ?? []).map((podName: any) => {
-    //       return { podName: podName, podNamespace: "default" };
-    //     })
-    //   );
-
-    //   // only set pod filter if the current pod is not found in the resulting data
-    //   if (!podFilter || !logPodValuesResp_default.data?.includes(podFilter)) {
-    //     setPodFilter({
-    //       podName: logPodValuesResp_default.data[0],
-    //       podNamespace: "default",
-    //     });
-    //   }
-    //   console.log("pod values set default", podFilter, podFilterOpts);
-    //   return;
-    // }
-
-    // console.log("pod values empty");
+    const filters = {
+      namespace: currentChart.namespace,
+      revision: currentChart.version.toString(),
+      match_prefix: currentChart.name,
+    };
+
+    const logPodValuesResp = await api.getLogPodValues("<TOKEN>", filters, {
+      project_id: currentProject.id,
+      cluster_id: currentCluster.id,
+    });
+
+    if (logPodValuesResp.data?.length != 0) {
+      setPodFilterOpts(
+        _.uniq(logPodValuesResp.data ?? []).map((podName: any) => {
+          return { podName: podName, podNamespace: currentChart.namespace };
+        })
+      );
+
+      // only set pod filter if the current pod is not found in the resulting data
+      if (!podFilter || !logPodValuesResp.data?.includes(podFilter)) {
+        setPodFilter({
+          podName: logPodValuesResp.data[0],
+          podNamespace: currentChart.namespace,
+        });
+      }
+      console.log("pod values set chart namespace", podFilter, podFilterOpts);
+      return;
+    }
+
+    // check if pods are in default namespace
+    const filters_default = {
+      namespace: "default",
+      revision: currentChart.version.toString(),
+      match_prefix: currentChart.name,
+    };
+
+    const logPodValuesResp_default = await api.getLogPodValues(
+      "<TOKEN>",
+      filters_default,
+      {
+        project_id: currentProject.id,
+        cluster_id: currentCluster.id,
+      }
+    );
+
+    if (logPodValuesResp_default.data?.length != 0) {
+      setPodFilterOpts(
+        _.uniq(logPodValuesResp_default.data ?? []).map((podName: any) => {
+          return { podName: podName, podNamespace: "default" };
+        })
+      );
+
+      // only set pod filter if the current pod is not found in the resulting data
+      if (!podFilter || !logPodValuesResp_default.data?.includes(podFilter)) {
+        setPodFilter({
+          podName: logPodValuesResp_default.data[0],
+          podNamespace: "default",
+        });
+      }
+      console.log("pod values set default", podFilter, podFilterOpts);
+      return;
+    }
+
+    console.log("pod values empty");
 
     // if we're on the latest revision and no pod values were returned, query for all release pods
     if (currentChart.info.status == "deployed") {
@@ -158,10 +171,6 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
     }
   };
 
-  useEffect(() => {
-    refreshPodLogsValues();
-  }, []);
-
   useEffect(() => {
     if (!loading && scrollToBottomRef.current && scrollToBottomEnabled) {
       scrollToBottomRef.current.scrollIntoView({
@@ -250,17 +259,22 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
             />
           </Flex>
           <Flex>
-            <Button onClick={() => setScrollToBottomEnabled((s) => !s)}>
+            <ScrollButton onClick={() => setScrollToBottomEnabled((s) => !s)}>
               <Checkbox checked={scrollToBottomEnabled}>
                 <i className="material-icons">done</i>
               </Checkbox>
               Scroll to bottom
-            </Button>
-            <Spacer />
-            <Button onClick={() => refresh()}>
+            </ScrollButton>
+            <Spacer inline width="10px" />
+            <ScrollButton
+              onClick={() => {
+                refreshPodLogsValues();
+                refresh();
+              }}
+            >
               <i className="material-icons">autorenew</i>
               Refresh
-            </Button>
+            </ScrollButton>
           </Flex>
         </FlexRow>
         <LogsSectionWrapper>
@@ -309,11 +323,149 @@ const LogSection: React.FC<Props> = ({ currentChart }) => {
     );
   };
 
-  return <>{renderContents()}</>;
+  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 = () => {
+    const project_id = currentProject?.id;
+    const cluster_id = currentCluster?.id;
+
+    api
+      .detectPorterAgent("<token>", {}, { project_id, cluster_id })
+      .then((res) => {
+        if (res.data?.version != "v3") {
+          setHasPorterAgent(false);
+        } else {
+          // next, check whether logs can be queried - if they can, we're good to go
+          const filters = {
+            revision: currentChart.version.toString(),
+            match_prefix: currentChart.name,
+          };
+
+          api
+            .getLogPodValues("<TOKEN>", filters, {
+              project_id: currentProject.id,
+              cluster_id: currentCluster.id,
+            })
+            .then((res) => {
+              setHasPorterAgent(true);
+              refreshPodLogsValues();
+              setIsPorterAgentInstalling(false);
+              setIsLoading(false);
+            })
+            .catch((err) => {
+              // do nothing - this is expected while installing
+              setLogsError(err);
+              setIsLoading(false);
+            });
+        }
+      })
+      .catch((err) => {
+        if (err.response?.status === 404) {
+          setHasPorterAgent(false);
+          setIsLoading(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();
+  };
+
+  const getFilters = () => {
+    return {
+      release_name: currentChart.name,
+      release_namespace: currentChart.namespace,
+    };
+  };
+
+  return (
+    isPorterAgentInstalling ? (
+      <Fieldset>
+        <Container row>
+          <Spinner src={spinner} />
+          <Spacer inline x={1} />
+          <Text color="helper">The Porter agent is being installed . . .</Text>
+        </Container>
+      </Fieldset>
+    ) : isLoading ? (
+      <Fieldset>
+        <Loading />
+      </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 BackButton = styled.div`
   display: flex;
   width: 30px;
@@ -399,12 +551,7 @@ const Checkbox = styled.div<{ checked: boolean }>`
   }
 `;
 
-const Spacer = styled.div<{ width?: string }>`
-  height: 100%;
-  width: ${(props) => props.width || "10px"};
-`;
-
-const Button = styled.div`
+const ScrollButton = styled.div`
   background: #26292e;
   border-radius: 5px;
   height: 30px;
@@ -650,3 +797,71 @@ const NotificationWrapper = styled.div<{ active?: boolean }>`
 const LogsSectionWrapper = styled.div`
   position: relative;
 `;
+
+const InstallPorterAgentButton = styled.button`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border: none;
+  border-radius: 5px;
+  color: white;
+  height: 35px;
+  padding: 0px 8px;
+  padding-bottom: 1px;
+  margin-top: 20px;
+  font-weight: 500;
+  padding-right: 15px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#5561C0"};
+  :hover {
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+  }
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const Placeholder = styled.div`
+  padding: 30px;
+  padding-bottom: 40px;
+  font-size: 13px;
+  color: #ffffff44;
+  min-height: 400px;
+  height: 50vh;
+  background: #ffffff08;
+  border-radius: 8px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 18px;
+    margin-right: 8px;
+  }
+`;
+
+const Header = styled.div`
+  font-weight: 500;
+  color: #aaaabb;
+  font-size: 16px;
+  margin-bottom: 15px;
+`;

+ 8 - 0
dashboard/src/main/home/app-dashboard/expanded-app/SharedBuildSettings.tsx

@@ -25,6 +25,8 @@ type Props = {
   setPorterYaml: (x: any) => void;
   imageUrl: string;
   setImageUrl: (x: string) => void;
+  buildView: string;
+  setBuildView: (x: string) => void;
 };
 
 const SharedBuildSettings: React.FC<Props> = ({
@@ -41,6 +43,8 @@ const SharedBuildSettings: React.FC<Props> = ({
   setPorterYaml,
   imageUrl,
   setImageUrl,
+  buildView,
+  setBuildView,
 }) => {
   const [isExpanded, setIsExpanded] = useState(false);
 
@@ -61,6 +65,7 @@ const SharedBuildSettings: React.FC<Props> = ({
         setBranch={setBranch}
         setDockerfilePath={setDockerfilePath}
         setFolderPath={setFolderPath}
+        setBuildView={setBuildView}
       />
       <DarkMatter antiHeight="-4px" />
       <Spacer y={0.3} />
@@ -81,6 +86,7 @@ const SharedBuildSettings: React.FC<Props> = ({
             setBranch={setBranch}
             setDockerfilePath={setDockerfilePath}
             setFolderPath={setFolderPath}
+            setBuildView={setBuildView}
           />
         </>
       )}
@@ -108,6 +114,8 @@ const SharedBuildSettings: React.FC<Props> = ({
             setBuildConfig={setBuildConfig}
             porterYaml={porterYaml}
             setPorterYaml={setPorterYaml}
+            buildView={buildView}
+            setBuildView={setBuildView}
           />
         </>
       )}

+ 4 - 3
dashboard/src/main/home/app-dashboard/expanded-app/useAgentLogs.ts

@@ -79,7 +79,6 @@ export const useLogs = (
   // if setDate is set, results are not live
   setDate?: Date
 ) => {
-  console.log("calling useLogs", currentPod, namespace, searchParam);
   const isLive = !setDate;
   const logsBufferRef = useRef<Log[]>([]);
   const { currentCluster, currentProject, setCurrentError } = useContext(
@@ -190,7 +189,8 @@ export const useLogs = (
 
     const q = new URLSearchParams({
       pod_selector: currentPod,
-      namespace,
+      // TODO: re-enable namespace when we properly install stack apps to namespace
+      // namespace,
       search_param: searchParam,
       revision: currentChart.version.toString(),
     }).toString();
@@ -237,7 +237,8 @@ export const useLogs = (
         "<token>",
         {
           pod_selector: currentPod,
-          namespace,
+          // TODO: re-enable namespace when we properly install stack apps to namespace
+          // namespace,
           revision: currentChart.version.toString(),
           search_param: searchParam,
           start_range: startDate,

+ 14 - 17
dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx

@@ -25,7 +25,9 @@ interface AdvancedBuildSettingsProps {
   folderPath: string;
   dockerfilePath?: string;
   setDockerfilePath: (x: string) => void;
-  setBuildConfig: (x: any) => void;
+  setBuildConfig?: (x: any) => void;
+  currentBuildConfig?: BuildConfig;
+  setBuildView: (x: string) => void;
 }
 
 type Buildpack = {
@@ -38,19 +40,11 @@ type Buildpack = {
 
 const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
   const [showSettings, setShowSettings] = useState<boolean>(props.showSettings);
-  const [buildView, setBuildView] = useState<string>(props.buildView);
+  const buildView = props.setBuildView(props.buildView || "buildpacks");
 
-  const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    setBuildView(e.target.value);
-  };
-  useEffect(() => {
-    if (props.dockerfilePath && props.dockerfilePath != "") {
-      setBuildView("docker");
-    } else {
-      setBuildView("buildpacks");
-    }
-  }, [props.dockerfilePath]);
+  useEffect(() => {}, [props.buildView]);
   const createDockerView = () => {
+    // props.setBuildConfig({});
     return (
       <>
         <Text color="helper">Dockerfile path</Text>
@@ -75,9 +69,10 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
           folderPath={props.folderPath}
           onChange={(config) => {
             props.setBuildConfig(config);
-            props.setDockerfilePath("");
           }}
           hide={false}
+          currentBuildConfig={props.currentBuildConfig}
+          setBuildConfig={props.setBuildConfig}
         />
       </>
     );
@@ -92,7 +87,7 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
           setShowSettings(!showSettings);
         }}
       >
-        {buildView == "docker" ? (
+        {props.buildView == "docker" ? (
           <AdvancedBuildTitle>
             <i className="material-icons dropdown">arrow_drop_down</i>
             Configure Dockerfile settings
@@ -108,17 +103,19 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
       <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
         <StyledSourceBox>
           <Select
-            value={buildView}
+            value={props.buildView}
             width="300px"
             options={[
               { value: "docker", label: "Docker" },
               { value: "buildpacks", label: "Buildpacks" },
             ]}
-            setValue={(option) => setBuildView(option)}
+            setValue={(option) => props.setBuildView(option)}
             label="Build method"
           />
           <Spacer y={1} />
-          {buildView === "docker" ? createDockerView() : createBuildpackView()}
+          {props.buildView === "docker"
+            ? createDockerView()
+            : createBuildpackView()}
         </StyledSourceBox>
       </AnimateHeight>
     </>

+ 24 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/GithubConnectModal.tsx

@@ -19,21 +19,29 @@ type Props = RouteComponentProps & {
   closeModal: () => void;
   hasClickedDoNotConnect: boolean;
   handleDoNotConnect: () => void;
+  setAccessError: (error: boolean) => void;
+  setAccessLoading: (loading: boolean) => void;
+  setAccessData: (data: GithubAppAccessData) => void;
+  accessData: GithubAppAccessData;
+  accessError: boolean;
 };
 
 interface GithubAppAccessData {
   username?: string;
   accounts?: string[];
+  accessError?: boolean;
 }
 
 const GithubConnectModal: React.FC<Props> = ({
   closeModal,
   hasClickedDoNotConnect,
   handleDoNotConnect,
+  accessError,
+  setAccessError,
+  setAccessLoading,
+  setAccessData,
+  accessData,
 }) => {
-  const [accessLoading, setAccessLoading] = useState(true);
-  const [accessError, setAccessError] = useState(false);
-  const [accessData, setAccessData] = useState<GithubAppAccessData>({});
   const [loading, setLoading] = React.useState<boolean>(false);
   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
   const encoded_redirect_uri = encodeURIComponent(url);
@@ -74,8 +82,12 @@ const GithubConnectModal: React.FC<Props> = ({
           <ButtonWrapper>
             <ConnectToGithubButton
               href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
+              target="_blank"
+              rel="noopener noreferrer"
+              onClick={closeModal}
             >
-              <GitHubIcon src={github} /> Connect to GitHub
+              <GitHubIcon src={github} />
+              Connect to GitHub
             </ConnectToGithubButton>
 
             <Button
@@ -190,6 +202,14 @@ const ConnectToGithubButton = styled.a`
     margin-right: 5px;
     justify-content: center;
   }
+  &:hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#353a3e"};
+  }
+
+  &:not([disabled]) {
+    cursor: pointer;
+  }
 `;
 
 const GitHubIcon = styled.img`

+ 117 - 54
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -77,6 +77,10 @@ type Detected = {
   detected: boolean;
   message: string;
 };
+interface GithubAppAccessData {
+  username?: string;
+  accounts?: string[];
+}
 
 const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [templateName, setTemplateName] = useState("");
@@ -94,6 +98,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [actionConfig, setActionConfig] = useState<ActionConfigType>({
     ...defaultActionConfig,
   });
+  const [buildView, setBuildView] = useState<string>("buildpacks");
   const [branch, setBranch] = useState("");
   const [dockerfilePath, setDockerfilePath] = useState(null);
   const [procfilePath, setProcfilePath] = useState(null);
@@ -101,16 +106,40 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [buildConfig, setBuildConfig] = useState({});
   const [porterYaml, setPorterYaml] = useState("");
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
-  const [showConnectModal, setConnectModal] = 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 [providers, setProviders] = useState([]);
+  const [currentProvider, setCurrentProvider] = useState(null);
+  const [hasProviders, setHasProviders] = useState(true);
 
   const [porterJson, setPorterJson] = useState<PorterJson | undefined>(
     undefined
   );
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
+  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 validatePorterYaml = (yamlString: string) => {
     let parsedYaml;
     try {
@@ -157,36 +186,57 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       console.log("Error converting porter yaml file to input: " + error);
     }
   };
+  const sortProviders = (providers: Provider[]) => {
+    const githubProviders = providers.filter(
+      (provider) => provider.provider === "github"
+    );
+
+    const gitlabProviders = providers.filter(
+      (provider) => provider.provider === "gitlab"
+    );
+
+    const githubSortedProviders = githubProviders.sort((a, b) => {
+      if (a.provider === "github" && b.provider === "github") {
+        return a.name.localeCompare(b.name);
+      }
+    });
+
+    const gitlabSortedProviders = gitlabProviders.sort((a, b) => {
+      if (a.provider === "gitlab" && b.provider === "gitlab") {
+        return a.instance_url.localeCompare(b.instance_url);
+      }
+    });
+    return [...gitlabSortedProviders, ...githubSortedProviders];
+  };
+  useEffect(() => {
+    let isSubscribed = true;
+
+    api
+      .getGitProviders("<token>", {}, { project_id: currentProject?.id })
+      .then((res) => {
+        const data = res.data;
+        if (!isSubscribed) {
+          return;
+        }
+
+        if (!Array.isArray(data)) {
+          setHasProviders(false);
+          return;
+        }
+
+        const sortedProviders = sortProviders(data);
+        setProviders(sortedProviders);
+        setCurrentProvider(sortedProviders[0]);
+      })
+      .catch((err) => {
+        setHasProviders(false);
+      });
+
+    return () => {
+      isSubscribed = false;
+    };
+  }, [currentProject]);
 
-  // const renderGithubConnect = () => {
-  //   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
-  //   const encoded_redirect_uri = encodeURIComponent(url);
-
-  //   if (accessError) {
-  //     return (
-  //       <ListWrapper>
-  //         <Helper>
-  //           No connected repositories found.
-  //           <A href={"/api/integrations/github-app/oauth"}>
-  //             Authorize Porter to view your repositories.
-  //           </A>
-  //         </Helper>
-  //       </ListWrapper>
-  //     );
-  //   } else if (!accessData.accounts || accessData.accounts?.length == 0) {
-  //     return (
-  //       <>
-  //         <Text size={16}>No connected repositories were found.</Text>
-  //         <ConnectToGithubButton
-  //           href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
-  //         >
-  //           <GitHubIcon src={github} /> Connect to GitHub
-  //         </ConnectToGithubButton>
-  //       </>
-  //     );
-  //   }
-  // };
-  // Deploys a Helm chart and writes build settings to the DB
   const isAppNameValid = (name: string) => {
     const regex = /^[a-z0-9-]{1,61}$/;
     return regex.test(name);
@@ -209,10 +259,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         formState.applicationName.length > 61)
     );
   };
-  const handleDoNotConnect = () => {
-    setHasClickedDoNotConnect(true);
-    localStorage.setItem("hasClickedDoNotConnect", "true");
-  };
 
   const deployPorterApp = async () => {
     try {
@@ -232,9 +278,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         formState.serviceList,
         formState.envVariables,
         porterJson,
-        formState.applicationName,
-        currentProject.id,
-        currentCluster.id
       );
 
       const yamlString = yaml.dump(finalPorterYaml);
@@ -256,8 +299,11 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           git_repo_id: actionConfig?.git_repo_id,
           build_context: folderPath,
           builder: (buildConfig as any)?.builder,
-          buildpacks: (buildConfig as any)?.buildpacks?.join(",") ?? "",
-          dockerfile: dockerfilePath,
+          buildpacks:
+            buildView === "buildpacks"
+              ? (buildConfig as any)?.buildpacks?.join(",") ?? ""
+              : "",
+          dockerfile: buildView === "docker" ? dockerfilePath : "",
           image_repo_uri: imageUrl,
           porter_yaml: base64Encoded,
           ...imageInfo,
@@ -287,28 +333,45 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       setDeploying(false);
     }
   };
+  useEffect(() => {
+    setFormState({ ...formState, serviceList: [] });
+  }, [actionConfig, branch]);
 
   // useEffect(() => {
-  //   api
-  //     .getGithubAccounts("<token>", {}, {})
-  //     .then(({ data }) => {
+  //   const fetchGithubAccounts = async () => {
+  //     try {
+  //       const { data } = await api.getGithubAccounts("<token>", {}, {});
   //       setAccessData(data);
-  //       setAccessLoading(false);
-  //     })
-  //     .catch(() => {
+  //       if (data) {
+  //         setHasProviders(false);
+  //       }
+  //     } catch (error) {
   //       setAccessError(true);
+  //     } finally {
   //       setAccessLoading(false);
-  //     });
-  // }, []);
+  //     }
+
+  //     setConnectModal(
+  //       !hasClickedDoNotConnect && (!hasProviders || accessError)
+  //     );
+  //   };
+
+  //   fetchGithubAccounts();
+  // }, [hasClickedDoNotConnect, accessData.accounts, accessError]);
 
   return (
     <CenterWrapper>
       <Div>
-        {showConnectModal && (
+        {showConnectModal && !hasProviders && (
           <GithubConnectModal
             closeModal={() => setConnectModal(false)}
             hasClickedDoNotConnect={hasClickedDoNotConnect}
             handleDoNotConnect={handleDoNotConnect}
+            accessData={accessData}
+            setAccessLoading={setAccessLoading}
+            accessError={accessError}
+            setAccessData={handleSetAccessData}
+            setAccessError={handleSetAccessError}
           />
         )}
         <StyledConfigureTemplate>
@@ -336,8 +399,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   width="300px"
                   error={
                     shouldHighlightAppNameInput() &&
-                    (formState.applicationName.length > 61
-                      ? "Maximum 61 characters allowed."
+                    (formState.applicationName.length > 30
+                      ? "Maximum 30 characters allowed."
                       : 'Lowercase letters, numbers, and "-" only.')
                   }
                   setValue={(e) => {
@@ -389,16 +452,16 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   setPorterYaml={(newYaml: string) => {
                     validatePorterYaml(newYaml);
                   }}
+                  buildView={buildView}
+                  setBuildView={setBuildView}
                 />
               </>,
               <>
                 <Text size={16}>
                   Application services{" "}
-                  {detected && (
+                  {detected && formState.serviceList.length > 0 && (
                     <AppearingDiv>
-                      <Text
-                        color={detected.detected ? "#4797ff" : "#fcba03"}
-                      >
+                      <Text color={detected.detected ? "#4797ff" : "#fcba03"}>
                         {detected.detected ? (
                           <I className="material-icons">check</I>
                         ) : (

+ 333 - 41
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -1,4 +1,4 @@
-import React from "react"
+import React, { useContext, useEffect, useMemo } from "react";
 import AnimateHeight, { Height } from "react-animate-height";
 import styled from "styled-components";
 
@@ -11,43 +11,256 @@ import WebTabs from "./WebTabs";
 import WorkerTabs from "./WorkerTabs";
 import JobTabs from "./JobTabs";
 import { Service } from "./serviceTypes";
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Button from "components/porter/Button";
+import {
+  NewWebsocketOptions,
+  useWebsockets,
+} from "../../../../shared/hooks/useWebsockets";
+import { Context } from "../../../../shared/Context";
+import api from "../../../../shared/api";
+import {
+  getAvailability,
+  getAvailabilityStacks,
+} from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
 
 interface ServiceProps {
   service: Service;
+  chart: any;
   editService: (service: Service) => void;
   deleteService: () => void;
 }
 
 const ServiceContainer: React.FC<ServiceProps> = ({
   service,
+  chart,
   deleteService,
   editService,
 }) => {
   const [showExpanded, setShowExpanded] = React.useState<boolean>(false);
-  const [height, setHeight] = React.useState<Height>('auto');
+  const [height, setHeight] = React.useState<Height>("auto");
+  const [controller, setController] = React.useState<any>(null);
+  const [available, setAvailable] = React.useState<number>(0);
+  const [total, setTotal] = React.useState<number>(0);
+  const [stale, setStale] = React.useState<number>(0);
+
+  console.log("initial controller", controller);
+  console.log("initial available", available);
+  console.log("initial total", total);
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    closeWebsocket,
+  } = useWebsockets();
+
+  const getSelectors = () => {
+    let ml =
+      controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
+    let i = 1;
+    let selector = "";
+    for (var key in ml) {
+      selector += key + "=" + ml[key];
+      if (i != Object.keys(ml).length) {
+        selector += ",";
+      }
+      i += 1;
+    }
+    return selector;
+  };
+
+  useEffect(() => {
+    const selectors = getSelectors();
+
+    console.log("effect selectors", selectors);
+
+    if (selectors.length > 0) {
+      console.log("initial webby", selectors);
+      // updatePods();
+      [controller?.kind].forEach((kind) => {
+        setupWebsocket(kind, controller?.metadata?.uid, selectors);
+      });
+      return () => closeAllWebsockets();
+    }
+  }, [controller]);
+
+  const { currentProject, currentCluster } = useContext(Context);
+
+  useEffect(() => {
+    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;
+        console.log("testing input", controllers);
+        const filteredControllers = controllers.filter((controller: any) => {
+          const name = getName(service);
+          console.log("filter name", name);
+          return name == controller.metadata.name;
+        });
+        console.log("filtered controllers", filteredControllers);
+        if (filteredControllers.length == 1) {
+          setController(filteredControllers[0]);
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  }, []);
+
+  const getName = (service: any) => {
+    const name = chart.name + "-" + service.name;
+
+    switch (service.type) {
+      case "web":
+        return name + "-web";
+      case "worker":
+        return name + "-wkr";
+      case "job":
+        return name + "job";
+    }
+  };
+
+  const setupWebsocket = (
+    kind: string,
+    controllerUid: string,
+    selectors: string
+  ) => {
+    console.log("called with", kind, controllerUid, selectors);
+    let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
+    if (kind == "pod" && selectors) {
+      apiEndpoint += `selectors=${selectors}`;
+    }
+
+    console.log("api end", apiEndpoint);
+
+    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;
+
+      // 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;
+      }
+
+      console.log("event object", event);
+
+      if (event.event_type == "ADD" && total == 0) {
+        let [available, total, stale] = getAvailabilityStacks(
+          object.metadata.kind,
+          object
+        );
+        console.log("response from object", object);
+        console.log("available response", available, total, stale);
+
+        setAvailable(available);
+        setTotal(total);
+        setStale(stale);
+        return;
+      }
+
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
+
+      // testing hot reload
+
+      if (event.Kind != "pod") {
+        let [available, total, stale] = getAvailabilityStacks(
+          object.metadata.kind,
+          object
+        );
+        console.log("response from object", object);
+        console.log("available response", available, total, stale);
+
+        setAvailable(available);
+        setTotal(total);
+        setStale(stale);
+        return;
+      }
+      // updatePods();
+    };
+
+    options.onclose = () => {
+      console.log("closing websocket");
+    };
+
+    options.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      closeWebsocket(kind);
+    };
+
+    newWebsocket(kind, apiEndpoint, options);
+    openWebsocket(kind);
+  };
 
   // TODO: calculate heights instead of hardcoding them
   const renderTabs = (service: Service) => {
     switch (service.type) {
-      case 'web':
-        return <WebTabs service={service} editService={editService} setHeight={setHeight} />
-      case 'worker':
-        return <WorkerTabs service={service} editService={editService} setHeight={setHeight} />
-      case 'job':
-        return <JobTabs service={service} editService={editService} setHeight={setHeight} />
+      case "web":
+        return (
+          <WebTabs
+            service={service}
+            editService={editService}
+            setHeight={setHeight}
+          />
+        );
+      case "worker":
+        return (
+          <WorkerTabs
+            service={service}
+            editService={editService}
+            setHeight={setHeight}
+          />
+        );
+      case "job":
+        return (
+          <JobTabs
+            service={service}
+            editService={editService}
+            setHeight={setHeight}
+          />
+        );
     }
-  }
+  };
 
   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 "web":
+        return <Icon src={web} />;
+      case "worker":
+        return <Icon src={worker} />;
+      case "job":
+        return <Icon src={job} />;
     }
-  }
+  };
+
+  const percentage = Number(1 - available / total).toLocaleString(undefined, {
+    style: "percent",
+    minimumFractionDigits: 2,
+  });
+  console.log(percentage);
 
   return (
     <>
@@ -56,35 +269,113 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         onClick={() => setShowExpanded(!showExpanded)}
       >
         <ServiceTitle>
-          <ActionButton >
+          <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) => {
-          deleteService();
-        }}>
-          <span className="material-icons">delete</span>
-        </ActionButton>}
+        {service.canDelete && (
+          <ActionButton
+            onClick={(e) => {
+              deleteService();
+            }}
+          >
+            <span className="material-icons">delete</span>
+          </ActionButton>
+        )}
       </ServiceHeader>
-      <AnimateHeight
-        height={showExpanded ? height : 0}
-      >
+      {showExpanded && (
         <StyledSourceBox showExpanded={showExpanded}>
           {renderTabs(service)}
         </StyledSourceBox>
-      </AnimateHeight>
+      )}
+      <StatusFooter showExpanded={showExpanded}>
+        {service.type === "job" && (
+          <Container row>
+            <Mi className="material-icons">check</Mi>
+            <Text color="helper">
+              Last run succeeded at 12:39 PM on 4/13/23
+            </Text>
+            <Spacer inline x={1} />
+            <Button
+              onClick={() => console.log("redirect to runs")}
+              height="30px"
+              width="87px"
+              color="#ffffff11"
+              withBorder
+            >
+              <I className="material-icons">open_in_new</I>
+              History
+            </Button>
+          </Container>
+        )}
+        {service.type !== "job" && (
+          <Container row>
+            <StatusCircle percentage={percentage} />
+            <Text color="helper">
+              Running {available}/{total} instances{" "}
+              {stale == 1 ? `(${stale} old instance)` : ""}
+              {stale > 1 ? `(${stale} old instances)` : ""}
+            </Text>
+            <Spacer inline x={1} />
+            <Button
+              onClick={() => console.log("redirect to logs")}
+              height="30px"
+              width="70px"
+              color="#ffffff11"
+              withBorder
+            >
+              <I className="material-icons">open_in_new</I>
+              Logs
+            </Button>
+          </Container>
+        )}
+      </StatusFooter>
       <Spacer y={0.5} />
     </>
-  )
-}
+  );
+};
 
 export default ServiceContainer;
 
+const Mi = styled.i`
+  font-size: 16px;
+  margin-right: 7px;
+  margin-top: -1px;
+  color: rgb(56, 168, 138);
+`;
+
+const I = styled.i`
+  font-size: 14px;
+  margin-right: 5px;
+`;
+
+const StatusCircle = styled.div<{ percentage?: any }>`
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  margin-right: 10px;
+  background: conic-gradient(
+    from 0deg,
+    #ffffff33 ${(props) => props.percentage},
+    #ffffffaa 0% ${(props) => props.percentage}
+  );
+`;
+
+const StatusFooter = styled.div<{ showExpanded: boolean }>`
+  width: 100%;
+  padding: 15px;
+  background: ${(props) => props.theme.fg2};
+  border-bottom-left-radius: 5px;
+  border-bottom-right-radius: 5px;
+  border: 1px solid #494b4f;
+  border-top: 0;
+`;
+
 const ServiceTitle = styled.div`
-    display: flex;
-    align-items: center;
+  display: flex;
+  align-items: center;
 `;
 
 const StyledSourceBox = styled.div<{ showExpanded: boolean }>`
@@ -93,12 +384,12 @@ const StyledSourceBox = styled.div<{ showExpanded: boolean }>`
   padding: 14px 25px 30px;
   position: relative;
   font-size: 13px;
-  border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
-  border-top: 0px;
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
+  border-top: 0;
+  border-bottom: 0;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
 `;
 
 const ActionButton = styled.button`
@@ -131,24 +422,25 @@ const ServiceHeader = styled.div`
   justify-content: space-between;
   cursor: pointer;
   padding: 20px;
-  color: ${props => props.theme.text.primary};
+  color: ${(props) => props.theme.text.primary};
   position: relative;
   border-radius: 5px;
-  background: ${props => props.theme.clickable.bg};
+  background: ${(props) => props.theme.clickable.bg};
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;
   }
 
-  border-bottom-left-radius: ${({ showExpanded }) => showExpanded && "0px"};
-  border-bottom-right-radius: ${({ showExpanded }) => showExpanded && "0px"};
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
 
   .dropdown {
     font-size: 30px;
     cursor: pointer;
     border-radius: 20px;
     margin-left: -10px;
-    transform: ${(props: { showExpanded: boolean }) => props.showExpanded ? "" : "rotate(-90deg)"};
+    transform: ${(props: { showExpanded: boolean }) =>
+      props.showExpanded ? "" : "rotate(-90deg)"};
   }
 
   animation: fadeIn 0.3s 0s;
@@ -175,4 +467,4 @@ const Icon = styled.img`
       opacity: 1;
     }
   }
-`;
+`;

+ 7 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, {useContext, useEffect, useState} from "react";
 import ServiceContainer from "./ServiceContainer";
 import styled from "styled-components";
 import Spacer from "components/porter/Spacer";
@@ -13,13 +13,16 @@ import web from "assets/web.png";
 import worker from "assets/worker.png";
 import job from "assets/job.png";
 import { Service, ServiceType } from "./serviceTypes";
+import api from "../../../../shared/api";
+import {Context} from "../../../../shared/Context";
 
 interface ServicesProps {
   services: Service[];
   setServices: (services: Service[]) => void;
+  chart: any
 }
 
-const Services: React.FC<ServicesProps> = ({ services, setServices }) => {
+const Services: React.FC<ServicesProps> = ({ services, setServices, chart }) => {
   const [showAddServiceModal, setShowAddServiceModal] = useState<boolean>(
     false
   );
@@ -45,6 +48,7 @@ const Services: React.FC<ServicesProps> = ({ services, setServices }) => {
                 <ServiceContainer
                   key={service.name}
                   service={service}
+                  chart={chart}
                   editService={(newService: Service) =>
                     setServices(
                       services.map((s, i) => (i === index ? newService : s))
@@ -103,7 +107,7 @@ const Services: React.FC<ServicesProps> = ({ services, setServices }) => {
               (serviceName != "" &&
                 !isServiceNameValid(serviceName) &&
                 'Lowercase letters, numbers, and "-" only.') ||
-              (serviceName.length > 61 && "Must be 61 characters or less.") ||
+              (serviceName.length > 30 && "Must be 30 characters or less.") ||
               (isServiceNameDuplicate(serviceName) &&
                 "Service name is duplicate")
             }

+ 6 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -36,6 +36,8 @@ type Props = {
   setBuildConfig: (x: any) => void;
   porterYaml: string;
   setPorterYaml: (x: any) => void;
+  buildView: string;
+  setBuildView: (x: string) => void;
 };
 
 const SourceSettings: React.FC<Props> = ({
@@ -55,6 +57,8 @@ const SourceSettings: React.FC<Props> = ({
   setBuildConfig,
   porterYaml,
   setPorterYaml,
+  buildView,
+  setBuildView,
   ...props
 }) => {
   const renderDockerSettings = () => {
@@ -129,6 +133,8 @@ const SourceSettings: React.FC<Props> = ({
               setBranch={setBranch}
               imageUrl={imageUrl}
               setImageUrl={setImageUrl}
+              buildView={buildView}
+              setBuildView={setBuildView}
             />
           ) : (
             renderDockerSettings()

+ 4 - 20
dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx

@@ -47,14 +47,11 @@ export const createFinalPorterYaml = (
     services: Service[],
     dashboardSetEnvVariables: KeyValueType[],
     porterJson: PorterJson | undefined,
-    stackName: string,
-    projectId: number,
-    clusterId: number,
 ): PorterJson => {
     return {
         version: "v1stack",
         env: combineEnv(dashboardSetEnvVariables, porterJson?.env),
-        apps: createApps(services, porterJson, stackName, projectId, clusterId),
+        apps: createApps(services, porterJson),
     };
 };
 
@@ -77,28 +74,14 @@ const combineEnv = (
 const createApps = (
     serviceList: Service[],
     porterJson: PorterJson | undefined,
-    stackName: string,
-    projectId: number,
-    clusterId: number,
 ): z.infer<typeof AppsSchema> => {
     const apps: z.infer<typeof AppsSchema> = {};
     for (const service of serviceList) {
         let config = Service.serialize(service);
-        // TODO: get rid of this block when we handle ingress on the backend
-        if (Service.isWeb(service)) {
-            const ingress = Service.handleWebIngress(
-                service,
-                stackName,
-                clusterId,
-                projectId
-            );
-            config = {
-                ...config,
-                ...ingress,
-            };
-        }
+
         if (
             porterJson != null &&
+            porterJson.apps != null &&
             porterJson.apps[service.name] != null &&
             porterJson.apps[service.name].config != null
         ) {
@@ -107,6 +90,7 @@ const createApps = (
                 porterJson.apps[service.name].config
             );
         }
+
         apps[service.name] = {
             type: service.type,
             run: service.startCommand.value,

+ 38 - 44
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -124,12 +124,38 @@ const WebService = {
         maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
         targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
         targetRAMUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
-        port: ServiceField.string('8080', porterJson?.apps?.[name]?.config?.container?.port),
+        port: ServiceField.string('80', porterJson?.apps?.[name]?.config?.container?.port),
         generateUrlForExternalTraffic: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
         customDomain: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
         canDelete: porterJson?.apps?.[name] == null,
     }),
     serialize: (service: WebService) => {
+        const getIngress = (service: WebService): Ingress => {
+            if (!service.generateUrlForExternalTraffic.value) {
+                return {
+                    ingress: {
+                        enabled: false,
+                        hosts: [],
+                        custom_domain: false,
+                        porter_hosts: [],
+                    }
+                }
+            }
+            const ingress: Ingress = {
+                ingress: {
+                    enabled: true,
+                    hosts: [],
+                    custom_domain: false,
+                    porter_hosts: [],
+                }
+            };
+            if (service.customDomain.value) {
+                ingress.ingress.hosts.push(service.customDomain.value);
+                ingress.ingress.custom_domain = true;
+            }
+            return ingress;
+        }
+
         const autoscaling = service.autoscalingOn.value ? {
             autoscaling: {
                 enabled: true,
@@ -139,6 +165,7 @@ const WebService = {
                 targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage.value,
             }
         } : {};
+
         return {
             replicaCount: service.replicas.value,
             resources: {
@@ -155,6 +182,7 @@ const WebService = {
                 port: service.port.value,
             },
             ...autoscaling,
+            ...getIngress(service),
         }
     },
     deserialize: (name: string, values: any, porterJson?: PorterJson): WebService => {
@@ -297,47 +325,6 @@ export const Service = {
     isWorker: (service: Service): service is WorkerService => service.type === 'worker',
     isJob: (service: Service): service is JobService => service.type === 'job',
 
-    // augments ingress of a web service, will be phased out
-    handleWebIngress: (service: WebService, stackName: string, projectId?: number, clusterId?: number) => {
-        if (projectId == null || clusterId == null) {
-            throw new Error('Project ID and Cluster ID must be provided to handle web ingress');
-        }
-        if (!service.generateUrlForExternalTraffic.value) {
-            return {}
-        }
-        const ingress: Ingress = {
-            ingress: {
-                enabled: true,
-                hosts: [],
-                custom_domain: false,
-                porter_hosts: [],
-            }
-        };
-        if (service.customDomain.value) {
-            ingress.ingress.hosts.push(service.customDomain.value);
-            ingress.ingress.custom_domain = true;
-        } else {
-            // const res = await api
-            //     .createSubdomain(
-            //         "<token>",
-            //         {},
-            //         {
-            //             id: projectId,
-            //             cluster_id: clusterId,
-            //             release_name: stackName,
-            //             namespace: `porter-stack-${stackName}`,
-            //         }
-            //     )
-            // if (res == null || res.data == null || res.data.external_url == null) {
-            //     throw new Error('Failed to create subdomain for web service');
-            // }
-            // ingress.porter_hosts.push(res.data.external_url)
-            //throw new Error('Generating external URLs without custom subdomains not yet supported!');
-        }
-
-        return ingress;
-    },
-
     // required because of https://github.com/helm/helm/issues/9214
     toHelmName: (service: Service): string => {
         return service.name + TYPE_TO_SUFFIX[service.type]
@@ -369,16 +356,23 @@ export const Service = {
             return "";
         }
 
+        const prefixSubdomain = (subdomain: string) => {
+            if (subdomain.startsWith('https://') || subdomain.startsWith('http://')) {
+                return subdomain;
+            }
+            return 'https://' + subdomain;
+        }
+
         for (const web of webServices) {
             const values = helmValues[Service.toHelmName(web)];
             if (values == null || values.ingress == null || !values.ingress.enabled) {
                 continue;
             }
             if (values.ingress.custom_domain && values.ingress.hosts?.length > 0) {
-                return values.ingress.hosts[0];
+                return prefixSubdomain(values.ingress.hosts[0]);
             }
             if (values.ingress.porter_hosts?.length > 0) {
-                return values.ingress.porter_hosts[0];
+                return prefixSubdomain(values.ingress.porter_hosts[0]);
             }
         }
 

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

@@ -51,3 +51,29 @@ export const getAvailability = (kind: string, c: any) => {
       return [1, 1];
   }
 };
+
+export const getAvailabilityStacks = (kind: string, c: any) => {
+  switch (kind?.toLowerCase()) {
+    case "deployment":
+    case "replicaset":
+      const available =
+        c.status?.updatedReplicas ||
+        c.status?.updatedReplicas ||
+        c.status?.replicas - c.status?.unavailableReplicas ||
+        0;
+      const total = c.spec.replicas;
+      const stale =
+        c.status?.availableReplicas - c.status?.updatedReplicas || 0;
+      return [available, total, stale];
+    case "statefulset":
+      return [c.status?.readyReplicas || 0, c.status?.replicas || 0, 0];
+    case "daemonset":
+      return [
+        c.status?.numberAvailable || 0,
+        c.status?.desiredNumberScheduled || 0,
+        0,
+      ];
+    case "job":
+      return [1, 1, 0];
+  }
+};

+ 1 - 1
dashboard/src/shared/api.tsx

@@ -2198,7 +2198,7 @@ const getLogs = baseApi<
     end_range?: string;
     revision?: string;
     pod_selector: string;
-    namespace: string;
+    namespace?: string;
     search_param?: string;
     direction?: string;
   },

+ 18 - 0
dashboard/src/shared/themes/opal.ts

@@ -0,0 +1,18 @@
+const theme = {
+  bg: "#f3f5f8",
+  fg: "#ffffff",
+  fg2: "#ffffff11",
+  border: "#bbbbcc",
+  border2: "#9999aa",
+  button: "#3A48CA",
+  clickable: {
+    bg: "linear-gradient(180deg, #ffffff, #f3f5f8)",
+  },
+  modalBg: "#171B2111",
+  text: {
+    primary: "#414142",
+    helper: "#aaaabb",
+  },
+}
+
+export default theme;

+ 5 - 1
internal/kubernetes/porter_agent/v2/agent_server.go

@@ -271,7 +271,11 @@ func GetHistoricalLogs(
 	}
 
 	vals["pod_selector"] = req.PodSelector
-	vals["namespace"] = req.Namespace
+
+	if req.Namespace != "" {
+		vals["namespace"] = req.Namespace
+	}
+
 	vals["revision"] = req.Revision
 
 	if req.SearchParam != "" {