Skip to content

Commit

Permalink
DEVPROD-10199: Make waterfall build variants pinnable (#471)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophstad authored Nov 6, 2024
1 parent 51eb2b3 commit a76e168
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 11 deletions.
6 changes: 4 additions & 2 deletions apps/spruce/cypress/integration/waterfall/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ describe("pagination", () => {
it("url query params update as page changes", () => {
cy.location("search").should("equal", "");

cy.dataCy("version-labels").should("contain.text", "2ab1c56");

cy.dataCy("next-page-button").click();
cy.dataCy("waterfall-skeleton").should("not.exist");
cy.dataCy("version-labels").should("contain.text", "e391612");
cy.location("search").should("contain", "maxOrder");

cy.dataCy("prev-page-button").click();
cy.dataCy("waterfall-skeleton").should("not.exist");
cy.dataCy("version-labels").should("contain.text", "2ab1c56");
cy.location("search").should("contain", "minOrder");
});

Expand Down
20 changes: 20 additions & 0 deletions apps/spruce/cypress/integration/waterfall/waterfall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,24 @@ describe("waterfall page", () => {
cy.dataCy("version-label-active").should("have.length", 5);
});
});

describe("pinned build variants", () => {
beforeEach(() => {
cy.visit("/project/evergreen/waterfall");
});

it("clicking the pin button moves the build variant to the top, persist on reload, and unpin on click", () => {
cy.dataCy("build-variant-link").first().should("have.text", "Lint");
cy.dataCy("pin-button").eq(1).click();
cy.dataCy("build-variant-link")
.first()
.should("have.text", "Ubuntu 16.04");
cy.reload();
cy.dataCy("build-variant-link")
.first()
.should("have.text", "Ubuntu 16.04");
cy.dataCy("pin-button").eq(1).click();
cy.dataCy("build-variant-link").first().should("have.text", "Lint");
});
});
});
1 change: 1 addition & 0 deletions apps/spruce/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const FASTER_POLL_INTERVAL = DEFAULT_POLL_INTERVAL / 3;
export const PAGE_SIZES = [10, 20, 50, 100];
export const RECENT_PAGE_SIZE_KEY = "recentPageSize";
export const DEFAULT_PAGE_SIZE = 10;
export const WATERFALL_PINNED_VARIANTS_KEY = "waterfall-pinned-variants";
30 changes: 28 additions & 2 deletions apps/spruce/src/pages/waterfall/BuildRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { memo, useCallback } from "react";
import styled from "@emotion/styled";
import IconButton from "@leafygreen-ui/icon-button";
import { palette } from "@leafygreen-ui/palette";
import { Link } from "react-router-dom";
import { taskStatusToCopy } from "@evg-ui/lib/constants/task";
import { TaskStatus } from "@evg-ui/lib/types/task";
import { useWaterfallAnalytics } from "analytics";
import Icon from "components/Icon";
import { StyledLink } from "components/styles";
import { getTaskRoute, getVariantHistoryRoute } from "constants/routes";
import { size } from "constants/tokens";
Expand All @@ -21,11 +23,21 @@ import { WaterfallVersion } from "./types";

const { black, gray, white } = palette;

