Kaynağa Gözat

merge conflict

Justin Rhee 3 yıl önce
ebeveyn
işleme
bc9334e8c2
29 değiştirilmiş dosya ile 580 ekleme ve 1848 silme
  1. 65 0
      api/server/handlers/cluster/get_porter_job_events.go
  2. 1 9
      api/server/handlers/cluster/list_incident_events.go
  3. 18 5
      api/server/handlers/cluster/notify_new_incident.go
  4. 18 5
      api/server/handlers/cluster/notify_resolved_incident.go
  5. 7 1
      api/server/handlers/cluster/update.go
  6. 31 2
      api/server/router/cluster.go
  7. 6 1
      api/types/cluster.go
  8. 10 0
      api/types/incident.go
  9. 75 16
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx
  10. 0 3
      dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx
  11. 0 4
      dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx
  12. 0 233
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/EventDrawer.tsx
  13. 0 62
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/ExpandedContainer.tsx
  14. 0 524
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentPage.tsx
  15. 0 215
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTab.tsx
  16. 0 209
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTable.tsx
  17. 9 9
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  18. 3 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  19. 102 28
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx
  20. 5 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventTable.tsx
  21. 22 8
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx
  22. 0 217
      dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx
  23. 0 217
      dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/IncidentsTable.tsx
  24. 95 59
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx
  25. 14 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx
  26. 28 7
      dashboard/src/shared/api.tsx
  27. 1 0
      dashboard/src/shared/types.tsx
  28. 58 2
      internal/kubernetes/porter_agent/v2/agent_server.go
  29. 12 8
      internal/models/cluster.go

+ 65 - 0
api/server/handlers/cluster/get_porter_job_events.go

@@ -0,0 +1,65 @@
+package cluster
+
+import (
+	"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/models"
+
+	porter_agent "github.com/porter-dev/porter/internal/kubernetes/porter_agent/v2"
+)
+
+type GetPorterJobEventsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetPorterJobEventsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetPorterJobEventsHandler {
+	return &GetPorterJobEventsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GetPorterJobEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	request := &types.ListJobEventsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// get agent service
+	agentSvc, err := porter_agent.GetAgentService(agent.Clientset)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	events, err := porter_agent.ListPorterJobEvents(agent.Clientset, agentSvc, request)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, events)
+}

+ 1 - 9
api/server/handlers/cluster/list_incident_events.go

