Quellcode durchsuchen

Stacks cards refactor + hide non-build cards (#3087)

* committing changes so far

* event cards refactor

---------

Co-authored-by: jusrhee <justin@porter.run>
Feroze Mohideen vor 2 Jahren
Ursprung
Commit
b7018ad074

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

@@ -7,14 +7,14 @@ import { Context } from "shared/Context";
 import Text from "components/porter/Text";
 import Container from "components/porter/Container";
 
-import EventCard from "./EventCard";
+import EventCard from "./events/EventCard";
 import Loading from "components/Loading";
 import Spacer from "components/porter/Spacer";
 import Fieldset from "components/porter/Fieldset";
 
 import { feedDate } from "shared/string_utils";
 import Pagination from "components/porter/Pagination";
-import { PorterAppEvent } from "shared/types";
+import { PorterAppEvent, PorterAppEventType } from "shared/types";
 
 type Props = {
   chart: any;
@@ -45,7 +45,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
         }
       );
       setNumPages(res.data.num_pages);
-      setEvents(res.data.events as PorterAppEvent[]);
+      setEvents((res.data.events as PorterAppEvent[]).filter(e => e.type === PorterAppEventType.BUILD));
       setLoading(false);
     } catch (err) {
       setError(err);

+ 0 - 429
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/EventCard.tsx

@@ -1,429 +0,0 @@
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-
-import app_event from "assets/app_event.png";
-import build from "assets/build.png";
-import deploy from "assets/deploy.png";
-import pre_deploy from "assets/pre_deploy.png";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.png";
-import run_for from "assets/run_for.png";
-import refresh from "assets/refresh.png";
-
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Icon from "components/porter/Icon";
-import Modal from "components/porter/Modal";
-import api from "shared/api";
-import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
-import JSZip from "jszip";
-import Anser, { AnserJsonEntry } from "anser";
-import GHALogsModal from "../status/GHALogsModal";
-import { PorterAppEvent, PorterAppEventType } from "shared/types";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const EventCard: React.FC<Props> = ({ event, appData }) => {
-  const [showModal, setShowModal] = useState<boolean>(false);
-  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
-  const [logModalVisible, setLogModalVisible] = useState(false);
-  const [logs, setLogs] = useState<Log[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-
-  const getIcon = (eventType: string) => {
-    switch (eventType) {
-      case "APP_EVENT":
-        return app_event;
-      case "BUILD":
-        return build;
-      case "DEPLOY":
-        return deploy;
-      case "PRE_DEPLOY":
-        return pre_deploy;
-      default:
-        return app_event;
-    }
-  };
-
-  const getTitle = (eventType: string) => {
-    switch (eventType) {
-      case "APP_EVENT":
-        return "Some application event";
-      case "BUILD":
-        return "Application build";
-      case "DEPLOY":
-        return "Application deploy";
-      case "PRE_DEPLOY":
-        return "Application pre-deploy";
-      default:
-        return "";
-    }
-  };
-
-  const getStatusIcon = (status: string) => {
-    switch (status) {
-      case "SUCCESS":
-        return healthy;
-      case "FAILED":
-        return failure;
-      case "PROGRESSING":
-        return loading;
-      default:
-        return loading;
-    }
-  };
-
-  const getDuration = (event: PorterAppEvent): string => {
-    const startTimeStamp = new Date(event.created_at).getTime();
-    const endTimeStamp = new Date(event.updated_at).getTime();
-
-    const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp;
-
-    const seconds = Math.floor(timeDifferenceMilliseconds / 1000);
-    const hours = Math.floor(seconds / 3600);
-    const minutes = Math.floor((seconds % 3600) / 60);
-    const remainingSeconds = seconds % 60;
-
-    let formattedTime = "";
-
-    if (hours > 0) {
-      formattedTime += `${hours} h `;
-    }
-
-    if (minutes > 0) {
-      formattedTime += `${minutes} m `;
-    }
-
-    if (hours === 0 && minutes === 0) {
-      formattedTime += `${remainingSeconds} s`;
-    }
-
-    return formattedTime.trim();
-  };
-
-  const renderStatusText = (event: PorterAppEvent) => {
-    if (event.type === PorterAppEventType.BUILD) {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Build succeeded</Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Build failed</Text>;
-        default:
-          return <Text color="#aaaabb66">Build in progress . . </Text>;
-      }
-    }
-
-    if (event.type === PorterAppEventType.DEPLOY) {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Deployed v100</Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Deploying v100 failed</Text>;
-        default:
-          return <Text color="#aaaabb66">Deploying v100 . . .</Text>;
-      }
-    }
-
-    if (event.type === PorterAppEventType.PRE_DEPLOY) {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Pre-deploy succeeded . . </Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Pre-deploy failed . . </Text>;
-        default:
-          return <Text color="#aaaabb66">Pre-deploy in progress . . </Text>;
-      }
-    }
-  };
-  const triggerWorkflow = async () => {
-    try {
-      const res = await api.reRunGHWorkflow(
-        "",
-        {},
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          git_installation_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          branch: appData.app.branch_name,
-          filename: "porter_stack_" + appData.chart.name + ".yml",
-        }
-      );
-      if (res.data != null) {
-        window.open(res.data, "_blank", "noreferrer");
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
-  const renderInfoCta = (event: any) => {
-    if (event.type === PorterAppEventType.APP_EVENT) {
-      return (
-        <>
-          <Link
-            hasunderline
-            onClick={() => {
-              setModalContent(
-                <>
-                  <Container row>
-                    <Icon height="20px" src={app_event} />
-                    <Spacer inline width="10px" />
-                    <Text size={16}>Event details</Text>
-                  </Container>
-                  <Spacer y={1} />
-                  <Text>TODO: display event logs</Text>
-                </>
-              );
-              setShowModal(true);
-            }}
-          >
-            View details
-          </Link>
-          <Spacer inline x={1} />
-        </>
-      );
-    }
-
-    const getBuildLogs = async () => {
-      try {
-        setLogs([]);
-        setLogModalVisible(true);
-
-        const res = await api.getGHWorkflowLogById(
-          "",
-          {},
-          {
-            project_id: appData.app.project_id,
-            cluster_id: appData.app.cluster_id,
-            git_installation_id: appData.app.git_repo_id,
-            owner: appData.app.repo_name?.split("/")[0],
-            name: appData.app.repo_name?.split("/")[1],
-            filename: "porter_stack_" + appData.chart.name + ".yml",
-            run_id: event.metadata.action_run_id,
-          }
-        );
-        let logs: Log[] = [];
-        if (res.data != null) {
-          // Fetch the logs
-          const logsResponse = await fetch(res.data);
-
-          // Ensure that the response body is only read once
-          const logsBlob = await logsResponse.blob();
-
-          if (logsResponse.headers.get("Content-Type") === "application/zip") {
-            const zip = await JSZip.loadAsync(logsBlob);
-            const promises: any[] = [];
-
-            zip.forEach(function (relativePath, zipEntry) {
-              promises.push(
-                (async function () {
-                  const fileData = await zip
-                    .file(relativePath)
-                    ?.async("string");
-
-                  if (
-                    fileData &&
-                    fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
-                  ) {
-                    const lines = fileData.split("\n");
-
-                    lines.forEach((line, index) => {
-                      const anserLine: AnserJsonEntry[] = Anser.ansiToJson(
-                        line
-                      );
-                      const log: Log = {
-                        line: anserLine,
-                        lineNumber: index + 1,
-                        timestamp: line.match(
-                          /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/
-                        )?.[0],
-                      };
-
-                      logs.push(log);
-                    });
-                  }
-                })()
-              );
-            });
-
-            await Promise.all(promises);
-            setLogs(logs);
-          }
-        }
-      } catch (error) {
-        console.log(appData);
-        console.log(error);
-      }
-    };
-
-    if (event.type === PorterAppEventType.BUILD) {
-      switch (event.status) {
-        case "SUCCESS":
-          return (
-            <>
-              <Link hasunderline onClick={() => getBuildLogs()}>
-                View logs
-              </Link>
-
-              {logModalVisible && (
-                <GHALogsModal
-                  appData={appData}
-                  logs={logs}
-                  modalVisible={logModalVisible}
-                  setModalVisible={setLogModalVisible}
-                  actionRunId={event.metadata?.action_run_id}
-                />
-              )}
-              <Spacer inline x={1} />
-            </>
-          );
-        case "FAILED":
-          return (
-            <>
-              <Link hasunderline onClick={() => getBuildLogs()}>
-                View logs
-              </Link>
-
-              {logModalVisible && (
-                <GHALogsModal
-                  appData={appData}
-                  logs={logs}
-                  modalVisible={logModalVisible}
-                  setModalVisible={setLogModalVisible}
-                  actionRunId={event.metadata?.action_run_id}
-                />
-              )}
-              <Spacer inline x={1} />
-            </>
-          );
-        default:
-          return (
-            <>
-              <Link
-                hasunderline
-                target="_blank"
-                to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
-              >
-                View live logs
-              </Link>
-              <Spacer inline x={1} />
-            </>
-          );
-      }
-    }
-    useEffect(() => {
-      getBuildLogs();
-    }, []);
-
-    if (event.type === PorterAppEventType.DEPLOY) {
-      if (event.status === "FAILED") {
-        return (
-          <>
-            <Link
-              hasunderline
-              onClick={() => alert("TODO: open deploy logs modal")}
-            >
-              View logs
-            </Link>
-            <Spacer inline x={1} />
-          </>
-        );
-      } else {
-        return;
-      }
-    }
-
-    if (event.type === PorterAppEventType.PRE_DEPLOY) {
-      return (
-        <>
-          <Link hasunderline onClick={() => alert("TODO: open logs modal")}>
-            View logs
-          </Link>
-          <Spacer inline x={1} />
-        </>
-      );
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="18px" src={getIcon(event.type)} />
-          <Spacer inline width="10px" />
-          <Text size={14}>{getTitle(event.type)}</Text>
-        </Container>
-        <Container row>
-          <Icon height="14px" src={run_for} />
-          <Spacer inline width="6px" />
-          <Text color="helper">{getDuration(event)}</Text>
-        </Container>
-      </Container>
-      <Spacer y={1} />
-      <Container row spaced>
-        <Container row>
-          {event.type !== PorterAppEventType.APP_EVENT && (
-            <>
-              <Icon height="18px" src={getStatusIcon(event.status)} />
-              <Spacer inline width="10px" />
-            </>
-          )}
-          {renderStatusText(event)}
-          {event.type !== PorterAppEventType.APP_EVENT && <Spacer inline x={1} />}
-          {renderInfoCta(event)}
-          {event.status === "FAILED" && event.type !== PorterAppEventType.APP_EVENT && (
-            <>
-              <Link hasunderline onClick={() => triggerWorkflow()}>
-                <Container row>
-                  <Icon height="10px" src={refresh} />
-                  <Spacer inline width="5px" />
-                  Retry
-                </Container>
-              </Link>
-            </>
-          )}
-        </Container>
-        {false && <Text color="helper">user@email.com</Text>}
-      </Container>
-      {showModal && (
-        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
-      )}
-    </StyledEventCard>
-  );
-};
-
-export default EventCard;
-
-const StyledEventCard = styled.div`
-  width: 100%;
-  padding: 15px;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  height: 85px;
-  border-radius: 5px;
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid ${({ theme }) => theme.border};
-  opacity: 0;
-  animation: slideIn 0.5s 0s;
-  animation-fill-mode: forwards;
-  @keyframes slideIn {
-    from {
-      margin-left: -10px;
-      opacity: 0;
-      margin-right: 10px;
-    }
-    to {
-      margin-left: 0;
-      opacity: 1;
-      margin-right: 0;
-    }
-  }
-`;

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

@@ -0,0 +1,87 @@
+import React, { useEffect, useState } from "react";
+
+import app_event from "assets/app_event.png";
+
+
+import run_for from "assets/run_for.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Link from "components/porter/Link";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
+
+import { PorterAppEvent } from "shared/types";
+import { getDuration } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const AppEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Deployed v100</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Deploying v100 failed</Text>;
+      default:
+        return <Text color="#aaaabb66">Deploying v100...</Text>;
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={app_event} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application build</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          {renderStatusText(event)}
+          <Link
+            hasunderline
+            onClick={() => {
+              setModalContent(
+                <>
+                  <Container row>
+                    <Icon height="20px" src={app_event} />
+                    <Spacer inline width="10px" />
+                    <Text size={16}>Event details</Text>
+                  </Container>
+                  <Spacer y={1} />
+                  <Text>TODO: display event logs</Text>
+                </>
+              );
+              setShowModal(true);
+            }}
+          >
+            View details
+          </Link>
+          <Spacer inline x={1} />
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default AppEventCard;

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

@@ -0,0 +1,239 @@
+import React, { useEffect, useState } from "react";
+
+import app_event from "assets/app_event.png";
+import build from "assets/build.png";
+
+import run_for from "assets/run_for.png";
+import refresh from "assets/refresh.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Link from "components/porter/Link";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import api from "shared/api";
+import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
+import JSZip from "jszip";
+import Anser, { AnserJsonEntry } from "anser";
+import GHALogsModal from "../../status/GHALogsModal";
+import { PorterAppEvent, PorterAppEventType } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+  const [logModalVisible, setLogModalVisible] = useState(false);
+  const [logs, setLogs] = useState<Log[]>([]);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Build succeeded.</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Build failed.</Text>;
+      default:
+        return <Text color="#aaaabb66">Build in progress...</Text>;
+    }
+  };
+  const triggerWorkflow = async () => {
+    try {
+      const res = await api.reRunGHWorkflow(
+        "",
+        {},
+        {
+          project_id: appData.app.project_id,
+          cluster_id: appData.app.cluster_id,
+          git_installation_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          branch: appData.app.branch_name,
+          filename: "porter_stack_" + appData.chart.name + ".yml",
+        }
+      );
+      if (res.data != null) {
+        window.open(res.data, "_blank", "noreferrer");
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  };
+  const getBuildLogs = async () => {
+    try {
+      setLogs([]);
+      setLogModalVisible(true);
+
+      const res = await api.getGHWorkflowLogById(
+        "",
+        {},
+        {
+          project_id: appData.app.project_id,
+          cluster_id: appData.app.cluster_id,
+          git_installation_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          filename: "porter_stack_" + appData.chart.name + ".yml",
+          run_id: event.metadata.action_run_id,
+        }
+      );
+      let logs: Log[] = [];
+      if (res.data != null) {
+        // Fetch the logs
+        const logsResponse = await fetch(res.data);
+
+        // Ensure that the response body is only read once
+        const logsBlob = await logsResponse.blob();
+
+        if (logsResponse.headers.get("Content-Type") === "application/zip") {
+          const zip = await JSZip.loadAsync(logsBlob);
+          const promises: any[] = [];
+
+          zip.forEach(function (relativePath, zipEntry) {
+            promises.push(
+              (async function () {
+                const fileData = await zip
+                  .file(relativePath)
+                  ?.async("string");
+
+                if (
+                  fileData &&
+                  fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
+                ) {
+                  const lines = fileData.split("\n");
+                  const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
+
+                  lines.forEach((line, index) => {
+                    const lineWithoutTimestamp = line.replace(timestampPattern, "").trimStart();
+                    const anserLine: AnserJsonEntry[] = Anser.ansiToJson(lineWithoutTimestamp);
+                    if (lineWithoutTimestamp.toLowerCase().includes("error")) {
+                      anserLine[0].fg = "238,75,43";
+                    }
+
+                    const log: Log = {
+                      line: anserLine,
+                      lineNumber: index + 1,
+                      timestamp: line.match(timestampPattern)?.[0],
+                    };
+
+                    logs.push(log);
+                  });
+                }
+              })()
+            );
+          });
+
+          await Promise.all(promises);
+          setLogs(logs);
+        }
+      }
+    } catch (error) {
+      console.log(appData);
+      console.log(error);
+    }
+  };
+
+  const renderInfoCta = (event: any) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return (
+          <>
+            <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} />
+          </>
+        );
+      case "FAILED":
+        return (
+          <>
+            <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} />
+          </>
+        );
+      default:
+        return (
+          <>
+            <Link
+              hasunderline
+              target="_blank"
+              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
+            >
+              View live logs
+            </Link>
+            <Spacer inline x={1} />
+          </>
+        );
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={build} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application build</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+          <Spacer inline x={1} />
+          {renderInfoCta(event)}
+          {event.status === "FAILED" && (
+            <>
+              <Link hasunderline onClick={() => triggerWorkflow()}>
+                <Container row>
+                  <Icon height="10px" src={refresh} />
+                  <Spacer inline width="5px" />
+                  Retry
+                </Container>
+              </Link>
+            </>
+          )}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default BuildEventCard;

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

