From 157582eb31c9c12fd11b415c72261759b63d8475 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Wed, 6 Nov 2024 14:57:09 -0500 Subject: [PATCH] DEVPROD-10195: Add task stats tooltip to waterfall (#480) --- .../integration/waterfall/waterfall.ts | 10 +++ .../src/gql/fragments/waterfall.graphql | 6 ++ apps/spruce/src/gql/generated/types.ts | 17 ++++ apps/spruce/src/pages/waterfall/BuildRow.tsx | 27 +++--- .../src/pages/waterfall/TaskStatsTooltip.tsx | 90 +++++++++++++++++++ .../VersionLabel/VersionLabel.stories.tsx | 40 +++++++++ .../VersionLabel_Broken.storyshot | 43 ++++----- .../VersionLabel_Default.storyshot | 33 +++---- .../VersionLabel_GitTag.storyshot | 33 +++---- .../VersionLabel_SmallSize.storyshot | 33 +++---- .../VersionLabel_TaskStatsTooltip.storyshot | 84 +++++++++++++++++ .../VersionLabel_UpstreamProject.storyshot | 33 +++---- .../pages/waterfall/VersionLabel/index.tsx | 60 ++++++++----- .../src/pages/waterfall/WaterfallGrid.tsx | 6 +- apps/spruce/src/pages/waterfall/styles.ts | 20 ++++- apps/spruce/src/pages/waterfall/types.ts | 11 ++- apps/spruce/src/pages/waterfall/useFilters.ts | 16 ++-- 17 files changed, 425 insertions(+), 137 deletions(-) create mode 100644 apps/spruce/src/pages/waterfall/TaskStatsTooltip.tsx create mode 100644 apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_TaskStatsTooltip.storyshot diff --git a/apps/spruce/cypress/integration/waterfall/waterfall.ts b/apps/spruce/cypress/integration/waterfall/waterfall.ts index 8012426a9..51a17ba2f 100644 --- a/apps/spruce/cypress/integration/waterfall/waterfall.ts +++ b/apps/spruce/cypress/integration/waterfall/waterfall.ts @@ -124,6 +124,16 @@ describe("waterfall page", () => { }); }); + describe("task stats tooltip", () => { + it("shows task stats when clicked", () => { + cy.dataCy("task-stats-tooltip").should("not.exist"); + cy.dataCy("task-stats-tooltip-button").eq(3).click(); + cy.dataCy("task-stats-tooltip").should("be.visible"); + cy.dataCy("task-stats-tooltip").should("contain.text", "Failed"); + cy.dataCy("task-stats-tooltip").should("contain.text", "Succeeded"); + }); + }); + describe("pinned build variants", () => { beforeEach(() => { cy.visit("/project/evergreen/waterfall"); diff --git a/apps/spruce/src/gql/fragments/waterfall.graphql b/apps/spruce/src/gql/fragments/waterfall.graphql index 205de025c..b07b20b29 100644 --- a/apps/spruce/src/gql/fragments/waterfall.graphql +++ b/apps/spruce/src/gql/fragments/waterfall.graphql @@ -13,5 +13,11 @@ fragment WaterfallVersion on Version { order requester revision + taskStatusStats(options: {}) { + counts { + count + status + } + } ...UpstreamProject } diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index f102b0207..a470e0691 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -3507,6 +3507,7 @@ export type WaterfallPagination = { export type WaterfallTask = { __typename?: "WaterfallTask"; displayName: Scalars["String"]["output"]; + displayStatus: Scalars["String"]["output"]; execution: Scalars["Int"]["output"]; id: Scalars["String"]["output"]; status: Scalars["String"]["output"]; @@ -4950,6 +4951,14 @@ export type WaterfallVersionFragment = { requester: string; revision: string; gitTags?: Array<{ __typename?: "GitTag"; tag: string }> | null; + taskStatusStats?: { + __typename?: "TaskStats"; + counts?: Array<{ + __typename?: "StatusCount"; + count: number; + status: string; + }> | null; + } | null; upstreamProject?: { __typename?: "UpstreamProject"; owner: string; @@ -9642,6 +9651,14 @@ export type WaterfallQuery = { requester: string; revision: string; gitTags?: Array<{ __typename?: "GitTag"; tag: string }> | null; + taskStatusStats?: { + __typename?: "TaskStats"; + counts?: Array<{ + __typename?: "StatusCount"; + count: number; + status: string; + }> | null; + } | null; upstreamProject?: { __typename?: "UpstreamProject"; owner: string; diff --git a/apps/spruce/src/pages/waterfall/BuildRow.tsx b/apps/spruce/src/pages/waterfall/BuildRow.tsx index 5cef0300d..fbc0bf56b 100644 --- a/apps/spruce/src/pages/waterfall/BuildRow.tsx +++ b/apps/spruce/src/pages/waterfall/BuildRow.tsx @@ -10,21 +10,21 @@ import Icon from "components/Icon"; import { StyledLink } from "components/styles"; import { getTaskRoute, getVariantHistoryRoute } from "constants/routes"; import { size } from "constants/tokens"; -import { WaterfallBuild, WaterfallBuildVariant } from "gql/generated/types"; -import { statusColorMap, statusIconMap } from "./icons"; import { BuildVariantTitle, columnBasis, gridGroupCss, InactiveVersion, Row, + SQUARE_SIZE, + taskStatusStyleMap, } from "./styles"; -import { WaterfallVersion } from "./types"; +import { Build, BuildVariant, WaterfallVersion } from "./types"; const { black, gray, white } = palette; type Props = { - build: WaterfallBuildVariant; + build: BuildVariant; handlePinClick: () => void; pinned: boolean; projectIdentifier: string; @@ -96,7 +96,7 @@ export const BuildRow: React.FC = ({ /> ); } - return ; + return ; })} @@ -104,10 +104,10 @@ export const BuildRow: React.FC = ({ }; const BuildGrid: React.FC<{ - build: WaterfallBuild; + build: Build; handleTaskClick: (s: string) => () => void; }> = ({ build, handleTaskClick }) => ( - { handleTaskClick( (event.target as HTMLDivElement)?.getAttribute("status") ?? "", @@ -128,7 +128,7 @@ const BuildGrid: React.FC<{ /> ); })} - + ); const BuildGroup = styled.div` @@ -139,7 +139,7 @@ const BuildGroup = styled.div` padding-top: ${size.xs}; `; -const Build = styled.div` +const BuildContainer = styled.div` ${columnBasis} `; @@ -148,8 +148,6 @@ const StyledIconButton = styled(IconButton)` ${({ active }) => active && "transform: rotate(-30deg);"} `; -const SQUARE_SIZE = 16; - const Square = styled(Link)<{ status: TaskStatus }>` width: ${SQUARE_SIZE}px; height: ${SQUARE_SIZE}px; @@ -159,12 +157,7 @@ const Square = styled(Link)<{ status: TaskStatus }>` cursor: pointer; position: relative; - ${({ status }) => { - const icon = statusIconMap?.[status]; - const iconStyle = icon ? `background-image: ${icon};` : ""; - return `${iconStyle} -background-color: ${statusColorMap[status]};`; - }} + ${({ status }) => taskStatusStyleMap[status]} /* Tooltip */ :before { diff --git a/apps/spruce/src/pages/waterfall/TaskStatsTooltip.tsx b/apps/spruce/src/pages/waterfall/TaskStatsTooltip.tsx new file mode 100644 index 000000000..8095bb0bf --- /dev/null +++ b/apps/spruce/src/pages/waterfall/TaskStatsTooltip.tsx @@ -0,0 +1,90 @@ +import { useRef, useState } from "react"; +import styled from "@emotion/styled"; +import IconButton from "@leafygreen-ui/icon-button"; +import Popover, { Align } from "@leafygreen-ui/popover"; +import { taskStatusToCopy } from "@evg-ui/lib/constants/task"; +import { TaskStatus } from "@evg-ui/lib/types/task"; +import Icon from "components/Icon"; +import { Divider } from "components/styles"; +import { PopoverContainer } from "components/styles/Popover"; +import { size } from "constants/tokens"; +import { WaterfallVersionFragment } from "gql/generated/types"; +import { useOnClickOutside } from "hooks"; +import { SQUARE_SIZE, taskStatusStyleMap } from "./styles"; + +export const TaskStatsTooltip: React.FC< + Pick +> = ({ taskStatusStats }) => { + const [open, setOpen] = useState(false); + + const buttonRef = useRef(null); + const popoverRef = useRef(null); + + useOnClickOutside([buttonRef, popoverRef], () => setOpen(false)); + + const totalTaskCount = + taskStatusStats?.counts?.reduce((total, { count }) => total + count, 0) ?? + 0; + + return ( + <> + + setOpen((o) => !o)} + > + + + + + + + {taskStatusStats?.counts?.map(({ count, status }) => ( + + {count} + + + + {taskStatusToCopy[status as TaskStatus]} + + ))} + + + + + + + {totalTaskCount} + Total tasks + +
+
+
+ + ); +}; + +const BtnContainer = styled.div` + display: inline-block; +`; + +const Table = styled.table``; + +const Row = styled.tr``; + +const Cell = styled.td` + padding: 0 ${size.xxs}; +`; + +const Count = styled(Cell)` + font-feature-settings: "tnum"; + text-align: right; +`; + +const Square = styled.div<{ status: TaskStatus }>` + ${({ status }) => taskStatusStyleMap[status]} + height: ${SQUARE_SIZE}px; + width: ${SQUARE_SIZE}px; +`; diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/VersionLabel.stories.tsx b/apps/spruce/src/pages/waterfall/VersionLabel/VersionLabel.stories.tsx index e2c2660cb..b6d4d58a0 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/VersionLabel.stories.tsx +++ b/apps/spruce/src/pages/waterfall/VersionLabel/VersionLabel.stories.tsx @@ -60,6 +60,46 @@ export const Broken: StoryObj = { args: versionBroken, }; +export const TaskStatsTooltip: StoryObj = { + ...Default, + args: { + ...version, + taskStatusStats: { + counts: [ + { + status: "blocked", + count: 4, + }, + { + status: "failed", + count: 3, + }, + { + status: "setup-failed", + count: 3, + }, + { + status: "started", + count: 22, + }, + { + status: "success", + count: 255, + }, + { + status: "unscheduled", + count: 2313, + }, + { + status: "will-run", + count: 100, + }, + ], + }, + view: VersionLabelView.Waterfall, + }, +}; + const Container = styled.div` max-width: 300px; `; diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Broken.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Broken.storyshot index 6cc294311..cbabede94 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Broken.storyshot +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Broken.storyshot @@ -3,32 +3,35 @@ class="css-1pp2qz6" >
-

