2
0
Эх сурвалжийг харах

live logs in pre-deploy page

Feroze Mohideen 2 жил өмнө
parent
commit
22b55321ca
18 өөрчлөгдсөн 254 нэмэгдсэн , 1416 устгасан
  1. 1 2
      api/server/handlers/porter_app/get_logs_within_time_range.go
  2. 1 1
      api/types/incident.go
  3. 0 637
      dashboard/src/main/home/app-dashboard/expanded-app/EventList.tsx
  4. 0 236
      dashboard/src/main/home/app-dashboard/expanded-app/EventsTab.tsx
  5. 16 14
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx
  6. 1 60
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx
  7. 4 4
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx
  8. 15 6
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx
  9. 68 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx
  10. 0 176
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployFailureEventFocusView.tsx
  11. 31 198
      dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx
  12. 33 0
      dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts
  13. 77 74
      dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts
  14. 1 1
      dashboard/src/main/home/app-dashboard/expanded-app/status/AppEventModal.tsx
  15. 2 2
      dashboard/src/main/home/app-dashboard/expanded-app/status/ExpandedIncidentLogs.tsx
  16. 2 3
      dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx
  17. 1 1
      dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx
  18. 1 1
      dashboard/src/shared/api.tsx

+ 1 - 2
api/server/handlers/porter_app/get_logs_within_time_range.go

@@ -70,7 +70,6 @@ func (c *GetLogsWithinTimeRangeHandler) ServeHTTP(w http.ResponseWriter, r *http
 		EndRange:    &request.EndRange,
 		Namespace:   request.Namespace,
 		MatchPrefix: request.ChartName,
-		Revision:    request.Revision,
 	}
 
 	var podSelector string
@@ -137,9 +136,9 @@ func (c *GetLogsWithinTimeRangeHandler) ServeHTTP(w http.ResponseWriter, r *http
 		Limit:       request.Limit,
 		StartRange:  &request.StartRange,
 		EndRange:    &request.EndRange,
-		Revision:    request.Revision,
 		PodSelector: podSelector,
 		Namespace:   request.Namespace,
+		Direction:   request.Direction,
 	}
 
 	logs, err := porter_agent.GetHistoricalLogs(agent.Clientset, agentSvc, logRequest)

+ 1 - 1
api/types/incident.go