export const BuildRow: React.FC<{
type Props = {
build: WaterfallBuildVariant;
handlePinClick: () => void;
pinned: boolean;
projectIdentifier: string;
versions: WaterfallVersion[];
}> = ({ build, projectIdentifier, versions }) => {
};

export const BuildRow: React.FC<Props> = ({
build,
handlePinClick,
pinned,
projectIdentifier,
versions,
}) => {
const { sendEvent } = useWaterfallAnalytics();
const handleVariantClick = useCallback(
() => sendEvent({ name: "Clicked variant label" }),
Expand All @@ -45,7 +57,16 @@ export const BuildRow: React.FC<{
return (
<Row>
<BuildVariantTitle data-cy="build-variant-label">
<StyledIconButton
active={pinned}
aria-label="Pin build variant"
data-cy="pin-button"
onClick={handlePinClick}
>
<Icon glyph="Pin" />
</StyledIconButton>
<StyledLink
data-cy="build-variant-link"
href={getVariantHistoryRoute(projectIdentifier, build.id)}
onClick={handleVariantClick}
>
Expand Down Expand Up @@ -122,6 +143,11 @@ const Build = styled.div`
${columnBasis}
`;

const StyledIconButton = styled(IconButton)`
top: -${size.xxs};
${({ active }) => active && "transform: rotate(-30deg);"}
`;

const SQUARE_SIZE = 16;

const Square = styled(Link)<{ status: TaskStatus }>`
Expand Down
38 changes: 36 additions & 2 deletions apps/spruce/src/pages/waterfall/WaterfallGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useSuspenseQuery } from "@apollo/client";
import styled from "@emotion/styled";
import { DEFAULT_POLL_INTERVAL } from "constants/index";
import {
DEFAULT_POLL_INTERVAL,
WATERFALL_PINNED_VARIANTS_KEY,
} from "constants/index";
import {
WaterfallPagination,
WaterfallQuery,
Expand All @@ -11,6 +14,7 @@ import { WATERFALL } from "gql/queries";
import { useDimensions } from "hooks/useDimensions";
import { useQueryParam } from "hooks/useQueryParam";
import { WaterfallFilterOptions } from "types/waterfall";
import { getObject, setObject } from "utils/localStorage";
import { BuildRow } from "./BuildRow";
import { InactiveVersionsButton } from "./InactiveVersions";
import {
Expand All @@ -35,6 +39,33 @@ export const WaterfallGrid: React.FC<WaterfallGridProps> = ({
}) => {
useWaterfallTrace();

const [pins, setPins] = useState<string[]>(
getObject(WATERFALL_PINNED_VARIANTS_KEY)?.[projectIdentifier] ?? [],
);

const handlePinBV = useCallback(
(buildVariant: string) => () => {
setPins((prev: string[]) => {
const bvIndex = prev.indexOf(buildVariant);
if (bvIndex > -1) {
const removed = [...prev];
removed.splice(bvIndex, 1);
return removed;
}
return [...prev, buildVariant];
});
},
[],
);

useEffect(() => {
const bvs = getObject(WATERFALL_PINNED_VARIANTS_KEY);
setObject(WATERFALL_PINNED_VARIANTS_KEY, {
...bvs,
[projectIdentifier]: pins,
});
}, [pins, projectIdentifier]);

const [maxOrder] = useQueryParam<number>(WaterfallFilterOptions.MaxOrder, 0);
const [minOrder] = useQueryParam<number>(WaterfallFilterOptions.MinOrder, 0);

Expand Down Expand Up @@ -66,6 +97,7 @@ export const WaterfallGrid: React.FC<WaterfallGridProps> = ({
const { buildVariants, versions } = useFilters({
buildVariants: data.waterfall.buildVariants,
flattenedVersions: data.waterfall.flattenedVersions,
pins,
});

return (
Expand Down Expand Up @@ -95,6 +127,8 @@ export const WaterfallGrid: React.FC<WaterfallGridProps> = ({
<BuildRow
key={b.id}
build={b}
handlePinClick={handlePinBV(b.id)}
pinned={pins.includes(b.id)}
projectIdentifier={projectIdentifier}
versions={versions}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/spruce/src/pages/waterfall/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const Waterfall: React.FC = () => {
}
>
<WaterfallGrid
key={projectIdentifier}
projectIdentifier={projectIdentifier ?? ""}
setPagination={setPagination}
/>
Expand All @@ -86,7 +87,7 @@ const Waterfall: React.FC = () => {
const PageContainer = styled.div`
display: flex;
flex-direction: column;
padding: ${size.m} ${size.l};
padding: ${size.m};
`;

/* Safari performance of the waterfall chokes if using overflow-y: scroll, so we need the page to scroll instead.
Expand Down
2 changes: 2 additions & 0 deletions apps/spruce/src/pages/waterfall/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export const gridGroupCss = css`

export const BuildVariantTitle = styled.div`
${wordBreakCss}
display: flex;
flex-grow: 0;
flex-shrink: 0;
gap: ${size.xxs};
width: ${BUILD_VARIANT_WIDTH}px;
`;

Expand Down
61 changes: 61 additions & 0 deletions apps/spruce/src/pages/waterfall/useFilters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("useFilters", () => {
useFilters({
buildVariants: waterfall.buildVariants,
flattenedVersions,
pins: [],
}),
{
wrapper: createWrapper(),
Expand All @@ -38,6 +39,7 @@ describe("useFilters", () => {
useFilters({
buildVariants: waterfall.buildVariants,
flattenedVersions,
pins: [],
}),
{
wrapper: createWrapper({
Expand All @@ -64,13 +66,43 @@ describe("useFilters", () => {
});
});

describe("pinned build variants", () => {
it("should push pins to the top of list of build variants and preserve their original order", () => {
const { result } = renderHook(
() =>
useFilters({
buildVariants: waterfall.buildVariants,
flattenedVersions,
pins: ["3", "2"],
}),
{
wrapper: createWrapper({
initialEntry: "/project/spruce/waterfall",
}),
},
);

const pinnedWaterfall = {
...waterfall,
buildVariants: [
waterfall.buildVariants[1],
waterfall.buildVariants[2],
waterfall.buildVariants[0],
],
};

expect(result.current).toMatchObject(pinnedWaterfall);
});
});

describe("build variant filters", () => {
it("should filter build variant list when filter is applied", () => {
const { result } = renderHook(
() =>
useFilters({
buildVariants: waterfall.buildVariants,
flattenedVersions,
pins: [],
}),
{
wrapper: createWrapper({
Expand All @@ -93,6 +125,7 @@ describe("useFilters", () => {
useFilters({
buildVariants: waterfall.buildVariants,
flattenedVersions,
pins: [],
}),
{
wrapper: createWrapper({
Expand Down Expand Up @@ -126,6 +159,34 @@ const waterfall = {
},
],
},
{
id: "2",
displayName: "BV 2",
version: "b",
builds: [
{
activated: true,
displayName: "Build B",
id: "ii",
tasks: [],
version: "b",
},
],
},
{
id: "3",
displayName: "BV 3",
version: "c",
builds: [
{
activated: true,
displayName: "Build C",
id: "iii",
tasks: [],
version: "c",
},
],
},
],
versions: [
{
Expand Down
28 changes: 24 additions & 4 deletions apps/spruce/src/pages/waterfall/useFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { groupInactiveVersions } from "./utils";
type UseFiltersProps = {
buildVariants: WaterfallBuildVariant[];
flattenedVersions: WaterfallVersionFragment[];
pins: string[];
};

export const useFilters = ({
buildVariants,
flattenedVersions,
pins,
}: UseFiltersProps) => {
const [requesters] = useQueryParam<string[]>(
WaterfallFilterOptions.Requesters,
Expand Down Expand Up @@ -101,11 +103,23 @@ export const useFilters = ({
);

const buildVariantsResult = useMemo(() => {
if (!hasFilters) {
if (!hasFilters && !pins.length) {
return buildVariants;
}

const bvs: WaterfallBuildVariant[] = [];

let pinIndex = 0;
const pushVariant = (variant: WaterfallBuildVariant) => {
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);
pinIndex += 1;
} else {
bvs.push(variant);
}
};

buildVariants.forEach((bv) => {
const passesBVFilter =
!buildVariantFilterRegex.length ||
Expand All @@ -119,15 +133,21 @@ export const useFilters = ({
}
});
if (activeBuilds.length) {
bvs.push({ ...bv, builds: activeBuilds });
pushVariant({ ...bv, builds: activeBuilds });
}
} else {
bvs.push(bv);
pushVariant(bv);
}
}
});
return bvs;
}, [activeVersionIds, hasFilters, buildVariants, buildVariantFilterRegex]);
}, [
activeVersionIds,
buildVariantFilterRegex,
buildVariants,
hasFilters,
pins,
]);

return { buildVariants: buildVariantsResult, versions: versionsResult };
};
Expand Down
19 changes: 19 additions & 0 deletions apps/spruce/src/utils/localStorage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { reportError } from "utils/errorReporting";

type LocalStorageObject = Record<string, any>;

export const getObject = (key: string): LocalStorageObject => {
const obj = localStorage.getItem(key);
try {
return obj ? JSON.parse(obj) : {};
} catch (e) {
reportError(
new Error(`Getting object '${key}' from localStorage`, { cause: e }),
).warning();
return {};
}
};

export const setObject = (key: string, obj: LocalStorageObject) => {
localStorage.setItem(key, JSON.stringify(obj));
};
Loading

0 comments on commit a76e168

Please sign in to comment.