ソースを参照

Port over more activity feed subviews to v2 porter yaml (#3553)

Feroze Mohideen 2 年 前
コミット
a6c7293ffe
16 ファイル変更264 行追加320 行削除
  1. 2 2
      dashboard/src/components/porter/Button.tsx
  2. 8 8
      dashboard/src/lib/porter-apps/index.ts
  3. 22 22
      dashboard/src/lib/porter-apps/services.ts
  4. 12 7
      dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
  5. 10 23
      dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx
  6. 9 5
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx
  7. 1 1
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx
  8. 16 15
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx
  9. 0 71
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx
  10. 60 60
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/EventFocusView.tsx
  11. 16 13
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx
  12. 1 0
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts
  13. 9 5
      dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts
  14. 17 10
      dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx
  15. 81 77
      dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx
  16. 0 1
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx

+ 2 - 2
dashboard/src/components/porter/Button.tsx

@@ -38,7 +38,7 @@ const Button: React.FC<Props> = ({
   withBorder,
   rounded,
   alt,
-  type,
+  type = "button",
   disabledTooltipMessage,
 }) => {
   const renderStatus = () => {
@@ -194,7 +194,7 @@ const StyledButton = styled.button<{
       : props.color || props.theme.button;
   }};
   display: flex;
-  ailgn-items: center;
+  align-items: center;
   justify-content: center;
   border-radius: ${(props) => (props.rounded ? "50px" : "5px")};
   border: ${(props) => (props.withBorder ? "1px solid #494b4f" : "none")};

+ 8 - 8
dashboard/src/lib/porter-apps/index.ts

@@ -308,15 +308,15 @@ export function clientAppFromProto(
   const predeployOverrides = serializeService(overrides.predeploy);
   const predeploy = proto.predeploy
     ? [
-        deserializeService({
-          service: serializedServiceFromProto({
-            name: "pre-deploy",
-            service: proto.predeploy,
-            isPredeploy: true,
-          }),
-          override: predeployOverrides,
+      deserializeService({
+        service: serializedServiceFromProto({
+          name: "pre-deploy",
+          service: proto.predeploy,
+          isPredeploy: true,
         }),
-      ]
+        override: predeployOverrides,
+      }),
+    ]
     : undefined;
 
   return {

+ 22 - 22
dashboard/src/lib/porter-apps/services.ts

@@ -72,27 +72,27 @@ export type SerializedService = {
   cpuCores: number;
   ramMegabytes: number;
   config:
-    | {
-        type: "web";
-        domains: {
-          name: string;
-        }[];
-        autoscaling?: SerializedAutoscaling;
-        healthCheck?: SerializedHealthcheck;
-        private: boolean;
-      }
-    | {
-        type: "worker";
-        autoscaling?: SerializedAutoscaling;
-      }
-    | {
-        type: "job";
-        allowConcurrent: boolean;
-        cron: string;
-      }
-    | {
-        type: "predeploy";
-      };
+  | {
+    type: "web";
+    domains: {
+      name: string;
+    }[];
+    autoscaling?: SerializedAutoscaling;
+    healthCheck?: SerializedHealthcheck;
+    private: boolean;
+  }
+  | {
+    type: "worker";
+    autoscaling?: SerializedAutoscaling;
+  }
+  | {
+    type: "job";
+    allowConcurrent: boolean;
+    cron: string;
+  }
+  | {
+    type: "predeploy";
+  };
 };
 
 export function isPredeployService(service: SerializedService | ClientService) {
@@ -285,7 +285,7 @@ export function deserializeService({
             health: config.healthCheck,
             override: overrideWebConfig?.healthCheck,
           }),
-          domains: config.domains.map((domain) => ({
+          domains: [...config.domains, ...(overrideWebConfig?.domains ?? [])].map((domain) => ({
             name: ServiceField.string(
               domain.name,
               overrideWebConfig?.domains.find(

+ 12 - 7
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -28,12 +28,13 @@ import LogsTab from "./tabs/LogsTab";
 import MetricsTab from "./tabs/MetricsTab";
 import RevisionsList from "../validate-apply/revisions-list/RevisionsList";
 import Activity from "./tabs/Activity";
+import EventFocusView from "./tabs/activity-feed/events/focus-views/EventFocusView";
 
 // commented out tabs are not yet implemented
 // will be included as support is available based on data from app revisions rather than helm releases
 const validTabs = [
   "activity",
-  // "events",
+  "events",
   "overview",
   "logs",
   "metrics",
@@ -197,7 +198,10 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         porterApp.name,
       ]);
       setPreviewRevision(null);
-    } catch (err) {}
+
+      // redirect to the default tab after save
+      history.push(`/apps/${porterApp.name}/${DEFAULT_TAB}`);
+    } catch (err) { }
   });
 
   useEffect(() => {
@@ -261,11 +265,11 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
             { label: "Environment", value: "environment" },
             ...(latestProto.build
               ? [
-                  {
-                    label: "Build Settings",
-                    value: "build-settings",
-                  },
-                ]
+                {
+                  label: "Build Settings",
+                  value: "build-settings",
+                },
+              ]
               : []),
             { label: "Settings", value: "settings" },
           ]}
@@ -288,6 +292,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           .with("settings", () => <Settings />)
           .with("logs", () => <LogsTab />)
           .with("metrics", () => <MetricsTab />)
+          .with("events", () => <EventFocusView />)
           .otherwise(() => null)}
         <Spacer y={2} />
       </form>

+ 10 - 23
dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx

@@ -1,35 +1,22 @@
-import { PorterApp } from "@porter-dev/api-contracts";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import { PorterAppFormData } from "lib/porter-apps";
-import React, { useMemo } from "react";
-import { useFormContext, useFormState } from "react-hook-form";
+import React from "react";
 import Logs from "../../validate-apply/logs/Logs"
-import {
-    defaultSerialized,
-    deserializeService,
-} from "lib/porter-apps/services";
-import Error from "components/porter/Error";
-import Button from "components/porter/Button";
 import { useLatestRevision } from "../LatestRevisionContext";
 
 const LogsTab: React.FC = () => {
-    const { projectId, clusterId, latestProto , deploymentTargetId, latestRevision} = useLatestRevision();
+    const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision();
 
     const appName = latestProto.name
     const serviceNames = Object.keys(latestProto.services)
 
     return (
-        <>
-            <Logs
-                projectId={projectId}
-                clusterId={clusterId}
-                appName={appName}
-                serviceNames={serviceNames}
-                deploymentTargetId={deploymentTargetId}
-                latestRevision={latestRevision}
-            />
-        </>
+        <Logs
+            projectId={projectId}
+            clusterId={clusterId}
+            appName={appName}
+            serviceNames={serviceNames}
+            deploymentTargetId={deploymentTargetId}
+            latestRevision={latestRevision}
+        />
     );
 };
 

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

@@ -12,19 +12,23 @@ import { PorterAppDeployEvent } from "../types";
 import AnimateHeight from "react-animate-height";
 import ServiceStatusDetail from "./ServiceStatusDetail";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import { useRevisionIdToNumber } from "lib/hooks/useRevisionList";
 
 type Props = {
   event: PorterAppDeployEvent;
   appName: string;
   showServiceStatusDetail?: boolean;
+  deploymentTargetId: string;
 };
 
-const DeployEventCard: React.FC<Props> = ({ event, appName, showServiceStatusDetail = false }) => {
+const DeployEventCard: React.FC<Props> = ({ event, appName, deploymentTargetId, showServiceStatusDetail = false }) => {
   const { latestRevision } = useLatestRevision();
   const [diffModalVisible, setDiffModalVisible] = useState(false);
   const [revertModalVisible, setRevertModalVisible] = useState(false);
   const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail);
 
+  const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId);
+
   const renderStatusText = () => {
     switch (event.status) {
       case "SUCCESS":
@@ -131,7 +135,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, showServiceStatusDet
         <Container row>
           <Icon height="16px" src={deploy} />
           <Spacer inline width="10px" />
-          <Text>Application version no. {event.metadata?.revision}</Text>
+          <Text>Application version no. {revisionIdToNumber[event.metadata.app_revision_id]}</Text>
         </Container>
       </Container>
       <Spacer y={0.5} />
@@ -140,12 +144,12 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, showServiceStatusDet
           <Icon height="12px" src={getStatusIcon(event.status)} />
           <Spacer inline width="10px" />
           {renderStatusText()}
-          {latestRevision.id !== event.metadata.app_revision_id && (
+          {revisionIdToNumber[event.metadata.app_revision_id] != null && latestRevision.revision_number !== revisionIdToNumber[event.metadata.app_revision_id] && (
             <>
               <Spacer inline x={1} />
               <TempWrapper>
                 <Link hasunderline onClick={() => setRevertModalVisible(true)}>
-                  Revert to version {event.metadata.revision}
+                  Revert to version {revisionIdToNumber[event.metadata.app_revision_id]}
                 </Link>
 
               </TempWrapper>
@@ -184,7 +188,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appName, showServiceStatusDet
           <ServiceStatusDetail
             serviceDeploymentMetadata={event.metadata.service_deployment_metadata}
             appName={appName}
-            revision={event.metadata.revision}
+            revision={revisionIdToNumber[event.metadata.app_revision_id]}
           />
         </AnimateHeight>
       }

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

@@ -21,7 +21,7 @@ const EventCard: React.FC<Props> = ({ event, deploymentTargetId, isLatestDeployE
   return match(event)
     .with({ type: "APP_EVENT" }, (ev) => <AppEventCard event={ev} deploymentTargetId={deploymentTargetId} projectId={projectId} clusterId={clusterId} appName={appName} />)
     .with({ type: "BUILD" }, (ev) => <BuildEventCard event={ev} projectId={projectId} clusterId={clusterId} appName={appName} />)
-    .with({ type: "DEPLOY" }, (ev) => <DeployEventCard event={ev} appName={appName} showServiceStatusDetail={isLatestDeployEvent} />)
+    .with({ type: "DEPLOY" }, (ev) => <DeployEventCard event={ev} appName={appName} showServiceStatusDetail={isLatestDeployEvent} deploymentTargetId={deploymentTargetId} appName={appName} />)
     .with({ type: "PRE_DEPLOY" }, (ev) => <PreDeployEventCard event={ev} appName={appName} projectId={projectId} clusterId={clusterId} />)
     .exhaustive();
 };

+ 16 - 15
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx

@@ -10,18 +10,19 @@ 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";
-import { PorterAppEvent } from "../types";
+import { PorterAppBuildEvent } from "../types";
+import { PorterLog } from "main/home/app-dashboard/expanded-app/logs/types";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 
 type Props = {
-    event: PorterAppEvent;
-    appData: any;
+    event: PorterAppBuildEvent;
 };
 
 const BuildFailureEventFocusView: React.FC<Props> = ({
     event,
-    appData,
 }) => {
+    const { porterApp, projectId, clusterId } = useLatestRevision();
+
     const [logs, setLogs] = useState<PorterLog[]>([]);
     const [isLoading, setIsLoading] = useState<boolean>(true);
     const scrollToBottomRef = useRef<HTMLDivElement>(null);
@@ -36,7 +37,7 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
     }, [isLoading, logs, scrollToBottomRef]);
 
     const getBuildLogs = async () => {
-        if (event == null) {
+        if (event == null || porterApp.git_repo_id == null || porterApp.repo_name == null) {
             return;
         }
         try {
@@ -46,13 +47,13 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
                 "",
                 {},
                 {
-                    project_id: appData.app.project_id,
-                    cluster_id: appData.app.cluster_id,
-                    git_installation_id: appData.app.git_repo_id,
-                    owner: appData.app.repo_name?.split("/")[0],
-                    name: appData.app.repo_name?.split("/")[1],
-                    filename: "porter_stack_" + appData.chart.name + ".yml",
-                    run_id: event.metadata.action_run_id,
+                    project_id: projectId,
+                    cluster_id: clusterId,
+                    git_installation_id: porterApp.git_repo_id,
+                    owner: porterApp.repo_name.split("/")[0],
+                    name: porterApp.repo_name.split("/")[1],
+                    filename: "porter_stack_" + porterApp.name + ".yml",
+                    run_id: event.metadata.action_run_id.toString(),
                 }
             );
             let logs: PorterLog[] = [];
@@ -175,8 +176,8 @@ const BuildFailureEventFocusView: React.FC<Props> = ({
                 target="_blank"
                 to={
                     event.metadata.action_run_id
-                        ? `https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata.action_run_id}`
-                        : `https://github.com/${appData.app.repo_name}/actions`
+                        ? `https://github.com/${porterApp.repo_name}/actions/runs/${event.metadata.action_run_id}`
+                        : `https://github.com/${porterApp.repo_name}/actions`
                 }
             >
                 View full build logs

+ 0 - 71
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/DeployEventFocusView.tsx

@@ -1,71 +0,0 @@
-import Spacer from "components/porter/Spacer";
-import React from "react";
-import dayjs from "dayjs";
-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";
-import { PorterAppDeployEvent } from "../types";
-import { LogFilterQueryParamOpts } from "../../../logs/types";
-
-type Props = {
-    event: PorterAppDeployEvent;
-    appData: any;
-    filterOpts?: LogFilterQueryParamOpts
-};
-
-const DeployEventFocusView: React.FC<Props> = ({
-    event,
-    appData,
-    filterOpts,
-}) => {
-    const renderHeaderText = () => {
-        switch (event.status) {
-            case "SUCCESS":
-                return <Text color="#68BF8B" size={16}>Deploy succeeded</Text>;
-            case "FAILED":
-                return <Text color="#FF6060" size={16}>Deploy failed</Text>;
-            case "CANCELED":
-                return <Text color="#FFBF00" size={16}>Deploy canceled</Text>;
-            default:
-                return (
-                    <Container row>
-                        <Icon height="16px" src={loading} />
-                        <Spacer inline width="10px" />
-                        <Text size={16}>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.chart}
-                appName={appData.app.name}
-                filterOpts={filterOpts}
-            />
-        </>
-    );
-};
-
-export default DeployEventFocusView;

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

@@ -1,90 +1,90 @@
 import Loading from "components/Loading";
 import Spacer from "components/porter/Spacer";
-import React, { useContext, useEffect, useState } from "react";
-import { Context } from "shared/Context";
+import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import Link from "components/porter/Link";
 import BuildFailureEventFocusView from "./BuildFailureEventFocusView";
 import PreDeployEventFocusView from "./PredeployEventFocusView";
 import _ from "lodash";
-import { PorterAppEvent, porterAppEventValidator } from "../types";
-import DeployEventFocusView from "./DeployEventFocusView";
-import { LogFilterQueryParamOpts } from "../../../logs/types";
-
-type Props = {
-    eventId: string;
-    appData: any;
-    filterOpts?: LogFilterQueryParamOpts;
-};
+import { PorterAppBuildEvent, 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
 
-const EventFocusView: React.FC<Props> = ({
-    eventId,
-    appData,
-    filterOpts,
-}) => {
-    const { currentProject, currentCluster } = useContext(Context);
-    const [event, setEvent] = useState<PorterAppEvent | null>(null);
+type SupportedEventFocusViewEvent = PorterAppBuildEvent | PorterAppPreDeployEvent;
 
-    useEffect(() => {
-        const getEvent = async () => {
-            if (currentProject == null || currentCluster == null) {
-                return;
+const EventFocusView: React.FC = ({ }) => {
+    const { search } = useLocation();
+    const queryParams = new URLSearchParams(search);
+    const eventId = queryParams.get("event_id");
+    const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision();
+
+    const [event, setEvent] = useState<SupportedEventFocusViewEvent | null>(null);
+
+    const { data } = useQuery(
+        [
+            "getPorterAppEvent",
+            projectId,
+            clusterId,
+            eventId,
+            event,
+        ],
+        async () => {
+            if (eventId == null || eventId === "") {
+                return null;
             }
-            try {
-                const eventResp = await api.getPorterAppEvent(
-                    "<token>",
-                    {},
-                    {
-                        project_id: currentProject.id,
-                        cluster_id: currentCluster.id,
-                        event_id: eventId,
-                    }
-                )
-                const newEvent = porterAppEventValidator.parse(eventResp.data.event);
-                setEvent(newEvent);
-                if (newEvent.metadata?.end_time != null) {
-                    clearInterval(intervalId);
+            const eventResp = await api.getPorterAppEvent(
+                "<token>",
+                {},
+                {
+                    project_id: projectId,
+                    cluster_id: clusterId,
+                    event_id: eventId,
                 }
-            } catch (err) {
-                console.log(err);
-            }
+            );
+            return porterAppEventValidator.parse(eventResp.data.event);
+        },
+        {
+            // last condition checks if the event is done running; then we stop refetching
+            enabled: eventId != null && eventId !== "" && !(event != null && event.metadata.end_time != null),
+            refetchInterval: EVENT_POLL_INTERVAL,
         }
-        const intervalId = setInterval(getEvent, EVENT_POLL_INTERVAL);
-        getEvent();
-        return () => clearInterval(intervalId);
-    }, []);
+    );
 
-    const getEventFocusView = (event: PorterAppEvent, appData: any) => {
-        switch (event.type) {
-            case "BUILD":
-                return <BuildFailureEventFocusView event={event} appData={appData} />
-            case "PRE_DEPLOY":
-                return <PreDeployEventFocusView event={event} appData={appData} />
-            case "DEPLOY":
-                return <DeployEventFocusView
-                    event={event as PorterAppDeployEvent}
-                    appData={appData}
-                    filterOpts={filterOpts}
-                />
-            default:
-                return null
+    useEffect(() => {
+        if (data != null && (data.type === "BUILD" || data.type === "PRE_DEPLOY")) {
+            setEvent(data);
         }
+    }, [data]);
+
+    const getEventFocusView = () => {
+        return match(event)
+            .with({ type: "BUILD" }, (ev) => <BuildFailureEventFocusView 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/${appData.app.name}/activity`}>
+            <Link to={`/apps/${latestProto.name}/activity`}>
                 <BackButton>
                     <i className="material-icons">keyboard_backspace</i>
                     Activity feed
                 </BackButton>
             </Link>
             <Spacer y={0.5} />
-            {event == null && <Loading />}
-            {event != null && getEventFocusView(event, appData)}
+            {getEventFocusView()}
         </AppearingView>
     );
 };

+ 16 - 13
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/focus-views/PredeployEventFocusView.tsx

@@ -4,22 +4,26 @@ import dayjs from "dayjs";
 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";
-import { PorterAppEvent } from "../types";
+import { PorterAppPreDeployEvent } from "../types";
+import Logs from "main/home/app-dashboard/validate-apply/logs/Logs";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 
 type Props = {
-  event: PorterAppEvent;
-  appData: any;
+  event: PorterAppPreDeployEvent;
 };
 
 const PreDeployEventFocusView: React.FC<Props> = ({
   event,
-  appData,
 }) => {
+  const { projectId, clusterId, latestProto, deploymentTargetId, latestRevision } = useLatestRevision();
+
+  const appName = latestProto.name
+  const serviceNames = [`${latestProto.name}-predeploy`]
+
   const renderHeaderText = () => {
     switch (event.status) {
       case "SUCCESS":
@@ -54,14 +58,13 @@ const PreDeployEventFocusView: React.FC<Props> = ({
       <Spacer y={0.5} />
       {renderDurationText()}
       <Spacer y={0.5} />
-      <LogSection
-        currentChart={appData.releaseChart}
-        timeRange={{
-          startTime: event.metadata.end_time != null ? dayjs(event.metadata.start_time).subtract(1, 'minute') : undefined,
-          endTime: event.metadata.end_time != null ? dayjs(event.metadata.end_time).add(1, 'minute') : undefined,
-        }}
-        showFilter={false}
-        appName={appData.app.name}
+      <Logs
+        projectId={projectId}
+        clusterId={clusterId}
+        appName={appName}
+        serviceNames={serviceNames}
+        deploymentTargetId={deploymentTargetId}
+        latestRevision={latestRevision}
       />
     </>
   );

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

@@ -25,6 +25,7 @@ const porterAppBuildEventMetadataValidator = z.object({
     repo: z.string(),
     action_run_id: z.number(),
     github_account_id: z.number(),
+    end_time: z.string().optional(),
 })
 const porterAppPreDeployEventMetadataValidator = z.object({
     start_time: z.string(),

+ 9 - 5
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/utils.ts

@@ -3,13 +3,17 @@ import failure from "assets/failure.svg";
 import loading from "assets/loading.gif";
 import canceled from "assets/canceled.svg"
 import api from "shared/api";
-import { PorterAppEvent } from "./types";
-import { SourceOptions } from "lib/porter-apps";
+import { PorterAppBuildEvent, PorterAppEvent, PorterAppPreDeployEvent } from "./types";
 import { PorterAppRecord } from "../../../AppView";
+import { match } from "ts-pattern";
 
-export const getDuration = (event: PorterAppEvent): string => {
-    const startTimeStamp = new Date(event.metadata.start_time ?? event.created_at).getTime();
-    const endTimeStamp = new Date(event.metadata.end_time ?? event.updated_at).getTime();
+export const getDuration = (event: PorterAppPreDeployEvent | PorterAppBuildEvent): string => {
+    const startTimeStamp = match(event)
+        .with({ type: "BUILD" }, (ev) => new Date(ev.created_at).getTime())
+        .with({ type: "PRE_DEPLOY" }, (ev) => new Date(ev.metadata.start_time).getTime())
+        .exhaustive();
+
+    const endTimeStamp = event.metadata.end_time ? new Date(event.metadata.end_time).getTime() : Date.now()
 
     const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp;
 

+ 17 - 10
dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx

@@ -23,12 +23,11 @@ import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import Container from "components/porter/Container";
 import Button from "components/porter/Button";
-import { Service } from "../../new-app-flow/serviceTypes";
 import LogFilterContainer from "../../expanded-app/logs/LogFilterContainer";
 import StyledLogs from "../../expanded-app/logs/StyledLogs";
-import {z} from "zod";
-import {AppRevision, appRevisionValidator} from "lib/revisions/types";
-import {useLatestRevisionNumber, useRevisionIdToNumber} from "lib/hooks/useRevisionList";
+import { AppRevision } from "lib/revisions/types";
+import { useLatestRevisionNumber, useRevisionIdToNumber } from "lib/hooks/useRevisionList";
+import { useLocation } from "react-router";
 
 type Props = {
     projectId: number;
@@ -47,6 +46,14 @@ const Logs: React.FC<Props> = ({
     deploymentTargetId,
     latestRevision,
 }) => {
+    const { search } = useLocation();
+    const queryParams = new URLSearchParams(search);
+    const logQueryParamOpts = {
+        revision: queryParams.get('version'),
+        output_stream: queryParams.get('output_stream'),
+        service: queryParams.get('service'),
+    }
+
     const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
     const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
     const [enteredSearchText, setEnteredSearchText] = useState("");
@@ -60,10 +67,10 @@ const Logs: React.FC<Props> = ({
     const [logsError, setLogsError] = useState<string | undefined>(undefined);
 
     const [selectedFilterValues, setSelectedFilterValues] = useState<Record<LogFilterName, string>>({
-        service_name:  GenericLogFilter.getDefaultOption("service_name").value,
+        service_name: logQueryParamOpts?.service ?? GenericLogFilter.getDefaultOption("service_name").value,
         pod_name: "", // not supported
-        revision: GenericLogFilter.getDefaultOption("revision").value,
-        output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
+        revision: logQueryParamOpts.revision ?? GenericLogFilter.getDefaultOption("revision").value,
+        output_stream: logQueryParamOpts.output_stream ?? GenericLogFilter.getDefaultOption("output_stream").value,
     });
 
     const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId)
@@ -235,10 +242,10 @@ const Logs: React.FC<Props> = ({
 
     const resetFilters = () => {
         setSelectedFilterValues({
-            output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
+            service_name: logQueryParamOpts?.service ?? GenericLogFilter.getDefaultOption("service_name").value,
             pod_name: "", // not supported
-            revision: GenericLogFilter.getDefaultOption("revision").value,
-            service_name: GenericLogFilter.getDefaultOption("service_name").value,
+            revision: logQueryParamOpts.revision ?? GenericLogFilter.getDefaultOption("revision").value,
+            output_stream: logQueryParamOpts.output_stream ?? GenericLogFilter.getDefaultOption("output_stream").value,
         });
     };
 

+ 81 - 77
dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx

@@ -1,4 +1,4 @@
-import React, {useEffect, useMemo, useState} from "react";
+import React, { useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
@@ -13,97 +13,101 @@ import MetricsChart from "../../expanded-app/metrics/MetricsChart";
 import { useQuery } from "@tanstack/react-query";
 import Loading from "components/Loading";
 import CheckboxRow from "components/CheckboxRow";
-import {PorterApp} from "@porter-dev/api-contracts";
+import { PorterApp } from "@porter-dev/api-contracts";
+import { useLocation } from "react-router";
 
 type PropsType = {
-    projectId: number;
-    clusterId: number;
-    appName: string;
-    services: PorterApp["services"];
-    deploymentTargetId: string;
+  projectId: number;
+  clusterId: number;
+  appName: string;
+  services: PorterApp["services"];
+  deploymentTargetId: string;
 };
 
 type ServiceOption = {
-    label: string;
-    value: string;
+  label: string;
+  value: string;
 }
 
 const MetricsSection: React.FunctionComponent<PropsType> = ({
-    projectId,
-    clusterId,
-    appName,
-    services,
-    deploymentTargetId,
+  projectId,
+  clusterId,
+  appName,
+  services,
+  deploymentTargetId,
 }) => {
-  const [selectedServiceName, setSelectedServiceName] = useState<string>("");
+  const { search } = useLocation();
+  const queryParams = new URLSearchParams(search);
+  const serviceFromQueryParams = queryParams.get("service");
+
+  const [selectedServiceName, setSelectedServiceName] = useState<string>(serviceFromQueryParams ?? "");
   const [selectedRange, setSelectedRange] = useState("1H");
   const [showAutoscalingThresholds, setShowAutoscalingThresholds] = useState(true);
 
-
   const serviceOptions: ServiceOption[] = useMemo(() => {
-      return Object.keys(services).map((name) => {
-          return {
-              label: name,
-              value: name,
-          };
-      });
+    return Object.keys(services).map((name) => {
+      return {
+        label: name,
+        value: name,
+      };
+    });
   }, [services]);
 
-    useEffect(() => {
-        if (serviceOptions.length > 0) {
-            setSelectedServiceName(serviceOptions[0].value)
-        }
-    }, []);
+  useEffect(() => {
+    if (serviceOptions.length > 0 && selectedServiceName === "") {
+      setSelectedServiceName(serviceOptions[0].value)
+    }
+  }, []);
 
-    const [serviceName, serviceKind, metricTypes, isHpaEnabled] = useMemo(() => {
-        if (selectedServiceName === "") {
-            return ["", "", [], false]
-        }
+  const [serviceName, serviceKind, metricTypes, isHpaEnabled] = useMemo(() => {
+    if (selectedServiceName === "") {
+      return ["", "", [], false]
+    }
 
-        const service = services[selectedServiceName]
+    const service = services[selectedServiceName]
 
-        const serviceName = service.absoluteName === "" ? (appName + "-" + selectedServiceName) : service.absoluteName
+    const serviceName = service.absoluteName === "" ? (appName + "-" + selectedServiceName) : service.absoluteName
 
-        let serviceKind = ""
-        const metricTypes: MetricType[] = ["cpu", "memory"];
-        let isHpaEnabled = false
+    let serviceKind = ""
+    const metricTypes: MetricType[] = ["cpu", "memory"];
+    let isHpaEnabled = false
 
-        if (service.config.case === "webConfig") {
-            serviceKind = "web"
-            metricTypes.push("network");
-            if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
-                isHpaEnabled = true
-            }
-            if (!service.config.value.private) {
-                metricTypes.push("nginx:status")
-            }
-        }
+    if (service.config.case === "webConfig") {
+      serviceKind = "web"
+      metricTypes.push("network");
+      if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
+        isHpaEnabled = true
+      }
+      if (!service.config.value.private) {
+        metricTypes.push("nginx:status")
+      }
+    }
 
-        if (service.config.case === "workerConfig") {
-            serviceKind = "worker"
-            if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
-                isHpaEnabled = true
-            }
-        }
+    if (service.config.case === "workerConfig") {
+      serviceKind = "worker"
+      if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
+        isHpaEnabled = true
+      }
+    }
 
 
 
-        if (isHpaEnabled) {
-            metricTypes.push("hpa_replicas");
-        }
+    if (isHpaEnabled) {
+      metricTypes.push("hpa_replicas");
+    }
 
-        return [serviceName, serviceKind, metricTypes, isHpaEnabled]
-    }, [selectedServiceName])
+    return [serviceName, serviceKind, metricTypes, isHpaEnabled]
+  }, [selectedServiceName])
 
 
   const { data: metricsData, isLoading: isMetricsDataLoading, refetch } = useQuery(
     [
-        "getMetrics",
-        projectId,
-        clusterId,
-        serviceName,
-        selectedRange,
-        deploymentTargetId,
+      "getMetrics",
+      projectId,
+      clusterId,
+      serviceName,
+      selectedRange,
+      deploymentTargetId,
     ],
     async () => {
 
@@ -118,17 +122,17 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
       const start = end - secondsBeforeNow[selectedRange];
 
       for (const metricType of metricTypes) {
-          var kind = "";
-          if (serviceKind === "web") {
-              kind = "deployment";
-          } else if (serviceKind === "worker") {
-              kind = "deployment";
-          } else if (serviceKind === "job") {
-              kind = "job";
-          }
-          if (metricType === "nginx:status") {
-              kind = "Ingress"
-          }
+        var kind = "";
+        if (serviceKind === "web") {
+          kind = "deployment";
+        } else if (serviceKind === "worker") {
+          kind = "deployment";
+        } else if (serviceKind === "job") {
+          kind = "job";
+        }
+        if (metricType === "nginx:status") {
+          kind = "Ingress"
+        }
 
         const aggregatedMetricsResponse = await api.appMetrics(
           "<token>",
@@ -260,9 +264,9 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
   }
 
   const renderShowAutoscalingThresholdsCheckbox = (serviceName: string, isHpaEnabled: boolean) => {
-  if (serviceName === "") {
-    return null;
-  }
+    if (serviceName === "") {
+      return null;
+    }
 
     if (!isHpaEnabled) {
       return null;

+ 0 - 1
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx

@@ -54,7 +54,6 @@ const CustomDomains: React.FC<Props> = ({ index }) => {
         </>
       )}
       <Button
-        type="button" // this is required so that CreateApp.tsx doesn't try to submit the form onClick
         onClick={() => {
           append({
             name: {