@@ -120,9 +120,9 @@ type GetChartLogsWithinTimeRangeRequest struct {
 	StartRange  time.Time `schema:"start_range,omitempty"`
 	EndRange    time.Time `schema:"end_range,omitempty"`
 	SearchParam string    `schema:"search_param"`
-	Revision    string    `schema:"revision"`
 	Namespace   string    `schema:"namespace"`
 	PodSelector string    `schema:"pod_selector"`
+	Direction   string    `schema:"direction"`
 }
 
 type GetPodValuesRequest struct {

+ 0 - 637
dashboard/src/main/home/app-dashboard/expanded-app/EventList.tsx

@@ -1,637 +0,0 @@
-import React, { useState, useEffect, useContext } from "react";
-import { CellProps } from "react-table";
-
-import styled from "styled-components";
-import Table from "components/Table";
-import Loading from "components/Loading";
-import danger from "assets/danger.svg";
-import rocket from "assets/rocket.png";
-import document from "assets/document.svg";
-import info from "assets/info-outlined.svg";
-import status from "assets/info-circle.svg";
-import { readableDate, relativeDate } from "shared/string_utils";
-import TitleSection from "components/TitleSection";
-import api from "shared/api";
-import Modal from "main/home/modals/Modal";
-import time from "assets/time.svg";
-import { Direction, PorterLog, parseLogs } from "./useAgentLogs";
-import { Context } from "shared/Context";
-import dayjs from "dayjs";
-import Anser from "anser";
-
-type Props = {
-  namespace: string;
-  filters: any;
-};
-
-interface ExpandedIncidentLogsProps {
-  logs: PorterLog[];
-}
-
-const ExpandedIncidentLogs = ({ logs }: ExpandedIncidentLogsProps) => {
-  if (!logs.length) {
-    return (
-      <LogsLoadWrapper>
-        <Loading />
-      </LogsLoadWrapper>
-    );
-  }
-
-  return (
-    <LogsSectionWrapper>
-      <StyledLogsSection>
-        {logs?.map((log, i) => {
-          return (
-            <LogSpan key={[log.lineNumber, i].join(".")}>
-              <span className="line-number">{log.lineNumber}.</span>
-              <span className="line-timestamp">
-                {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
-              </span>
-              <LogOuter key={[log.lineNumber, i].join(".")}>
-                {log.line?.map((ansi, j) => {
-                  if (ansi.clearLine) {
-                    return null;
-                  }
-
-                  return (
-                    <LogInnerSpan
-                      key={[log.lineNumber, i, j].join(".")}
-                      ansi={ansi}
-                    >
-                      {ansi.content.replace(/ /g, "\u00a0")}
-                    </LogInnerSpan>
-                  );
-                })}
-              </LogOuter>
-            </LogSpan>
-          );
-        })}
-      </StyledLogsSection>
-    </LogsSectionWrapper>
-  );
-};
-
-const EventList: React.FC<Props> = ({ filters, namespace }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [events, setEvents] = useState([]);
-  const [logs, setLogs] = useState<PorterLog[]>([]);
-  const [expandedEvent, setExpandedEvent] = useState(null);
-  const [expandedIncidentEvents, setExpandedIncidentEvents] = useState(null);
-  const [isLoading, setIsLoading] = useState(true);
-  const [refresh, setRefresh] = useState(true);
-
-  useEffect(() => {
-    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) {
-      return;
-    }
-
-    api
-      .getIncidentEvents(
-        "<token>",
-        {
-          incident_id: expandedEvent.id,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        if (!expandedEvent.should_view_logs) {
-          setExpandedIncidentEvents(res.data.events);
-          return null;
-        }
-
-        const events = res.data?.events ?? [];
-
-        api
-          .getLogs(
-            "<token>",
-            {
-              pod_selector: events[0]?.pod_name,
-              namespace,
-              revision: events[0]?.revision,
-              start_range: dayjs(events[0]?.updated_at)
-                .subtract(14, "day")
-                .toISOString(),
-              end_range: dayjs(events[0]?.updated_at).toISOString(),
-              limit: 100,
-              direction: Direction.backward,
-              search_param: "",
-            },
-            {
-              cluster_id: currentCluster.id,
-              project_id: currentProject.id,
-            }
-          )
-          .then((res) => {
-            const logs = parseLogs(
-              res.data.logs
-                ?.filter(Boolean)
-                .map((logLine: any) => logLine.line)
-                .reverse()
-            );
-            setLogs(logs);
-          });
-
-        setExpandedIncidentEvents(res.data.events);
-      });
-  }, [expandedEvent]);
-
-  const renderExpandedEventMessage = () => {
-    if (!expandedIncidentEvents) {
-      return <Loading />;
-    }
-
-    return (
-      <>
-        <Message>
-          <img src={document} />
-          {expandedIncidentEvents[0].detail}
-        </Message>
-        {expandedEvent.should_view_logs ? (
-          <ExpandedIncidentLogs logs={logs} />
-        ) : null}
-      </>
-    );
-  };
-
-  const renderIncidentSummaryCell = (incident: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={danger} />
-        {incident.short_summary}
-        {incident.severity === "normal" ? (
-          <></>
-        ) : (
-          <Status color="#cc3d42">Critical</Status>
-        )}
-      </NameWrapper>
-    );
-  };
-
-  const renderDeploymentFinishedCell = (release: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={rocket} />
-        Revision {release.revision} was successfully deployed
-      </NameWrapper>
-    );
-  };
-
-  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(
-    () => [
-      {
-        Header: "Monitors",
-        columns: [
-          {
-            Header: "Description",
-            accessor: "type",
-            width: 500,
-            Cell: ({ row }: CellProps<any>) => {
-              if (row.original.type == "incident") {
-                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;
-            },
-          },
-          {
-            Header: "Last seen",
-            accessor: "timestamp",
-            width: 140,
-            Cell: ({ row }: CellProps<any>) => {
-              return <Flex>{relativeDate(row.original.timestamp)}</Flex>;
-            },
-          },
-          {
-            id: "details",
-            accessor: "",
-            width: 20,
-            Cell: ({ row }: CellProps<any>) => {
-              if (row.original.type == "incident") {
-                return (
-                  <TableButton
-                    onClick={() => {
-                      setExpandedEvent(row.original.data);
-                    }}
-                  >
-                    <Icon src={info} />
-                    Details
-                  </TableButton>
-                );
-              }
-
-              return null;
-            },
-          },
-        ],
-      },
-    ],
-    []
-  );
-
-  return (
-    <>
-      {expandedEvent && (
-        <Modal
-          onRequestClose={() => {
-            setExpandedEvent(null);
-            setLogs([]);
-          }}
-          height="auto"
-        >
-          <TitleSection icon={danger}>
-            <Text>{expandedEvent.release_name}</Text>
-          </TitleSection>
-          <InfoRow>
-            <InfoTab>
-              <img src={time} /> <Bold>Last updated:</Bold>
-              {readableDate(expandedEvent.updated_at)}
-            </InfoTab>
-            <InfoTab>
-              <img src={info} /> <Bold>Status:</Bold>
-              <Capitalize>{expandedEvent.status}</Capitalize>
-            </InfoTab>
-            <InfoTab>
-              <img src={status} /> <Bold>Priority:</Bold>{" "}
-              <Capitalize>{expandedEvent.severity}</Capitalize>
-            </InfoTab>
-          </InfoRow>
-          {expandedEvent?.porter_doc_link && (
-            <DocsLink target="_blank" href={expandedEvent?.porter_doc_link}>
-              View troubleshooting steps
-              <i className="material-icons">open_in_new</i>{" "}
-            </DocsLink>
-          )}
-          {renderExpandedEventMessage()}
-        </Modal>
-      )}
-      {isLoading ? (
-        <LoadWrapper>
-          <Loading />
-        </LoadWrapper>
-      ) : (
-        <TableWrapper>
-          <Table
-            columns={columns}
-            data={events}
-            placeholder="No events found."
-          />
-          <FlexRow>
-            <Flex>
-              <Button
-                onClick={() => {
-                  setIsLoading(true);
-                  setRefresh(true);
-                }}
-              >
-                <i className="material-icons">autorenew</i>
-                Refresh
-              </Button>
-            </Flex>
-          </FlexRow>
-        </TableWrapper>
-      )}
-    </>
-  );
-};
-
-export default EventList;
-
-const LogsLoadWrapper = styled.div`
-  height: 50px;
-`;
-
-const Message = styled.div`
-  padding: 20px;
-  background: #26292e;
-  border-radius: 5px;
-  line-height: 1.5em;
-  border: 1px solid #aaaabb33;
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  > img {
-    width: 13px;
-    margin-right: 20px;
-  }
-`;
-
-const Capitalize = styled.div`
-  text-transform: capitalize;
-`;
-
-const Bold = styled.div`
-  font-weight: 500;
-  margin-right: 5px;
-`;
-
-const InfoTab = styled.div`
-  display: flex;
-  align-items: center;
-  opacity: 50%;
-  font-size: 13px;
-  margin-right: 15px;
-  justify-content: center;
-
-  > img {
-    width: 13px;
-    margin-right: 7px;
-  }
-`;
-
-const InfoRow = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: flex-start;
-  margin-bottom: 12px;
-`;
-
-const Text = styled.div`
-  font-weight: 500;
-  font-size: 18px;
-  z-index: 999;
-`;
-
-const Icon = styled.img`
-  width: 16px;
-  margin-right: 6px;
-`;
-
-const TableButton = styled.div<{ width?: string }>`
-  border-radius: 5px;
-  height: 30px;
-  color: white;
-  width: ${(props) => props.width || "85px"};
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #ffffff11;
-  border: 1px solid #aaaabb33;
-  margin-right: -17px;
-  cursor: pointer;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;
-
-const ClusterName = styled.div`
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  background: blue;
-  width: 100px;
-`;
-
-const Title = styled.div`
-  font-size: 18px;
-  margin-bottom: 10px;
-  color: #ffffff;
-`;
-
-const Placeholder = styled.div`
-  width: 100%;
-  height: 300px;
-  color: #aaaabb55;
-  display: flex;
-  font-size: 14px;
-  padding-right: 50px;
-  align-items: center;
-  justify-content: center;
-`;
-
-const ClusterIcon = styled.img`
-  width: 14px;
-  margin-right: 9px;
-  opacity: 70%;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const AlertIcon = styled.img`
-  width: 20px;
-  margin-right: 15px;
-  margin-left: 0px;
-`;
-
-const NameWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  color: white;
-`;
-
-const LoadWrapper = styled.div`
-  width: 100%;
-  height: 300px;
-`;
-
-const Status = styled.div<{ color: string }>`
-  padding: 5px 7px;
-  background: ${(props) => props.color};
-  font-size: 12px;
-  border-radius: 3px;
-  word-break: keep-all;
-  display: flex;
-  color: white;
-  margin-right: 50px;
-  align-items: center;
-  margin-left: 15px;
-  justify-content: center;
-  height: 20px;
-`;
-
-const TableWrapper = styled.div`
-  overflow-x: auto;
-  animation: fadeIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-`;
-
-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;
-`;
-
-const DocsLink = styled.a`
-  display: inline-block;
-  color: #8590ff;
-  border-bottom: 1px solid #8590ff;
-  cursor: pointer;
-  user-select: none;
-  padding: 3px 0;
-  margin-bottom: 18px;
-
-  > i {
-    font-size: 12px;
-    margin-left: 5px;
-  }
-`;
-
-const LogsSectionWrapper = styled.div`
-  position: relative;
-`;
-
-const StyledLogsSection = styled.div`
-  margin-top: 20px;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  max-height: 400px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  border-top: none;
-  background: #101420;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const LogSpan = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-end;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;
-
-export const ViewLogsWrapper = styled.div`
-  margin-bottom: -15px;
-  margin-top: 15px;
-`;