@@ -0,0 +1,65 @@
+import React, { useEffect, useState } from "react";
+
+
+import run_for from "assets/run_for.png";
+import deploy from "assets/deploy.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import { PorterAppEvent } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Deployment succeeded.</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Deployment failed.</Text>;
+      default:
+        return <Text color="#aaaabb66">Deployment in progress...</Text>;
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={deploy} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application deploy</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default DeployEventCard;

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

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

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

@@ -0,0 +1,134 @@
+import React, { useEffect, useState } from "react";
+
+import pre_deploy from "assets/pre_deploy.png";
+
+import run_for from "assets/run_for.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+
+import { PorterAppEvent } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+};
+
+const PreDeployEventCard: React.FC<Props> = ({ event }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Pre-deploy succeeded.</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Pre-deploy failed.</Text>;
+      default:
+        return <Text color="#aaaabb66">Pre-deploy in progress...</Text>;
+    }
+  };
+
+  const renderInfoCta = (event: any) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return (
+          <>
+            {/* <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} /> */}
+          </>
+        );
+      case "FAILED":
+        return (
+          <>
+            {/* <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} /> */}
+          </>
+        );
+      default:
+        return (
+          <>
+            {/* <Link
+              hasunderline
+              target="_blank"
+              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
+            >
+              View live logs
+            </Link>
+            <Spacer inline x={1} /> */}
+          </>
+        );
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={pre_deploy} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application pre-deploy</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+          <Spacer inline x={1} />
+          {renderInfoCta(event)}
+          {/* {event.status === "FAILED" && (
+            <>
+              <Link hasunderline onClick={() => triggerWorkflow()}>
+                <Container row>
+                  <Icon height="10px" src={refresh} />
+                  <Spacer inline width="5px" />
+                  Retry
+                </Container>
+              </Link>
+            </>
+          )} */}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default PreDeployEventCard;

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

