diff --git a/api/server/handlers/porter_app/latest_app_revisions.go b/api/server/handlers/porter_app/latest_app_revisions.go index de90c1bfe5..b65d57365a 100644 --- a/api/server/handlers/porter_app/latest_app_revisions.go +++ b/api/server/handlers/porter_app/latest_app_revisions.go @@ -15,7 +15,7 @@ import ( "github.com/porter-dev/porter/internal/telemetry" ) -// LatestAppRevisionsHandler handles requests to the /apps/{porter_app_name}/revisions endpoint +// LatestAppRevisionsHandler handles requests to the /apps/revisions endpoint type LatestAppRevisionsHandler struct { handlers.PorterHandlerReadWriter } @@ -31,7 +31,7 @@ func NewLatestAppRevisionsHandler( } } -// LatestAppRevisionsRequest represents the response from the /apps/{porter_app_name}/revisions endpoint +// LatestAppRevisionsRequest represents the response from the /apps/revisions endpoint type LatestAppRevisionsRequest struct{} // LatestRevisionWithSource is an app revision and its source porter app @@ -40,7 +40,7 @@ type LatestRevisionWithSource struct { Source types.PorterApp `json:"source"` } -// LatestAppRevisionsResponse represents the response from the /apps/{porter_app_name}/revisions endpoint +// LatestAppRevisionsResponse represents the response from the /apps/revisions endpoint type LatestAppRevisionsResponse struct { AppRevisions []LatestRevisionWithSource `json:"app_revisions"` } diff --git a/api/server/handlers/porter_app/pod_status.go b/api/server/handlers/porter_app/pod_status.go index 150f681083..7da142e40b 100644 --- a/api/server/handlers/porter_app/pod_status.go +++ b/api/server/handlers/porter_app/pod_status.go @@ -1,6 +1,7 @@ package porter_app import ( + "fmt" "net/http" "connectrpc.com/connect" @@ -10,6 +11,7 @@ import ( "github.com/porter-dev/porter/api/server/shared" "github.com/porter-dev/porter/api/server/shared/apierrors" "github.com/porter-dev/porter/api/server/shared/config" + "github.com/porter-dev/porter/api/server/shared/requestutils" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/telemetry" @@ -37,7 +39,7 @@ func NewPodStatusHandler( // PodStatusRequest is the expected format for a request body on GET /apps/pods type PodStatusRequest struct { DeploymentTargetID string `schema:"deployment_target_id"` - Selectors string `schema:"selectors"` + ServiceName string `schema:"service"` } func (c *PodStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -51,10 +53,17 @@ func (c *PodStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName) + if reqErr != nil { + err := telemetry.Error(ctx, span, reqErr, "porter app name not found in request") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster) project, _ := r.Context().Value(types.ProjectScope).(*models.Project) - telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "selectors", Value: request.Selectors}) + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: request.ServiceName}, telemetry.AttributeKV{Key: "app-name", Value: appName}) if request.DeploymentTargetID == "" { err := telemetry.Error(ctx, span, nil, "must provide deployment target id") @@ -99,7 +108,8 @@ func (c *PodStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { pods := []v1.Pod{} - podsList, err := agent.GetPodsByLabel(request.Selectors, namespace) + selectors := fmt.Sprintf("porter.run/service-name=%s,porter.run/deployment-target-id=%s,porter.run/app-name=%s", request.ServiceName, request.DeploymentTargetID, appName) + podsList, err := agent.GetPodsByLabel(selectors, namespace) if err != nil { err = telemetry.Error(ctx, span, err, "unable to get pods by label") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) diff --git a/api/server/router/porter_app.go b/api/server/router/porter_app.go index 005f5b9db9..ff7dfade7d 100644 --- a/api/server/router/porter_app.go +++ b/api/server/router/porter_app.go @@ -980,14 +980,14 @@ func getPorterAppRoutes( Router: r, }) - // GET /api/projects/{project_id}/clusters/{cluster_id}/apps/pods -> cluster.NewPodStatusHandler + // GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/pods -> cluster.NewPodStatusHandler appPodStatusEndpoint := factory.NewAPIEndpoint( &types.APIRequestMetadata{ Verb: types.APIVerbGet, Method: types.HTTPVerbGet, Path: &types.Path{ Parent: basePath, - RelativePath: fmt.Sprintf("%s/pods", relPathV2), + RelativePath: fmt.Sprintf("%s/{%s}/pods", relPathV2, types.URLParamPorterAppName), }, Scopes: []types.PermissionScope{ types.UserScope, diff --git a/dashboard/src/lib/hooks/useAppStatus.ts b/dashboard/src/lib/hooks/useAppStatus.ts index c4e66809fc..44175fc164 100644 --- a/dashboard/src/lib/hooks/useAppStatus.ts +++ b/dashboard/src/lib/hooks/useAppStatus.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { useEffect, useMemo, useState } from "react"; import api from "shared/api"; import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import { useRevisionIdToNumber } from "./useRevisionList"; +import { useRevisionList } from "./useRevisionList"; import { valueExists } from "shared/util"; export type PorterAppVersionStatus = { @@ -38,7 +38,7 @@ export const useAppStatus = ( ) => { const [servicePodMap, setServicePodMap] = useState>({}); - const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId); + const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId }); const { newWebsocket, @@ -76,18 +76,17 @@ export const useAppStatus = ( }; const updatePods = async (serviceName: string) => { - const selectors = `porter.run/service-name=${serviceName},porter.run/deployment-target-id=${deploymentTargetId}`; - try { const res = await api.appPodStatus( "", { deployment_target_id: deploymentTargetId, - selectors, + service: serviceName, }, { - id: projectId, + project_id: projectId, cluster_id: clusterId, + app_name: appName, } ); // TODO: type the response @@ -143,7 +142,7 @@ export const useAppStatus = ( setupWebsocket(serviceName); } return () => closeAllWebsockets(); - }, [projectId, clusterId, deploymentTargetId, appName, JSON.stringify(revisionIdToNumber)]); + }, [projectId, clusterId, deploymentTargetId, appName]); const processReplicaSetArray = (replicaSetArray: ClientPod[][]): PorterAppVersionStatus[] => { return replicaSetArray.map((replicaSet, i) => { @@ -161,7 +160,8 @@ export const useAppStatus = ( message = `${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are" } failing to run Version ${version}`; } else if ( - i > 0 && replicaSetArray[i - 1].every(p => !p.isFailing) + // last check ensures that we don't say 'spinning down' unless there exists a version status above it + i > 0 && replicaSetArray[i - 1].every(p => !p.isFailing) && revisionIdToNumber[replicaSetArray[i - 1][0].revisionId] != null ) { status = "spinningDown"; message = `${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are" @@ -207,7 +207,7 @@ export const useAppStatus = ( })); return serviceReplicaSetMap; - }, [JSON.stringify(servicePodMap)]); + }, [JSON.stringify(servicePodMap), JSON.stringify(revisionIdToNumber)]); return { serviceVersionStatus, diff --git a/dashboard/src/lib/hooks/useRevisionList.ts b/dashboard/src/lib/hooks/useRevisionList.ts index dc0b0dfa34..56db0e32ee 100644 --- a/dashboard/src/lib/hooks/useRevisionList.ts +++ b/dashboard/src/lib/hooks/useRevisionList.ts @@ -1,26 +1,30 @@ import { useQuery } from "@tanstack/react-query"; -import { useContext, useEffect, useState } from "react"; -import { Context } from "shared/Context"; +import { useEffect, useState } from "react"; import api from "shared/api"; import { z } from "zod"; import { AppRevision, appRevisionValidator } from "../revisions/types"; -import { useLatestRevision } from "../../main/home/app-dashboard/app-view/LatestRevisionContext"; - -export function useRevisionList(appName: string, deploymentTargetId: string) { - const { currentProject, currentCluster } = useContext(Context); - const { latestRevision } = useLatestRevision(); - +import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; + +export function useRevisionList({ + appName, + deploymentTargetId, + projectId, + clusterId, +}: { + appName: string, + deploymentTargetId: string, + projectId: number, + clusterId: number +}): { revisionList: AppRevision[], revisionIdToNumber: Record } { const [ revisionList, setRevisionList, ] = useState([]); - - if (currentProject == null || currentCluster == null) { - return []; - } + const [revisionIdToNumber, setRevisionIdToNumber] = useState>({}); + const { latestRevision } = useLatestRevision(); const { data } = useQuery( - ["listAppRevisions", currentProject.id, currentCluster.id, appName, deploymentTargetId, latestRevision], + ["listAppRevisions", projectId, clusterId, appName, deploymentTargetId, latestRevision], async () => { const res = await api.listAppRevisions( "", @@ -28,8 +32,8 @@ export function useRevisionList(appName: string, deploymentTargetId: string) { deployment_target_id: deploymentTargetId, }, { - project_id: currentProject.id, - cluster_id: currentCluster.id, + project_id: projectId, + cluster_id: clusterId, porter_app_name: appName, } ); @@ -39,32 +43,21 @@ export function useRevisionList(appName: string, deploymentTargetId: string) { app_revisions: z.array(appRevisionValidator), }) .parseAsync(res.data); - return revisions; }, { - enabled: !!currentProject && !!currentCluster, refetchInterval: 5000, + refetchOnWindowFocus: false, } ); useEffect(() => { if (data) { - setRevisionList(data.app_revisions); + const revisionList = data.app_revisions + setRevisionList(revisionList); + setRevisionIdToNumber(Object.fromEntries(revisionList.map(r => ([r.id, r.revision_number])))) } }, [data]); - return revisionList; -} - -export function useRevisionIdToNumber(appName: string, deploymentTargetId: string) { - const revisionList = useRevisionList(appName, deploymentTargetId); - const revisionIdToNumber: Record = Object.fromEntries(revisionList.map(r => ([r.id, r.revision_number]))) - - return revisionIdToNumber; -} - -export function useLatestRevisionNumber(appName: string, deploymentTargetId: string) { - const revisionList = useRevisionList(appName, deploymentTargetId); - return revisionList.map((revision) => revision.revision_number).reduce((a, b) => Math.max(a, b), 0) + return { revisionList, revisionIdToNumber }; } diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx index a6122fe72b..a8502d73c3 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx @@ -77,7 +77,7 @@ const AppHeader: React.FC = () => { [] ); - return domains.length === 1 ? prefixSubdomain(domains[0]) : ""; + return domains.length === 1 ? domains[0] : ""; }, [latestProto]); return ( @@ -125,7 +125,7 @@ const AppHeader: React.FC = () => { <> - + {displayDomain} diff --git a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx index 516358a03f..49a1afa8fb 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx @@ -109,12 +109,12 @@ export const LatestRevisionProvider = ({ app_revision: appRevisionValidator, }) .parseAsync(res.data); - return revisionData.app_revision; }, { enabled: appParamsExist, refetchInterval: 5000, + refetchOnWindowFocus: false, } ); diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx index 6aae30f543..cf34db29f5 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/DeployEventCard.tsx @@ -12,22 +12,24 @@ 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"; +import { useRevisionList } from "lib/hooks/useRevisionList"; type Props = { event: PorterAppDeployEvent; appName: string; showServiceStatusDetail?: boolean; deploymentTargetId: string; + projectId: number; + clusterId: number; }; -const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, showServiceStatusDetail = false }) => { +const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, projectId, clusterId, 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 { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId }); const renderStatusText = () => { switch (event.status) { diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx index e98f33530c..dd46e014d7 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/EventCard.tsx @@ -21,7 +21,7 @@ const EventCard: React.FC = ({ event, deploymentTargetId, isLatestDeployE return match(event) .with({ type: "APP_EVENT" }, (ev) => ) .with({ type: "BUILD" }, (ev) => ) - .with({ type: "DEPLOY" }, (ev) => ) + .with({ type: "DEPLOY" }, (ev) => ) .with({ type: "PRE_DEPLOY" }, (ev) => ) .exhaustive(); }; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx index 4722165d51..4fd0479f03 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx @@ -25,8 +25,9 @@ import Container from "components/porter/Container"; import Button from "components/porter/Button"; import LogFilterContainer from "../../expanded-app/logs/LogFilterContainer"; import StyledLogs from "../../expanded-app/logs/StyledLogs"; -import { useLatestRevisionNumber, useRevisionIdToNumber } from "lib/hooks/useRevisionList"; +import { useRevisionList } from "lib/hooks/useRevisionList"; import { useLocation } from "react-router"; +import { useLatestRevision } from "../../app-view/LatestRevisionContext"; type Props = { projectId: number; @@ -81,8 +82,8 @@ const Logs: React.FC = ({ output_stream: logQueryParamOpts.output_stream ?? GenericLogFilter.getDefaultOption("output_stream").value, }); - const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId) - const latestRevisionNumber = useLatestRevisionNumber(appName, deploymentTargetId) + const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId }); + const { latestRevision: { revision_number: latestRevisionNumber } } = useLatestRevision(); const isAgentVersionUpdated = (agentImage: string | undefined) => { if (agentImage == null) { diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts b/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts index 7e39d60764..1f3f1bc907 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts +++ b/dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts @@ -461,7 +461,7 @@ export const useLogs = ({ setDate, JSON.stringify(selectedFilterValues), JSON.stringify(timeRange?.endTime), - filterPredeploy + filterPredeploy, ]); useEffect(() => { diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 039851195e..1ead7c0586 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -320,11 +320,11 @@ const appJobs = baseApi< const appPodStatus = baseApi< { deployment_target_id: string; - selectors: string; + service: string; }, - { id: number; cluster_id: number } ->("GET", (pathParams) => { - return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/apps/pods`; + { project_id: number; cluster_id: number, app_name: string } +>("GET", ({ project_id, cluster_id, app_name }) => { + return `/api/projects/${project_id}/clusters/${cluster_id}/apps/${app_name}/pods`; }); const getFeedEvents = baseApi<