+ 0 - 236
dashboard/src/main/home/app-dashboard/expanded-app/EventsTab.tsx

@@ -1,236 +0,0 @@
-import React, { useEffect, useContext, useState } from "react";
-import api from "shared/api";
-import styled from "styled-components";
-import EventList from "./EventList";
-import Loading from "components/Loading";
-import { Context } from "shared/Context";
-import Fieldset from "components/porter/Fieldset";
-import Button from "components/porter/Button";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-
-type Props = {
-  currentChart: any;
-};
-
-const EventsTab: React.FC<Props> = ({ currentChart }) => {
-  const [hasPorterAgent, setHasPorterAgent] = useState(true);
-  const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
-  const { currentProject, currentCluster } = useContext(Context);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    // determine if the agent is installed properly - if not, start by render upgrade screen
-    checkForAgent();
-  }, []);
-
-  useEffect(() => {
-    if (!isPorterAgentInstalling) {
-      return;
-    }
-
-    const checkForAgentInterval = setInterval(checkForAgent, 3000);
-
-    return () => clearInterval(checkForAgentInterval);
-  }, [isPorterAgentInstalling]);
-
-  const checkForAgent = () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    api
-      .detectPorterAgent("<token>", {}, { project_id, cluster_id })
-      .then((res) => {
-        if (res.data?.version != "v3") {
-          setHasPorterAgent(false);
-        } else {
-          // next, check whether events can be queried - if they can, we're good to go
-          let filters: any = getFilters();
-
-          let apiQuery = api.listPorterEvents;
-
-          if (filters.job_name) {
-            apiQuery = api.listPorterJobEvents;
-          }
-
-          apiQuery("<token>", filters, {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-          })
-            .then((res) => {
-              setHasPorterAgent(true);
-              setIsPorterAgentInstalling(false);
-            })
-            .catch((err) => {
-              // do nothing - this is expected while installing
-            });
-        }
-
-        setIsLoading(false);
-      })
-      .catch((err) => {
-        if (err.response?.status === 404) {
-          setHasPorterAgent(false);
-          setIsLoading(false);
-        }
-      });
-  };
-
-  const installAgent = async () => {
-    const project_id = currentProject?.id;
-    const cluster_id = currentCluster?.id;
-
-    setIsPorterAgentInstalling(true);
-
-    api
-      .installPorterAgent("<token>", {}, { project_id, cluster_id })
-      .then()
-      .catch((err) => {
-        setIsPorterAgentInstalling(false);
-        console.log(err);
-      });
-  };
-
-  const triggerInstall = () => {
-    installAgent();
-  };
-
-  const getFilters = () => {
-    return {
-      release_name: currentChart.name,
-      release_namespace: currentChart.namespace,
-    };
-  };
-
-  if (isPorterAgentInstalling) {
-    return (
-      <Placeholder>
-        <Header>Installing agent...</Header>
-      </Placeholder>
-    );
-  }
-
-  if (isLoading) {
-    return (
-      <Fieldset>
-        <Loading />
-      </Fieldset>
-    );
-  }
-
-  if (!hasPorterAgent) {
-    return (
-      <Fieldset>
-        <Text size={16}>We couldn't detect the Porter agent on your cluster</Text>
-        <Spacer y={0.5} />
-        <Text color="helper">In order to use the Events tab, you need to install the Porter agent.</Text>
-        <Spacer y={1} />
-        <Button onClick={() => triggerInstall()}>
-          <I className="material-icons">add</I> Install Porter agent
-        </Button>
-      </Fieldset>
-    );
-  }
-
-  return (
-    <EventsPageWrapper>
-      <EventList namespace={currentChart.namespace} filters={getFilters()} />
-    </EventsPageWrapper>
-  );
-};
-
-export default EventsTab;
-
-const EventsPageWrapper = styled.div`
-  font-size: 13px;
-  border-radius: 8px;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const InstallPorterAgentButton = styled.button`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border: none;
-  border-radius: 5px;
-  color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  margin-top: 20px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#5561C0"};
-  :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
-  }
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const Placeholder = styled.div`
-  padding: 30px;
-  padding-bottom: 40px;
-  font-size: 13px;
-  color: #ffffff44;
-  min-height: 400px;
-  height: 50vh;
-  background: #ffffff08;
-  border-radius: 8px;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  > i {
-    font-size: 18px;
-    margin-right: 8px;
-  }
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;
-
-const I = styled.i`
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-  justify-content: center;
-`;

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

@@ -179,6 +179,21 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData, eventId }) =
 
   return (
     <StyledActivityFeed>
+      <Container row spaced>
+        <Spacer inline x={1} />
+        <Button
+          onClick={getEvents}
+          height="20px"
+          color="fg"
+          withBorder
+        >
+          <Icon src={refresh} height="10px"></Icon>
+          <Spacer inline x={0.5} />
+          Refresh feed
+        </Button>
+      </Container>
+      <Spacer y={1} />
+
       {events.map((event, i) => {
         return (
           <EventWrapper isLast={i === events.length - 1} key={i}>
@@ -199,20 +214,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData, eventId }) =
           <Pagination page={page} setPage={setPage} totalPages={numPages} />
         </>
       )}
-      <Spacer y={1} />
-      <Container row spaced>
-        <Spacer inline x={1} />
-        <Button
-          onClick={getEvents}
-          height="20px"
-          color="fg"
-          withBorder
-        >
-          <Icon src={refresh} height="10px"></Icon>
-          <Spacer inline x={0.5} />
-          Refresh feed
-        </Button>
-      </Container>
+
     </StyledActivityFeed>
   );
 };

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

@@ -10,16 +10,11 @@ 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, triggerWorkflow } from '../utils';
 import { StyledEventCard } from "./EventCard";
 import Link from "components/porter/Link";
-import LogsModal from "../../../status/LogsModal";
-import api from "shared/api";
-import dayjs from "dayjs";
-import Anser from "anser";
 import document from "assets/document.svg";
 
 type Props = {
@@ -28,11 +23,6 @@ type Props = {
 };
 
 const PreDeployEventCard: 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([]);
-
   const renderStatusText = (event: PorterAppEvent) => {
     switch (event.status) {
       case "SUCCESS":
@@ -44,45 +34,6 @@ const PreDeployEventCard: React.FC<Props> = ({ event, appData }) => {
     }
   };
 
-  const getPredeployLogs = async () => {
-    setLogModalVisible(true);
-    try {
-      const logResp = await api.getLogsWithinTimeRange(
-        "<token>",
-        {
-          chart_name: appData.releaseChart.name,
-          namespace: appData.releaseChart.namespace,
-          start_range: dayjs(event.metadata.start_time).subtract(1, 'minute').toISOString(),
-          end_range: dayjs(event.metadata.end_time).add(1, 'minute').toISOString(),
-          limit: 1000,
-        },
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-        }
-      )
-      const updatedLogs = logResp.data.logs.map((l: { line: string; timestamp: string; }, index: number) => {
-        try {
-          return {
-            line: JSON.parse(l.line)?.log ?? Anser.ansiToJson(l.line),
-            lineNumber: index + 1,
-            timestamp: l.timestamp,
-          }
-        } catch (err) {
-          return {
-            line: Anser.ansiToJson(l.line),
-            lineNumber: index + 1,
-            timestamp: l.timestamp,
-          }
-        }
-      });
-
-      setLogs(updatedLogs);
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
   return (
     <StyledEventCard>
       <Container row spaced>
@@ -103,7 +54,7 @@ const PreDeployEventCard: React.FC<Props> = ({ event, appData }) => {
           <Icon height="16px" src={getStatusIcon(event.status)} />
           <Spacer inline width="10px" />
           {renderStatusText(event)}
-          {(event.status === "FAILED") &&
+          {(event.status !== "SUCCESS") &&
             <>
               <Spacer inline x={1} />
               <Wrapper>
@@ -125,19 +76,9 @@ const PreDeployEventCard: React.FC<Props> = ({ event, appData }) => {
               </Wrapper>
             </>
           }
-          {logModalVisible && (
-            <LogsModal
-              logs={logs}
-              logsName={"pre-deploy"}
-              setModalVisible={setLogModalVisible}
-            />
-          )}
           <Spacer inline x={1} />
         </Container>
       </Container>
-      {showModal && (
-        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
-      )}
     </StyledEventCard>
   );
 };

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

@@ -6,12 +6,12 @@ import styled from "styled-components";
 import Anser, { AnserJsonEntry } from "anser";
 import JSZip from "jszip";
 import dayjs from "dayjs";
-import { PorterLog as LogType } from "../../../useAgentLogs";
 import { PorterAppEvent } from "shared/types";
 import Text from "components/porter/Text";
 import { readableDate } from "shared/string_utils";
 import { getDuration } from "../utils";
 import Link from "components/porter/Link";
+import { PorterLog } from "../../../logs/types";
 
 type Props = {
     event: PorterAppEvent;
@@ -22,7 +22,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
     event,
     appData,
 }) => {
-    const [logs, setLogs] = useState<LogType[]>([]);
+    const [logs, setLogs] = useState<PorterLog[]>([]);
     const [isLoading, setIsLoading] = useState<boolean>(true);
     const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
 
@@ -55,7 +55,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
                     run_id: event.metadata.action_run_id,
                 }
             );
-            let logs: LogType[] = [];
+            let logs: PorterLog[] = [];
             if (res.data != null) {
                 // Fetch the logs
                 const logsResponse = await fetch(res.data);
@@ -92,7 +92,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
                                             anserLine[0].fg = "238,75,43";
                                         }
 
-                                        const log: LogType = {
+                                        const log: PorterLog = {
                                             line: anserLine,
                                             lineNumber: i + 1,
                                             timestamp: line.match(timestampPattern)?.[0],

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

@@ -7,13 +7,16 @@ import styled from "styled-components";
 import { PorterAppEvent } from "shared/types";
 import Link from "components/porter/Link";
 import BuildFailureEventFocusView from "./BuildFailureEventFocusView";
-import PreDeployFailureEventFocusView from "./PredeployFailureEventFocusView";
+import PreDeployEventFocusView from "./PredeployEventFocusView";
+import _ from "lodash";
 
 type Props = {
     eventId: string;
     appData: any;
 };
 
+const EVENT_POLL_INTERVAL = 15000; // poll every 15 seconds
+
 const EventFocusView: React.FC<Props> = ({
     eventId,
     appData,
@@ -36,12 +39,18 @@ const EventFocusView: React.FC<Props> = ({
                         event_id: eventId,
                     }
                 )
-                setEvent(eventResp.data.event as PorterAppEvent)
+                const newEvent = eventResp.data.event as PorterAppEvent;
+                setEvent(newEvent);
+                if (newEvent.metadata.end_time != null) {
+                    clearInterval(intervalId);
+                }
             } catch (err) {
                 console.log(err);
             }
         }
+        const intervalId = setInterval(getEvent, EVENT_POLL_INTERVAL);
         getEvent();
+        return () => clearInterval(intervalId);
     }, []);
 
     const getEventFocusView = (event: PorterAppEvent, appData: any) => {
@@ -49,14 +58,14 @@ const EventFocusView: React.FC<Props> = ({
             case "BUILD":
                 return <BuildFailureEventFocusView event={event} appData={appData} />
             case "PRE_DEPLOY":
-                return <PreDeployFailureEventFocusView event={event} appData={appData} />
+                return <PreDeployEventFocusView event={event} appData={appData} />
             default:
                 return null
         }
     }
 
     return (
-        <StyledEventFocusView>
+        <AppearingView>
             <Link to={`/apps/${appData.app.name}/activity`}>
                 <BackButton>
                     <i className="material-icons">keyboard_backspace</i>
@@ -66,13 +75,13 @@ const EventFocusView: React.FC<Props> = ({
             <Spacer y={0.5} />
             {event == null && <Loading />}
             {event != null && getEventFocusView(event, appData)}
-        </StyledEventFocusView>
+        </AppearingView>
     );
 };
 
 export default EventFocusView;
 
-const StyledEventFocusView = styled.div`
+export const AppearingView = styled.div`
     width: 100%;
     animation: fadeIn 0.3s 0s;
     @keyframes fadeIn {

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

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

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

@@ -1,176 +0,0 @@
-import Loading from "components/Loading";
-import Spacer from "components/porter/Spacer";
-import React, { useEffect, useRef, useState } from "react";
-import api from "shared/api";
-import styled from "styled-components";
-import Anser from "anser";
-import dayjs from "dayjs";
-import { Log as LogType, parseLogs } from "../../../logs/useAgentLogs";
-import { PorterAppEvent } from "shared/types";
-import Text from "components/porter/Text";
-import { readableDate } from "shared/string_utils";
-import { getDuration } from "../utils";
-import LogSection from "../../../logs/LogSection";
-
-type Props = {
-  event: PorterAppEvent;
-  appData: any;
-};
-
-const PreDeployFailureEventFocusView: React.FC<Props> = ({
-  event,
-  appData,
-}) => {
-  const [logs, setLogs] = useState<LogType[]>([]);
-  const [isLoading, setIsLoading] = useState<boolean>(true);
-  const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
-  const [noLogsMessage, setNoLogsMessage] = useState<string>("Waiting for logs...");
-
-  useEffect(() => {
-    if (!isLoading && scrollToBottomRef.current) {
-      scrollToBottomRef.current.scrollIntoView({
-        behavior: "smooth",
-        block: "end",
-      });
-    }
-  }, [isLoading, logs, scrollToBottomRef]);
-
-  // const getPredeployLogs = async () => {
-  //     setIsLoading(true);
-  //     try {
-  //         if (appData.releaseChart == null) {
-  //             setNoLogsMessage("Unable to retrieve logs because the pre-deploy job no longer exists.")
-  //             return;
-  //         }
-  //         const logResp = await api.getLogsWithinTimeRange(
-  //             "<token>",
-  //             {
-  //                 chart_name: appData.releaseChart.name,
-  //                 namespace: appData.releaseChart.namespace,
-  //                 start_range: dayjs(event.metadata.start_time).subtract(1, 'minute').toISOString(),
-  //                 end_range: dayjs(event.metadata.end_time).add(1, 'minute').toISOString(),
-  //                 limit: 1000,
-  //             },
-  //             {
-  //                 project_id: appData.app.project_id,
-  //                 cluster_id: appData.app.cluster_id,
-  //             }
-  //         )
-  //         if (logResp.data == null || logResp.data.logs == null || logResp.data.logs.length == 0) {
-  //             setNoLogsMessage("No logs found.")
-  //             return;
-  //         }
-  //         setLogs(parseLogs(logResp.data.logs));
-  //     } catch (error) {
-  //         console.log(error);
-  //     } finally {
-  //         setIsLoading(false);
-  //     }
-  // };
-
-  // useEffect(() => {
-  //     getPredeployLogs();
-  // }, [event]);
-
-  return (
-    <>
-      <Text size={16} color="#FF6060">Pre-deploy failed</Text>
-      <Spacer y={0.5} />
-      <Text color="helper">Started {readableDate(event.created_at)} and ran for {getDuration(event)}.</Text>
-      <Spacer y={0.5} />
-      <LogSection currentChart={appData.releaseChart} />
-    </>
-  );
-};
-
-export default PreDeployFailureEventFocusView;
-
-const StyledLogsSection = styled.div`
-  width: 100%;
-  min-height: 600px;
-  height: calc(100vh - 460px);
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  font-size: 13px;
-  border-radius: 8px;
-  border: 1px solid #ffffff33;
-  background: #000000;
-  animation: floatIn 0.3s;
-  animation-timing-function: ease-out;
-  animation-fill-mode: forwards;
-  overflow-y: auto;
-  overflow-wrap: break-word;
-  position: relative;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const Message = styled.div`
-  display: flex;
-  height: 100%;
-  width: calc(100% - 150px);
-  align-items: center;
-  justify-content: center;
-  margin-left: 75px;
-  text-align: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
-const Log = styled.div`
-  font-family: monospace;
-  user-select: text;
-  display: flex;
-  align-items: flex-end;
-  gap: 8px;
-  width: 100%;
-  & > * {
-    padding-block: 5px;
-  }
-  & > .line-timestamp {
-    height: 100%;
-    color: #949effff;
-    opacity: 0.5;
-    font-family: monospace;
-    min-width: fit-content;
-    padding-inline-end: 5px;
-  }
-  & > .line-number {
-    height: 100%;
-    background: #202538;
-    display: inline-block;
-    text-align: right;
-    min-width: 45px;
-    padding-inline-end: 5px;
-    opacity: 0.3;
-    font-family: monospace;
-  }
-`;
-
-const LogOuter = styled.div`
-  display: inline-block;
-  word-wrap: anywhere;
-  flex-grow: 1;
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-`;
-
-const LogInnerSpan = styled.span`
-  font-family: monospace, sans-serif;
-  font-size: 12px;
-  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
-  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
-  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
-    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
-`;

+ 31 - 198
dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx

@@ -12,13 +12,12 @@ import RadioFilter from "components/RadioFilter";
 import spinner from "assets/loading.gif";
 import filterOutline from "assets/filter-outline.svg";
 import filterOutlineWhite from "assets/filter-outline-white.svg";
-import time from "assets/time.svg";
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { Direction, useLogs } from "./useAgentLogs";
+import { useLogs } from "./utils";
+import { Direction } from "./types";
 import Anser from "anser";
-import DateTimePicker from "components/date-time-picker/DateTimePicker";
-import dayjs from "dayjs";
+import dayjs, { Dayjs } from "dayjs";
 import Loading from "components/Loading";
 import _ from "lodash";
 import { ChartType } from "shared/types";
@@ -35,6 +34,12 @@ import { Service } from "../../new-app-flow/serviceTypes";
 type Props = {
   currentChart?: ChartType;
   services?: Service[];
+  startTime?: string;
+  timeRange?: {
+    startTime: Dayjs;
+    endTime?: Dayjs;
+  };
+  showFilter?: boolean;
 };
 
 type PodFilter = {
@@ -42,7 +47,12 @@ type PodFilter = {
   podType: string;
 };
 
-const LogSection: React.FC<Props> = ({ currentChart, services }) => {
+const LogSection: React.FC<Props> = ({
+  currentChart,
+  services,
+  timeRange,
+  showFilter = true,
+}) => {
   const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
   const { currentProject, currentCluster } = useContext(Context);
   const [podFilter, setPodFilter] = useState<PodFilter>({
@@ -79,7 +89,8 @@ const LogSection: React.FC<Props> = ({ currentChart, services }) => {
     notify,
     currentChart,
     setIsLoading,
-    selectedDate
+    selectedDate,
+    timeRange,
   );
 
   const refreshPodLogsValues = async () => {
@@ -99,9 +110,10 @@ const LogSection: React.FC<Props> = ({ currentChart, services }) => {
 
   useEffect(() => {
     if (!isLoading && scrollToBottomRef.current && scrollToBottomEnabled) {
+      const scrollPosition = scrollToBottomRef.current.offsetTop + scrollToBottomRef.current.offsetHeight - window.innerHeight;
       scrollToBottomRef.current.scrollIntoView({
         behavior: "smooth",
-        block: "end",
+        top: scrollPosition,
       });
     }
   }, [isLoading, logs, scrollToBottomRef, scrollToBottomEnabled]);
@@ -215,15 +227,17 @@ const LogSection: React.FC<Props> = ({ currentChart, services }) => {
               setSelectedDate={setSelectedDate}
               resetSearch={resetSearch}
             />
-            <RadioFilter
-              icon={
-                podFilter.podName == "" ? filterOutline : filterOutlineWhite
-              }
-              selected={podFilter.podName}
-              setSelected={setPodFilterWithPodName}
-              options={radioOptions}
-              name="Filter logs"
-            />
+            {showFilter &&
+              <RadioFilter
+                icon={
+                  podFilter.podName == "" ? filterOutline : filterOutlineWhite
+                }
+                selected={podFilter.podName}
+                setSelected={setPodFilterWithPodName}
+                options={radioOptions}
+                name="Filter logs"
+              />
+            }
           </Flex>
           <Flex>
             <ScrollButton onClick={() => setScrollToBottomEnabled((s) => !s)}>
@@ -369,13 +383,6 @@ const LogSection: React.FC<Props> = ({ currentChart, services }) => {
     installAgent();
   };
 
-  const getFilters = () => {
-    return {
-      release_name: currentChart.name,
-      release_namespace: currentChart.namespace,
-    };
-  };
-
   return isPorterAgentInstalling ? (
     <Fieldset>
       <Container row>
@@ -434,73 +441,6 @@ const Spinner = styled.img`
   height: 15px;
 `;
 
-const BackButton = styled.div`
-  display: flex;
-  width: 30px;
-  z-index: 2;
-  cursor: pointer;
-  height: 30px;
-  align-items: center;
-  margin-right: 15px;
-  justify-content: center;
-  cursor: pointer;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  > i {
-    font-size: 18px;
-  }
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const AbsoluteTitle = styled.div`
-  position: absolute;
-  top: 0px;
-  left: 0px;
-  width: 100%;
-  height: 60px;
-  display: flex;
-  align-items: center;
-  padding-left: 20px;
-  font-size: 18px;
-  font-weight: 500;
-  user-select: text;
-`;
-
-const Fullscreen = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  padding-top: 60px;
-`;
-
-const Icon = styled.div`
-  background: #26292e;
-  border-radius: 5px;
-  height: 30px;
-  width: 30px;
-  display: flex;
-  cursor: pointer;
-  align-items: center;
-  justify-content: center;
-  > i {
-    font-size: 14px;
-  }
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-  }
-`;
-
 const Checkbox = styled.div<{ checked: boolean }>`
   width: 16px;
   height: 16px;