@@ -8,7 +8,6 @@ import (
 	"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/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	porter_agent "github.com/porter-dev/porter/internal/kubernetes/porter_agent/v2"
 	"github.com/porter-dev/porter/internal/models"
@@ -33,13 +32,6 @@ func NewListIncidentEventsHandler(
 func (c *ListIncidentEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	incidentID, reqErr := requestutils.GetURLParamString(r, types.URLParamIncidentID)
-
-	if reqErr != nil {
-		c.HandleAPIError(w, r, reqErr)
-		return
-	}
-
 	request := &types.ListIncidentEventsRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -61,7 +53,7 @@ func (c *ListIncidentEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	events, err := porter_agent.ListIncidentEvents(agent.Clientset, agentSvc, incidentID, request)
+	events, err := porter_agent.ListIncidentEvents(agent.Clientset, agentSvc, request)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 18 - 5
api/server/handlers/cluster/notify_new_incident.go

@@ -3,6 +3,7 @@ package cluster
 import (
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -94,16 +95,28 @@ func (c *NotifyNewIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	)
 
 	if !cluster.NotificationsDisabled {
-		err := multi.NotifyNew(
-			request, fmt.Sprintf(
-				"%s/applications/%s/%s/%s?project_id=%d",
+		url := fmt.Sprintf(
+			"%s/applications/%s/%s/%s?project_id=%d",
+			c.Config().ServerConf.ServerURL,
+			cluster.Name,
+			request.ReleaseNamespace,
+			request.ReleaseName,
+			cluster.ProjectID,
+		)
+
+		if strings.ToLower(string(request.InvolvedObjectKind)) == "job" {
+			url = fmt.Sprintf(
+				"%s/jobs/%s/%s/%s?project_id=%d&job=%s",
 				c.Config().ServerConf.ServerURL,
 				cluster.Name,
 				request.ReleaseNamespace,
 				request.ReleaseName,
 				cluster.ProjectID,
-			),
-		)
+				request.InvolvedObjectName,
+			)
+		}
+
+		err := multi.NotifyNew(request, url)
 
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 18 - 5
api/server/handlers/cluster/notify_resolved_incident.go

@@ -3,6 +3,7 @@ package cluster
 import (
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -93,16 +94,28 @@ func (c *NotifyResolvedIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http
 	)
 
 	if !cluster.NotificationsDisabled {
-		err := multi.NotifyResolved(
-			request, fmt.Sprintf(
-				"%s/applications/%s/%s/%s?project_id=%d",
+		url := fmt.Sprintf(
+			"%s/applications/%s/%s/%s?project_id=%d",
+			c.Config().ServerConf.ServerURL,
+			cluster.Name,
+			request.ReleaseNamespace,
+			request.ReleaseName,
+			cluster.ProjectID,
+		)
+
+		if strings.ToLower(string(request.InvolvedObjectKind)) == "job" {
+			url = fmt.Sprintf(
+				"%s/jobs/%s/%s/%s?project_id=%d&job=%s",
 				c.Config().ServerConf.ServerURL,
 				cluster.Name,
 				request.ReleaseNamespace,
 				request.ReleaseName,
 				cluster.ProjectID,
-			),
-		)
+				request.InvolvedObjectName,
+			)
+		}
+
+		err := multi.NotifyResolved(request, url)
 
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 7 - 1
api/server/handlers/cluster/update.go

@@ -61,7 +61,13 @@ func (c *ClusterUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		cluster.AWSClusterID = request.AWSClusterID
 	}
 
-	cluster.Name = request.Name
+	if request.AgentIntegrationEnabled != nil {
+		cluster.AgentIntegrationEnabled = *request.AgentIntegrationEnabled
+	}
+
+	if request.Name != "" && cluster.Name != request.Name {
+		cluster.Name = request.Name
+	}
 
 	cluster, err := c.Repo().Cluster().UpdateCluster(cluster)
 

+ 31 - 2
api/server/router/cluster.go

@@ -1062,14 +1062,14 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/incidents/{incident_id}/events -> cluster.NewListIncidentEventsHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/incidents/events -> cluster.NewListIncidentEventsHandler
 	listIncidentEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/incidents/{%s}/events", relPath, types.URLParamIncidentID),
+				RelativePath: fmt.Sprintf("%s/incidents/events", relPath),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -1207,6 +1207,35 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/events/job -> cluster.NewGetPorterJobEventsHandler
+	getPorterJobEventsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/events/job", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterJobEventsHandler := cluster.NewGetPorterJobEventsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterJobEventsEndpoint,
+		Handler:  getPorterJobEventsHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/k8s_events -> cluster.NewGetEventsHandler
 	getK8sEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 6 - 1
api/types/cluster.go

@@ -24,6 +24,9 @@ type Cluster struct {
 	// The integration service for this cluster
 	Service ClusterService `json:"service"`
 
+	// Whether or not the Porter agent integration is enabled
+	AgentIntegrationEnabled bool `json:"agent_integration_enabled"`
+
 	// The infra id, if cluster was provisioned with Porter
 	InfraID uint `json:"infra_id"`
 
@@ -262,9 +265,11 @@ type CreateClusterCandidateRequest struct {
 }
 
 type UpdateClusterRequest struct {
-	Name string `json:"name" form:"required"`
+	Name string `json:"name"`
 
 	AWSClusterID string `json:"aws_cluster_id"`
+
+	AgentIntegrationEnabled *bool `json:"agent_integration_enabled"`
 }
 
 type ListClusterResponse []*Cluster

+ 10 - 0
api/types/incident.go

@@ -86,9 +86,11 @@ type IncidentEvent struct {
 
 type ListIncidentEventsRequest struct {
 	*PaginationRequest
+	IncidentID   *string `schema:"incident_id"`
 	PodName      *string `schema:"pod_name"`
 	PodNamespace *string `schema:"pod_namespace"`
 	Summary      *string `schema:"summary"`
+	PodPrefix    *string `schema:"pod_prefix"`
 }
 
 type ListIncidentEventsResponse struct {
@@ -180,3 +182,11 @@ type ListEventsResponse struct {
 	Events     []*Event            `json:"events" form:"required"`
 	Pagination *PaginationResponse `json:"pagination"`
 }
+
+type ListJobEventsRequest struct {
+	*PaginationRequest
+	ReleaseName      *string `schema:"release_name"`
+	ReleaseNamespace *string `schema:"release_namespace"`
+	Type             *string `schema:"type"`
+	JobName          string  `schema:"job_name" form:"required"`
+}

+ 75 - 16
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx

@@ -1,18 +1,26 @@
-import React, { useContext, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import Heading from "components/form-components/Heading";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import { Context } from "shared/Context";
 import api from "shared/api";
+import CheckboxRow from "components/form-components/CheckboxRow";
+import Loading from "components/Loading";
 
 const ClusterSettings: React.FC = () => {
-  const context = useContext(Context);
+  const {
+    currentProject,
+    currentCluster,
+    setCurrentCluster,
+    setCurrentModal,
+    capabilities,
+  } = useContext(Context);
   const [newClusterName, setNewClusterName] = useState<string>(
-    context.currentCluster.name
+    currentCluster.name
   );
   const [newAWSClusterID, setNewAWSClusterID] = useState<string>(
-    context.currentCluster.aws_cluster_id
+    currentCluster.aws_cluster_id
   );
   const [successfulRename, setSuccessfulRename] = useState<boolean>(false);
 
@@ -20,19 +28,23 @@ const ClusterSettings: React.FC = () => {
   const [secretKey, setSecretKey] = useState<string>("");
   const [startRotateCreds, setStartRotateCreds] = useState<boolean>(false);
   const [successfulRotate, setSuccessfulRotate] = useState<boolean>(false);
+  const [enableAgent, setEnableAgent] = useState(
+    currentCluster.agent_integration_enabled
+  );
+  const [agentLoading, setAgentLoading] = useState(false);
 
   let rotateCredentials = () => {
     api
       .overwriteAWSIntegration(
         "<token>",
         {
-          aws_integration_id: context.currentCluster.aws_integration_id,
+          aws_integration_id: currentCluster.aws_integration_id,
           aws_access_key_id: accessKeyId,
           aws_secret_access_key: secretKey,
-          cluster_id: context.currentCluster.id,
+          cluster_id: currentCluster.id,
         },
         {
-          project_id: context.currentProject.id,
+          project_id: currentProject.id,
         }
       )
       .then(({ data }) => {
@@ -45,15 +57,15 @@ const ClusterSettings: React.FC = () => {
 
   let updateClusterName = () => {
     api
-      .updateClusterName(
+      .updateCluster(
         "<token>",
         {
           name: newClusterName,
           aws_cluster_id: newAWSClusterID,
         },
         {
-          project_id: context.currentProject.id,
-          cluster_id: context.currentCluster.id,
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
         }
       )
       .then(({ data }) => {
@@ -64,6 +76,29 @@ const ClusterSettings: React.FC = () => {
       });
   };
 
+  let updateAgentIntegrationEnabled = () => {
+    setAgentLoading(true);
+
+    api
+      .updateCluster(
+        "<token>",
+        {
+          agent_integration_enabled: enableAgent,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      )
+      .then(({ data }) => {
+        setCurrentCluster(data);
+        setAgentLoading(false);
+      })
+      .catch(() => {
+        setAgentLoading(false);
+      });
+  };
+
   let helperText = (
     <Helper>
       Delete this cluster and underlying infrastructure. To ensure that
@@ -80,7 +115,7 @@ const ClusterSettings: React.FC = () => {
     </Helper>
   );
 
-  if (!context.currentCluster?.infra_id || !context.currentCluster?.service) {
+  if (!currentCluster?.infra_id || !currentCluster?.service) {
     helperText = (
       <Helper>
         Remove this cluster from Porter. Since this cluster was not provisioned
@@ -94,8 +129,8 @@ const ClusterSettings: React.FC = () => {
   let keyRotationSection = null;
 
   if (
-    context.currentCluster?.aws_integration_id &&
-    context.currentCluster?.aws_integration_id != 0
+    currentCluster?.aws_integration_id &&
+    currentCluster?.aws_integration_id != 0
   ) {
     if (successfulRotate) {
       keyRotationSection = (
@@ -148,8 +183,8 @@ const ClusterSettings: React.FC = () => {
   }
 
   let overrideAWSClusterNameSection =
-    context.currentCluster?.aws_integration_id &&
-    context.currentCluster?.aws_integration_id != 0 ? (
+    currentCluster?.aws_integration_id &&
+    currentCluster?.aws_integration_id != 0 ? (
       <InputRow
         type="text"
         value={newAWSClusterID}
@@ -180,6 +215,28 @@ const ClusterSettings: React.FC = () => {
     </div>
   );
 
+  let enableAgentIntegration = (
+    <div>
+      <Heading>Enable Agent</Heading>
+      <CheckboxRow
+        label={"Allow the Porter agent to be installed on the cluster"}
+        toggle={() => setEnableAgent(!enableAgent)}
+        checked={enableAgent}
+      />
+      <Button color="#616FEEcc" onClick={updateAgentIntegrationEnabled}>
+        Save
+      </Button>
+    </div>
+  );
+
+  if (agentLoading) {
+    enableAgentIntegration = <Loading />;
+  }
+
+  if (capabilities.version == "production") {
+    enableAgentIntegration = null;
+  }
+
   if (successfulRename) {
     renameClusterSection = (
       <div>
@@ -192,6 +249,8 @@ const ClusterSettings: React.FC = () => {
   return (
     <div>
       <StyledSettingsSection>
+        {enableAgentIntegration}
+        <DarkMatter />
         {keyRotationSection}
         <DarkMatter />
         {renameClusterSection}
@@ -200,7 +259,7 @@ const ClusterSettings: React.FC = () => {
         {helperText}
         <Button
           color="#b91133"
-          onClick={() => context.setCurrentModal("UpdateClusterModal")}
+          onClick={() => setCurrentModal("UpdateClusterModal")}
         >
           Delete Cluster
         </Button>

+ 0 - 3
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -14,7 +14,6 @@ import useAuth from "shared/auth/useAuth";
 import Metrics from "./Metrics";
 import { useLocation } from "react-router";
 import { getQueryParam } from "shared/routing";
-import IncidentsTab from "./incidents/IncidentsTab";
 
 import CopyToClipboard from "components/CopyToClipboard";
 import Loading from "components/Loading";
@@ -47,8 +46,6 @@ export const Dashboard: React.FunctionComponent = () => {
   const context = useContext(Context);
   const renderTab = () => {
     switch (currentTab) {
-      case "incidents":
-        return <IncidentsTab />;
       case "settings":
         return <ClusterSettings />;
       case "metrics":

+ 0 - 4
dashboard/src/main/home/cluster-dashboard/dashboard/Routes.tsx

@@ -2,7 +2,6 @@ import React, { useContext } from "react";
 import { Redirect, Route, Switch, useRouteMatch } from "react-router";
 import { Context } from "shared/Context";
 import { Dashboard } from "./Dashboard";
-import IncidentPage from "./incidents/IncidentPage";
 import ExpandedNodeView from "./node-view/ExpandedNodeView";
 
 export const Routes = () => {
@@ -11,9 +10,6 @@ export const Routes = () => {
   return (
     <>
       <Switch>
-        <Route path={`${url}/incidents/:incident_id`}>
-          <IncidentPage />
-        </Route>
         <Route path={`${url}/node-view/:nodeId`}>
           <ExpandedNodeView />
         </Route>

+ 0 - 233
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/EventDrawer.tsx

@@ -1,233 +0,0 @@
-import Description from "components/Description";
-import useLastSeenPodStatus from "components/events/useLastSeenPodStatus";
-import Heading from "components/form-components/Heading";
-import Loading from "components/Loading";
-import { isEmpty } from "lodash";
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { capitalize } from "shared/string_utils";
-import styled from "styled-components";
-import ExpandedContainer from "./ExpandedContainer";
-import { IncidentContainerEvent, IncidentEvent } from "./IncidentPage";
-
-const EventDrawer: React.FC<{
-  event: IncidentEvent;
-  closeDrawer: () => void;
-}> = ({ event, closeDrawer }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-
-  const [containerLogs, setContainerLogs] = useState<{ [key: string]: string }>(
-    null
-  );
-
-  const {
-    status,
-    hasError: hasPodStatusErrored,
-    isLoading: isPodStatusLoading,
-  } = useLastSeenPodStatus({
-    podName: event?.pod_name,
-    namespace: event?.namespace,
-    resource_type: "pod",
-  });
-
-  const containers: IncidentContainerEvent[] = useMemo(() => {
-    if (isEmpty(event?.container_events)) {
-      return [];
-    }
-
-    return Object.values(event?.container_events || {});
-  }, [event]);
-
-  useEffect(() => {
-    if (!event) {
-      return () => {};
-    }
-
-    let isSubscribed = true;
-
-    const containersWithLogs = containers.filter(
-      (container) => container.log_id
-    );
-
-    const promises = containersWithLogs.map((container) => {
-      return api
-        .getIncidentLogsByLogId<{ contents: string }>(
-          "<token>",
-          {
-            log_id: container.log_id,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        )
-        .then((res) => ({
-          contents: res.data?.contents,
-          container_name: container.container_name,
-        }));
-    });
-
-    Promise.all(promises)
-      .then((data) => {
-        if (!isSubscribed) {
-          return;
-        }
-
-        const tmpContainerLogs = data.reduce<{ [key: string]: string }>(
-          (acc, c) => {
-            acc[c.container_name] = c.contents;
-            return acc;
-          },
-          {}
-        );
-
-        setContainerLogs(tmpContainerLogs);
-      })
-      .catch(() => console.log("nope"));
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [containers]);
-
-  if (!event) {
-    return null;
-  }
-
-  if (!containerLogs) {
-    return <Loading />;
-  }
-
-  return (
-    <EventDrawerContainer>
-      <EventDrawerTitleContainer>
-        <EventDrawerTitle>Pod: {event?.pod_name}</EventDrawerTitle>
-        <BackButton onClick={closeDrawer}>
-          <i className="material-icons">close</i>
-        </BackButton>
-      </EventDrawerTitleContainer>
-
-      <StyledHelper>
-        {hasPodStatusErrored ? (
-          "We couldn't retrieve last pod status, please try again later"
-        ) : (
-          <>
-            {isPodStatusLoading ? (
-              <Loading />
-            ) : (
-              <>
-                Latest pod status: {capitalize(status)}{" "}
-                <StatusColor status={status?.toLowerCase()}></StatusColor>
-              </>
-            )}
-          </>
-        )}
-      </StyledHelper>
-      <MetadataContainer>
-        <Heading>Overview</Heading>
-        <Description>
-          Event reported on{" "}
-          {Intl.DateTimeFormat([], {
-            // @ts-ignore
-            dateStyle: "full",
-            timeStyle: "long",
-          }).format(new Date(event?.timestamp))}
-        </Description>
-        <Description>{event?.message}</Description>
-        <Br />
-      </MetadataContainer>
-      {containers.map((container) => (
-        <ExpandedContainer
-          container={container}
-          logs={containerLogs[container.container_name]}
-        />
-      ))}
-    </EventDrawerContainer>
-  );
-};
-
-export default EventDrawer;
-
-const EventDrawerContainer = styled.div`
-  position: relative;
-  color: #ffffff;
-  padding: 25px 30px;
-`;
-
-const EventDrawerTitle = styled.span`
-  display: block;
-  font-size: 24px;
-  font-weight: bold;
-  color: #ffffff;
-`;
-
-const Br = styled.div`
-  width: 100%;
-  height: 20px;
-`;
-
-const MetadataContainer = styled.div`
-  border-radius: 6px;
-  background: #2e3135;
-  padding: 0 20px;
-  overflow-y: auto;
-  min-height: 100px;
-  font-size: 13px;
-  margin: 12px 0;
-`;
-
-const StyledHelper = styled.div`
-  color: #aaaabb;
-  line-height: 1.6em;
-  font-size: 13px;
-  margin-top: 6px;
-`;
-
-const BackButton = styled.div`
-  display: flex;
-  width: 37px;
-  z-index: 1;
-  cursor: pointer;
-  height: 37px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-  color: #ffffffaa;
-
-  > i {
-    font-size: 20px;
-  }
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const StatusColor = styled.div`
-  display: inline-block;
-  margin-right: 7px;
-  width: 7px;
-  min-width: 7px;
-  height: 7px;
-  background: ${(props: { status: string }) =>
-    props.status === "running"
-      ? "#4797ff"
-      : props.status === "failed" || props.status === "deleted"
-      ? "#ed5f85"
-      : props.status === "completed"
-      ? "#00d12a"
-      : "#f5cb42"};
-  border-radius: 20px;
-`;
-
-const EventDrawerTitleContainer = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-`;

+ 0 - 62
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/ExpandedContainer.tsx

@@ -1,62 +0,0 @@
-import Description from "components/Description";
-import Heading from "components/form-components/Heading";
-import React from "react";
-import styled from "styled-components";
-import { IncidentContainerEvent } from "./IncidentPage";
-
-type Props = {
-  container: IncidentContainerEvent;
-  logs: string;
-};
-
-const ExpandedContainer: React.FC<Props> = ({ container, logs }) => {
-  return (
-    <StyledCard>
-      <MetadataContainer>
-        <Heading>Container: {container.container_name}</Heading>
-        <Description>
-          Container exited with code {container.exit_code}, {container.message}
-        </Description>
-        <Description>
-          The following are the container logs from this application instance:
-        </Description>
-        <LogContainer>
-          {logs ? <>{logs}</> : <>No logs available for this container.</>}
-        </LogContainer>
-      </MetadataContainer>
-    </StyledCard>
-  );
-};
-
-export default ExpandedContainer;
-
-const StyledCard = styled.div`
-  display: grid;
-  grid-row-gap: 15px;
-  grid-template-columns: 1;
-`;
-
-const MetadataContainer = styled.div`
-  margin-bottom: 3px;
-  border-radius: 6px;
-  background: #2e3135;
-  padding: 0 20px;
-  overflow-y: auto;
-  min-height: 100px;
-  font-size: 13px;
-  margin: 12px 0;
-`;
-
-const LogContainer = styled.div`
-  padding: 14px;
-  font-size: 13px;
-  background: #121318;
-  user-select: text;
-  overflow-wrap: break-word;
-  overflow-y: auto;
-  min-height: 55px;
-  color: #aaaabb;
-  height: 400px;
-  border-radius: 4px;
-  margin: 12px 0 24px 0;
-`;

+ 0 - 524
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentPage.tsx

@@ -1,524 +0,0 @@
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import { useParams } from "react-router";
-import styled from "styled-components";
-
-import loading from "assets/loading.gif";
-import { Drawer, withStyles } from "@material-ui/core";
-import EventDrawer from "./EventDrawer";
-import { useRouting } from "shared/routing";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import DynamicLink from "components/DynamicLink";
-import Header from "components/expanded-object/Header";
-import { capitalize } from "shared/string_utils";
-import Description from "components/Description";
-import { dateFormatter } from "../../chart/JobRunTable";
-
-type IncidentPageParams = {
-  incident_id: string;
-};
-
-const IncidentPage = () => {
-  const { incident_id } = useParams<IncidentPageParams>();
-
-  const { currentProject, currentCluster } = useContext(Context);
-
-  const [incident, setIncident] = useState<Incident>(null);
-
-  const [isRefreshing, setIsRefreshing] = useState(false);
-  const [selectedEvent, setSelectedEvent] = useState<IncidentEvent>(null);
-
-  const { getQueryParam, pushFiltered } = useRouting();
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    setIncident(null);
-
-    api
-      .getIncidentById<Incident>(
-        "<token>",
-        { incident_id },
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
-        }
-      )
-      .then((res) => {
-        if (!isSubscribed) {
-          return;
-        }
-
-        let incident = res.data;
-
-        incident.events = convertEventsTimestampsToMilliseconds(
-          incident.events
-        );
-
-        setIncident(incident);
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [incident_id]);
-
-  const refreshIncident = async () => {
-    setIsRefreshing(true);
-    try {
-      let incident = await api
-        .getIncidentById<Incident>(
-          "<token>",
-          { incident_id },
-          {
-            cluster_id: currentCluster.id,
-            project_id: currentProject.id,
-          }
-        )
-        .then((res) => res.data);
-
-      incident.events = convertEventsTimestampsToMilliseconds(incident.events);
-
-      setIncident(incident);
-    } catch (error) {
-    } finally {
-      setIsRefreshing(false);
-    }
-  };
-
-  const events = useMemo(() => {
-    return groupEventsByDate(incident?.events);
-  }, [incident]);
-
-  if (incident === null) {
-    return <Loading />;
-  }
-
-  const getBackLink = () => {
-    return (
-      getQueryParam("redirect_url") ||
-      "/cluster-dashboard?selected_tab=incidents"
-    );
-  };
-
-  const getResourceLink = () => {
-    let chartName = incident?.chart_name.split("-")[0] || "web";
-    let namespace = incident?.incident_id.split(":")[2] || "default";
-
-    if (chartName == "job") {
-      return `/jobs/${currentCluster.name}/${namespace}/${incident?.release_name}`;
-    }
-
-    return `/applications/${currentCluster.name}/${namespace}/${incident?.release_name}`;
-  };
-
-  return (
-    <StyledExpandedNodeView>
-      <HeaderWrapper>
-        <Header
-          last_updated={dateFormatter(incident.updated_at * 1000)}
-          back_link={getBackLink()}
-          name={"Incident for " + incident.release_name}
-          icon={"error"}
-          materialIconClass="material-icons"
-          inline_title_items={[
-            <ResourceLink
-              key="resource_link"
-              to={getResourceLink()}
-              target="_blank"
-              onClick={(e) => e.stopPropagation()}
-            >
-              {incident.release_name}
-              <i className="material-icons">open_in_new</i>
-            </ResourceLink>,
-          ]}
-          sub_title_items={[
-            <StatusContainer>
-              <Status>
-                <StatusDot status={incident.latest_state} />
-                {capitalize(incident.latest_state)}
-              </Status>
-              <StatusText>
-                - started {dateFormatter(incident.created_at * 1000)}, last
-                updated {dateFormatter(incident.updated_at * 1000)}
-              </StatusText>
-              <Description></Description>
-            </StatusContainer>,
-          ]}
-        />
-      </HeaderWrapper>
-      <LineBreak />
-      <BodyWrapper>
-        <RefreshButton onClick={refreshIncident} disabled={isRefreshing}>
-          {isRefreshing ? (
-            <>
-              <img src={loading} alt="loading icon" />
-            </>
-          ) : (
-            <i className="material-icons">refresh</i>
-          )}
-        </RefreshButton>
-        {Object.entries(events).map(([date, events_list]) => (
-          <React.Fragment key={date}>
-            <StyledDate>{date}</StyledDate>
-
-            {events_list.map((event) => {
-              return (
-                <StyledCard
-                  key={event.event_id}
-                  onClick={() => setSelectedEvent(event)}
-                  active={selectedEvent?.event_id === event.event_id}
-                >
-                  <ContentContainer>
-                    <Icon status={"normal"} className="material-icons-outlined">
-                      info
-                    </Icon>
-                    <EventInformation>
-                      <EventName>
-                        <Helper>Pod:</Helper>
-                        {event.pod_name}
-                      </EventName>
-                      <EventReason>{event.message}</EventReason>
-                    </EventInformation>
-                  </ContentContainer>
-                  <ActionContainer>
-                    <TimestampContainer>
-                      <TimestampIcon className="material-icons-outlined">
-                        access_time
-                      </TimestampIcon>
-                      <span>
-                        {Intl.DateTimeFormat([], {
-                          // @ts-ignore
-                          dateStyle: "full",
-                          timeStyle: "long",
-                        }).format(new Date(event.timestamp))}
-                      </span>
-                    </TimestampContainer>
-                  </ActionContainer>
-                </StyledCard>
-              );
-            })}
-          </React.Fragment>
-        ))}
-      </BodyWrapper>
-      <StyledDrawer
-        anchor="right"
-        open={!!selectedEvent}
-        onClose={() => setSelectedEvent(null)}
-      >
-        <EventDrawer
-          event={selectedEvent}
-          closeDrawer={() => setSelectedEvent(null)}
-        />
-      </StyledDrawer>
-    </StyledExpandedNodeView>
-  );
-};
-
-export default IncidentPage;
-
-const convertEventsTimestampsToMilliseconds = (events: IncidentEvent[]) => {
-  return events.map((e) => {
-    let newEvent = e;
-
-    newEvent.timestamp = newEvent.timestamp * 1000;
-
-    return newEvent;
-  });
-};
-
-const groupEventsByDate = (
-  events: IncidentEvent[]
-): { [key: string]: IncidentEvent[] } => {
-  if (!events?.length) {
-    return {};
-  }
-
-  return events.reduce<{ [key: string]: IncidentEvent[] }>(
-    (accumulator, current) => {
-      // @ts-ignore
-      const date = Intl.DateTimeFormat([], { dateStyle: "full" }).format(
-        new Date(current.timestamp)
-      );
-
-      if (accumulator[date]?.length) {
-        accumulator[date].push(current);
-      } else {
-        accumulator[date] = [current];
-      }
-
-      return accumulator;
-    },
-    {}
-  );
-};
-
-export type IncidentContainerEvent = {
-  container_name: string;
-  reason: string;
-  message: string;
-  exit_code: number;
-  log_id: string;
-};
-
-export type IncidentEvent = {
-  event_id: string;
-  pod_name: string;
-  cluster: string;
-  namespace: string;
-  release_name: string;
-  release_type: string;
-  timestamp: number;
-  pod_phase: string;
-  pod_status: string;
-  reason: string;
-  message: string;
-  container_events: {
-    [key: string]: IncidentContainerEvent;
-  };
-};
-
-export type Incident = {
-  incident_id: string;
-  release_name: string; // eg: "sample-web"
-  latest_state: string; // "ONGOING" or "RESOLVED"
-  latest_reason: string; // eg: "Out of memory",
-  latest_message: string; // eg: "Application crash due to out of memory issue"
-  events: IncidentEvent[];
-  created_at: number;
-  updated_at: number;
-  chart_name: string;
-};
-
-const RefreshButton = styled.button`
-  position: absolute;
-  right: 0px;
-  top: 20px;
-  border: 1px solid #ffffff00;
-  border-radius: 50%;
-  background: inherit;
-  color: #ffffff;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 35px;
-  height: 35px;
-
-  > i {
-    font-size: 20px;
-  }
-  > img {
-    width: 20px;
-    height: 20px;
-  }
-
-  :hover {
-    color: #ffffff88;
-    border-color: #ffffff88;
-  }
-`;
-
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 1px;
-  background: #494b4f;
-  margin: 10px 0px 35px;
-`;
-
-const BodyWrapper = styled.div`
-  position: relative;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-`;
-
-const StyledExpandedNodeView = 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 StyledDate = styled.div`
-  font-size: 18px;
-  font-weight: bold;
-  color: #ffffff;
-  margin-bottom: 20px;
-  margin-top: 20px;
-  :first-child {
-    margin-top: 0px;
-  }
-`;
-
-const StyledCard = styled.div<{ active: boolean }>`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  border: 1px solid ${({ active }) => (active ? "#819bfd" : "#ffffff44")};
-  background: #ffffff08;
-  margin-bottom: 5px;
-  border-radius: 10px;
-  padding: 14px;
-  overflow: hidden;
-  height: 80px;
-  font-size: 13px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-    border: 1px solid ${({ active }) => (active ? "#819bfd" : "#ffffff66")};
-  }
-  animation: fadeIn 0.5s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-  :not(:last-child) {
-    margin-bottom: 15px;
-  }
-`;
-
-const ContentContainer = styled.div`
-  display: flex;
-  height: 100%;
-  width: 100%;
-  align-items: center;
-`;
-
-const Icon = styled.span<{ status: "critical" | "normal" }>`
-  font-size: 20px;
-  margin-left: 10px;
-  margin-right: 20px;
-  color: ${({ status }) => (status === "critical" ? "#ff385d" : "#aaaabb")};
-`;
-
-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 Helper = styled.span`
-  text-transform: capitalize;
-  color: #ffffff44;
-  margin-right: 5px;
-`;
-
-const EventReason = styled.div`
-  font-family: "Work Sans", sans-serif;
-  color: #aaaabb;
-  margin-top: 5px;
-`;
-
-const ActionContainer = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  height: 100%;
-`;
-
-const TimestampContainer = styled.div`
-  display: flex;
-  white-space: nowrap;
-  align-items: center;
-  justify-self: flex-end;
-  color: #ffffff55;
-  margin-right: 10px;
-  font-size: 13px;
-  min-width: 130px;
-  justify-content: space-between;
-`;
-
-const TimestampIcon = styled.span`
-  margin-right: 7px;
-  font-size: 18px;
-`;
-
-const StyledDrawer = withStyles({
-  paperAnchorRight: {
-    background: "#202227",
-    minWidth: "700px",
-  },
-})(Drawer);
-
-const ResourceLink = styled(DynamicLink)`
-  font-size: 13px;
-  font-weight: 400;
-  margin-left: 20px;
-  color: #aaaabb;
-  display: flex;
-  align-items: center;
-
-  :hover {
-    text-decoration: underline;
-    color: white;
-  }
-
-  > i {
-    margin-left: 7px;
-    font-size: 17px;
-  }
-`;
-
-const Status = styled.span`
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  margin-left: 1px;
-  min-height: 17px;
-  color: #a7a6bb;
-  margin-right: 6px;
-`;
-
-const StatusDot = styled.div`
-  width: 8px;
-  height: 8px;
-  background: ${(props: { status: string }) =>
-    props.status === "ONGOING" ? "#ed5f85" : "#4797ff"};
-  border-radius: 20px;
-  margin-left: 3px;
-  margin-right: 15px;
-`;
-
-const StatusContainer = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-  color: #aaaabb;
-  width: 100%;
-`;
-
-const StatusText = styled.div`
-  width: 100%;
-`;

+ 0 - 215
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTab.tsx

@@ -1,215 +0,0 @@
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import styled from "styled-components";
-import IncidentsTable from "./IncidentsTable";
-
-export type DetectAgentResponse = {
-  version: string;
-};
-
-const IncidentsTab = () => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [isAgentInstalled, setIsAgentInstalled] = useState(false);
-  const [isAgentOutdated, setIsAgentOutdated] = useState(false);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    api
-      .detectPorterAgent<DetectAgentResponse>(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => res.data)
-      .then((data) => {
-        if (data.version === "v1") {
-          setIsAgentInstalled(true);
-          setIsAgentOutdated(true);
-        } else {
-          setIsAgentInstalled(true);
-          setIsAgentOutdated(false);
-        }
-      })
-      .catch(() => {
-        setIsAgentInstalled(false);
-      })
-      .finally(() => {
-        setIsLoading(false);
-      });
-  }, []);
-
-  const upgradeAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-    try {
-      await api.upgradePorterAgent(
-        "<token>",
-        {},
-        {
-          project_id,
-          cluster_id,
-        }
-      );
-      setIsAgentOutdated(false);
-    } catch (err) {
-      setIsAgentOutdated(true);
-    }
-  };
-
-  const installAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    api
-      .installPorterAgent("<token>", {}, { project_id, cluster_id })
-      .then(() => {
-        setIsAgentInstalled(true);
-      })
-      .catch(() => {
-        setIsAgentInstalled(false);
-      });
-  };
-
-  const triggerInstall = () => {
-    if (isAgentOutdated) {
-      upgradeAgent();
-      return;
-    }
-
-    installAgent();
-  };
-
-  if (isLoading) {
-    return (
-      <StyledCard>
-        <Loading height="200px" />
-      </StyledCard>
-    );
-  }
-
-  if (!isAgentInstalled || isAgentOutdated) {
-    return (
-      <Placeholder>
-        <AgentButtonContainer>
-          <Header>Incident detection is not enabled on this cluster.</Header>
-          <Subheader>
-            In order to view incidents, you must enable incident detection on
-            this cluster.
-          </Subheader>
-          <InstallPorterAgentButton onClick={() => triggerInstall()}>
-            <i className="material-icons">add</i> Enable Incident Detection
-          </InstallPorterAgentButton>
-        </AgentButtonContainer>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <StyledCard>
-      <IncidentsTable />
-    </StyledCard>
-  );
-};
-
-export default IncidentsTab;
-
-const StyledCard = styled.div`
-  margin-top: 35px;
-  background: #26282f;
-  padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
-  position: relative;
-  border: 2px solid #9eb4ff00;
-  width: 100%;
-  :not(:last-child) {
-    margin-bottom: 25px;
-  }
-`;
-
-const InstallPorterAgentButton = styled.button`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  width: 200px;
-  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;
-  box-shadow: 0 5px 8px 0px #00000010;
-  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;
-  margin-top: 35px;
-  padding-bottom: 40px;
-  font-size: 13px;
-  color: #ffffff44;
-  min-height: 400px;
-  height: 50vh;
-  background: #ffffff11;
-  border-radius: 8px;
-  display: flex;
-  align-items: left;
-  justify-content: center;
-  flex-direction: column;
-
-  > i {
-    font-size: 18px;
-    margin-right: 8px;
-  }
-`;
-
-const AgentButtonContainer = styled.div`
-  display: flex;
-  align-items: left;
-  justify-content: center;
-  flex-direction: column;
-  width: 500px;
-  margin: 0 auto;
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;
-
-const Subheader = styled.div``;

+ 0 - 209
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTable.tsx

@@ -1,209 +0,0 @@
-import Table from "components/Table";
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import { Column } from "react-table";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { hardcodedIcons } from "shared/hardcodedNameDict";
-import { useRouting } from "shared/routing";
-import { capitalize } from "shared/string_utils";
-import styled from "styled-components";
-import { dateFormatter } from "../../chart/JobRunTable";
-import { Incident } from "./IncidentPage";
-
-export type IncidentsWithoutEvents = Omit<
-  Incident,
-  "events" | "incident_id"
-> & {
-  id: string;
-};
-
-const IncidentsTable = () => {
-  const { currentCluster, currentProject, setCurrentError } = useContext(
-    Context
-  );
-  const { pushFiltered } = useRouting();
-
-  const [incidents, setIncidents] = useState<IncidentsWithoutEvents[]>(null);
-  const [hasError, setHasError] = useState(false);
-
-  const [isRefreshing, setIsRefreshing] = useState(false);
-
-  useEffect(() => {
-    let isSubscribed = true;
-    setIncidents(null);
-    setHasError(false);
-
-    api
-      .getIncidents<{ incidents: IncidentsWithoutEvents[] }>(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (!isSubscribed) {
-          return;
-        }
-
-        setIncidents(res.data?.incidents || []);
-      })
-      .catch((err) => {
-        setHasError(true);
-        setCurrentError(err);
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [currentCluster, currentProject]);
-
-  const refreshIncidents = async () => {
-    setIsRefreshing(true);
-    try {
-      const incidents = await api
-        .getIncidents<{ incidents: IncidentsWithoutEvents[] }>(
-          "<token>",
-          {},
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        )
-        .then((res) => res.data?.incidents || []);
-
-      setIncidents(incidents);
-    } catch (err) {
-      setHasError(true);
-      setCurrentError(err);
-    } finally {
-      setIsRefreshing(false);
-    }
-  };
-
-  const columns = useMemo(() => {
-    return [
-      {
-        Header: "Release",
-        accessor: "release_name",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          let chartName = original?.chart_name.split("-")[0] || "web";
-
-          return (
-            <KindContainer>
-              <Icon src={hardcodedIcons[chartName] || hardcodedIcons["web"]} />
-              <Kind>{original.release_name}</Kind>
-            </KindContainer>
-          );
-        },
-      },
-      {
-        Header: "Status",
-        accessor: "latest_state",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return (
-            <Status>
-              <StatusDot status={original.latest_state} />
-              {capitalize(original.latest_state)}
-            </Status>
-          );
-        },
-      },
-      {
-        Header: "Message",
-        accessor: "latest_message",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return <Message>{original.latest_message}</Message>;
-        },
-      },
-      {
-        Header: "Started",
-        accessor: "created_at",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return dateFormatter(original.created_at * 1000);
-        },
-      },
-      {
-        Header: "Last Updated",
-        accessor: "updated_at",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return dateFormatter(original.updated_at * 1000);
-        },
-      },
-    ] as Column<IncidentsWithoutEvents>[];
-  }, []);
-
-  const data = useMemo(() => {
-    if (!incidents) {
-      return [];
-    }
-    return incidents;
-  }, [incidents]);
-
-  return (
-    <Table
-      columns={columns}
-      data={data}
-      isLoading={incidents === null}
-      onRowClick={(row: any) => {
-        pushFiltered(`/cluster-dashboard/incidents/${row?.original?.id}`, []);
-      }}
-      hasError={hasError}
-      onRefresh={refreshIncidents}
-      isRefreshing={isRefreshing}
-    />
-  );
-};
-
-export default IncidentsTable;
-
-const KindContainer = styled.div`
-  display: flex;
-  align-items: center;
-  min-width: 200px;
-`;
-
-const Kind = styled.div`
-  margin-left: 8px;
-`;
-
-const Icon = styled.img`
-  height: 20px;
-`;
-
-const Status = styled.span`
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  margin-left: 1px;
-  min-height: 17px;
-  color: #a7a6bb;
-`;
-
-const StatusDot = styled.div`
-  width: 8px;
-  height: 8px;
-  background: ${(props: { status: string }) =>
-    props.status === "ONGOING" ? "#ed5f85" : "#4797ff"};
-  border-radius: 20px;
-  margin-left: 3px;
-  margin-right: 15px;
-`;
-
-const Message = styled.div`
-  white-space: nowrap;
-  overflow-x: hidden;
-  text-overflow: ellipsis;
-  max-width: 500px;
-`;

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

@@ -560,7 +560,8 @@ const ExpandedChart: React.FC<Props> = (props) => {
       currentChart.chart.metadata.home === "https://getporter.dev/" &&
       (currentChart.chart.metadata.name === "web" ||
         currentChart.chart.metadata.name === "worker" ||
-        currentChart.chart.metadata.name === "job")
+        currentChart.chart.metadata.name === "job") &&
+      currentCluster.agent_integration_enabled
     ) {
       leftTabOptions.push({ label: "Events", value: "events" });
 
@@ -762,7 +763,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
         })
         .catch(console.log);
 
-        return;
+      return;
     }
 
     setCurrentChart(props.currentChart);
@@ -851,6 +852,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
               isFullscreen={true}
               setIsFullscreen={setIsFullscreen}
               currentChart={currentChart}
+              setInitData={() => {}}
             />
           ) : (
             <StyledExpandedChart>
@@ -979,14 +981,12 @@ const ExpandedChart: React.FC<Props> = (props) => {
                               },
                             }}
                             overrideCurrentTab={overrideCurrentTab}
-                            onTabChange={
-                              (newTab) => {
-                                if (newTab !== "logs") {
-                                  setOverrideCurrentTab("");
-                                  setLogData({});
-                                }
+                            onTabChange={(newTab) => {
+                              if (newTab !== "logs") {
+                                setOverrideCurrentTab("");
+                                setLogData({});
                               }
-                            }
+                            }}
                           />
                         </BodyWrapper>
                       )}

+ 3 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useMemo, useState } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
 
@@ -28,6 +28,7 @@ import CronParser from "cron-parser";
 import CronPrettifier from "cronstrue";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import { useStackEnvGroups } from "./useStackEnvGroups";
+import api from "shared/api";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -46,7 +47,7 @@ export const ExpandedJobChartFC: React.FC<{
   closeChart: () => void;
   setSidebar: (x: boolean) => void;
 }> = ({ currentChart: oldChart, closeChart, currentCluster }) => {
-  const { setCurrentOverlay } = useContext(Context);
+  const { currentProject, setCurrentOverlay } = useContext(Context);
   const [isAuthorized] = useAuth();
   const {
     chart,

+ 102 - 28
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -16,6 +16,7 @@ import Modal from "main/home/modals/Modal";
 import time from "assets/time.svg";
 import { Context } from "shared/Context";
 import { InitLogData } from "../logs-section/LogsSection";
+import { setServers } from "dns";
 
 type Props = {
   filters: any;
@@ -28,18 +29,37 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
   const [expandedEvent, setExpandedEvent] = useState(null);
   const [expandedIncidentEvents, setExpandedIncidentEvents] = useState(null);
   const [isLoading, setIsLoading] = useState(true);
+  const [refresh, setRefresh] = useState(true);
 
   useEffect(() => {
-    api
-      .listPorterEvents("<token>", filters, {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-      })
-      .then((res) => {
-        setEvents(res.data.events);
-        setIsLoading(false);
-      });
-  }, []);
+    if (!refresh) {
+      return;
+    }
+
+    if (filters.job_name) {
+      api
+        .listPorterJobEvents("<token>", filters, {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        })
+        .then((res) => {
+          setEvents(res.data.events);
+          setIsLoading(false);
+          setRefresh(false);
+        });
+    } else {
+      api
+        .listPorterEvents("<token>", filters, {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        })
+        .then((res) => {
+          setEvents(res.data.events);
+          setIsLoading(false);
+          setRefresh(false);
+        });
+    }
+  }, [refresh]);
 
   useEffect(() => {
     if (!expandedEvent) {
@@ -49,11 +69,12 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
     api
       .getIncidentEvents(
         "<token>",
-        {},
+        {
+          incident_id: expandedEvent.id,
+        },
         {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
-          incident_id: expandedEvent.id,
         }
       )
       .then((res) => {
@@ -65,11 +86,12 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
     api
       .getIncidentEvents(
         "<token>",
-        {},
+        {
+          incident_id: incident.id,
+        },
         {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
-          incident_id: incident.id,
         }
       )
       .then((res) => {
@@ -121,6 +143,24 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
     );
   };
 
+  const renderJobStartedCell = (timestamp: any) => {
+    return (
+      <NameWrapper>
+        <AlertIcon src={time} />
+        The job started at {readableDate(timestamp)}
+      </NameWrapper>
+    );
+  };
+
+  const renderJobFinishedCell = (timestamp: any) => {
+    return (
+      <NameWrapper>
+        <AlertIcon src={time} />
+        The job finished at {readableDate(timestamp)}
+      </NameWrapper>
+    );
+  };
+
   const columns = React.useMemo(
     () => [
       {
@@ -135,6 +175,10 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
                 return renderIncidentSummaryCell(row.original.data);
               } else if (row.original.type == "deployment_finished") {
                 return renderDeploymentFinishedCell(row.original.data);
+              } else if (row.original.type == "job_started") {
+                return renderJobStartedCell(row.original.timestamp);
+              } else if (row.original.type == "job_finished") {
+                return renderJobFinishedCell(row.original.timestamp);
               }
 
               return null;
@@ -230,20 +274,22 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
           <Loading />
         </LoadWrapper>
       ) : (
-        <>
-          {events?.length > 0 ? (
-            <TableWrapper>
-              <EventTable columns={columns} data={events} />
-            </TableWrapper>
-          ) : (
-            <Placeholder>
-              <NoResultsFoundWrapper>
-                <Title>No results found</Title>
-                There were no results found for this filter.
-              </NoResultsFoundWrapper>
-            </Placeholder>
-          )}
-        </>
+        <TableWrapper>
+          <EventTable columns={columns} data={events} />
+          <FlexRow>
+            <Flex>
+              <Button
+                onClick={() => {
+                  setIsLoading(true);
+                  setRefresh(true);
+                }}
+              >
+                <i className="material-icons">autorenew</i>
+                Refresh
+              </Button>
+            </Flex>
+          </FlexRow>
+        </TableWrapper>
       )}
     </>
   );
@@ -411,3 +457,31 @@ const NoResultsFoundWrapper = styled(Flex)`
   flex-direction: column;
   justify-contents: center;
 `;
+
+const Button = 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;
+  }
+`;
+
+const FlexRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  flex-wrap: wrap;
+  margin-top: 20px;
+`;

+ 5 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventTable.tsx

@@ -1,3 +1,4 @@
+import Placeholder from "components/Placeholder";
 import React from "react";
 import {
   Column,
@@ -25,6 +26,10 @@ const EventTable: React.FC<TableProps> = ({
   data,
   onRowClick,
 }) => {
+  if (data.length == 0) {
+    return <Placeholder>No events found.</Placeholder>;
+  }
+
   const {
     rows,
     getTableProps,

+ 22 - 8
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -11,9 +11,14 @@ import { InitLogData } from "../logs-section/LogsSection";
 type Props = {
   currentChart: any;
   setLogData?: (logData: InitLogData) => void;
+  overridingJobName?: string;
 };
 
-const EventsTab: React.FC<Props> = ({ currentChart, setLogData }) => {
+const EventsTab: React.FC<Props> = ({
+  currentChart,
+  setLogData,
+  overridingJobName,
+}) => {
   const [hasPorterAgent, setHasPorterAgent] = useState(true);
   const { currentProject, currentCluster } = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
@@ -55,6 +60,21 @@ const EventsTab: React.FC<Props> = ({ currentChart, setLogData }) => {
     installAgent();
   };
 
+  const getFilters = () => {
+    if (overridingJobName) {
+      return {
+        release_name: currentChart.name,
+        release_namespace: currentChart.namespace,
+        job_name: overridingJobName,
+      };
+    }
+
+    return {
+      release_name: currentChart.name,
+      release_namespace: currentChart.namespace,
+    };
+  };
+
   if (isLoading) {
     return (
       <Placeholder>
@@ -79,13 +99,7 @@ const EventsTab: React.FC<Props> = ({ currentChart, setLogData }) => {
 
   return (
     <EventsPageWrapper>
-      <EventList
-        setLogData={setLogData}
-        filters={{
-          release_name: currentChart.name,
-          release_namespace: currentChart.namespace,
-        }}
-      />
+      <EventList setLogData={setLogData} filters={getFilters()} />
     </EventsPageWrapper>
   );
 };

+ 0 - 217
dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx

@@ -1,217 +0,0 @@
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import styled from "styled-components";
-import IncidentsTable from "./IncidentsTable";
-
-export type DetectAgentResponse = {
-  version: string;
-};
-
-const IncidentsTab = (props: {
-  releaseName: string;
-  namespace: string;
-}): JSX.Element => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [isAgentInstalled, setIsAgentInstalled] = useState(false);
-  const [isAgentOutdated, setIsAgentOutdated] = useState(false);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    api
-      .detectPorterAgent<DetectAgentResponse>(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => res.data)
-      .then((data) => {
-        if (data.version === "v1") {
-          setIsAgentInstalled(true);
-          setIsAgentOutdated(true);
-        } else {
-          setIsAgentInstalled(true);
-          setIsAgentOutdated(false);
-        }
-      })
-      .catch(() => {
-        setIsAgentInstalled(false);
-      })
-      .finally(() => {
-        setIsLoading(false);
-      });
-  }, []);
-
-  const upgradeAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-    try {
-      await api.upgradePorterAgent(
-        "<token>",
-        {},
-        {
-          project_id,
-          cluster_id,
-        }
-      );
-      setIsAgentOutdated(false);
-    } catch (err) {
-      setIsAgentOutdated(true);
-    }
-  };
-
-  const installAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    api
-      .installPorterAgent("<token>", {}, { project_id, cluster_id })
-      .then(() => {
-        setIsAgentInstalled(true);
-      })
-      .catch(() => {
-        setIsAgentInstalled(false);
-      });
-  };
-
-  const triggerInstall = () => {
-    if (isAgentOutdated) {
-      upgradeAgent();
-      return;
-    }
-
-    installAgent();
-  };
-
-  if (isLoading) {
-    return (
-      <StyledCard>
-        <Loading height="200px" />
-      </StyledCard>
-    );
-  }
-
-  if (!isAgentInstalled || isAgentOutdated) {
-    return (
-      <Placeholder>
-        <AgentButtonContainer>
-          <Header>Incident detection is not enabled on this cluster.</Header>
-          <Subheader>
-            In order to view incidents, you must enable incident detection on
-            this cluster.
-          </Subheader>
-          <InstallPorterAgentButton onClick={() => triggerInstall()}>
-            <i className="material-icons">add</i> Enable Incident Detection
-          </InstallPorterAgentButton>
-        </AgentButtonContainer>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <StyledCard>
-      <IncidentsTable {...props} />
-    </StyledCard>
-  );
-};
-
-export default IncidentsTab;
-
-const StyledCard = styled.div`
-  background: #26282f;
-  padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
-  position: relative;
-  border: 2px solid #9eb4ff00;
-  width: 100%;
-  :not(:last-child) {
-    margin-bottom: 25px;
-  }
-`;
-
-const InstallPorterAgentButton = styled.button`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  width: 200px;
-  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;
-  box-shadow: 0 5px 8px 0px #00000010;
-  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;
-  margin-top: 35px;
-  padding-bottom: 40px;
-  font-size: 13px;
-  color: #ffffff44;
-  min-height: 400px;
-  height: 50vh;
-  background: #ffffff11;
-  border-radius: 8px;
-  display: flex;
-  align-items: left;
-  justify-content: center;
-  flex-direction: column;
-
-  > i {
-    font-size: 18px;
-    margin-right: 8px;
-  }
-`;
-
-const AgentButtonContainer = styled.div`
-  display: flex;
-  align-items: left;
-  justify-content: center;
-  flex-direction: column;
-  width: 500px;
-  margin: 0 auto;
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;
-
-const Subheader = styled.div``;

+ 0 - 217
dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/IncidentsTable.tsx

@@ -1,217 +0,0 @@
-import Table from "components/Table";
-import React, { useContext, useEffect, useMemo, useState } from "react";
-import { useLocation } from "react-router";
-import { Column } from "react-table";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { hardcodedIcons } from "shared/hardcodedNameDict";
-import { useRouting } from "shared/routing";
-import { capitalize } from "shared/string_utils";
-import styled from "styled-components";
-import { dateFormatter } from "../../chart/JobRunTable";
-import { IncidentsWithoutEvents } from "../../dashboard/incidents/IncidentsTable";
-
-const IncidentsTable = ({
-  releaseName,
-  namespace,
-}: {
-  releaseName: string;
-  namespace: string;
-}) => {
-  const { currentCluster, currentProject, setCurrentError } = useContext(
-    Context
-  );
-  const { pushFiltered } = useRouting();
-  const location = useLocation();
-
-  const [incidents, setIncidents] = useState<IncidentsWithoutEvents[]>(null);
-  const [hasError, setHasError] = useState(false);
-  const [isRefreshing, setIsRefreshing] = useState(false);
-
-  useEffect(() => {
-    let isSubscribed = true;
-    setIncidents(null);
-    setHasError(false);
-
-    api
-      .getIncidentsByReleaseName<{ incidents: IncidentsWithoutEvents[] }>(
-        "<token>",
-        {
-          namespace: namespace,
-          release_name: releaseName,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (!isSubscribed) {
-          return;
-        }
-
-        setIncidents(res.data?.incidents || []);
-      })
-      .catch((err) => {
-        setHasError(true);
-        setCurrentError(err);
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [currentCluster, currentProject]);
-
-  const refreshIncidents = async () => {
-    setIsRefreshing(true);
-    try {
-      const incidents = await api
-        .getIncidentsByReleaseName<{ incidents: IncidentsWithoutEvents[] }>(
-          "<token>",
-          {
-            namespace: namespace,
-            release_name: releaseName,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          }
-        )
-        .then((res) => res.data?.incidents || []);
-
-      setIncidents(incidents);
-    } catch (err) {
-      setHasError(true);
-      setCurrentError(err);
-    } finally {
-      setIsRefreshing(false);
-    }
-  };
-
-  const columns = useMemo(() => {
-    return [
-      {
-        Header: "Status",
-        accessor: "latest_state",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return (
-            <Status>
-              <StatusDot status={original.latest_state} />
-              {capitalize(original.latest_state)}
-            </Status>
-          );
-        },
-      },
-      {
-        Header: "Message",
-        accessor: "latest_message",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return <Message>{original.latest_message}</Message>;
-        },
-      },
-      {
-        Header: "Started",
-        accessor: "created_at",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return dateFormatter(original.created_at * 1000);
-        },
-      },
-      {
-        Header: "Last Updated",
-        accessor: "updated_at",
-        Cell: ({ row }) => {
-          let original = row.original;
-
-          return dateFormatter(original.updated_at * 1000);
-        },
-      },
-    ] as Column<IncidentsWithoutEvents>[];
-  }, []);
-
-  const data = useMemo(() => {
-    if (!incidents) {
-      return [];
-    }
-    return incidents;
-  }, [incidents]);
-
-  return (
-    <Table
-      columns={columns}
-      data={data}
-      isLoading={incidents === null}
-      onRowClick={(row: any) => {
-        pushFiltered(`/cluster-dashboard/incidents/${row?.original?.id}/`, [], {
-          redirect_url: location.pathname,
-        });
-      }}
-      hasError={hasError}
-      onRefresh={refreshIncidents}
-      isRefreshing={isRefreshing}
-    />
-  );
-};
-
-export default IncidentsTable;
-
-const TableWrapper = styled.div``;
-
-const StyledCard = styled.div`
-  background: #26282f;
-  padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
-  position: relative;
-  border: 2px solid #9eb4ff00;
-  width: 100%;
-  height: 100%;
-  :not(:last-child) {
-    margin-bottom: 25px;
-  }
-`;
-
-const KindContainer = styled.div`
-  display: flex;
-  align-items: center;
-  min-width: 200px;
-`;
-
-const Kind = styled.div`
-  margin-left: 8px;
-`;
-
-const Icon = styled.img`
-  height: 20px;
-`;
-
-const Status = styled.span`
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  margin-left: 1px;
-  min-height: 17px;
-  color: #a7a6bb;
-`;
-
-const StatusDot = styled.div`
-  width: 8px;
-  height: 8px;
-  background: ${(props: { status: string }) =>
-    props.status === "ONGOING" ? "#ed5f85" : "#4797ff"};
-  border-radius: 20px;
-  margin-left: 3px;
-  margin-right: 15px;
-`;
-
-const Message = styled.div`
-  white-space: nowrap;
-  overflow-x: hidden;
-  text-overflow: ellipsis;
-  max-width: 500px;
-`;

+ 95 - 59
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx

@@ -5,7 +5,7 @@ import styled from "styled-components";
 import leftArrow from "assets/left-arrow.svg";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import Loading from "components/Loading";
-import TabRegion from "components/TabRegion";
+import TabRegion, { TabOption } from "components/TabRegion";
 import TitleSection from "components/TitleSection";
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -14,6 +14,9 @@ import DeploymentType from "../DeploymentType";
 import JobMetricsSection from "../metrics/JobMetricsSection";
 import Logs from "../status/Logs";
 import { useRouting } from "shared/routing";
+import Banner from "components/Banner";
+import LogsSection from "../logs-section/LogsSection";
+import EventsTab from "../events/EventsTab";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -54,36 +57,7 @@ const renderStatus = (job: any, pods: any[], time: string) => {
   }
 
   if (job.status?.failed >= 1) {
-    const appPod = getLatestPod(pods);
-
-    if (appPod) {
-      const appContainerStatus = appPod?.status?.containerStatuses?.find(
-        (container: any) =>
-          container?.state?.terminated?.reason !== "Completed" &&
-          !container?.state?.running
-      );
-
-      if (appContainerStatus) {
-        const reason = appContainerStatus.state.terminated.reason;
-        const exitCode = appContainerStatus.state.terminated.exitCode;
-        const finishTime = appContainerStatus.state.terminated.finishedAt;
-
-        return (
-          <Status color="#cc3d42">
-            Failed at {time ? time : readableDate(finishTime)} - Reason:{" "}
-            {reason} - Exit Code: {exitCode}
-          </Status>
-        );
-      }
-    }
-
-    return (
-      <Status color="#cc3d42">
-        Failed {time}
-        {job.status.conditions.length > 0 &&
-          `: ${job.status.conditions[0].reason}`}
-      </Status>
-    );
+    return <Status color="#cc3d42">Failed</Status>;
   }
 
   return <Status color="#ffffff11">Running</Status>;
@@ -102,11 +76,12 @@ const ExpandedJobRun = ({
     Context
   );
   const [currentTab, setCurrentTab] = useState<
-    "logs" | "metrics" | "config" | string
-  >("logs");
+    "events" | "logs" | "metrics" | "config" | string
+  >(currentCluster.agent_integration_enabled ? "events" : "logs");
   const [pods, setPods] = useState<any>(null);
   const [isLoading, setIsLoading] = useState(true);
   const { pushQueryParams } = useRouting();
+  const [useDeprecatedLogs, setUseDeprecatedLogs] = useState(false);
 
   let chart = currentChart;
   let run = jobRun;
@@ -191,10 +166,80 @@ const ExpandedJobRun = ({
     );
   };
 
+  const renderEventsSection = () => {
+    return (
+      <EventsTab
+        currentChart={currentChart}
+        overridingJobName={jobRun.metadata?.name}
+      />
+    );
+  };
+
+  const renderLogsSection = () => {
+    if (useDeprecatedLogs || !currentCluster.agent_integration_enabled) {
+      return (
+        <JobLogsWrapper>
+          <Logs
+            selectedPod={pods[0]}
+            podError={!pods[0] ? "Pod no longer exists." : ""}
+            rawText={true}
+          />
+        </JobLogsWrapper>
+      );
+    }
+
+    return (
+      <JobLogsWrapper>
+        <DeprecatedWarning>
+          Not seeing your logs? Switch back to{" "}
+          <DeprecatedSelect
+            onClick={() => {
+              setUseDeprecatedLogs(true);
+            }}
+          >
+            {" "}
+            deprecated logging.
+          </DeprecatedSelect>
+        </DeprecatedWarning>
+        <LogsSection
+          isFullscreen={false}
+          setIsFullscreen={() => {}}
+          overridingPodName={pods[0].metadata.name}
+          setInitData={() => {}}
+          currentChart={currentChart}
+        />
+      </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: "Metrics",
+      value: "metrics",
+    },
+    {
+      label: "Config",
+      value: "config",
+    }
+  );
+
   return (
     <StyledExpandedChart>
       <BreadcrumbRow>
@@ -207,7 +252,6 @@ const ExpandedJobRun = ({
         <TitleSection icon={currentChart.chart.metadata.icon} iconWidth="33px">
           {chart.name} <Gray>at {readableDate(run.status.startTime)}</Gray>
         </TitleSection>
-
         <InfoWrapper>
           <LastDeployed>
             {renderStatus(
@@ -228,30 +272,10 @@ const ExpandedJobRun = ({
         <TabRegion
           currentTab={currentTab}
           setCurrentTab={(x: string) => setCurrentTab(x)}
-          options={[
-            {
-              label: "Logs",
-              value: "logs",
-            },
-            {
-              label: "Metrics",
-              value: "metrics",
-            },
-            {
-              label: "Config",
-              value: "config",
-            },
-          ]}
+          options={options}
         >
-          {currentTab === "logs" && (
-            <JobLogsWrapper>
-              <Logs
-                selectedPod={pods[0]}
-                podError={!pods[0] ? "Pod no longer exists." : ""}
-                rawText={true}
-              />
-            </JobLogsWrapper>
-          )}
+          {currentTab === "events" && renderEventsSection()}
+          {currentTab === "logs" && renderLogsSection()}
           {currentTab === "config" && <>{renderConfigSection(run)}</>}
           {currentTab === "metrics" && (
             <JobMetricsSection jobChart={currentChart} jobRun={run} />
@@ -323,10 +347,9 @@ const ConfigSection = styled.div`
 
 const JobLogsWrapper = styled.div`
   min-height: 450px;
-  height: 55vh;
+  height: 65vh;
   width: 100%;
   border-radius: 8px;
-  background-color: black;
   overflow-y: auto;
 `;
 
@@ -468,3 +491,16 @@ const StyledExpandedChart = styled.div`
     }
   }
 `;
+
+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;
+`;

+ 14 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -33,6 +33,7 @@ type Props = {
   setIsFullscreen: (x: boolean) => void;
   initData?: InitLogData;
   setInitData?: (initData: InitLogData) => void;
+  overridingPodName?: string;
 };
 
 const escapeRegExp = (str: string) => {
@@ -100,12 +101,19 @@ const LogsSection: React.FC<Props> = ({
   setIsFullscreen,
   initData = {},
   setInitData,
+  overridingPodName,
 }) => {
   const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
   const { currentProject, currentCluster } = useContext(Context);
-  const [podFilter, setPodFilter] = useState(initData.podName);
+  const [podFilter, setPodFilter] = useState(
+    initData.podName || overridingPodName
+  );
   const [podFilterOpts, setPodFilterOpts] = useState<string[]>(
-    _.compact([initData.podName])
+    initData?.podName
+      ? _.compact([initData.podName])
+      : overridingPodName
+      ? _.compact([overridingPodName])
+      : []
   );
   const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
   const [searchText, setSearchText] = useState("");
@@ -123,6 +131,10 @@ const LogsSection: React.FC<Props> = ({
   );
 
   useEffect(() => {
+    if (overridingPodName) {
+      return;
+    }
+
     api
       .getLogPodValues(
         "<TOKEN>",

+ 28 - 7
dashboard/src/shared/api.tsx

@@ -96,10 +96,11 @@ const overwriteAWSIntegration = baseApi<
   return `/api/projects/${pathParams.project_id}/integrations/aws/overwrite`;
 });
 
-const updateClusterName = baseApi<
+const updateCluster = baseApi<
   {
-    name: string;
+    name?: string;
     aws_cluster_id?: string;
+    agent_integration_enabled?: boolean;
   },
   {
     project_id: number;
@@ -2004,6 +2005,23 @@ const listPorterEvents = baseApi<
     `/api/projects/${project_id}/clusters/${cluster_id}/events`
 );
 
+const listPorterJobEvents = baseApi<
+  {
+    release_name?: number;
+    release_namespace?: string;
+    type?: string;
+    job_name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>(
+  "GET",
+  ({ project_id, cluster_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/events/job`
+);
+
 const listIncidents = baseApi<
   {
     release_name?: number;
@@ -2034,16 +2052,18 @@ const getIncident = baseApi<
 );
 
 const getIncidentEvents = baseApi<
-  {},
+  {
+    incident_id?: string;
+    pod_prefix?: string;
+  },
   {
     project_id: number;
     cluster_id: number;
-    incident_id: string;
   }
 >(
   "GET",
-  ({ project_id, cluster_id, incident_id }) =>
-    `/api/projects/${project_id}/clusters/${cluster_id}/incidents/${incident_id}/events`
+  ({ project_id, cluster_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/incidents/events`
 );
 
 // STACKS
@@ -2233,7 +2253,7 @@ export default {
   getGitlabIntegration,
   createAWSIntegration,
   overwriteAWSIntegration,
-  updateClusterName,
+  updateCluster,
   createAzureIntegration,
   createGitlabIntegration,
   createEmailVerification,
@@ -2407,6 +2427,7 @@ export default {
   getLogPodValues,
   getLogs,
   listPorterEvents,
+  listPorterJobEvents,
   listIncidents,
   getIncident,
   getIncidentEvents,

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

@@ -5,6 +5,7 @@ export interface ClusterType {
   name: string;
   server: string;
   service_account_id: number;
+  agent_integration_enabled: boolean;
   infra_id?: number;
   service?: string;
   aws_integration_id?: number;

+ 58 - 2
internal/kubernetes/porter_agent/v2/agent_server.go

@@ -67,6 +67,54 @@ func ListPorterEvents(
 	return eventsResp, nil
 }
 
+func ListPorterJobEvents(
+	clientset kubernetes.Interface,
+	service *v1.Service,
+	req *types.ListJobEventsRequest,
+) (*types.ListEventsResponse, error) {
+	vals := make(map[string]string)
+
+	vals["job_name"] = req.JobName
+
+	if req.Type != nil {
+		vals["type"] = *req.Type
+	}
+
+	if req.ReleaseName != nil {
+		vals["release_name"] = *req.ReleaseName
+	}
+
+	if req.ReleaseNamespace != nil {
+		vals["release_namespace"] = *req.ReleaseNamespace
+	}
+
+	if req.PaginationRequest != nil {
+		vals["page"] = fmt.Sprintf("%d", req.PaginationRequest.Page)
+	}
+
+	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
+		"http",
+		service.Name,
+		fmt.Sprintf("%d", service.Spec.Ports[0].Port),
+		"/events/job",
+		vals,
+	)
+
+	rawQuery, err := resp.DoRaw(context.Background())
+	if err != nil {
+		return nil, err
+	}
+
+	eventsResp := &types.ListEventsResponse{}
+
+	err = json.Unmarshal(rawQuery, eventsResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return eventsResp, nil
+}
+
 func ListIncidents(
 	clientset kubernetes.Interface,
 	service *v1.Service,
@@ -143,11 +191,14 @@ func GetIncidentByID(
 func ListIncidentEvents(
 	clientset kubernetes.Interface,
 	service *v1.Service,
-	incidentID string,
 	req *types.ListIncidentEventsRequest,
 ) (*types.ListIncidentEventsResponse, error) {
 	vals := make(map[string]string)
 
+	if req.IncidentID != nil {
+		vals["incident_id"] = *req.IncidentID
+	}
+
 	if req.PodName != nil {
 		vals["pod_name"] = *req.PodName
 	}
@@ -159,15 +210,20 @@ func ListIncidentEvents(
 	if req.Summary != nil {
 		vals["summary"] = *req.Summary
 	}
+
 	if req.PaginationRequest != nil {
 		vals["page"] = fmt.Sprintf("%d", req.PaginationRequest.Page)
 	}
 
+	if req.PodPrefix != nil {
+		vals["pod_prefix"] = *req.PodPrefix
+	}
+
 	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
 		"http",
 		service.Name,
 		fmt.Sprintf("%d", service.Spec.Ports[0].Port),
-		fmt.Sprintf("/incidents/%s/events", incidentID),
+		"/incidents/events",
 		vals,
 	)
 

+ 12 - 8
internal/models/cluster.go

@@ -36,6 +36,9 @@ type Cluster struct {
 	// The project that this integration belongs to
 	ProjectID uint `json:"project_id"`
 
+	// Whether or not the Porter agent integration is enabled on the cluster
+	AgentIntegrationEnabled bool
+
 	// Name of the cluster
 	Name string `json:"name"`
 
@@ -95,14 +98,15 @@ func (c *Cluster) ToClusterType() *types.Cluster {
 	}
 
 	return &types.Cluster{
-		ID:               c.ID,
-		ProjectID:        c.ProjectID,
-		Name:             c.Name,
-		Server:           c.Server,
-		Service:          serv,
-		InfraID:          c.InfraID,
-		AWSIntegrationID: c.AWSIntegrationID,
-		AWSClusterID:     c.AWSClusterID,
+		ID:                      c.ID,
+		ProjectID:               c.ProjectID,
+		Name:                    c.Name,
+		Server:                  c.Server,
+		Service:                 serv,
+		AgentIntegrationEnabled: c.AgentIntegrationEnabled,
+		InfraID:                 c.InfraID,
+		AWSIntegrationID:        c.AWSIntegrationID,
+		AWSClusterID:            c.AWSClusterID,
 	}
 }