فهرست منبع

basic cron support on stacks (#3049)

* basic cron support on stacks

* remove logs

---------

Co-authored-by: Justin Rhee <jusrhee@Justins-MacBook-Air.local>
jusrhee 3 سال پیش
والد
کامیت
8869ebc874

BIN
dashboard/src/assets/history.png


+ 1 - 0
dashboard/src/main/home/Home.tsx

@@ -39,6 +39,7 @@ import Spacer from "components/porter/Spacer";
 import Button from "components/porter/Button";
 import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
 import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
+import ExpandedJob from "./app-dashboard/expanded-app/expanded-job/ExpandedJob";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [

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

@@ -46,6 +46,7 @@ import ActivityFeed from "./ActivityFeed";
 import JobRuns from "./JobRuns";
 import MetricsSection from "./MetricsSection";
 import StatusSectionFC from "./status/StatusSection";
+import ExpandedJob from "./expanded-job/ExpandedJob";
 
 type Props = RouteComponentProps & {};
 
@@ -85,6 +86,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [porterJson, setPorterJson] = useState<
     z.infer<typeof PorterYamlSchema> | undefined
   >(undefined);
+  const [expandedJob, setExpandedJob] = useState(null);
 
   const [services, setServices] = useState<Service[]>([]);
   const [releaseJob, setReleaseJob] = useState<ReleaseService[]>([]);
@@ -559,6 +561,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               chart={appData.chart}
               services={services}
               addNewText={"Add a new service"}
+              setExpandedJob={(x: string) => setExpandedJob(x)}
             />
             <Spacer y={1} />
             <Button
@@ -667,6 +670,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               lastRunStatus="all"
               namespace={appData.chart?.namespace}
               sortType="Newest"
+              releaseName={appData.app.name + "-r"}
             />
             }
           </>
@@ -676,6 +680,16 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     }
   };
 