- - - aec8832 - - - - 09/19/2024, 10:56 -

- Broken -
-

+ + aec8832 + + + 09/19/2024, 10:56 +
+ Broken +
+

+

diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Default.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Default.storyshot index e0ce86bd9..a71e81c53 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Default.storyshot +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_Default.storyshot @@ -3,27 +3,30 @@ class="css-1pp2qz6" >

-

- - - aec8832 - - - - 09/19/2024, 10:56 -

+ + aec8832 + + + 09/19/2024, 10:56 +

+

diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_GitTag.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_GitTag.storyshot index 024858b28..28c8a28ae 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_GitTag.storyshot +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_GitTag.storyshot @@ -3,27 +3,30 @@ class="css-1pp2qz6" >

-

- - - deb77a3 - - - - 09/19/2024, 12:14 -

+ + deb77a3 + + + 09/19/2024, 12:14 +

+

diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_SmallSize.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_SmallSize.storyshot index 4a31dc104..c6308e589 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_SmallSize.storyshot +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_SmallSize.storyshot @@ -3,27 +3,30 @@ class="css-1pp2qz6" >

-

- - - aec8832 - - - - 09/19/2024, 10:56 -

+ + aec8832 + + + 09/19/2024, 10:56 +

+

diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_TaskStatsTooltip.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_TaskStatsTooltip.storyshot new file mode 100644 index 000000000..41d49f871 --- /dev/null +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_TaskStatsTooltip.storyshot @@ -0,0 +1,84 @@ +