@@ -578,48 +518,9 @@ const FlexRow = styled.div`
   flex-wrap: wrap;
 `;
 
-const SearchBarWrapper = styled.div`
-  display: flex;
-  flex: 1;
-
-  > i {
-    color: #aaaabb;
-    padding-top: 1px;
-    margin-left: 8px;
-    font-size: 16px;
-    margin-right: 8px;
-  }
-`;
-
-const SearchInput = styled.input`
-  outline: none;
-  border: none;
-  font-size: 13px;
-  background: none;
-  width: 100%;
-  color: white;
-  height: 100%;
-`;
-
-const SearchRow = styled.div`
-  display: flex;
-  align-items: center;
-  height: 30px;
-  margin-right: 10px;
-  background: #26292e;
-  border-radius: 5px;
-  border: 1px solid #aaaabb33;
-`;
-
-const SearchRowWrapper = styled(SearchRow)`
-  border-radius: 5px;
-  width: 250px;
-`;
-
 const StyledLogsSection = styled.div`
   width: 100%;
-  min-height: 400px;
-  height: calc(100vh - 460px);
+  height: 600px;
   display: flex;
   flex-direction: column;
   position: relative;
@@ -765,71 +666,3 @@ const NotificationWrapper = styled.div<{ active?: boolean }>`
 const LogsSectionWrapper = styled.div`
   position: relative;
 `;