+  if (expandedJob) {
+    return (
+      <ExpandedJob 
+        appName={appData.app.name}
+        jobName={expandedJob}
+        goBack={() => setExpandedJob(null)}
+      />
+    )
+  }
+
   return (
     <>
       {isLoading && !appData && <Loading />}

+ 78 - 29
dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx

@@ -15,6 +15,9 @@ type Props = {
   lastRunStatus: "failed" | "succeeded" | "active" | "all";
   namespace: string;
   sortType: "Newest" | "Oldest" | "Alphabetical";
+  releaseName?: string;
+  jobName?: string;
+  setExpandedRun?: any;
 };
 
 const runnedFor = (start: string | number, end?: string | number) => {
@@ -32,6 +35,9 @@ const JobRuns: React.FC<Props> = ({
   lastRunStatus,
   namespace,
   sortType,
+  releaseName,
+  jobName,
+  setExpandedRun,
 }) => {
   const { currentCluster, currentProject } = useContext(Context);
   const [jobRuns, setJobRuns] = useState<JobRun[]>(null);
@@ -102,35 +108,35 @@ const JobRuns: React.FC<Props> = ({
     () => [
       {
         Header: "Started",
-        accessor: (originalRow) => relativeDate(originalRow.status.startTime),
+        accessor: (originalRow) => relativeDate(originalRow?.status.startTime),
       },
       {
         Header: "Run for",
         accessor: (originalRow) => {
-          if (originalRow.status?.completionTime) {
-            return originalRow.status?.completionTime;
+          if (originalRow?.status?.completionTime) {
+            return originalRow?.status?.completionTime;
           } else if (
-            Array.isArray(originalRow.status?.conditions) &&
-            originalRow.status?.conditions[0]?.lastTransitionTime
+            Array.isArray(originalRow?.status?.conditions) &&
+            originalRow?.status?.conditions[0]?.lastTransitionTime
           ) {
-            return originalRow.status?.conditions[0]?.lastTransitionTime;
+            return originalRow?.status?.conditions[0]?.lastTransitionTime;
           } else {
             return "Still running...";
           }
         },
         Cell: ({ row }) => {
-          if (row.original.status?.completionTime) {
+          if (row.original?.status?.completionTime) {
             return runnedFor(
-              row.original.status?.startTime,
-              row.original.status?.completionTime
+              row.original?.status?.startTime,
+              row.original?.status?.completionTime
             );
           } else if (
-            Array.isArray(row.original.status?.conditions) &&
-            row.original.status?.conditions[0]?.lastTransitionTime
+            Array.isArray(row.original?.status?.conditions) &&
+            row.original?.status?.conditions[0]?.lastTransitionTime
           ) {
             return runnedFor(
-              row.original.status?.startTime,
-              row.original.status?.conditions[0]?.lastTransitionTime
+              row.original?.status?.startTime,
+              row.original?.status?.conditions[0]?.lastTransitionTime
             );
           } else {
             return "Still running...";
@@ -144,11 +150,11 @@ const JobRuns: React.FC<Props> = ({
         Header: "Status",
         id: "status",
         Cell: ({ row }: CellProps<JobRun>) => {
-          if (row.original.status?.succeeded >= 1) {
+          if (row.original?.status?.succeeded >= 1) {
             return <Status color="#38a88a">Succeeded</Status>;
           }
 
-          if (row.original.status?.failed >= 1) {
+          if (row.original?.status?.failed >= 1) {
             return <Status color="#cc3d42">Failed</Status>;
           }
 
@@ -181,17 +187,28 @@ const JobRuns: React.FC<Props> = ({
           urlParams.append("project_id", String(currentProject.id));
           urlParams.append("chart_revision", String(0));
           urlParams.append("job", row.original.metadata.name);
-
-          return (
-            <RedirectButton
-              to={{
-                pathname: `/jobs/${currentCluster.name}/${row.original?.metadata?.namespace}/${row.original?.metadata?.labels["meta.helm.sh/release-name"]}`,
-                search: `app=${row.original?.metadata?.namespace.split("porter-stack-")[1]}&` + urlParams.toString(),
-              }}
-            >
-              <i className="material-icons">open_in_new</i>
-            </RedirectButton>
-          );
+          if (!setExpandedRun) {
+            return (
+              <RedirectButton
+                to={{
+                  pathname: `/jobs/${currentCluster.name}/${row.original?.metadata?.namespace}/${row.original?.metadata?.labels["meta.helm.sh/release-name"]}`,
+                  search: `app=${row.original?.metadata?.namespace.split("porter-stack-")[1]}&` + urlParams.toString(),
+                }}
+              >
+                <i className="material-icons">open_in_new</i>
+              </RedirectButton>
+            );
+          } else {
+            return (
+              <ExpandButton
+                onClick={() => {
+                  setExpandedRun(row.original);
+                }}
+              >
+                <i className="material-icons">open_in_new</i>
+              </ExpandButton>
+            )
+          }
         },
         maxWidth: 40,
       },
@@ -216,7 +233,7 @@ const JobRuns: React.FC<Props> = ({
         tmp = filter.filterBySucceded();
         break;
       default:
-        tmp = filter.dontFilter();
+        tmp = filter.dontFilter(releaseName, jobName, namespace);
         break;
     }
 
@@ -252,7 +269,7 @@ const JobRuns: React.FC<Props> = ({
   }
 
   if (!jobRuns?.length) {
-    return <Placeholder>No pre-deploy job runs were found.</Placeholder>;
+    return <Placeholder>No job runs were found.</Placeholder>;
   }
 
   return (
@@ -329,6 +346,24 @@ const RedirectButton = styled(DynamicLink)`
   }
 `;
 
+const ExpandButton = styled.div`
+  user-select: none;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  > i {
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    margin: 0 5px;
+    color: #ffffff44;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
 type JobRun = {
   metadata: {
     name: string;
@@ -442,6 +477,7 @@ class JobRunsFilter {
     this.jobRuns = newJobRuns;
   }
 
+  // TODO: to support this filter, add appName filter (see dontFilter())
   filterByFailed() {
     return this.jobRuns.filter((jobRun) => jobRun?.status?.failed);
   }
@@ -459,7 +495,20 @@ class JobRunsFilter {
     );
   }
 
-  dontFilter() {
+  dontFilter(releaseName?: string, jobName?: string, namespace?: string) {
+    if (releaseName) {
+      const filteredJobs = this.jobRuns.filter(x => {
+        return releaseName === x?.metadata?.labels["meta.helm.sh/release-name"];
+      });
+      return filteredJobs;
+    } else if (jobName) {
+      const filteredJobs = this.jobRuns.filter(x => {
+        let name = x?.metadata?.name;
+        let appName = namespace.split("porter-stack-")[1];
+        return name.startsWith(`${appName}-${jobName}`) && name.split(`${appName}-${jobName}-`).length > 1 && name.split(`${appName}-${jobName}-`)[1].split("-").length === 2;
+      });
+      return filteredJobs;
+    } 
     return this.jobRuns;
   }
 }

+ 35 - 2
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -16,14 +16,17 @@ import {
   getAvailabilityStacks,
 } from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
 import Spacer from "components/porter/Spacer";
+import { pushFiltered } from "shared/routing";
+import { RouteComponentProps, useLocation, withRouter } from "react-router";
 import { timeFormat } from "d3-time-format";
 import AnimateHeight, { Height } from "react-animate-height";
 import { ControllerTabPodType } from "./status/ControllerTab";
 import _ from "lodash";
 
-type Props = {
+type Props = RouteComponentProps & {
   chart: any;
   service: any;
+  setExpandedJob: any;
 };
 
 interface ErrorMessage {
@@ -34,12 +37,15 @@ interface ErrorMessage {
 const StatusFooter: React.FC<Props> = ({
   chart,
   service,
+  setExpandedJob,
+  ...props
 }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [controller, setController] = React.useState<any>(null);
   const [available, setAvailable] = React.useState<number>(0);
   const [total, setTotal] = React.useState<number>(0);
   const [stale, setStale] = React.useState<number>(0);
+  const location = useLocation();
   const [unavailable, setUnavailable] = React.useState<number>(0);
   const [height, setHeight] = useState<Height>(0);
   const [expanded, setExpanded] = useState<boolean>(false);
@@ -264,6 +270,33 @@ const StatusFooter: React.FC<Props> = ({
     }
   };
 
+  if (service.type === "job") {
+    return (
+      <StyledStatusFooter>
+        {service.type === "job" && (
+          <Container row>
+            {/*
+            <Mi className="material-icons">check</Mi>
+            <Text color="helper">
+              Last run succeeded at 12:39 PM on 4/13/23
+            </Text>
+            */}
+            <Button
+              onClick={() => setExpandedJob(service.name)}
+              height="30px"
+              width="87px"
+              color="#ffffff11"
+              withBorder
+            >
+              <I className="material-icons">open_in_new</I>
+              History
+            </Button>
+          </Container>
+        )}
+    </StyledStatusFooter>
+    );
+  };
+
   return (
     <>
       {replicaSetArray != null && replicaSetArray.length > 0 && replicaSetArray.map((replicaSet, i) => {
@@ -329,7 +362,7 @@ const StatusFooter: React.FC<Props> = ({
 };
 
 
-export default StatusFooter;
+export default withRouter(StatusFooter);
 
 const StatusDot = styled.div<{ color?: string }>`
   min-width: 7px;

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

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

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

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

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

@@ -21,6 +21,7 @@ interface ServiceProps {
   editService: (service: Service) => void;
   deleteService: () => void;
   defaultExpanded: boolean;
+  setExpandedJob: (x: string) => void;
 }
 
 const ServiceContainer: React.FC<ServiceProps> = ({
@@ -29,6 +30,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   deleteService,
   editService,
   defaultExpanded,
+  setExpandedJob,
 }) => {
   const [showExpanded, setShowExpanded] = React.useState<boolean>(defaultExpanded);
   const [height, setHeight] = React.useState<Height>("auto");
@@ -136,6 +138,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         getHasBuiltImage()
       ) && (
           <StatusFooter
+            setExpandedJob={setExpandedJob}
             chart={chart}
             service={service}
           />

+ 12 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -22,9 +22,19 @@ interface ServicesProps {
   chart?: any;
   limitOne?: boolean;
   customOnClick?: () => void;
+  setExpandedJob?: (x: string) => void;
 }
 
-const Services: React.FC<ServicesProps> = ({ services, setServices, addNewText, chart, defaultExpanded = false, limitOne = false, customOnClick }) => {
+const Services: React.FC<ServicesProps> = ({ 
+  services,
+  setServices,
+  addNewText,
+  chart,
+  defaultExpanded = false,
+  limitOne = false,
+  customOnClick,
+  setExpandedJob,
+}) => {
   const [showAddServiceModal, setShowAddServiceModal] = useState<boolean>(
     false
   );
@@ -72,6 +82,7 @@ const Services: React.FC<ServicesProps> = ({ services, setServices, addNewText,
             return (
               <ServiceContainer
                 key={service.name}
+                setExpandedJob={setExpandedJob}
                 service={service}
                 chart={chart}
                 editService={(newService: Service) =>