@@ -0,0 +1,45 @@
+import { PorterAppEvent } from "shared/types";
+import healthy from "assets/status-healthy.png";
+import failure from "assets/failure.png";
+import loading from "assets/loading.gif";
+
+export const getDuration = (event: PorterAppEvent): string => {
+    const startTimeStamp = new Date(event.created_at).getTime();
+    const endTimeStamp = new Date(event.updated_at).getTime();
+
+    const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp;
+
+    const seconds = Math.floor(timeDifferenceMilliseconds / 1000);
+    const hours = Math.floor(seconds / 3600);
+    const minutes = Math.floor((seconds % 3600) / 60);
+    const remainingSeconds = seconds % 60;
+
+    let formattedTime = "";
+
+    if (hours > 0) {
+        formattedTime += `${hours} h `;
+    }
+
+    if (minutes > 0) {
+        formattedTime += `${minutes} m `;
+    }
+
+    if (hours === 0 && minutes === 0) {
+        formattedTime += `${remainingSeconds} s`;
+    }
+
+    return formattedTime.trim();
+};
+
+export const getStatusIcon = (status: string) => {
+    switch (status) {
+        case "SUCCESS":
+            return healthy;
+        case "FAILED":
+            return failure;
+        case "PROGRESSING":
+            return loading;
+        default:
+            return loading;
+    }
+};

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

@@ -91,7 +91,7 @@ export const useLogs = (
         ...pl,
         [containerName]: processedLogs,
       }));
-    } catch (error) {}
+    } catch (error) { }
   };
 
   const setupWebsocket = (containerName: string, websocketKey: string) => {
@@ -156,7 +156,7 @@ export const useLogs = (
   useEffect(() => {
     // console.log("Selected pod updated");
     if (currentPod?.metadata?.name === currentPodName.current) {
-      return () => {};
+      return () => { };
     }
     currentPodName.current = currentPod?.metadata?.name;
     const currentContainers =