Feroze Mohideen %!s(int64=2) %!d(string=hai) anos
pai
achega
79d9683c5e

+ 1 - 1
api/server/handlers/porter_app/create_and_update_events.go

@@ -619,7 +619,7 @@ func (p *CreateUpdatePorterAppEventHandler) handleNotification(ctx context.Conte
 	projectId, clusterId uint,
 	projectId, clusterId uint,
 	agent *kubernetes.Agent,
 	agent *kubernetes.Agent,
 ) error {
 ) error {
-	ctx, span := telemetry.NewSpan(ctx, "handle-notification")
+	ctx, span := telemetry.NewSpan(ctx, "serve-handle-notification")
 	defer span.End()
 	defer span.End()
 
 
 	// get the namespace associated with the deployment target id
 	// get the namespace associated with the deployment target id

+ 62 - 0
dashboard/src/lib/porter-apps/notification.ts

@@ -0,0 +1,62 @@
+import _ from "lodash";
+import { PorterAppNotification } from "main/home/app-dashboard/app-view/tabs/activity-feed/events/types";
+
+export type ClientNotification = {
+    isDeployRelated: boolean;
+    serviceName: string;
+    messages: PorterAppNotification[];
+    timestamp: string;
+    id: string;
+    appRevisionId: string;
+}
+
+export function deserializeNotifications (
+    notifications: PorterAppNotification[] 
+): ClientNotification[] {
+    const deployRelatedNotificationMap = _.groupBy(notifications.filter(
+        (notification) => notification.deployment.status === "PENDING"
+    ), (notification) => notification.service_name);
+    const deployRelatedNotifications = Object.keys(deployRelatedNotificationMap).map(
+        (serviceName) => {
+            const notifications = orderNotificationsByTimestamp(deployRelatedNotificationMap[serviceName], 'asc');
+            const timestamp = notifications[0].timestamp;
+            const id = notifications[0].id;
+            return {
+                isDeployRelated: true,
+                serviceName,
+                timestamp,
+                id,
+                messages: notifications,
+                appRevisionId: notifications[0].app_revision_id
+            };
+        }
+    );
+    const nonDeployRelatedNotifications = notifications.filter(
+        (notification) => notification.deployment.status !== "PENDING"
+    ).map(
+        (notification) => {
+            return {
+                isDeployRelated: false,
+                serviceName: notification.service_name,
+                timestamp: notification.timestamp,
+                id: notification.id,
+                messages: [notification],
+                appRevisionId: notification.app_revision_id
+            };
+        }
+    );
+    
+    return orderNotificationsByTimestamp([...deployRelatedNotifications, ...nonDeployRelatedNotifications], 'asc');
+}
+
+const orderNotificationsByTimestamp = <T extends {timestamp: string}[]>(notifications: T, sortOrder: 'asc' | 'desc'): T => {
+    return notifications.sort((a, b) => {
+        const aTimestamp = new Date(a.timestamp);
+        const bTimestamp = new Date(b.timestamp);
+        if (sortOrder === 'asc') {
+            return aTimestamp.getTime() - bTimestamp.getTime();
+        } else {
+            return bTimestamp.getTime() - aTimestamp.getTime();
+        }
+    });
+}

+ 5 - 4
dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx

@@ -25,13 +25,14 @@ import {
   PopulatedEnvGroup,
   PopulatedEnvGroup,
   populatedEnvGroup,
   populatedEnvGroup,
 } from "../validate-apply/app-settings/types";
 } from "../validate-apply/app-settings/types";
-import { PorterAppNotification, porterAppNotificationEventMetadataValidator } from "./tabs/activity-feed/events/types";
+import { porterAppNotificationEventMetadataValidator } from "./tabs/activity-feed/events/types";
+import { ClientNotification, deserializeNotifications } from "lib/porter-apps/notification";
 
 
 export const LatestRevisionContext = createContext<{
 export const LatestRevisionContext = createContext<{
   porterApp: PorterAppRecord;
   porterApp: PorterAppRecord;
   latestRevision: AppRevision;
   latestRevision: AppRevision;
   latestProto: PorterApp;
   latestProto: PorterApp;
-  latestNotifications: PorterAppNotification[];
+  latestNotifications: ClientNotification[];
   servicesFromYaml: DetectedServices | null;
   servicesFromYaml: DetectedServices | null;
   clusterId: number;
   clusterId: number;
   projectId: number;
   projectId: number;
@@ -122,7 +123,7 @@ export const LatestRevisionProvider = ({
         }
         }
       );
       );
 
 