+
+
+
+

+ + + aec8832 + + + 09/19/2024, 10:56 +

+
+ +
+
+

+ + Sophie Stadler + + • + + + + DEVPROD-11387 + + + : Remove CSS grid layout, plus some additional description to demonstrate the overflow capabilities of the component (#397) +

+
+
+
\ No newline at end of file diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_UpstreamProject.storyshot b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_UpstreamProject.storyshot index 7145a37ee..2736b6154 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_UpstreamProject.storyshot +++ b/apps/spruce/src/pages/waterfall/VersionLabel/__snapshots__/VersionLabel_UpstreamProject.storyshot @@ -3,25 +3,28 @@ class="css-1pp2qz6" >
-

- - - 1309488 - - - - 09/19/2024, 12:06 -

+ + 1309488 + + + 09/19/2024, 12:06 +

+

@@ -37,7 +40,7 @@

diff --git a/apps/spruce/src/pages/waterfall/VersionLabel/index.tsx b/apps/spruce/src/pages/waterfall/VersionLabel/index.tsx index 3e007cd05..2e4a82c10 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel/index.tsx +++ b/apps/spruce/src/pages/waterfall/VersionLabel/index.tsx @@ -12,6 +12,7 @@ import { WaterfallVersionFragment } from "gql/generated/types"; import { useSpruceConfig, useDateFormat } from "hooks"; import { shortenGithash, jiraLinkify } from "utils/string"; import { columnBasis } from "../styles"; +import { TaskStatsTooltip } from "../TaskStatsTooltip"; export enum VersionLabelView { Modal = "modal", @@ -35,6 +36,7 @@ export const VersionLabel: React.FC = ({ message, revision, shouldDisableText = false, + taskStatusStats, upstreamProject, view, }) => { @@ -56,28 +58,33 @@ export const VersionLabel: React.FC = ({ shouldDisableText={shouldDisableText} view={view} > - - { - sendEvent({ - name: "Clicked commit label", - "commit.type": commitType, - link: "githash", - }); - }} - to={getVersionRoute(id)} - > - {shortenGithash(revision)} - {" "} - {getDateCopy(createDate, { omitSeconds: true, omitTimezone: true })} - {commitType === "inactive" && ( - Inactive - )} - {errors.length > 0 && ( - Broken + + + { + sendEvent({ + name: "Clicked commit label", + "commit.type": commitType, + link: "githash", + }); + }} + to={getVersionRoute(id)} + > + {shortenGithash(revision)} + + {getDateCopy(createDate, { omitSeconds: true, omitTimezone: true })} + {commitType === "inactive" && ( + Inactive + )} + {errors.length > 0 && ( + Broken + )} + + {view === VersionLabelView.Waterfall && !!taskStatusStats && ( + )} - + {upstreamProject && ( Triggered by:{" "} @@ -126,11 +133,10 @@ const VersionContainer = styled.div< Pick >` ${columnBasis} - ${({ activated, shouldDisableText, view }) => view === VersionLabelView.Waterfall ? ` - > * { + div, p { font-size: 12px; line-height: 1.3; } @@ -159,3 +165,11 @@ const CommitMessage = styled(Body)>` const StyledBadge = styled(Badge)` margin-left: ${sizeToken.xs}; `; + +const HeaderLine = styled.div` + align-items: center; + display: flex; + > p { + flex-grow: 1; + } +`; diff --git a/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx b/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx index 851b7b398..84e368710 100644 --- a/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx +++ b/apps/spruce/src/pages/waterfall/WaterfallGrid.tsx @@ -107,11 +107,7 @@ export const WaterfallGrid: React.FC = ({ {versions.map(({ inactiveVersions, version }) => version ? ( - + ) : ( { + const icon = statusIconMap?.[status]; + const iconStyle = icon ? `background-image: ${icon};` : ""; + return { + ...obj, + [status]: css` + ${iconStyle} + background-color: ${statusColorMap[status]}; + `, + }; + }, + {} as Record, +); diff --git a/apps/spruce/src/pages/waterfall/types.ts b/apps/spruce/src/pages/waterfall/types.ts index 04931f5ae..b01ec3115 100644 --- a/apps/spruce/src/pages/waterfall/types.ts +++ b/apps/spruce/src/pages/waterfall/types.ts @@ -1,4 +1,5 @@ -import { WaterfallVersionFragment } from "gql/generated/types"; +import { Unpacked } from "@evg-ui/lib/types/utils"; +import { WaterfallVersionFragment, WaterfallQuery } from "gql/generated/types"; // Although this is pretty much a duplicate of the GraphQL type, it is // necessary to resolve type errors. @@ -8,3 +9,11 @@ export type WaterfallVersion = { inactiveVersions: WaterfallVersionFragment[] | null; version: WaterfallVersionFragment | null; }; + +export type Build = Unpacked< + Unpacked["builds"] +>; + +export type BuildVariant = Unpacked< + WaterfallQuery["waterfall"]["buildVariants"] +>; diff --git a/apps/spruce/src/pages/waterfall/useFilters.ts b/apps/spruce/src/pages/waterfall/useFilters.ts index a60a599aa..923e6cd38 100644 --- a/apps/spruce/src/pages/waterfall/useFilters.ts +++ b/apps/spruce/src/pages/waterfall/useFilters.ts @@ -1,16 +1,12 @@ import { useMemo } from "react"; -import { - WaterfallBuild, - WaterfallBuildVariant, - WaterfallVersionFragment, -} from "gql/generated/types"; +import { WaterfallVersionFragment } from "gql/generated/types"; import { useQueryParam } from "hooks/useQueryParam"; import { WaterfallFilterOptions } from "types/waterfall"; -import { WaterfallVersion } from "./types"; +import { Build, BuildVariant, WaterfallVersion } from "./types"; import { groupInactiveVersions } from "./utils"; type UseFiltersProps = { - buildVariants: WaterfallBuildVariant[]; + buildVariants: BuildVariant[]; flattenedVersions: WaterfallVersionFragment[]; pins: string[]; }; @@ -107,10 +103,10 @@ export const useFilters = ({ return buildVariants; } - const bvs: WaterfallBuildVariant[] = []; + const bvs: BuildVariant[] = []; let pinIndex = 0; - const pushVariant = (variant: WaterfallBuildVariant) => { + const pushVariant = (variant: BuildVariant) => { if (pins.includes(variant.id)) { // If build variant is pinned, insert it at the end of the list of pinned variants bvs.splice(pinIndex, 0, variant); @@ -126,7 +122,7 @@ export const useFilters = ({ buildVariantFilterRegex.some((r) => bv.displayName.match(r)); if (passesBVFilter) { if (activeVersionIds.size !== bv.builds.length) { - const activeBuilds: WaterfallBuild[] = []; + const activeBuilds: Build[] = []; bv.builds.forEach((b) => { if (activeVersionIds.has(b.version)) { activeBuilds.push(b);