From 4c2e5d255d3d7de92ae32dce490a44b76cd31c6c Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Tue, 1 Oct 2024 10:57:26 -0400 Subject: [PATCH] DEVPROD-10188: Render waterfall (#412) --- .evergreen/evergreen.yml | 8 +- .../integration/waterfall/waterfall.ts | 41 +++++ .../waterfall/useWaterfallAnalytics.ts | 13 +- apps/spruce/src/gql/generated/types.ts | 19 +- apps/spruce/src/gql/queries/waterfall.graphql | 18 +- apps/spruce/src/pages/waterfall/BuildRow.tsx | 173 ++++++++++++++++++ .../src/pages/waterfall/VersionLabel.tsx | 9 +- .../src/pages/waterfall/WaterfallGrid.tsx | 51 ++++-- .../VersionLabel_Default.storyshot | 2 +- .../VersionLabel_GitTag.storyshot | 2 +- .../VersionLabel_UpstreamProject.storyshot | 2 +- apps/spruce/src/pages/waterfall/index.tsx | 43 ++++- apps/spruce/src/pages/waterfall/styles.ts | 39 ++++ 13 files changed, 383 insertions(+), 37 deletions(-) create mode 100644 apps/spruce/cypress/integration/waterfall/waterfall.ts create mode 100644 apps/spruce/src/pages/waterfall/BuildRow.tsx create mode 100644 apps/spruce/src/pages/waterfall/styles.ts diff --git a/.evergreen/evergreen.yml b/.evergreen/evergreen.yml index 22004a221..ffa2ee64d 100644 --- a/.evergreen/evergreen.yml +++ b/.evergreen/evergreen.yml @@ -47,7 +47,7 @@ buildvariants: app_dir: apps/spruce goroot: /opt/golang/go1.20 mongodb_tools_url: https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2204-x86_64-100.8.0.tgz - mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.2.tgz + mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.14.tgz mongosh_url_2204: https://downloads.mongodb.com/compass/mongosh-2.0.2-linux-x64.tgz node_version: 20.11.0 modules: @@ -73,7 +73,7 @@ buildvariants: app_dir: apps/parsley goroot: /opt/golang/go1.20 mongodb_tools_url: https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2204-x86_64-100.8.0.tgz - mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.2.tgz + mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.14.tgz mongosh_url_2204: https://downloads.mongodb.com/compass/mongosh-2.0.2-linux-x64.tgz node_version: 20.11.0 run_on: @@ -102,7 +102,7 @@ buildvariants: app_dir: packages/lib goroot: /opt/golang/go1.20 mongodb_tools_url: https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2204-x86_64-100.8.0.tgz - mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.2.tgz + mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.14.tgz mongosh_url_2204: https://downloads.mongodb.com/compass/mongosh-2.0.2-linux-x64.tgz node_version: 20.11.0 run_on: @@ -121,7 +121,7 @@ buildvariants: app_dir: packages/deploy-utils goroot: /opt/golang/go1.20 mongodb_tools_url: https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2204-x86_64-100.8.0.tgz - mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.2.tgz + mongodb_url_2204: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.14.tgz mongosh_url_2204: https://downloads.mongodb.com/compass/mongosh-2.0.2-linux-x64.tgz node_version: 20.11.0 run_on: diff --git a/apps/spruce/cypress/integration/waterfall/waterfall.ts b/apps/spruce/cypress/integration/waterfall/waterfall.ts new file mode 100644 index 000000000..236577d35 --- /dev/null +++ b/apps/spruce/cypress/integration/waterfall/waterfall.ts @@ -0,0 +1,41 @@ +describe("waterfall page", () => { + beforeEach(() => { + cy.visit("/project/spruce/waterfall"); + }); + + describe("version labels", () => { + it("shows a git tag label", () => { + cy.dataCy("version-labels") + .children() + .eq(4) + .contains("Git Tags: v2.28.5"); + }); + }); + + describe("inactive commits", () => { + it("renders an inactive version column", () => { + cy.dataCy("version-labels") + .children() + .eq(2) + .should("have.attr", "data-cy", "inactive-label"); + cy.dataCy("build-group") + .first() + .children() + .eq(2) + .should("have.attr", "data-cy", "inactive-column"); + }); + }); + + describe("task grid", () => { + it("correctly renders child tasks", () => { + cy.dataCy("build-group").children().as("builds"); + + cy.get("@builds").eq(0).children().should("have.length", 1); + cy.get("@builds").eq(1).children().should("have.length", 8); + cy.get("@builds").eq(2).children().should("have.length", 0); + cy.get("@builds").eq(3).children().should("have.length", 1); + cy.get("@builds").eq(4).children().should("have.length", 8); + cy.get("@builds").eq(5).children().should("have.length", 8); + }); + }); +}); diff --git a/apps/spruce/src/analytics/waterfall/useWaterfallAnalytics.ts b/apps/spruce/src/analytics/waterfall/useWaterfallAnalytics.ts index 9ac4ae853..2741a5285 100644 --- a/apps/spruce/src/analytics/waterfall/useWaterfallAnalytics.ts +++ b/apps/spruce/src/analytics/waterfall/useWaterfallAnalytics.ts @@ -3,11 +3,14 @@ import { useAnalyticsRoot } from "@evg-ui/lib/analytics/hooks"; import { AnalyticsIdentifier } from "analytics/types"; import { slugs } from "constants/routes"; -type Action = { - name: "Clicked commit label"; - link: "jira" | "githash" | "upstream project"; - "commit.type": "active" | "inactive"; -}; +type Action = + | { + name: "Clicked commit label"; + link: "jira" | "githash" | "upstream project"; + "commit.type": "active" | "inactive"; + } + | { name: "Clicked variant label" } + | { name: "Clicked task box"; "task.status": string }; export const useWaterfallAnalytics = () => { const { [slugs.projectIdentifier]: projectIdentifier } = useParams(); diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index a48a0103a..8c0175bb9 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -9509,11 +9509,28 @@ export type WaterfallQuery = { __typename?: "Query"; waterfall: { __typename?: "Waterfall"; + buildVariants: Array<{ + __typename?: "WaterfallBuildVariant"; + displayName: string; + id: string; + builds: Array<{ + __typename?: "WaterfallBuild"; + displayName: string; + id: string; + version: string; + tasks: Array<{ + __typename?: "WaterfallTask"; + displayName: string; + id: string; + status: string; + }>; + }>; + }>; versions: Array<{ __typename?: "WaterfallVersion"; + inactiveVersions?: Array<{ __typename?: "Version"; id: string }> | null; version?: { __typename?: "Version"; - activated?: boolean | null; author: string; createTime: Date; id: string; diff --git a/apps/spruce/src/gql/queries/waterfall.graphql b/apps/spruce/src/gql/queries/waterfall.graphql index 4ff977191..7beed7d35 100644 --- a/apps/spruce/src/gql/queries/waterfall.graphql +++ b/apps/spruce/src/gql/queries/waterfall.graphql @@ -2,9 +2,25 @@ query Waterfall($options: WaterfallOptions!) { waterfall(options: $options) { + buildVariants { + builds { + displayName + id + tasks { + displayName + id + status + } + version + } + displayName + id + } versions { + inactiveVersions { + id + } version { - activated author createTime gitTags { diff --git a/apps/spruce/src/pages/waterfall/BuildRow.tsx b/apps/spruce/src/pages/waterfall/BuildRow.tsx new file mode 100644 index 000000000..cc6f85392 --- /dev/null +++ b/apps/spruce/src/pages/waterfall/BuildRow.tsx @@ -0,0 +1,173 @@ +import { memo, useCallback } from "react"; +import styled from "@emotion/styled"; +import { palette } from "@leafygreen-ui/palette"; +import { Link } from "react-router-dom"; +import { TaskStatus } from "@evg-ui/lib/types/task"; +import { useWaterfallAnalytics } from "analytics"; +import { StyledLink } from "components/styles"; +import { getTaskRoute, getVariantHistoryRoute } from "constants/routes"; +import { size } from "constants/tokens"; +import { + WaterfallBuild, + WaterfallBuildVariant, + WaterfallQuery, +} from "gql/generated/types"; +import { + BuildVariantTitle, + columnBasis, + gridGroupCss, + InactiveVersion, + Row, +} from "./styles"; + +const { black, gray, green, white } = palette; + +export const BuildRow: React.FC<{ + build: WaterfallBuildVariant; + projectIdentifier: string; + versions: WaterfallQuery["waterfall"]["versions"]; +}> = ({ build, projectIdentifier, versions }) => { + const { sendEvent } = useWaterfallAnalytics(); + const handleVariantClick = useCallback( + () => sendEvent({ name: "Clicked variant label" }), + [sendEvent], + ); + const handleTaskClick = useCallback( + (status: string) => () => + sendEvent({ + name: "Clicked task box", + "task.status": status, + }), + [sendEvent], + ); + + const { builds, displayName } = build; + let buildIndex = 0; + return ( + + + + {displayName} + + + + {versions.map(({ inactiveVersions, version }) => { + if (inactiveVersions?.length) { + return ( + + ); + } + /* The list of builds returned does not include a placeholder for inactive builds, so we need to check whether the build matches the version in the current column. + Builds are sorted in descending revision order and so match the versions' sort order. */ + if (version && version.id === builds?.[buildIndex]?.version) { + const b = builds[buildIndex]; + buildIndex += 1; + return ( + + ); + } + return ; + })} + + + ); +}; + +const BuildGrid: React.FC<{ + build: WaterfallBuild; + handleTaskClick: (s: string) => () => void; +}> = ({ build, handleTaskClick }) => ( + { + handleTaskClick( + (event.target as HTMLDivElement)?.getAttribute("status") ?? "", + ); + }} + > + {build.tasks.map(({ displayName, id, status }) => ( + + ))} + +); + +const BuildGroup = styled.div` + ${gridGroupCss} + border: 1px solid ${gray.light2}; + border-radius: ${size.xs}; + padding-bottom: ${size.xs}; + padding-top: ${size.xs}; +`; + +const Build = styled.div` + ${columnBasis} +`; + +const SQUARE_SIZE = 16; + +const Square = styled(Link)<{ status: string }>` + width: ${SQUARE_SIZE}px; + height: ${SQUARE_SIZE}px; + border: 1px solid ${white}; + box-sizing: content-box; + float: left; + cursor: pointer; + position: relative; + + /* TODO DEVPROD-11368: Render colors for all statuses. Could use background-image property to render icons. */ + ${({ status }) => + status === TaskStatus.Succeeded + ? `background-color: ${green.dark1};` + : `background-color: ${gray.light2}; + `} + + /* Tooltip */ + :before { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 5px); + left: 50%; + transform: translate(-50%); + z-index: 1; + width: max-content; + max-width: 450px; + overflow-wrap: break-word; + padding: ${size.xs}; + border-radius: 6px; + background: ${black}; + color: ${white}; + text-align: center; + display: none; + } + :hover:before { + display: block; + } + + /* Tooltip caret */ + :hover:after { + content: ""; + position: absolute; + bottom: calc(100% - 5px); + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: ${black} transparent transparent transparent; + } +`; + +const SquareMemo = memo(Square); diff --git a/apps/spruce/src/pages/waterfall/VersionLabel.tsx b/apps/spruce/src/pages/waterfall/VersionLabel.tsx index 60fb4eaca..66bfa4fae 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel.tsx +++ b/apps/spruce/src/pages/waterfall/VersionLabel.tsx @@ -3,11 +3,12 @@ import { Body, InlineCode } from "@leafygreen-ui/typography"; import { Link } from "react-router-dom"; import { Unpacked } from "@evg-ui/lib/types/utils"; import { useWaterfallAnalytics } from "analytics"; -import { StyledRouterLink } from "components/styles"; +import { StyledRouterLink, wordBreakCss } from "components/styles"; import { getVersionRoute, getTriggerRoute } from "constants/routes"; import { WaterfallQuery } from "gql/generated/types"; import { useSpruceConfig, useDateFormat } from "hooks"; import { shortenGithash, jiraLinkify } from "utils/string"; +import { columnBasis } from "./styles"; type VersionFields = NonNullable< Unpacked["version"] @@ -89,10 +90,16 @@ export const VersionLabel: React.FC = ({ }; const VersionContainer = styled.div` + ${columnBasis} + > * { font-size: 12px; line-height: 1.3; } + + p { + ${wordBreakCss} + } `; const CommitMessage = styled(Body)` diff --git a/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx b/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx index 9f10563f5..8e2f3028b 100644 --- a/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx +++ b/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx @@ -4,10 +4,16 @@ import { useParams } from "react-router-dom"; import { slugs } from "constants/routes"; import { WaterfallQuery, WaterfallQueryVariables } from "gql/generated/types"; import { WATERFALL } from "gql/queries"; +import { BuildRow } from "./BuildRow"; +import { + BuildVariantTitle, + gridGroupCss, + InactiveVersion, + Row, + VERSION_LIMIT, +} from "./styles"; import { VersionLabel } from "./VersionLabel"; -const LIMIT = 5; - export const WaterfallGrid: React.FC = () => { const { [slugs.projectIdentifier]: projectIdentifier } = useParams(); @@ -19,7 +25,7 @@ export const WaterfallGrid: React.FC = () => { options: { // @ts-expect-error projectIdentifier, - limit: LIMIT, + limit: VERSION_LIMIT, }, }, }, @@ -28,23 +34,36 @@ export const WaterfallGrid: React.FC = () => { return ( -
{/* Placeholder div for the build variant label column */} - {data.waterfall.versions.map(({ version }) => - version ? : null, - )} + + + {data.waterfall.versions.map(({ inactiveVersions, version }, i) => + version ? ( + + ) : ( + + inactive + + ), + )} + + {data.waterfall.buildVariants.map((b) => ( + + ))} ); }; -const Container = styled.div` - display: grid; - grid-template-columns: repeat(${LIMIT + 1}, minmax(0, 1fr)); - gap: 12px; -`; +const Container = styled.div``; -const Row = styled.div` - display: grid; - grid-column: 1/-1; - grid-template-columns: subgrid; +const Versions = styled.div` + ${gridGroupCss} `; diff --git a/apps/spruce/src/pages/waterfall/__snapshots__/VersionLabel_Default.storyshot b/apps/spruce/src/pages/waterfall/__snapshots__/VersionLabel_Default.storyshot index cb405e342..37868699c 100644 --- a/apps/spruce/src/pages/waterfall/__snapshots__/VersionLabel_Default.storyshot +++ b/apps/spruce/src/pages/waterfall/__snapshots__/VersionLabel_Default.storyshot @@ -3,7 +3,7 @@ class="css-ee05cj" >

( - - Loading waterfall...}> - - - + <> + {/* Safari performance of the waterfall chokes if using overflow-y: scroll, so we need the page to scroll instead. + Update navbar layout to accommodate this. */} + + + {/* TODO DEVPROD-11708: Use dynamic column limit in skeleton */} + } + > + + + + ); +const PageContainer = styled.div` + padding: ${size.m} ${size.l}; +`; + export default Waterfall; diff --git a/apps/spruce/src/pages/waterfall/styles.ts b/apps/spruce/src/pages/waterfall/styles.ts new file mode 100644 index 000000000..e6d3b69d8 --- /dev/null +++ b/apps/spruce/src/pages/waterfall/styles.ts @@ -0,0 +1,39 @@ +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import { wordBreakCss } from "components/styles"; +import { size } from "constants/tokens"; + +const BUILD_VARIANT_WIDTH = 200; +const INACTIVE_WIDTH = 80; + +// TODO DEVPROD-11708: Update with dynamic column count +export const VERSION_LIMIT = 5; + +export const columnBasis = css` + flex-basis: calc(100% / ${VERSION_LIMIT}); +`; + +export const gridGroupCss = css` + display: flex; + gap: ${size.s}; + flex-grow: 1; + padding-left: ${size.xs}; + padding-right: ${size.xs}; +`; + +export const BuildVariantTitle = styled.div` + ${wordBreakCss} + flex-grow: 0; + flex-shrink: 0; + width: ${BUILD_VARIANT_WIDTH}px; +`; + +export const Row = styled.div` + display: flex; + gap: ${size.xs}; + margin-bottom: ${size.s}; +`; + +export const InactiveVersion = styled.div` + width: ${INACTIVE_WIDTH}px; +`;