-      const { app_revision, notifications } = await z
+      const { app_revision, notifications: porterAppNotifications } = await z
         .object({
         .object({
           app_revision: appRevisionValidator,
           app_revision: appRevisionValidator,
           notifications: z.array(porterAppNotificationEventMetadataValidator)
           notifications: z.array(porterAppNotificationEventMetadataValidator)
@@ -130,7 +131,7 @@ export const LatestRevisionProvider = ({
         .parseAsync(res.data);
         .parseAsync(res.data);
       return {
       return {
         app_revision,
         app_revision,
-        notifications,
+        notifications: deserializeNotifications(porterAppNotifications),
       };
       };
     },
     },
     {
     {

+ 10 - 0
dashboard/src/main/home/app-dashboard/app-view/tabs/Notifications.tsx

@@ -5,12 +5,22 @@ import NotificationFeed from "./notifications/NotificationFeed";
 const Notifications: React.FC = () => {
 const Notifications: React.FC = () => {
   const {
   const {
     latestNotifications,
     latestNotifications,
+    projectId,
+    clusterId,
+    appName,
+    porterApp: { id: appId },
+    deploymentTarget: { id: deploymentTargetId },
   } = useLatestRevision();
   } = useLatestRevision();
 
 
   return (
   return (
     <>
     <>
       <NotificationFeed
       <NotificationFeed
         notifications={latestNotifications}
         notifications={latestNotifications}
+        projectId={projectId}
+        clusterId={clusterId}
+        appName={appName}
+        deploymentTargetId={deploymentTargetId}
+        appId={appId}
       />
       />
     </>
     </>
   );
   );

+ 11 - 0
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts

@@ -44,6 +44,17 @@ export const porterAppNotificationEventMetadataValidator = z.object({
     human_readable_detail: z.string(),
     human_readable_detail: z.string(),
     human_readable_summary: z.string(),
     human_readable_summary: z.string(),
     timestamp: z.string(),
     timestamp: z.string(),
+    deployment: z.discriminatedUnion("status", [
+        z.object({
+            status: z.literal("PENDING")
+        }),
+        z.object({
+            status: z.literal("SUCCESS")
+        }),
+        z.object({
+            status: z.literal("FAILURE")
+        }),
+    ])
 });
 });
 export type PorterAppNotification = z.infer<typeof porterAppNotificationEventMetadataValidator>;
 export type PorterAppNotification = z.infer<typeof porterAppNotificationEventMetadataValidator>;
 export const porterAppEventValidator = z.discriminatedUnion("type", [
 export const porterAppEventValidator = z.discriminatedUnion("type", [

+ 144 - 21
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationExpandedView.tsx

@@ -1,30 +1,75 @@
-import React from "react";
+import React, { useEffect } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import { PorterAppNotification } from "../activity-feed/events/types";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import document from "assets/document.svg";
 import document from "assets/document.svg";
 import Button from "components/porter/Button";
 import Button from "components/porter/Button";
+import { ClientNotification } from "lib/porter-apps/notification";
+import { feedDate } from "shared/string_utils";
+import Container from "components/porter/Container";
+import Logs from "main/home/app-dashboard/validate-apply/logs/Logs";
 
 
 type Props = {
 type Props = {
-  notification: PorterAppNotification;
+  notification: ClientNotification;
+  projectId: number;
+  clusterId: number;
+  appName: string;
+  deploymentTargetId: string;
+  appId: number;
 }
 }
 
 
 const NotificationExpandedView: React.FC<Props> = ({
 const NotificationExpandedView: React.FC<Props> = ({
     notification,
     notification,
+    projectId,
+    clusterId,
+    appName,
+    deploymentTargetId,
+    appId,
 }) => {
 }) => {
   return (
   return (
     <StyledNotificationExpandedView>
     <StyledNotificationExpandedView>
       <ExpandedViewContent>
       <ExpandedViewContent>
         <Text color="helper">Event ID: {notification.id}</Text>
         <Text color="helper">Event ID: {notification.id}</Text>
         <Spacer y={0.5} />
         <Spacer y={0.5} />
-        <Text size={16}>{notification.human_readable_summary}</Text>
-        <Spacer y={0.5} />
-        <Message>
-          <img src={document} />
-          {notification.human_readable_detail}
-        </Message>
-        <Spacer y={0.5} />
+        <StyledActivityFeed>
+          {notification.messages.map((message, i) => {
+              return (
+                  <NotificationWrapper isLast={i === notification.messages.length - 1} key={i}>
+                      {i !== notification.messages.length - 1 && notification.messages.length > 1 && <Line />}
+                      <Dot />
+                      <Time>
+                          <Text>{feedDate(message.timestamp)}</Text>
+                      </Time>
+                      <Message key={i}>
+                        <Container row>
+                          <img src={document} style={{width: "15px", marginRight: "15px"}} />
+                          {message.human_readable_summary}
+                        </Container>
+                        <Spacer y={0.5} />
+                        <Container row>
+                          {message.human_readable_detail}
+                        </Container>
+                      </Message>
+                  </NotificationWrapper>
+              );
+          })}
+        </StyledActivityFeed>
+        <Spacer y={1} />
+        <LogsContainer>
+          <Logs 
+            projectId={projectId}
+            clusterId={clusterId}
+            appName={appName}
+            serviceNames={[notification.serviceName]}
+            deploymentTargetId={deploymentTargetId}
+            appRevisionId={notification.appRevisionId}
+            logFilterNames={["service_name"]}
+            appId={appId}
+            selectedService={notification.serviceName}
+            selectedRevisionId={notification.appRevisionId}
+            defaultScrollToBottomEnabled={false}
+          />
+        </LogsContainer>
       </ExpandedViewContent>
       </ExpandedViewContent>
       <ExpandedViewFooter>
       <ExpandedViewFooter>
         <Button>Take recommended action</Button>
         <Button>Take recommended action</Button>
@@ -37,10 +82,12 @@ export default NotificationExpandedView;
 
 
 const StyledNotificationExpandedView = styled.div`
 const StyledNotificationExpandedView = styled.div`
 width: 100%;
 width: 100%;
+overflow-y: scroll;
 display: flex;
 display: flex;
 flex-direction: column;
 flex-direction: column;
 animation: fadeIn 0.3s 0s;
 animation: fadeIn 0.3s 0s;
-padding: 15px 20px;
+padding: 70px;
+padding-top: 15px;
 @keyframes fadeIn {
 @keyframes fadeIn {
   from {
   from {
     opacity: 0;
     opacity: 0;
@@ -49,9 +96,30 @@ padding: 15px 20px;
     opacity: 1;
     opacity: 1;
   }
   }
 }
 }
-border-bottom: 1px solid #494b4f;
-border-right: 1px solid #494b4f;
 justify-content: space-between;
 justify-content: space-between;
+::-webkit-scrollbar {
+  width: 3px;
+  :horizontal {
+    height: 3px;
+  }
+}
+
+::-webkit-scrollbar-corner {
+  width: 3px;
+  background: #ffffff11;
+  color: white;
+}
+
+::-webkit-scrollbar-track {
+  width: 3px;
+  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+  box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+}
+
+::-webkit-scrollbar-thumb {
+  background-color: darkgrey;
+  outline: 1px solid slategrey;
+}
 `;
 `;
 
 
 const ExpandedViewContent = styled.div`
 const ExpandedViewContent = styled.div`
@@ -60,21 +128,76 @@ const ExpandedViewContent = styled.div`
 `;
 `;
 
 
 const Message = styled.div`
 const Message = styled.div`
+  margin-left: 20px;
+  width: 100%;
   padding: 20px;
   padding: 20px;
-  background: #26292e;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid ${({ theme }) => theme.border};
   border-radius: 5px;
   border-radius: 5px;
   line-height: 1.5em;
   line-height: 1.5em;
-  border: 1px solid #aaaabb33;
   font-size: 13px;
   font-size: 13px;
   display: flex;
   display: flex;
-  align-items: center;
-  > img {
-    width: 13px;
-    margin-right: 20px;
-  }
+  flex-direction: column;
 `;
 `;
 
 
 const ExpandedViewFooter = styled.div`
 const ExpandedViewFooter = styled.div`
   display: flex;
   display: flex;
   justify-content: flex-end;
   justify-content: flex-end;
-`;
+`;
+
+const Time = styled.div`
+  opacity: 0;
+  animation: fadeIn 0.3s 0.1s;
+  animation-fill-mode: forwards;
+  width: 150px;
+`;
+
+const Line = styled.div`
+  width: 1px;
+  height: calc(100% + 30px);
+  background: #414141;
+  position: absolute;
+  left: 3px;
+  top: 36px;
+  opacity: 0;
+  animation: fadeIn 0.3s 0.1s;
+  animation-fill-mode: forwards;
+`;
+
+const Dot = styled.div`
+  width: 7px;
+  height: 7px;
+  background: #fff;
+  border-radius: 50%;
+  margin-left: -29px;
+  margin-right: 20px;
+  z-index: 1;
+  opacity: 0;
+  animation: fadeIn 0.3s 0.1s;
+  animation-fill-mode: forwards;
+`;
+
+const NotificationWrapper = styled.div<{isLast: boolean}>`
+  padding-left: 30px;
+  display: flex;
+  align-items: center;
+  position: relative;
+  margin-bottom: ${(props) => (props.isLast ? "" : "25px")};
+`;
+
+const StyledActivityFeed = styled.div`
+  width: 100%;
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const LogsContainer = styled.div`
+
+`;

+ 21 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationFeed.tsx

@@ -2,18 +2,28 @@ import React, { useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import NotificationList from "./NotificationList";
 import NotificationList from "./NotificationList";
 import NotificationExpandedView from "./NotificationExpandedView";
 import NotificationExpandedView from "./NotificationExpandedView";
-import { PorterAppNotification } from "../activity-feed/events/types";
+import { ClientNotification } from "lib/porter-apps/notification";
 
 
 type Props = {
 type Props = {
-    notifications: PorterAppNotification[];
+    notifications: ClientNotification[];
+    projectId: number;
+    clusterId: number;
+    appName: string;
+    deploymentTargetId: string;
+    appId: number;
 };
 };
 
 
 const NotificationFeed: React.FC<Props> = ({
 const NotificationFeed: React.FC<Props> = ({
     notifications,
     notifications,
+    projectId,
+    clusterId,
+    appName,
+    deploymentTargetId,
+    appId,
 }) => {
 }) => {
-    const [selectedNotification, setSelectedNotification] = useState<PorterAppNotification | undefined>(notifications.length ? notifications[0] : undefined);
+    const [selectedNotification, setSelectedNotification] = useState<ClientNotification | undefined>(notifications.length ? notifications[0] : undefined);
 
 
-    const handleTileClick = (notification: PorterAppNotification) => {
+    const handleTileClick = (notification: ClientNotification) => {
         setSelectedNotification(notification);
         setSelectedNotification(notification);
     };
     };
 
 
@@ -28,6 +38,11 @@ const NotificationFeed: React.FC<Props> = ({
                     />
                     />
                     <NotificationExpandedView 
                     <NotificationExpandedView 
                         notification={selectedNotification} 
                         notification={selectedNotification} 
+                        projectId={projectId}
+                        clusterId={clusterId}
+                        appName={appName}
+                        deploymentTargetId={deploymentTargetId}
+                        appId={appId}
                     />
                     />
                 </>
                 </>
             )}
             )}
@@ -39,7 +54,8 @@ export default NotificationFeed;
 
 
 const StyledNotificationFeed = styled.div`
 const StyledNotificationFeed = styled.div`
     display: flex;
     display: flex;
-    height: 600px;
+    margin-bottom: -50px;
+    height: 800px;
     width: 100%;
     width: 100%;
     animation: fadeIn 0.3s 0s;
     animation: fadeIn 0.3s 0s;
     @keyframes fadeIn {
     @keyframes fadeIn {

+ 7 - 7
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationList.tsx

@@ -1,12 +1,12 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import { PorterAppNotification } from "../activity-feed/events/types";
 import NotificationTile from "./NotificationTile";
 import NotificationTile from "./NotificationTile";
+import { ClientNotification } from "lib/porter-apps/notification";
 
 
 type Props = {
 type Props = {
-    onTileClick: (event: PorterAppNotification) => void;
-    notifications: PorterAppNotification[];
-    selectedNotification: PorterAppNotification;
+    onTileClick: (event: ClientNotification) => void;
+    notifications: ClientNotification[];
+    selectedNotification: ClientNotification;
 };
 };
 
 
 const NotificationList: React.FC<Props> = ({
 const NotificationList: React.FC<Props> = ({
@@ -31,12 +31,12 @@ const NotificationList: React.FC<Props> = ({
 export default NotificationList;
 export default NotificationList;
 
 
 const StyledNotificationList = styled.div`
 const StyledNotificationList = styled.div`
-    width: 300px;
+    width: 200px;
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    height: 600px;
+    height: 100%;
     overflow: auto;
     overflow: auto;
-    border-bottom: 1px solid #494b4f;
+    border-right: 1px solid #ffffff44;
     ::-webkit-scrollbar {
     ::-webkit-scrollbar {
         width: 3px;
         width: 3px;
         :horizontal {
         :horizontal {

+ 4 - 4
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationTile.tsx

@@ -1,12 +1,12 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import { PorterAppNotification } from "../activity-feed/events/types";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import { feedDate } from "shared/string_utils";
 import { feedDate } from "shared/string_utils";
+import { ClientNotification } from "lib/porter-apps/notification";
 
 
 type Props = {
 type Props = {
-  notification: PorterAppNotification;
+  notification: ClientNotification;
   selected: boolean;
   selected: boolean;
   onClick: () => void;
   onClick: () => void;
 };
 };
@@ -21,9 +21,9 @@ const NotificationTile: React.FC<Props> = ({
       <NotificationContent>
       <NotificationContent>
         <Text color="helper">{feedDate(notification.timestamp)}</Text>
         <Text color="helper">{feedDate(notification.timestamp)}</Text>
         <Spacer y={0.5} />
         <Spacer y={0.5} />
-        <NotificationSummary>{notification.human_readable_summary}</NotificationSummary>
+        <Text color="helper">Service: <ServiceName>{notification.serviceName}</ServiceName></Text>
         <Spacer y={0.5} />
         <Spacer y={0.5} />
-        <Text color="helper">Service: <ServiceName>{notification.service_name}</ServiceName></Text>
+        <NotificationSummary>{notification.isDeployRelated ? "Your service failed to deploy" : "Your service is unhealthy"}</NotificationSummary>
       </NotificationContent>
       </NotificationContent>
     </StyledNotificationTile>
     </StyledNotificationTile>
   );
   );

+ 0 - 1
dashboard/src/main/home/app-dashboard/validate-apply/build-settings/docker/DockerfileSettings.tsx

@@ -6,7 +6,6 @@ import { PorterAppFormData } from 'lib/porter-apps';
 import Input from 'components/porter/Input';
 import Input from 'components/porter/Input';
 import FileSelector from '../FileSelector';
 import FileSelector from '../FileSelector';
 import styled from 'styled-components';
 import styled from 'styled-components';
-import CollapsibleContainer from 'components/porter/CollapsibleContainer';
 import Button from 'components/porter/Button';
 import Button from 'components/porter/Button';
 
 
 type Props = {
 type Props = {

+ 23 - 26
dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx

@@ -43,6 +43,9 @@ type Props = {
     };
     };
     filterPredeploy?: boolean;
     filterPredeploy?: boolean;
     appId: number;
     appId: number;
+    selectedService?: string;
+    selectedRevisionId?: string;
+    defaultScrollToBottomEnabled?: boolean;
 };
 };
 
 
 const DEFAULT_LOG_TIMEOUT_SECONDS = 60;
 const DEFAULT_LOG_TIMEOUT_SECONDS = 60;
@@ -58,6 +61,9 @@ const Logs: React.FC<Props> = ({
     logFilterNames = ["service_name", "revision", "output_stream"], // these are the names of filters that will be displayed in the UI
     logFilterNames = ["service_name", "revision", "output_stream"], // these are the names of filters that will be displayed in the UI
     filterPredeploy = false,
     filterPredeploy = false,
     appId,
     appId,
+    selectedService,
+    selectedRevisionId,
+    defaultScrollToBottomEnabled = true,
 }) => {
 }) => {
     const { search } = useLocation();
     const { search } = useLocation();
     const queryParams = new URLSearchParams(search);
     const queryParams = new URLSearchParams(search);
@@ -69,7 +75,7 @@ const Logs: React.FC<Props> = ({
     }
     }
 
 
     const scrollToBottomRef = useRef<HTMLDivElement | null>(null);
     const scrollToBottomRef = useRef<HTMLDivElement | null>(null);
-    const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
+    const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(defaultScrollToBottomEnabled);
     const [enteredSearchText, setEnteredSearchText] = useState("");
     const [enteredSearchText, setEnteredSearchText] = useState("");
     const [searchText, setSearchText] = useState("");
     const [searchText, setSearchText] = useState("");
     const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
     const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
@@ -78,15 +84,22 @@ const Logs: React.FC<Props> = ({
     const [hasPorterAgent, setHasPorterAgent] = useState(true);
     const [hasPorterAgent, setHasPorterAgent] = useState(true);
     const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
     const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
     const [isLoading, setIsLoading] = useState(true);
     const [isLoading, setIsLoading] = useState(true);
-    const [logsError, setLogsError] = useState<string | undefined>(undefined);
 
 
     const [selectedFilterValues, setSelectedFilterValues] = useState<Record<FilterName, string>>({
     const [selectedFilterValues, setSelectedFilterValues] = useState<Record<FilterName, string>>({
-        service_name: logQueryParamOpts?.service ?? GenericFilter.getDefaultOption("service_name").value,
+        service_name: logQueryParamOpts.service ?? selectedService ?? GenericFilter.getDefaultOption("service_name").value,
         pod_name: "", // not supported in v2
         pod_name: "", // not supported in v2
         revision: logQueryParamOpts.revision ?? GenericFilter.getDefaultOption("revision").value, // refers to revision number
         revision: logQueryParamOpts.revision ?? GenericFilter.getDefaultOption("revision").value, // refers to revision number
         output_stream: logQueryParamOpts.output_stream ?? GenericFilter.getDefaultOption("output_stream").value,
         output_stream: logQueryParamOpts.output_stream ?? GenericFilter.getDefaultOption("output_stream").value,
-        revision_id: logQueryParamOpts.revision_id ?? GenericFilter.getDefaultOption("revision_id").value,
+        revision_id: logQueryParamOpts.revision_id ?? selectedRevisionId ?? GenericFilter.getDefaultOption("revision_id").value,
     });
     });
+    // for some reason the filters were not being updated when the service name or revision number changed in the notification feed, so this ensures it
+    useEffect(() => {
+        setSelectedFilterValues({
+            ...selectedFilterValues,
+            service_name: logQueryParamOpts.service ?? selectedService ?? GenericFilter.getDefaultOption("service_name").value,
+            revision_id: logQueryParamOpts.revision_id ?? selectedRevisionId ?? GenericFilter.getDefaultOption("revision_id").value,
+        })
+    }, [selectedService, selectedRevisionId]);
 
 
     const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
     const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
     const { latestRevision: { revision_number: latestRevisionNumber } } = useLatestRevision();
     const { latestRevision: { revision_number: latestRevisionNumber } } = useLatestRevision();
@@ -263,7 +276,11 @@ const Logs: React.FC<Props> = ({
             } as GenericFilter,
             } as GenericFilter,
         ].filter((f: GenericFilter) => logFilterNames.includes(f.name)))
         ].filter((f: GenericFilter) => logFilterNames.includes(f.name)))
 
 
-        if (latestRevisionNumber && !logQueryParamOpts.revision && !logQueryParamOpts.revision_id) { // default to filter by latest revision number if no revision-related query params supplied
+        // default to filter by latest revision number if no revision-related filter options are selected
+        if (latestRevisionNumber && 
+            selectedFilterValues.revision === GenericFilter.getDefaultOption("revision").value && 
+            selectedFilterValues.revision_id === GenericFilter.getDefaultOption("revision_id").value
+        ) { 
             setSelectedFilterValues({
             setSelectedFilterValues({
                 ...selectedFilterValues,
                 ...selectedFilterValues,
                 revision: latestRevisionNumber.toString(),
                 revision: latestRevisionNumber.toString(),
@@ -477,18 +494,7 @@ const Logs: React.FC<Props> = ({
                 <I className="material-icons">add</I> Install Porter agent
                 <I className="material-icons">add</I> Install Porter agent
             </Button>
             </Button>
         </Fieldset>
         </Fieldset>
-    ) : logsError ? (
-        <Fieldset>
-            <Container row>
-                <WarnI className="material-icons">warning</WarnI>
-                <Text color="helper">
-                    Porter encountered an error retrieving logs for this application.
-                </Text>
-            </Container>
-        </Fieldset>
-    ) : (
-        renderContents()
-    );
+    ) : renderContents();
 };
 };
 
 
 export default Logs;
 export default Logs;
@@ -501,15 +507,6 @@ const I = styled.i`
   justify-content: center;
   justify-content: center;
 `;
 `;
 
 
-const WarnI = styled.i`
-  font-size: 18px;
-  display: flex;
-  align-items: center;
-  margin-right: 10px;
-  justify-content: center;
-  opacity: 0.6;
-`;
-
 const Spinner = styled.img`
 const Spinner = styled.img`
   width: 15px;
   width: 15px;
   height: 15px;
   height: 15px;

+ 1 - 0
dashboard/src/shared/themes/midnight.ts

@@ -6,6 +6,7 @@ const theme = {
   button: "#3A48CA",
   button: "#3A48CA",
   clickable: {
   clickable: {
     bg: "linear-gradient(180deg, #171B21, #121212)",
     bg: "linear-gradient(180deg, #171B21, #121212)",
+    clickedBg: "#202126",
   },
   },
   modalBg: "#171B2111",
   modalBg: "#171B2111",
   text: {
   text: {

+ 10 - 1
internal/porter_app/notifications/app_event.go

@@ -85,7 +85,7 @@ func isNotificationDuplicate(
 	eventRepo repository.PorterAppEventRepository,
 	eventRepo repository.PorterAppEventRepository,
 	deploymentTargetID string,
 	deploymentTargetID string,
 ) (bool, error) {
 ) (bool, error) {
-	ctx, span := telemetry.NewSpan(ctx, "is-app-event-duplicate")
+	ctx, span := telemetry.NewSpan(ctx, "is-notification-duplicate")
 	defer span.End()
 	defer span.End()
 
 
 	deploymentTargetUUID, err := uuid.Parse(deploymentTargetID)
 	deploymentTargetUUID, err := uuid.Parse(deploymentTargetID)
@@ -100,6 +100,15 @@ func isNotificationDuplicate(
 	if err != nil {
 	if err != nil {
 		return false, telemetry.Error(ctx, span, err, "error converting app id to int")
 		return false, telemetry.Error(ctx, span, err, "error converting app id to int")
 	}
 	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "app-id", Value: notification.AppID},
+		telemetry.AttributeKV{Key: "app-name", Value: notification.AppName},
+		telemetry.AttributeKV{Key: "app-revision-id", Value: notification.AppRevisionID},
+		telemetry.AttributeKV{Key: "agent-event-id", Value: notification.AgentEventID},
+		telemetry.AttributeKV{Key: "service-name", Value: notification.ServiceName},
+	)
+
 	existingEvents, _, err := eventRepo.ListEventsByPorterAppIDAndDeploymentTargetID(ctx, uint(appIdInt), deploymentTargetUUID)
 	existingEvents, _, err := eventRepo.ListEventsByPorterAppIDAndDeploymentTargetID(ctx, uint(appIdInt), deploymentTargetUUID)
 	if err != nil {
 	if err != nil {
 		return false, telemetry.Error(ctx, span, err, "error listing porter app events for event type with deployment target id")
 		return false, telemetry.Error(ctx, span, err, "error listing porter app events for event type with deployment target id")

+ 19 - 1
internal/porter_app/notifications/notification.go

@@ -21,7 +21,7 @@ type HandleNotificationInput struct {
 
 
 // HandleNotification handles the logic for processing agent events
 // HandleNotification handles the logic for processing agent events
 func HandleNotification(ctx context.Context, inp HandleNotificationInput) error {
 func HandleNotification(ctx context.Context, inp HandleNotificationInput) error {
-	ctx, span := telemetry.NewSpan(ctx, "handle-notification")
+	ctx, span := telemetry.NewSpan(ctx, "internal-handle-notification")
 	defer span.End()
 	defer span.End()
 
 
 	// 1. parse agent event
 	// 1. parse agent event
@@ -36,6 +36,15 @@ func HandleNotification(ctx context.Context, inp HandleNotificationInput) error
 	// 2. convert agent event to baseNotification
 	// 2. convert agent event to baseNotification
 	baseNotification := agentEventToNotification(*agentEventMetadata)
 	baseNotification := agentEventToNotification(*agentEventMetadata)
 
 
+	// telemetry.WithAttributes(span,
+	// 	telemetry.AttributeKV{Key: "app-id", Value: baseNotification.AppID},
+	// 	telemetry.AttributeKV{Key: "app-name", Value: baseNotification.AppName},
+	// 	telemetry.AttributeKV{Key: "service-name", Value: baseNotification.ServiceName},
+	// 	telemetry.AttributeKV{Key: "app-revision-id", Value: baseNotification.AppRevisionID},
+	// 	telemetry.AttributeKV{Key: "agent-event-id", Value: baseNotification.AgentEventID},
+	// 	telemetry.AttributeKV{Key: "agent-detail", Value: baseNotification.AgentDetail},
+	// )
+
 	// 3. dedupe notification
 	// 3. dedupe notification
 	isDuplicate, err := isNotificationDuplicate(ctx, baseNotification, inp.EventRepo, inp.DeploymentTargetID)
 	isDuplicate, err := isNotificationDuplicate(ctx, baseNotification, inp.EventRepo, inp.DeploymentTargetID)
 	if err != nil {
 	if err != nil {
@@ -46,6 +55,15 @@ func HandleNotification(ctx context.Context, inp HandleNotificationInput) error
 		return nil
 		return nil
 	}
 	}
 
 
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "app-id", Value: baseNotification.AppID},
+		telemetry.AttributeKV{Key: "app-name", Value: baseNotification.AppName},
+		telemetry.AttributeKV{Key: "service-name", Value: baseNotification.ServiceName},
+		telemetry.AttributeKV{Key: "app-revision-id", Value: baseNotification.AppRevisionID},
+		telemetry.AttributeKV{Key: "agent-event-id", Value: baseNotification.AgentEventID},
+		telemetry.AttributeKV{Key: "agent-detail", Value: baseNotification.AgentDetail},
+	)
+
 	// 4. hydrate notification with k8s deployment info
 	// 4. hydrate notification with k8s deployment info
 	hydratedNotification, err := hydrateNotification(ctx, hydrateNotificationInput{
 	hydratedNotification, err := hydrateNotification(ctx, hydrateNotificationInput{
 		Notification:       baseNotification,
 		Notification:       baseNotification,