-
-const InstallPorterAgentButton = styled.button`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border: none;
-  border-radius: 5px;
-  color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  margin-top: 20px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#5561C0"};
-  :hover {
-    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
-  }
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const Placeholder = styled.div`
-  padding: 30px;
-  padding-bottom: 40px;
-  font-size: 13px;
-  color: #ffffff44;
-  min-height: 400px;
-  height: 50vh;
-  background: #ffffff08;
-  border-radius: 8px;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  > i {
-    font-size: 18px;
-    margin-right: 8px;
-  }
-`;
-
-const Header = styled.div`
-  font-weight: 500;
-  color: #aaaabb;
-  font-size: 16px;
-  margin-bottom: 15px;
-`;

+ 33 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts

@@ -0,0 +1,33 @@
+import { z } from "zod";
+import { AnserJsonEntry } from "anser";
+
+export enum Direction {
+    forward = "forward",
+    backward = "backward",
+}
+
+export interface PorterLog {
+    line: AnserJsonEntry[];
+    lineNumber: number;
+    timestamp?: string;
+}
+
+export interface PaginationInfo {
+    previousCursor: string | null;
+    nextCursor: string | null;
+}
+
+const AgentLogMetadataSchema = z.object({
+    pod_name: z.string(),
+    pod_namespace: z.string(),
+    revision: z.string(),
+    output_stream: z.string(),
+    app_name: z.string(),
+});
+
+export const AgentLogSchema = z.object({
+    line: z.string(),
+    timestamp: z.string(),
+    metadata: AgentLogMetadataSchema.optional(),
+});
+export type AgentLog = z.infer<typeof AgentLogSchema>;

+ 77 - 74
dashboard/src/main/home/app-dashboard/expanded-app/logs/useAgentLogs.ts → dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts

@@ -1,44 +1,17 @@
-import Anser, { AnserJsonEntry } from "anser";
-import dayjs from "dayjs";
+import dayjs, { Dayjs } from "dayjs";
 import _ from "lodash";
-import { z } from "zod";
 import { useContext, useEffect, useRef, useState } from "react";
 import api from "shared/api";
+import Anser from "anser";
 import { Context } from "shared/Context";
 import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
 import { ChartType } from "shared/types";
-import { isJSON } from "shared/util";
+import { AgentLog, AgentLogSchema, Direction, PorterLog, PaginationInfo } from "./types";
 
 const MAX_LOGS = 5000;
 const MAX_BUFFER_LOGS = 1000;
 const QUERY_LIMIT = 1000;
 
-export enum Direction {
-  forward = "forward",
-  backward = "backward",
-}
-
-export interface PorterLog {
-  line: AnserJsonEntry[];
-  lineNumber: number;
-  timestamp?: string;
-}
-
-const AgentLogMetadataSchema = z.object({
-  pod_name: z.string(),
-  pod_namespace: z.string(),
-  revision: z.string(),
-  output_stream: z.string(),
-  app_name: z.string(),
-});
-
-const AgentLogSchema = z.object({
-  line: z.string(),
-  timestamp: z.string(),
-  metadata: AgentLogMetadataSchema.optional(),
-});
-type AgentLog = z.infer<typeof AgentLogSchema>;
-
 export const parseLogs = (logs: any[] = []): PorterLog[] => {
   return logs.map((log: any, idx) => {
     try {
@@ -53,7 +26,7 @@ export const parseLogs = (logs: any[] = []): PorterLog[] => {
       };
     } catch (err) {
       return {
-        line: Anser.ansiToJson(log),
+        line: Anser.ansiToJson(log.toString()),
         lineNumber: idx + 1,
         timestamp: undefined,
       }
@@ -61,11 +34,6 @@ export const parseLogs = (logs: any[] = []): PorterLog[] => {
   });
 };
 
-interface PaginationInfo {
-  previousCursor: string | null;
-  nextCursor: string | null;
-}
-
 export const useLogs = (
   currentPodName: string,
   currentPodType: string,
@@ -75,7 +43,11 @@ export const useLogs = (
   currentChart: ChartType | undefined,
   setLoading: (isLoading: boolean) => void,
   // if setDate is set, results are not live
-  setDate?: Date
+  setDate?: Date,
+  timeRange?: {
+    startTime: Dayjs,
+    endTime?: Dayjs,
+  }
 ) => {
   const isLive = !setDate;
   const logsBufferRef = useRef<PorterLog[]>([]);
@@ -233,7 +205,7 @@ export const useLogs = (
     openWebsocket(websocketKey);
   };
 
-  const queryLogs = (
+  const queryLogs = async (
     startDate: string,
     endDate: string,
     direction: Direction,
@@ -243,50 +215,77 @@ export const useLogs = (
     previousCursor: string | null;
     nextCursor: string | null;
   }> => {
-    return api
-      .getLogs(
+    if (currentCluster == null || currentProject == null) {
+      return {
+        logs: [],
+        previousCursor: null,
+        nextCursor: null,
+      };
+    }
+
+    const getLogsReq = {
+      namespace,
+      search_param: searchParam,
+      start_range: startDate,
+      end_range: endDate,
+      limit,
+      chart_name: "",
+      pod_selector: currentPodName,
+      direction,
+    };
+
+    if (currentPodName === "") {
+      if (currentChart == null) {
+        return {
+          logs: [],
+          previousCursor: null,
+          nextCursor: null,
+        };
+      } else if (currentChart.name.endsWith("-r")) {
+        getLogsReq.chart_name = currentChart.name;
+      } else {
+        getLogsReq.pod_selector = currentPod + "-.*";
+      }
+    }
+
+    try {
+      const logsResp = await api.getLogsWithinTimeRange(
         "<token>",
-        {
-          pod_selector: currentPod + "-.*",
-          namespace,
-          revision: currentChart.version.toString(),
-          search_param: searchParam,
-          start_range: startDate,
-          end_range: endDate,
-          limit,
-          direction,
-        },
+        getLogsReq,
         {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
         }
       )
-      .then((res) => {
-        const newLogs = parseLogs(res.data?.logs);
-
-        if (direction === Direction.backward) {
-          newLogs.reverse();
-        }
-
-        return {
-          logs: newLogs,
-          previousCursor:
-            // There are no more historical logs so don't set the previous cursor
-            newLogs.length < QUERY_LIMIT && direction == Direction.backward
-              ? null
-              : res.data.backward_continue_time,
-          nextCursor: res.data.forward_continue_time,
-        };
-      })
-      .catch((err) => {
-        setCurrentError(err);
 
+      if (logsResp.data == null) {
         return {
           logs: [],
           previousCursor: null,
           nextCursor: null,
         };
-      });
+      }
+
+      const newLogs = parseLogs(logsResp.data.logs);
+      if (direction === Direction.backward) {
+        newLogs.reverse();
+      }
+      return {
+        logs: newLogs,
+        previousCursor:
+          // There are no more historical logs so don't set the previous cursor
+          newLogs.length < QUERY_LIMIT && direction == Direction.backward
+            ? null
+            : logsResp.data.backward_continue_time,
+        nextCursor: logsResp.data.forward_continue_time,
+      };
+    } catch {
+      return {
+        logs: [],
+        previousCursor: null,
+        nextCursor: null,
+      };
+    }
   };
 
   const refresh = async () => {
@@ -298,8 +297,8 @@ export const useLogs = (
     setLogs([]);
     flushLogsBuffer(true);
     const websocketKey = `${currentPod}-${namespace}-websocket`;
-    const endDate = dayjs(setDate);
-    const oneDayAgo = endDate.subtract(1, "day");
+    const endDate = timeRange?.endTime != null ? timeRange.endTime : dayjs(setDate);
+    const oneDayAgo = timeRange != null ? timeRange.startTime : endDate.subtract(1, "day");
 
     const { logs: initialLogs, previousCursor, nextCursor } = await queryLogs(
       oneDayAgo.toISOString(),
@@ -327,8 +326,6 @@ export const useLogs = (
     if (isLive) {
       setupWebsocket(websocketKey);
     }
-
-    return () => isLive && closeWebsocket(websocketKey);
   };
 
   const moveCursor = async (direction: Direction) => {
@@ -428,6 +425,12 @@ export const useLogs = (
     }
   }, [isLive]);
 
+  useEffect(() => {
+    return () => {
+      closeAllWebsockets();
+    };
+  }, []);
+
   return {
     logs,
     refresh,

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

@@ -1,7 +1,6 @@
 import React, { useEffect, useRef } from "react";
 import Modal from "components/porter/Modal";
 import TitleSection from "components/TitleSection";
-import { PorterLog } from "../useAgentLogs";
 import Text from "components/porter/Text";
 import danger from "assets/danger.svg";
 
@@ -11,6 +10,7 @@ import Spacer from "components/porter/Spacer";
 import document from "assets/document.svg";
 import styled from "styled-components";
 import time from "assets/time.svg";
+import { PorterLog } from "../logs/types";
 
 interface AppEventModalProps {
     logs: PorterLog[];

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

@@ -1,10 +1,10 @@
-import { useEffect, useRef, useState } from "react";
-import { PorterLog } from "../useAgentLogs";
+import { useEffect, useRef } from "react";
 import React from "react";
 import styled from "styled-components";
 import Loading from "components/Loading";
 import dayjs from "dayjs";
 import Anser from "anser";
+import { PorterLog } from "../logs/types";
 
 interface ExpandedIncidentLogsProps {
   logs: PorterLog[];

+ 2 - 3
dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx

@@ -2,16 +2,15 @@ import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import Modal from "components/porter/Modal";
 import TitleSection from "components/TitleSection";
-import { PorterLog } from "../useAgentLogs";
 import Loading from "components/Loading";
 import Text from "components/porter/Text";
 import danger from "assets/danger.svg";
-import Anser, { AnserJsonEntry } from "anser";
+import Anser from "anser";
 
 import dayjs from "dayjs";
 import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
-import Checkbox from "components/porter/Checkbox";
+import { PorterLog } from "../logs/types";
 type Props = {
   appData: any;
   logs: PorterLog[];

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

@@ -1,11 +1,11 @@
 import React, { useEffect, useRef } from "react";
 import Modal from "components/porter/Modal";
 import TitleSection from "components/TitleSection";
-import { PorterLog } from "../useAgentLogs";
 import Text from "components/porter/Text";
 import danger from "assets/danger.svg";
 
 import ExpandedIncidentLogs from "./ExpandedIncidentLogs";
+import { PorterLog } from "../logs/types";
 
 interface LogsModalProps {
     logs: PorterLog[];

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

@@ -244,9 +244,9 @@ const getLogsWithinTimeRange = baseApi<
     start_range?: string;
     end_range?: string;
     search_param?: string;
-    revision?: string;
     namespace?: string;
     pod_selector?: string;
+    direction?: string;
   },
   {
     project_id: number;