From 9cdd24a2ee4e16747cf4424444ce4e9fdf3ebcd3 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Fri, 13 Oct 2023 14:19:20 -0400 Subject: [PATCH] [POR-1885] [POR-1864] Add links commit sha to events, clear up unnumbered revisions (#3810) --- cli/cmd/v2/app_events.go | 7 +- cli/cmd/v2/apply.go | 4 +- dashboard/src/components/porter/Link.tsx | 18 +- .../events/cards/BuildEventCard.tsx | 60 +++++-- .../events/cards/DeployEventCard.tsx | 165 ++++++++---------- .../activity-feed/events/cards/EventCard.tsx | 117 ++++++++++++- .../events/cards/PreDeployEventCard.tsx | 27 ++- .../tabs/activity-feed/events/types.ts | 2 + .../revisions-list/RevisionTableContents.tsx | 11 +- 9 files changed, 284 insertions(+), 127 deletions(-) diff --git a/cli/cmd/v2/app_events.go b/cli/cmd/v2/app_events.go index c2c7c47646..3de79470b5 100644 --- a/cli/cmd/v2/app_events.go +++ b/cli/cmd/v2/app_events.go @@ -13,7 +13,7 @@ import ( "github.com/porter-dev/porter/internal/telemetry" ) -func createBuildEvent(ctx context.Context, client api.Client, applicationName string, projectId uint, clusterId uint, deploymentTargetID string) (string, error) { +func createBuildEvent(ctx context.Context, client api.Client, applicationName string, projectId uint, clusterId uint, deploymentTargetID string, commitSHA string) (string, error) { ctx, span := telemetry.NewSpan(ctx, "create-build-event") defer span.End() @@ -53,6 +53,8 @@ func createBuildEvent(ctx context.Context, client api.Client, applicationName st } } + req.Metadata["commit_sha"] = commitSHA + event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req) if err != nil { fmt.Println("could not create build event") @@ -62,7 +64,7 @@ func createBuildEvent(ctx context.Context, client api.Client, applicationName st return event.ID, nil } -func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time, appRevisionID string) (string, error) { +func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, deploymentTargetID string, createdAt time.Time, appRevisionID string, commitSHA string) (string, error) { ctx, span := telemetry.NewSpan(ctx, "create-predeploy-event") defer span.End() @@ -74,6 +76,7 @@ func createPredeployEvent(ctx context.Context, client api.Client, applicationNam } req.Metadata["start_time"] = createdAt req.Metadata["app_revision_id"] = appRevisionID + req.Metadata["commit_sha"] = commitSHA event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req) if err != nil { diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 2ab7bd9241..f081f14965 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -181,7 +181,7 @@ func Apply(ctx context.Context, inp ApplyInput) error { if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_BUILD { color.New(color.FgGreen).Printf("Building new image...\n") // nolint:errcheck,gosec - eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID) + eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, commitSHA) reportBuildFailureInput := reportBuildFailureInput{ client: client, @@ -279,7 +279,7 @@ func Apply(ctx context.Context, inp ApplyInput) error { color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec now := time.Now().UTC() - eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, now, applyResp.AppRevisionId) + eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, now, applyResp.AppRevisionId, commitSHA) metadata := make(map[string]interface{}) eventStatus := types.PorterAppEventStatus_Success for { diff --git a/dashboard/src/components/porter/Link.tsx b/dashboard/src/components/porter/Link.tsx index 2ccdc444d5..e4002c96ca 100644 --- a/dashboard/src/components/porter/Link.tsx +++ b/dashboard/src/components/porter/Link.tsx @@ -10,6 +10,7 @@ type Props = { hasunderline?: boolean; color?: string; hoverColor?: string; + showTargetBlankIcon?: boolean; }; const Link: React.FC = ({ @@ -20,16 +21,15 @@ const Link: React.FC = ({ hasunderline, color = "#ffffff", hoverColor, + showTargetBlankIcon = true, }) => { return ( {to ? ( {children} - {target === "_blank" && ( -
- -
+ {target === "_blank" && showTargetBlankIcon && ( + )}
) : ( @@ -44,11 +44,9 @@ const Link: React.FC = ({ export default Link; -const Svg = styled.svg` - margin-bottom: -1px; +const Svg = styled.svg<{ color: string, hoverColor?: string }>` margin-left: 5px; - color: #ffffff; - stroke: #ffffff; + stroke: ${(props) => props.color}; stroke-width: 2; `; @@ -93,5 +91,9 @@ const LinkWrapper = styled.span<{ hoverColor?: string, color: string }>` ${Underline} { background-color: ${({ hoverColor, color }) => hoverColor ?? color}; } + + svg { + stroke: ${({ hoverColor, color }) => hoverColor ?? color}; + } }; `; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx index 21d7322154..29be403b26 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/BuildEventCard.tsx @@ -12,29 +12,43 @@ import Spacer from "components/porter/Spacer"; import Link from "components/porter/Link"; import Icon from "components/porter/Icon"; import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils'; -import { StyledEventCard } from "./EventCard"; +import { Code, ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard"; import document from "assets/document.svg"; import { PorterAppBuildEvent } from "../types"; -import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; +import { match } from "ts-pattern"; +import pull_request_icon from "assets/pull_request_icon.svg"; +import { PorterAppRecord } from "main/home/app-dashboard/app-view/AppView"; type Props = { event: PorterAppBuildEvent; appName: string; projectId: number; clusterId: number; + gitCommitUrl: string; + displayCommitSha: string; + porterApp: PorterAppRecord; }; -const BuildEventCard: React.FC = ({ event, appName, projectId, clusterId }) => { - const { porterApp } = useLatestRevision(); +const BuildEventCard: React.FC = ({ + event, + appName, + projectId, + clusterId, + gitCommitUrl, + displayCommitSha, + porterApp, +}) => { const renderStatusText = (event: PorterAppBuildEvent) => { - switch (event.status) { - case "SUCCESS": - return Build succeeded; - case "FAILED": - return Build failed; - default: - return Build in progress...; - } + const color = getStatusColor(event.status); + return ( + + {match(event.status) + .with("SUCCESS", () => "Build successful") + .with("FAILED", () => "Build failed") + .otherwise(() => "Build in progress...") + } + + ); }; const renderInfoCta = (event: PorterAppBuildEvent) => { @@ -48,7 +62,7 @@ const BuildEventCard: React.FC = ({ event, appName, projectId, clusterId - View details + View build logs @@ -88,6 +102,17 @@ const BuildEventCard: React.FC = ({ event, appName, projectId, clusterId Application build + {gitCommitUrl && displayCommitSha && + <> + + + + + {displayCommitSha} + + + + } @@ -113,5 +138,14 @@ const BuildEventCard: React.FC = ({ event, appName, projectId, clusterId export default BuildEventCard; const Wrapper = styled.div` + display: flex; + height: 20px; margin-top: -3px; `; + +const StatusContainer = styled.div<{ color: string }>` + display: flex; + align-items: center; + color: ${props => props.color}; + font-size: 13px; +`; 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 2b9ac66c0b..737e887cb6 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 @@ -5,15 +5,16 @@ import Container from "components/porter/Container"; import Spacer from "components/porter/Spacer"; import Icon from "components/porter/Icon"; import { getStatusColor, getStatusIcon } from '../utils'; -import { StyledEventCard } from "./EventCard"; +import { ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard"; import styled from "styled-components"; import Link from "components/porter/Link"; 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 { useRevisionList } from "lib/hooks/useRevisionList"; import RevisionDiffModal from "../modals/RevisionDiffModal"; +import pull_request_icon from "assets/pull_request_icon.svg"; +import { match } from "ts-pattern"; type Props = { event: PorterAppDeployEvent; @@ -22,10 +23,20 @@ type Props = { deploymentTargetId: string; projectId: number; clusterId: number; + gitCommitUrl: string; + displayCommitSha: string; }; -const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, projectId, clusterId, showServiceStatusDetail = false }) => { - const { latestRevision } = useLatestRevision(); +const DeployEventCard: React.FC = ({ + event, + appName, + deploymentTargetId, + projectId, + clusterId, + showServiceStatusDetail = false, + gitCommitUrl, + displayCommitSha, +}) => { const [diffModalVisible, setDiffModalVisible] = useState(false); const [revertModalVisible, setRevertModalVisible] = useState(false); const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail); @@ -33,94 +44,54 @@ const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, const { revisionIdToNumber, numberToRevisionId } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId }); const renderStatusText = () => { - switch (event.status) { - case "SUCCESS": - return event.metadata.image_tag != null ? - event.metadata.service_deployment_metadata != null ? - - - Deployed {event.metadata.image_tag} to - - - {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))} - - : - - Deployed {event.metadata.image_tag} - - : - - Deployment successful - ; - case "FAILED": - if (event.metadata.service_deployment_metadata != null) { - let failedServices = 0; - for (const key in event.metadata.service_deployment_metadata) { - if (event.metadata.service_deployment_metadata[key].status === "FAILED") { - failedServices++; - } - } - return ( - - - Failed to deploy {event.metadata.image_tag} to - - - {renderServiceDropdownCta(failedServices, getStatusColor(event.status))} - - ); - } else { - return ( - - Deployment failed - - ); + const versionNumber = revisionIdToNumber[event.metadata.app_revision_id]; + const serviceMetadata = event.metadata.service_deployment_metadata; + + const getStatusText = (status: string, text: string, numServices: number, addEllipsis?: boolean) => { + if (versionNumber) { + text += ` version ${versionNumber}`; + } + + return serviceMetadata != null ? ( + + {text} to + + {renderServiceDropdownCta(numServices, getStatusColor(status))} + + ) : ( + {text} {addEllipsis && "..."} + ); + }; + + let failedServices = 0; + let canceledServices = 0; + let successfulServices = 0; + let progressingServices = 0; + + if (serviceMetadata != null) { + for (const key in serviceMetadata) { + if (serviceMetadata[key].status === "FAILED") { + failedServices++; } - case "CANCELED": - if (event.metadata.service_deployment_metadata != null) { - let canceledServices = 0; - for (const key in event.metadata.service_deployment_metadata) { - if (event.metadata.service_deployment_metadata[key].status === "CANCELED") { - canceledServices++; - } - } - return ( - - - Canceled deploy of {event.metadata.image_tag} to - - - {renderServiceDropdownCta(canceledServices, getStatusColor(event.status))} - - ); - } else { - return ( - - Deployment canceled - - ); + if (serviceMetadata[key].status === "CANCELED") { + canceledServices++; } - default: - if (event.metadata.service_deployment_metadata != null) { - return ( - - - Deploying {event.metadata.image_tag} to - - - {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))} - - ); - } else { - return ( - - Deploying {event.metadata.image_tag}... - - ); + if (serviceMetadata[key].status === "SUCCESS") { + successfulServices++; } + if (serviceMetadata[key].status === "PROGRESSING") { + progressingServices++; + } + } } + + return match(event.status) + .with("SUCCESS", () => getStatusText(event.status, "Deployed", successfulServices)) + .with("FAILED", () => getStatusText(event.status, "Failed to deploy", failedServices)) + .with("CANCELED", () => getStatusText(event.status, "Canceled deployment", canceledServices)) + .otherwise(() => getStatusText(event.status, "Deploying", progressingServices, true)); }; - + const renderRevisionDiffModal = (event: PorterAppDeployEvent) => { const changedRevisionId = event.metadata.app_revision_id; const changedRevisionNumber = revisionIdToNumber[event.metadata.app_revision_id]; @@ -168,7 +139,25 @@ const DeployEventCard: React.FC = ({ event, appName, deploymentTargetId, - Application version no. {revisionIdToNumber[event.metadata.app_revision_id]} + Application deploy + {gitCommitUrl && displayCommitSha ? + <> + + + + + {displayCommitSha} + + + + : + <> + + + {event.metadata.image_tag} + + + } 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 dd46e014d7..b9f7531ca7 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 @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import styled from "styled-components"; import BuildEventCard from "./BuildEventCard"; @@ -7,6 +7,7 @@ import AppEventCard from "./AppEventCard"; import DeployEventCard from "./DeployEventCard"; import { PorterAppEvent } from "../types"; import { match } from "ts-pattern"; +import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; type Props = { event: PorterAppEvent; @@ -18,11 +19,95 @@ type Props = { }; const EventCard: React.FC = ({ event, deploymentTargetId, isLatestDeployEvent, projectId, clusterId, appName }) => { + const { porterApp } = useLatestRevision(); + + const gitCommitUrl = useMemo(() => { + if (!porterApp.repo_name) { + return ""; + } + + return match(event) + .with({ type: "APP_EVENT" }, () => "") + .with({ type: "BUILD" }, (event) => + event.metadata.commit_sha + ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.commit_sha}` + : "" + ) + .with({ type: "PRE_DEPLOY" }, (event) => + event.metadata.commit_sha + ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.commit_sha}` + : "" + ) + .with({ type: "DEPLOY" }, (event) => + event.metadata.image_tag + ? `https://www.github.com/${porterApp.repo_name}/commit/${event.metadata.image_tag}` + : "" + ) + .exhaustive(); + }, [JSON.stringify(event), porterApp]) + + const displayCommitSha = useMemo(() => { + if (!porterApp.repo_name) { + return ""; + } + + return match(event) + .with({ type: "APP_EVENT" }, () => "") + .with({ type: "BUILD" }, (event) => + event.metadata.commit_sha ? event.metadata.commit_sha.slice(0, 7) : "" + ) + .with({ type: "PRE_DEPLOY" }, (event) => + event.metadata.commit_sha ? event.metadata.commit_sha.slice(0, 7) : "" + ) + .with({ type: "DEPLOY" }, (event) => + event.metadata.image_tag ? event.metadata.image_tag.slice(0, 7) : "" + ) + .exhaustive(); + }, [JSON.stringify(event), porterApp]); + return match(event) - .with({ type: "APP_EVENT" }, (ev) => ) - .with({ type: "BUILD" }, (ev) => ) - .with({ type: "DEPLOY" }, (ev) => ) - .with({ type: "PRE_DEPLOY" }, (ev) => ) + .with({ type: "APP_EVENT" }, (ev) => ( + + )) + .with({ type: "BUILD" }, (ev) => ( + + )) + .with({ type: "DEPLOY" }, (ev) => ( + + )) + .with({ type: "PRE_DEPLOY" }, (ev) => ( + + )) .exhaustive(); }; @@ -53,3 +138,25 @@ export const StyledEventCard = styled.div<{ row?: boolean }>` } } `; + +export const Code = styled.span` + font-family: monospace; +`; + +export const CommitIcon = styled.img` + height: 12px; + margin-right: 3px; +`; + +export const ImageTagContainer = styled.div<{ hoverable?: boolean }>` + display: flex; + justify-content: center; + padding: 3px 5px; + border-radius: 5px; + background: #ffffff22; + user-select: text; + ${({hoverable = true}) => hoverable && `:hover { + background: #ffffff44; + cursor: pointer; + }`} +`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx index efc5401ce7..cfeb476cff 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/PreDeployEventCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import styled from "styled-components"; import pre_deploy from "assets/pre_deploy.png"; @@ -12,20 +12,30 @@ import Spacer from "components/porter/Spacer"; import Icon from "components/porter/Icon"; import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils'; -import { StyledEventCard } from "./EventCard"; +import { Code, ImageTagContainer, CommitIcon, StyledEventCard } from "./EventCard"; import Link from "components/porter/Link"; import document from "assets/document.svg"; import { PorterAppPreDeployEvent } from "../types"; import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; +import pull_request_icon from "assets/pull_request_icon.svg"; type Props = { event: PorterAppPreDeployEvent; appName: string; projectId: number; clusterId: number; + gitCommitUrl: string; + displayCommitSha: string; }; -const PreDeployEventCard: React.FC = ({ event, appName, projectId, clusterId }) => { +const PreDeployEventCard: React.FC = ({ + event, + appName, + projectId, + clusterId, + gitCommitUrl, + displayCommitSha, +}) => { const { porterApp } = useLatestRevision(); const renderStatusText = (event: PorterAppPreDeployEvent) => { @@ -48,6 +58,17 @@ const PreDeployEventCard: React.FC = ({ event, appName, projectId, cluste Application pre-deploy + {gitCommitUrl && displayCommitSha && + <> + + + + + {displayCommitSha} + + + + } diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts index 384342590a..bfcceac1e3 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts @@ -26,11 +26,13 @@ const porterAppBuildEventMetadataValidator = z.object({ action_run_id: z.number().optional(), github_account_id: z.number().optional(), end_time: z.string().optional(), + commit_sha: z.string().optional(), }) const porterAppPreDeployEventMetadataValidator = z.object({ start_time: z.string(), end_time: z.string().optional(), app_revision_id: z.string(), + commit_sha: z.string().optional(), }); export const porterAppEventValidator = z.discriminatedUnion("type", [ z.object({ diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx index 5b6b2e5a27..ad07893865 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/revisions-list/RevisionTableContents.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useMemo } from "react"; +import React, { Dispatch, SetStateAction } from "react"; import { PorterApp } from "@porter-dev/api-contracts"; import { AppRevision } from "lib/revisions/types"; import { match } from "ts-pattern"; @@ -7,7 +7,6 @@ import styled from "styled-components"; import { readableDate } from "shared/string_utils"; import Text from "components/porter/Text"; import { SourceOptions } from "lib/porter-apps"; -import api from "shared/api"; type RevisionTableContentsProps = { latestRevisionNumber: number; @@ -77,14 +76,14 @@ const RevisionTableContents: React.FC = ({ const getTableHeader = (latestRevision?: AppRevision) => { if (!latestRevision) { - return "Revisions"; + return "Versions"; } if (previewRevision) { - return "Previewing revision (not deployed) -"; + return "Previewing version (not deployed) -"; } - return "Current revision - "; + return "Current version - "; }; const getSelectedRevisionNumber = (args: { @@ -132,7 +131,7 @@ const RevisionTableContents: React.FC = ({ - Revision no. + Version no. {revisionsWithProto[0]?.app_proto.build ? "Commit SHA"