Explorar el Código

fix routing for multiple deployment targets (#4089)

Feroze Mohideen hace 2 años
padre
commit
550fdea46b
Se han modificado 15 ficheros con 309 adiciones y 156 borrados
  1. 41 0
      dashboard/src/lib/porter-apps/routing.ts
  2. 10 19
      dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
  3. 16 0
      dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx
  4. 3 3
      dashboard/src/main/home/app-dashboard/app-view/tabs/Activity.tsx
  5. 2 2
      dashboard/src/main/home/app-dashboard/app-view/tabs/Notifications.tsx
  6. 32 17
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx
  7. 12 5
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx
  8. 12 4
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx
  9. 0 1
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx
  10. 17 5
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx
  11. 32 9
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx
  12. 95 83
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx
  13. 18 6
      dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationFeed.tsx
  14. 9 1
      dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/expanded-views/ServiceNotificationExpandedView.tsx
  15. 10 1
      dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/expanded-views/messages/ServiceMessage.tsx

+ 41 - 0
dashboard/src/lib/porter-apps/routing.ts

@@ -0,0 +1,41 @@
+import { type DeploymentTarget } from "lib/hooks/useDeploymentTarget";
+
+import { type ProjectType } from "shared/types";
+
+export const formattedPath = ({
+  currentProject,
+  tab,
+  deploymentTarget,
+  appName,
+  queryParams,
+}: {
+  currentProject?: ProjectType;
+  tab: string;
+  deploymentTarget: DeploymentTarget;
+  appName: string;
+  queryParams?: Record<string, string>;
+}): string => {
+  let path = `/apps/${appName}/${tab}`;
+  const query = new URLSearchParams();
+
+  if (currentProject?.managed_deployment_targets_enabled) {
+    query.set("target", deploymentTarget.id);
+  }
+
+  if (deploymentTarget.is_preview) {
+    query.set("target", deploymentTarget.id);
+    path = `/preview-environments/apps/${appName}/${tab}`;
+  }
+
+  if (queryParams) {
+    Object.entries(queryParams).forEach(([key, value]) => {
+      query.set(key, value);
+    });
+  }
+
+  if (query.toString()) {
+    path += `?${query.toString()}`;
+  }
+
+  return path;
+};

+ 10 - 19
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -113,6 +113,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     appEnv,
     setPreviewRevision,
     latestClientNotifications,
+    tabUrlGenerator,
   } = useLatestRevision();
   const { validateApp, setServiceDeletions } = useAppValidation({
     deploymentTargetID: deploymentTarget.id,
@@ -335,7 +336,9 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       setPreviewRevision(null);
 
       history.push(
-        formattedPath(DEFAULT_TAB, deploymentTarget.id, porterAppRecord.name)
+        tabUrlGenerator({
+          tab: DEFAULT_TAB,
+        })
       );
     } catch (err) {
       showIntercomWithMessage({
@@ -482,7 +485,9 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           numNotifications > 0 ? (
             <Tag borderColor={"#FFBF00"}>
               <Link
-                to={`/apps/${latestProto.name}/notifications`}
+                to={tabUrlGenerator({
+                  tab: "notifications",
+                })}
                 color={"#FFBF00"}
               >
                 <TagIcon src={alert} />
@@ -537,22 +542,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     latestClientNotifications.length,
   ]);
 
-  const formattedPath = (
-    tab: string,
-    deploymentTargetId: string,
-    appName: string
-  ): string => {
-    let path = `/apps/${appName}/${tab}`;
-    if (currentProject?.managed_deployment_targets_enabled) {
-      path = `/apps/${appName}/${tab}?target=${deploymentTargetId}`;
-    }
-    if (deploymentTarget.is_preview) {
-      path = `/preview-environments/apps/${appName}/${tab}?target=${deploymentTargetId}`;
-    }
-
-    return path;
-  };
-
   useEffect(() => {
     const newProto = previewRevision
       ? PorterApp.fromJsonString(atob(previewRevision.b64_app_proto), {
@@ -637,7 +626,9 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           currentTab={currentTab}
           setCurrentTab={(tab) => {
             history.push(
-              formattedPath(tab, deploymentTarget.id, porterAppRecord.name)
+              tabUrlGenerator({
+                tab,
+              })
             );
           }}
         />

+ 16 - 0
dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx

@@ -23,6 +23,7 @@ import {
   deserializeNotifications,
   type ClientNotification,
 } from "lib/porter-apps/notification";
+import { formattedPath } from "lib/porter-apps/routing";
 import {
   type ClientService,
   type DetectedServices,
@@ -62,6 +63,13 @@ type LatestRevisionContextType = {
   setPreviewRevision: Dispatch<SetStateAction<AppRevision | null>>;
   latestClientServices: ClientService[];
   loading: boolean;
+  tabUrlGenerator: ({
+    tab,
+    queryParams,
+  }: {
+    tab: string;
+    queryParams?: Record<string, string>;
+  }) => string;
 };
 
 const LatestRevisionContext = createContext<LatestRevisionContextType | null>(
@@ -372,6 +380,14 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
         latestClientServices,
         appName,
         loading,
+        tabUrlGenerator: ({ tab, queryParams }) =>
+          formattedPath({
+            currentProject,
+            deploymentTarget: currentDeploymentTarget,
+            appName,
+            tab,
+            queryParams,
+          }),
       }}
     >
       {children}

+ 3 - 3
dashboard/src/main/home/app-dashboard/app-view/tabs/Activity.tsx

@@ -9,10 +9,10 @@ const Activity: React.FC = () => {
 
   return (
     <ActivityFeed
-      currentProject={projectId}
-      currentCluster={clusterId}
+      projectId={projectId}
+      clusterId={clusterId}
       appName={latestProto.name}
-      deploymentTargetId={deploymentTarget.id}
+      deploymentTarget={deploymentTarget}
     />
   );
 };

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

@@ -10,7 +10,7 @@ const Notifications: React.FC = () => {
     clusterId,
     appName,
     porterApp: { id: appId },
-    deploymentTarget: { id: deploymentTargetId },
+    deploymentTarget,
   } = useLatestRevision();
 
   return (
@@ -19,7 +19,7 @@ const Notifications: React.FC = () => {
       projectId={projectId}
       clusterId={clusterId}
       appName={appName}
-      deploymentTargetId={deploymentTargetId}
+      deploymentTarget={deploymentTarget}
       appId={appId}
     />
   );

+ 32 - 17
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
 import axios from "axios";
 import _ from "lodash";
@@ -11,26 +11,30 @@ import Fieldset from "components/porter/Fieldset";
 import Pagination from "components/porter/Pagination";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
+import { type DeploymentTarget } from "lib/hooks/useDeploymentTarget";
+import { formattedPath } from "lib/porter-apps/routing";
 
 import api from "shared/api";
+import { Context } from "shared/Context";
 import { feedDate } from "shared/string_utils";
+
 import EventCard from "./events/cards/EventCard";
 import { porterAppEventValidator, type PorterAppEvent } from "./events/types";
 
 type Props = {
   appName: string;
-  currentProject: number;
-  currentCluster: number;
-  deploymentTargetId: string;
+  projectId: number;
+  clusterId: number;
+  deploymentTarget: DeploymentTarget;
 };
 
 const EVENTS_POLL_INTERVAL = 5000; // poll every 5 seconds
 
 const ActivityFeed: React.FC<Props> = ({
   appName,
-  deploymentTargetId,
-  currentCluster,
-  currentProject,
+  deploymentTarget,
+  clusterId,
+  projectId,
 }) => {
   const [events, setEvents] = useState<PorterAppEvent[] | undefined>(undefined);
   const [page, setPage] = useState<number>(1);
@@ -40,22 +44,24 @@ const ActivityFeed: React.FC<Props> = ({
   );
   const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false);
 
+  const { currentProject } = useContext(Context);
+
   const {
     data: eventFetchData,
     isLoading: isEventFetchLoading,
     isRefetching,
   } = useQuery(
-    ["appEvents", deploymentTargetId, page],
+    ["appEvents", deploymentTarget.id, page],
     async () => {
       const res = await api.appEvents(
         "<token>",
         {
-          deployment_target_id: deploymentTargetId,
+          deployment_target_id: deploymentTarget.id,
           page,
         },
         {
-          cluster_id: currentCluster,
-          project_id: currentProject,
+          cluster_id: clusterId,
+          project_id: projectId,
           porter_app_name: appName,
         }
       );
@@ -95,12 +101,12 @@ const ActivityFeed: React.FC<Props> = ({
 
   const { data: porterAgentCheck, isLoading: porterAgentCheckLoading } =
     useQuery(
-      ["detectPorterAgent", currentProject, currentCluster],
+      ["detectPorterAgent", projectId, clusterId],
       async () => {
         const res = await api.detectPorterAgent(
           "<token>",
           {},
-          { project_id: currentProject, cluster_id: currentCluster }
+          { project_id: projectId, cluster_id: clusterId }
         );
         // response will either have version key if porter agent found, or error key if not
         const parsed = await z
@@ -131,7 +137,7 @@ const ActivityFeed: React.FC<Props> = ({
       await api.installPorterAgent(
         "<token>",
         {},
-        { project_id: currentProject, cluster_id: currentCluster }
+        { project_id: projectId, cluster_id: clusterId }
       );
       window.location.reload();
     } catch (err) {
@@ -211,13 +217,22 @@ const ActivityFeed: React.FC<Props> = ({
               <Text>{feedDate(event.created_at).split(", ")[1]}</Text>
             </Time>
             <EventCard
-              deploymentTargetId={deploymentTargetId}
+              deploymentTargetId={deploymentTarget.id}
               event={event}
               key={i}
               isLatestDeployEvent={i === getLatestDeployEventIndex()}
-              projectId={currentProject}
-              clusterId={currentCluster}
+              projectId={projectId}
+              clusterId={clusterId}
               appName={appName}
+              tabUrlGenerator={({ tab, queryParams }) =>
+                formattedPath({
+                  currentProject,
+                  deploymentTarget,
+                  tab,
+                  appName,
+                  queryParams,
+                })
+              }
             />
           </EventWrapper>
         );

+ 12 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx

@@ -9,6 +9,7 @@ import Spacer from "components/porter/Spacer";
 import Tag from "components/porter/Tag";
 import Text from "components/porter/Text";
 import { type PorterAppRecord } from "main/home/app-dashboard/app-view/AppView";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 
 import build from "assets/build.png";
 import document from "assets/document.svg";
@@ -42,13 +43,14 @@ type Props = {
 
 const BuildEventCard: React.FC<Props> = ({
   event,
-  appName,
   projectId,
   clusterId,
   gitCommitUrl,
   displayCommitSha,
   porterApp,
 }) => {
+  const { tabUrlGenerator } = useLatestRevision();
+
   const renderStatusText = (event: PorterAppBuildEvent): JSX.Element => {
     const color = getStatusColor(event.status);
     return (
@@ -62,11 +64,16 @@ const BuildEventCard: React.FC<Props> = ({
     );
   };
 
-  const renderLogsAndRetry = (event: PorterAppBuildEvent): JSX.Element => {
+  const renderLogsAndRetry = (): JSX.Element => {
     return (
       <Container row>
         <Tag>
-          <Link to={`/apps/${appName}/events?event_id=${event.id}`}>
+          <Link
+            to={tabUrlGenerator({
+              tab: "events",
+              queryParams: { event_id: event.id },
+            })}
+          >
             <TagIcon src={document} />
             Logs
           </Link>
@@ -95,9 +102,9 @@ const BuildEventCard: React.FC<Props> = ({
       case "SUCCESS":
         return null;
       case "CANCELED":
-        return renderLogsAndRetry(event);
+        return renderLogsAndRetry();
       case "FAILED":
-        return renderLogsAndRetry(event);
+        return renderLogsAndRetry();
       default:
         return (
           <Container row>

+ 12 - 4
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx

@@ -78,8 +78,12 @@ const DeployEventCard: React.FC<Props> = ({
     projectId,
     clusterId,
   });
-  const { latestRevision, porterApp, latestClientNotifications } =
-    useLatestRevision();
+  const {
+    latestRevision,
+    porterApp,
+    latestClientNotifications,
+    tabUrlGenerator,
+  } = useLatestRevision();
 
   const rollbackTargetVersionNumber = useMemo(() => {
     if (
@@ -322,7 +326,12 @@ const DeployEventCard: React.FC<Props> = ({
             <>
               <Spacer inline x={0.5} />
               <Tag borderColor="#FFBF00">
-                <Link to={`/apps/${appName}/notifications`} color={"#FFBF00"}>
+                <Link
+                  to={tabUrlGenerator({
+                    tab: "notifications",
+                  })}
+                  color={"#FFBF00"}
+                >
                   <TagIcon src={alert} />
                   Notifications
                 </Link>
@@ -373,7 +382,6 @@ const DeployEventCard: React.FC<Props> = ({
             serviceDeploymentMetadata={
               event.metadata.service_deployment_metadata
             }
-            appName={appName}
             revisionNumber={revisionIdToNumber[event.metadata.app_revision_id]}
             revisionId={event.metadata.app_revision_id}
           />

+ 0 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx

@@ -123,7 +123,6 @@ const EventCard: React.FC<Props> = ({
     .with({ type: "PRE_DEPLOY" }, (ev) => (
       <PreDeployEventCard
         event={ev}
-        appName={appName}
         projectId={projectId}
         clusterId={clusterId}
         gitCommitUrl={gitCommitUrl}

+ 17 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx

@@ -34,7 +34,6 @@ import {
 
 type Props = {
   event: PorterAppPreDeployEvent;
-  appName: string;
   projectId: number;
   clusterId: number;
   gitCommitUrl: string;
@@ -43,13 +42,13 @@ type Props = {
 
 const PreDeployEventCard: React.FC<Props> = ({
   event,
-  appName,
   projectId,
   clusterId,
   gitCommitUrl,
   displayCommitSha,
 }) => {
-  const { porterApp, latestClientNotifications } = useLatestRevision();
+  const { porterApp, latestClientNotifications, tabUrlGenerator } =
+    useLatestRevision();
 
   const renderStatusText = (event: PorterAppPreDeployEvent): JSX.Element => {
     const color = getStatusColor(event.status);
@@ -110,7 +109,14 @@ const PreDeployEventCard: React.FC<Props> = ({
           <Spacer inline x={1} />
           <Tag>
             <Link
-              to={`/apps/${appName}/events?event_id=${event.id}&service=predeploy&revision_id=${event.metadata.app_revision_id}`}
+              to={tabUrlGenerator({
+                tab: "events",
+                queryParams: {
+                  event_id: event.id,
+                  service: "predeploy",
+                  revision_id: event.metadata.app_revision_id,
+                },
+              })}
             >
               <TagIcon src={document} />
               Logs
@@ -140,7 +146,13 @@ const PreDeployEventCard: React.FC<Props> = ({
               <Spacer inline x={0.5} />
               <Container row>
                 <Tag borderColor="#FFBF00">
-                  <Link to={`/apps/${appName}/notifications`} color={"#FFBF00"}>
+                  <Link
+                    to={tabUrlGenerator({
+                      tab: "notifications",
+                      queryParams: {},
+                    })}
+                    color={"#FFBF00"}
+                  >
                     <TagIcon src={alert} />
                     Notifications
                   </Link>

+ 32 - 9
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx

@@ -30,18 +30,16 @@ type Props = {
       type: string;
     }
   >;
-  appName: string;
   revisionId: string;
-  revisionNumber: number;
+  revisionNumber?: number;
 };
 
 const ServiceStatusDetail: React.FC<Props> = ({
   serviceDeploymentMetadata,
-  appName,
   revisionId,
   revisionNumber,
 }) => {
-  const { latestClientServices, latestClientNotifications } =
+  const { latestClientServices, latestClientNotifications, tabUrlGenerator } =
     useLatestRevision();
   const convertEventStatusToCopy = (status: string): string => {
     switch (status) {
@@ -103,7 +101,12 @@ const ServiceStatusDetail: React.FC<Props> = ({
                     <>
                       <Tag borderColor="#FFBF00">
                         <Link
-                          to={`/apps/${appName}/notifications?service=${key}`}
+                          to={tabUrlGenerator({
+                            tab: "notifications",
+                            queryParams: {
+                              service: key,
+                            },
+                          })}
                           color={"#FFBF00"}
                         >
                           <TagIcon src={alert} />
@@ -113,11 +116,17 @@ const ServiceStatusDetail: React.FC<Props> = ({
                       <Spacer inline x={0.5} />
                     </>
                   )}
-                  {serviceType !== "job" && (
+                  {serviceType !== "job" && revisionNumber && (
                     <>
                       <Tag>
                         <Link
-                          to={`/apps/${appName}/logs?version=${revisionNumber}&service=${key}`}
+                          to={tabUrlGenerator({
+                            tab: "logs",
+                            queryParams: {
+                              version: revisionNumber.toString(),
+                              service: key,
+                            },
+                          })}
                         >
                           <TagIcon src={document} />
                           Logs
@@ -125,7 +134,14 @@ const ServiceStatusDetail: React.FC<Props> = ({
                       </Tag>
                       <Spacer inline x={0.5} />
                       <Tag>
-                        <Link to={`/apps/${appName}/metrics?service=${key}`}>
+                        <Link
+                          to={tabUrlGenerator({
+                            tab: "metrics",
+                            queryParams: {
+                              service: key,
+                            },
+                          })}
+                        >
                           <TagIcon src={metrics} />
                           Metrics
                         </Link>
@@ -135,7 +151,14 @@ const ServiceStatusDetail: React.FC<Props> = ({
                   {serviceType === "job" && (
                     <Tag>
                       <TagIcon src={calendar} style={{ marginTop: "2px" }} />
-                      <Link to={`/apps/${appName}/job-history?service=${key}`}>
+                      <Link
+                        to={tabUrlGenerator({
+                          tab: "job-history",
+                          queryParams: {
+                            service: key,
+                          },
+                        })}
+                      >
                         History
                       </Link>
                     </Tag>

+ 95 - 83
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx

@@ -1,107 +1,119 @@
-import Loading from "components/Loading";
-import Spacer from "components/porter/Spacer";
 import React, { useEffect, useState } from "react";
-import api from "shared/api";
+import { useQuery } from "@tanstack/react-query";
+import _ from "lodash";
+import { useLocation } from "react-router";
 import styled from "styled-components";
+import { match } from "ts-pattern";
+
+import Loading from "components/Loading";
 import Link from "components/porter/Link";
+import Spacer from "components/porter/Spacer";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+
+import api from "shared/api";
+
+import {
+  porterAppEventValidator,
+  type PorterAppBuildEvent,
+  type PorterAppPreDeployEvent,
+} from "../types";
 import BuildEventFocusView from "./BuildEventFocusView";
 import PreDeployEventFocusView from "./PredeployEventFocusView";
-import _ from "lodash";
-import { type PorterAppBuildEvent, type PorterAppPreDeployEvent, porterAppEventValidator } from "../types";
-import { useLocation } from "react-router";
-import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
-import { useQuery } from "@tanstack/react-query";
-import { match } from "ts-pattern";
 
 const EVENT_POLL_INTERVAL = 5000; // poll every 5 seconds
 
-type SupportedEventFocusViewEvent = PorterAppBuildEvent | PorterAppPreDeployEvent;
-
-const EventFocusView: React.FC = ({ }) => {
-    const { search } = useLocation();
-    const queryParams = new URLSearchParams(search);
-    const eventId = queryParams.get("event_id");
-    const { projectId, clusterId, latestProto } = useLatestRevision();
-
-    const [event, setEvent] = useState<SupportedEventFocusViewEvent | null>(null);
-
-    const { data } = useQuery(
-        [
-            "getPorterAppEvent",
-            projectId,
-            clusterId,
-            eventId,
-            event,
-        ],
-        async () => {
-            if (eventId == null || eventId === "") {
-                return null;
-            }
-            const eventResp = await api.getPorterAppEvent(
-                "<token>",
-                {},
-                {
-                    project_id: projectId,
-                    cluster_id: clusterId,
-                    event_id: eventId,
-                }
-            );
-            return porterAppEventValidator.parse(eventResp.data.event);
-        },
+type SupportedEventFocusViewEvent =
+  | PorterAppBuildEvent
+  | PorterAppPreDeployEvent;
+
+const EventFocusView: React.FC = () => {
+  const { search } = useLocation();
+  const queryParams = new URLSearchParams(search);
+  const eventId = queryParams.get("event_id");
+  const { projectId, clusterId, tabUrlGenerator } = useLatestRevision();
+
+  const [event, setEvent] = useState<SupportedEventFocusViewEvent | null>(null);
+
+  const { data } = useQuery(
+    ["getPorterAppEvent", projectId, clusterId, eventId, event],
+    async () => {
+      if (eventId == null || eventId === "") {
+        return null;
+      }
+      const eventResp = await api.getPorterAppEvent(
+        "<token>",
+        {},
         {
-            // last condition checks if the event is done running; then we stop refetching
-            enabled: eventId != null && eventId !== "" && !(event?.metadata.end_time != null),
-            refetchInterval: EVENT_POLL_INTERVAL,
+          project_id: projectId,
+          cluster_id: clusterId,
+          event_id: eventId,
         }
-    );
+      );
+      return porterAppEventValidator.parse(eventResp.data.event);
+    },
+    {
+      // last condition checks if the event is done running; then we stop refetching
+      enabled:
+        eventId != null &&
+        eventId !== "" &&
+        !(event?.metadata.end_time != null),
+      refetchInterval: EVENT_POLL_INTERVAL,
+    }
+  );
 
-    useEffect(() => {
-        if (data != null && (data.type === "BUILD" || data.type === "PRE_DEPLOY")) {
-            setEvent(data);
-        }
-    }, [data]);
-
-    const getEventFocusView = () => {
-        return match(event)
-            .with({ type: "BUILD" }, (ev) => <BuildEventFocusView event={ev} />)
-            .with({ type: "PRE_DEPLOY" }, (ev) => <PreDeployEventFocusView event={ev} />)
-            .with(null, () => {
-                if (eventId != null && eventId !== "") {
-                    return <Loading />;
-                } else {
-                    return <div>Event not found</div>;
-                }
-            })
-            .exhaustive();
+  useEffect(() => {
+    if (data != null && (data.type === "BUILD" || data.type === "PRE_DEPLOY")) {
+      setEvent(data);
     }
+  }, [data]);
+
+  const getEventFocusView = (): JSX.Element => {
+    return match(event)
+      .with({ type: "BUILD" }, (ev) => <BuildEventFocusView event={ev} />)
+      .with({ type: "PRE_DEPLOY" }, (ev) => (
+        <PreDeployEventFocusView event={ev} />
+      ))
+      .with(null, () => {
+        if (eventId != null && eventId !== "") {
+          return <Loading />;
+        } else {
+          return <div>Event not found</div>;
+        }
+      })
+      .exhaustive();
+  };
 
-    return (
-        <AppearingView>
-            <Link to={`/apps/${latestProto.name}/activity`}>
-                <BackButton>
-                    <i className="material-icons">keyboard_backspace</i>
-                    Activity feed
-                </BackButton>
-            </Link>
-            <Spacer y={0.5} />
-            {getEventFocusView()}
-        </AppearingView>
-    );
+  return (
+    <AppearingView>
+      <Link
+        to={tabUrlGenerator({
+          tab: "activity",
+        })}
+      >
+        <BackButton>
+          <i className="material-icons">keyboard_backspace</i>
+          Activity feed
+        </BackButton>
+      </Link>
+      <Spacer y={0.5} />
+      {getEventFocusView()}
+    </AppearingView>
+  );
 };
 
 export default EventFocusView;
 
 export const AppearingView = styled.div`
-    width: 100%;
-    animation: fadeIn 0.3s 0s;
-    @keyframes fadeIn {
+  width: 100%;
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
     from {
-        opacity: 0;
+      opacity: 0;
     }
     to {
-        opacity: 1;
-    }
+      opacity: 1;
     }
+  }
 `;
 
 const BackButton = styled.div`
@@ -126,4 +138,4 @@ const BackButton = styled.div`
     font-size: 16px;
     margin-right: 6px;
   }
-`;
+`;

+ 18 - 6
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/NotificationFeed.tsx

@@ -6,8 +6,10 @@ import Fieldset from "components/porter/Fieldset";
 import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
+import { type DeploymentTarget } from "lib/hooks/useDeploymentTarget";
 import { type ClientNotification } from "lib/porter-apps/notification";
 
+import { useLatestRevision } from "../../LatestRevisionContext";
 import NotificationExpandedView from "./expanded-views/NotificationExpandedView";
 import NotificationList from "./NotificationList";
 
@@ -16,7 +18,7 @@ type Props = {
   projectId: number;
   clusterId: number;
   appName: string;
-  deploymentTargetId: string;
+  deploymentTarget: DeploymentTarget;
   appId: number;
 };
 
@@ -25,10 +27,11 @@ const NotificationFeed: React.FC<Props> = ({
   projectId,
   clusterId,
   appName,
-  deploymentTargetId,
+  deploymentTarget,
   appId,
 }) => {
   const { search } = useLocation();
+  const { tabUrlGenerator } = useLatestRevision();
   const history = useHistory();
   const queryParams = new URLSearchParams(search);
   const notificationId = queryParams.get("notification_id");
@@ -68,7 +71,11 @@ const NotificationFeed: React.FC<Props> = ({
     <StyledNotificationFeed>
       {selectedNotification ? (
         <>
-          <Link to={`/apps/${appName}/notifications`}>
+          <Link
+            to={tabUrlGenerator({
+              tab: "notifications",
+            })}
+          >
             <BackButton>
               <i className="material-icons">keyboard_backspace</i>
               Notifications
@@ -80,7 +87,7 @@ const NotificationFeed: React.FC<Props> = ({
             projectId={projectId}
             clusterId={clusterId}
             appName={appName}
-            deploymentTargetId={deploymentTargetId}
+            deploymentTargetId={deploymentTarget.id}
             appId={appId}
           />
         </>
@@ -89,13 +96,18 @@ const NotificationFeed: React.FC<Props> = ({
           notifications={notifications}
           onNotificationClick={(notification: ClientNotification) => {
             history.push(
-              `/apps/${appName}/notifications?notification_id=${notification.id}`
+              tabUrlGenerator({
+                tab: "notifications",
+                queryParams: {
+                  notification_id: notification.id,
+                },
+              })
             );
           }}
           projectId={projectId}
           clusterId={clusterId}
           appName={appName}
-          deploymentTargetId={deploymentTargetId}
+          deploymentTargetId={deploymentTarget.id}
         />
       )}
     </StyledNotificationFeed>

+ 9 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/expanded-views/ServiceNotificationExpandedView.tsx

@@ -14,6 +14,7 @@ import job from "assets/job.png";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 
+import { useLatestRevision } from "../../../LatestRevisionContext";
 import { isServiceNotification } from "../../activity-feed/events/types";
 import ServiceMessage from "./messages/ServiceMessage";
 import {
@@ -39,6 +40,8 @@ const ServiceNotificationExpandedView: React.FC<Props> = ({
   deploymentTargetId,
   appId,
 }) => {
+  const { tabUrlGenerator } = useLatestRevision();
+
   return (
     <StyledNotificationExpandedView>
       <ExpandedViewContent>
@@ -69,7 +72,12 @@ const ServiceNotificationExpandedView: React.FC<Props> = ({
                   style={{ marginTop: "3px", marginLeft: "5px" }}
                 />
                 <Link
-                  to={`/apps/${appName}/job-history?service=${notification.service.name.value}`}
+                  to={tabUrlGenerator({
+                    tab: "job-history",
+                    queryParams: {
+                      service: notification.service.name.value,
+                    },
+                  })}
                 >
                   <Text size={16}>Job history</Text>
                 </Link>

+ 10 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/notifications/expanded-views/messages/ServiceMessage.tsx

@@ -8,6 +8,7 @@ import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Tag from "components/porter/Tag";
 import Text from "components/porter/Text";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import Logs from "main/home/app-dashboard/validate-apply/logs/Logs";
 import { useIntercom } from "lib/hooks/useIntercom";
 import { type ClientService } from "lib/porter-apps/services";
@@ -46,6 +47,8 @@ const ServiceMessage: React.FC<Props> = ({
   showLiveLogs,
 }) => {
   const { showIntercomWithMessage } = useIntercom();
+  const { tabUrlGenerator } = useLatestRevision();
+
   const [logsVisible, setLogsVisible] = useState<boolean>(isFirst);
   const serviceNames = useMemo(() => {
     if (service.config.type === "predeploy") {
@@ -125,7 +128,13 @@ const ServiceMessage: React.FC<Props> = ({
           <Container row>
             <Tag>
               <Link
-                to={`/apps/${appName}/job-history?job_run_id=${message.metadata.job_run_id}&service=${service.name.value}`}
+                to={tabUrlGenerator({
+                  tab: "job-history",
+                  queryParams: {
+                    job_run_id: message.metadata.job_run_id,
+                    service: service.name.value,
+                  },
+                })}
               >
                 <Text size={16}>Job run</Text>
                 <i