From fcae119792ca9a4781758087475199a612604755 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Fri, 31 May 2024 12:56:26 -0400 Subject: [PATCH 1/2] DEVPROD-7046: Fix e2e tests (#145) --- .../cypress/integration/hosts/hosts_sorting.ts | 3 +++ apps/spruce/cypress/integration/spawn/host.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/spruce/cypress/integration/hosts/hosts_sorting.ts b/apps/spruce/cypress/integration/hosts/hosts_sorting.ts index 96e0bbc2e..e05338828 100644 --- a/apps/spruce/cypress/integration/hosts/hosts_sorting.ts +++ b/apps/spruce/cypress/integration/hosts/hosts_sorting.ts @@ -36,6 +36,7 @@ const sortByTests = [ sortBy: "CURRENT_TASK", expectedIds: [ "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", "i-0d0ae8b83366d22", "i-0d0ae8b83366d22b", "i-0d0ae8b83366d22be", @@ -68,6 +69,7 @@ const sortByTests = [ sortBy: "ELAPSED", expectedIds: [ "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", "i-0d0ae8b83366d22", "i-0d0ae8b83366d22b", "i-0d0ae8b83366d22be", @@ -119,6 +121,7 @@ const sortDirectionTests = [ order: "ascending", expectedIds: [ "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", "i-0d0ae8b83366d22", "i-0d0ae8b83366d22b", "i-0d0ae8b83366d22be", diff --git a/apps/spruce/cypress/integration/spawn/host.ts b/apps/spruce/cypress/integration/spawn/host.ts index 4b2278abb..ea08f3f63 100644 --- a/apps/spruce/cypress/integration/spawn/host.ts +++ b/apps/spruce/cypress/integration/spawn/host.ts @@ -1,19 +1,23 @@ const ascendingSortSpawnHostOrderByHostId = [ "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", "i-092593689871a50dc", ]; const descendingSortSpawnHostOrderByHostId = [ "i-092593689871a50dc", + "i-07669e7a3cd2c238c", "i-04ade558e1e26b0ad", ]; const descendingSortSpawnHostOrderByExpiration = [ - "i-04ade558e1e26b0ad", "i-092593689871a50dc", + "i-07669e7a3cd2c238c", + "i-04ade558e1e26b0ad", ]; const ascendingSortSpawnHostOrderByExpiration = [ - "i-092593689871a50dc", "i-04ade558e1e26b0ad", + "i-07669e7a3cd2c238c", + "i-092593689871a50dc", ]; const hostTaskId = @@ -25,7 +29,7 @@ describe("Navigating to Spawn Host page", () => { cy.visit("/spawn/host"); }); it("Visiting the spawn host page should display all of your spawned hosts", () => { - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("leafygreen-table-row").should("have.length", 3); }); it("Visiting the spawn host page should not have any cards expanded by default", () => { cy.dataCy("spawn-host-card").should("not.exist"); @@ -55,7 +59,7 @@ describe("Navigating to Spawn Host page", () => { }); it("Visiting the spawn host page should display all of your spawned hosts not sorted by default", () => { - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("leafygreen-table-row").should("have.length", 3); }); it("Clicking on the host column header should sort spawn hosts by ascending order, then descending, then remove sort", () => { @@ -68,7 +72,7 @@ describe("Navigating to Spawn Host page", () => { cy.wrap($el).contains(descendingSortSpawnHostOrderByHostId[index]), ); cy.get("@hostSortControl").click(); - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("leafygreen-table-row").should("have.length", 3); }); it("Clicking on the expiration column header should sort the hosts by ascending order, then descending, then remove sort", () => { @@ -83,7 +87,7 @@ describe("Navigating to Spawn Host page", () => { cy.wrap($el).contains(descendingSortSpawnHostOrderByExpiration[index]), ); cy.get("@expiresInSortControl").click(); - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("leafygreen-table-row").should("have.length", 3); }); }); From 869e89701a39ee04fed96630b80780b59def1b2d Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Fri, 31 May 2024 15:41:05 -0400 Subject: [PATCH 2/2] DEVPROD-5291: Create SectionHeader component (#115) --- .../logWindow/useLogWindowAnalytics.ts | 5 +- .../src/components/LogRow/AnsiRow/index.tsx | 4 +- .../src/components/LogRow/BaseRow/index.tsx | 4 +- .../components/LogRow/ResmokeRow/index.tsx | 4 +- .../SectionHeader/SectionHeader.stories.tsx | 47 +++ .../SectionHeader/SectionHeader.test.tsx | 100 +++++++ ...ectionHeader_SectionHeaderSingle.storyshot | 273 ++++++++++++++++++ .../components/LogRow/SectionHeader/index.tsx | 96 ++++++ .../SkippedLinesRow/SkippedLinesRow.test.tsx | 1 + .../LogRow/SkippedLinesRow/index.tsx | 9 +- apps/parsley/src/components/LogRow/types.ts | 10 +- apps/parsley/src/constants/logs.ts | 7 +- apps/parsley/src/constants/tokens.ts | 3 +- apps/parsley/src/gql/generated/types.ts | 4 +- apps/spruce/src/gql/generated/types.ts | 4 +- 15 files changed, 546 insertions(+), 25 deletions(-) create mode 100644 apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx create mode 100644 apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx create mode 100644 apps/parsley/src/components/LogRow/SectionHeader/__snapshots__/SectionHeader_SectionHeaderSingle.storyshot create mode 100644 apps/parsley/src/components/LogRow/SectionHeader/index.tsx diff --git a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts index c1f8b4dba..4108dd63f 100644 --- a/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts +++ b/apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts @@ -25,7 +25,10 @@ type Action = | { name: "Applied Search Suggestion"; suggestion: string } | { name: "Expanded Lines"; option: "All" | "Five"; lineCount: number } | { name: "Collapsed Lines" } - | { name: "Paginated Through Search Results"; direction: DIRECTION }; + | { name: "Paginated Through Search Results"; direction: DIRECTION } + | { name: "Focused Section"; functionName: string } + | { name: "Opened Section"; functionName: string } + | { name: "Closed Section"; functionName: string }; export const useLogWindowAnalytics = () => useAnalyticsRoot("LogWindow"); diff --git a/apps/parsley/src/components/LogRow/AnsiRow/index.tsx b/apps/parsley/src/components/LogRow/AnsiRow/index.tsx index 0114af860..5fec340be 100644 --- a/apps/parsley/src/components/LogRow/AnsiRow/index.tsx +++ b/apps/parsley/src/components/LogRow/AnsiRow/index.tsx @@ -2,10 +2,10 @@ import { AnsiUp } from "ansi_up"; import linkifyHtml from "linkify-html"; import BaseRow from "components/LogRow/BaseRow"; import { trimSeverity } from "utils/string"; -import { LogRowProps } from "../types"; +import { LogLineRow } from "../types"; import { getSeverityMapping, mapLogLevelToColor } from "./utils"; -interface AnsiRowProps extends LogRowProps {} +interface AnsiRowProps extends LogLineRow {} const AnsiRow: React.FC = ({ getLine, lineNumber, ...rest }) => { const ansiUp = new AnsiUp(); diff --git a/apps/parsley/src/components/LogRow/BaseRow/index.tsx b/apps/parsley/src/components/LogRow/BaseRow/index.tsx index 2b6195074..f919f2d2a 100644 --- a/apps/parsley/src/components/LogRow/BaseRow/index.tsx +++ b/apps/parsley/src/components/LogRow/BaseRow/index.tsx @@ -9,7 +9,7 @@ import { QueryParams } from "constants/queryParams"; import { fontSize, size } from "constants/tokens"; import { useMultiLineSelectContext } from "context/MultiLineSelectContext"; import { useQueryParam } from "hooks/useQueryParam"; -import { LogRowProps } from "../types"; +import { LogLineRow } from "../types"; import { isLineInRange } from "../utils"; import Highlighter from "./Highlighter"; import LineNumber from "./LineNumber"; @@ -17,7 +17,7 @@ import SharingMenu from "./SharingMenu"; const { red, yellow } = palette; -interface BaseRowProps extends Omit { +interface BaseRowProps extends Omit { children: string; "data-cy"?: string; color?: string; diff --git a/apps/parsley/src/components/LogRow/ResmokeRow/index.tsx b/apps/parsley/src/components/LogRow/ResmokeRow/index.tsx index 68e298e68..a8abfe57f 100644 --- a/apps/parsley/src/components/LogRow/ResmokeRow/index.tsx +++ b/apps/parsley/src/components/LogRow/ResmokeRow/index.tsx @@ -2,9 +2,9 @@ import BaseRow from "components/LogRow/BaseRow"; import { QueryParams } from "constants/queryParams"; import { useQueryParam } from "hooks/useQueryParam"; import { formatPrettyPrint } from "utils/prettyPrint"; -import { LogRowProps } from "../types"; +import { LogLineRow } from "../types"; -interface ResmokeRowProps extends LogRowProps { +interface ResmokeRowProps extends LogLineRow { prettyPrint: boolean; getResmokeLineColor: (lineNumber: number) => string | undefined; } diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx new file mode 100644 index 000000000..61d00efc0 --- /dev/null +++ b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.stories.tsx @@ -0,0 +1,47 @@ +import styled from "@emotion/styled"; +import { SectionStatus } from "constants/logs"; +import { CustomMeta, CustomStoryObj } from "test_utils/types"; +import SectionHeader from "."; + +export default { + component: SectionHeader, +} satisfies CustomMeta; + +const SectionHeaderStory = () => ( + + + + + +); + +export const SectionHeaderSingle: CustomStoryObj = { + render: () => , +}; + +// TODO: Update this story with LogPane examples which should handle log rendering internally. + +const Container = styled.div` + height: 400px; + width: 800px; +`; + +const sectionHeaderProps = { + defaultOpen: false, + onFocus: () => {}, + onOpen: () => {}, + status: SectionStatus.Pass, +}; diff --git a/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx new file mode 100644 index 000000000..55dc07e0c --- /dev/null +++ b/apps/parsley/src/components/LogRow/SectionHeader/SectionHeader.test.tsx @@ -0,0 +1,100 @@ +import { SectionStatus } from "constants/logs"; +import { render, screen, userEvent } from "test_utils"; +import SectionHeader from "."; + +describe("sectionHeader", () => { + it("displays function name", () => { + render(); + expect(screen.getByText("Function: load_data")).toBeVisible(); + }); + + it("displays checkmark icon if status is passing", () => { + render( + , + ); + expect( + screen.getByLabelText("Checkmark With Circle Icon"), + ).toBeInTheDocument(); + }); + + it("displays X icon if status is failing", () => { + render( + , + ); + expect(screen.getByLabelText("XWith Circle Icon")).toBeInTheDocument(); + }); + + it("renders as opened if 'defaultOpen' prop is true", async () => { + render(); + expect(screen.getByDataCy("section-header")).toHaveAttribute( + "aria-expanded", + "true", + ); + }); + + it("renders as closed if 'defaultOpen' prop is false", async () => { + render(); + expect(screen.getByDataCy("section-header")).toHaveAttribute( + "aria-expanded", + "false", + ); + }); + + it("should call onOpen function when 'open' button is clicked", async () => { + const user = userEvent.setup(); + const onOpen = vi.fn(); + render(); + const openButton = screen.getByRole("button", { + name: "Open", + }); + await user.click(openButton); + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onOpen).toHaveBeenCalledWith("load_data"); + }); + + it("should call onFocus function when 'focus' button is clicked", async () => { + const user = userEvent.setup(); + const onFocus = vi.fn(); + render(); + const focusButton = screen.getByRole("button", { + name: "Focus", + }); + await user.click(focusButton); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocus).toHaveBeenCalledWith("load_data"); + }); + + it("can open and close the section header using 'open' and 'close' buttons", async () => { + const user = userEvent.setup(); + render(); + expect(screen.getByDataCy("section-header")).toHaveAttribute( + "aria-expanded", + "false", + ); + const openButton = screen.getByRole("button", { + name: "Open", + }); + await user.click(openButton); + expect(screen.getByDataCy("section-header")).toHaveAttribute( + "aria-expanded", + "true", + ); + const closeButton = screen.getByRole("button", { + name: "Close", + }); + await user.click(closeButton); + expect(screen.getByDataCy("section-header")).toHaveAttribute( + "aria-expanded", + "false", + ); + }); +}); + +const sectionHeaderProps = { + defaultOpen: false, + functionName: "load_data", + lineIndex: 0, + onFocus: vi.fn(), + onOpen: vi.fn(), + status: SectionStatus.Pass, +}; diff --git a/apps/parsley/src/components/LogRow/SectionHeader/__snapshots__/SectionHeader_SectionHeaderSingle.storyshot b/apps/parsley/src/components/LogRow/SectionHeader/__snapshots__/SectionHeader_SectionHeaderSingle.storyshot new file mode 100644 index 000000000..f1c18f02f --- /dev/null +++ b/apps/parsley/src/components/LogRow/SectionHeader/__snapshots__/SectionHeader_SectionHeaderSingle.storyshot @@ -0,0 +1,273 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/apps/parsley/src/components/LogRow/SectionHeader/index.tsx b/apps/parsley/src/components/LogRow/SectionHeader/index.tsx new file mode 100644 index 000000000..32449164d --- /dev/null +++ b/apps/parsley/src/components/LogRow/SectionHeader/index.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import styled from "@emotion/styled"; +import Button, { Size } from "@leafygreen-ui/button"; +import IconButton from "@leafygreen-ui/icon-button"; +import { palette } from "@leafygreen-ui/palette"; +import { Body } from "@leafygreen-ui/typography"; +import { useLogWindowAnalytics } from "analytics"; +import Icon from "components/Icon"; +import { Row } from "components/LogRow/types"; +import { SectionStatus } from "constants/logs"; +import { size, transitionDuration } from "constants/tokens"; + +const { gray } = palette; + +interface SectionHeaderProps extends Row { + defaultOpen?: boolean; + functionName: string; + onFocus: (functionName: string) => void; + onOpen: (functionName: string) => void; + status: SectionStatus; +} + +const SectionHeader: React.FC = ({ + defaultOpen = false, + functionName, + onFocus, + onOpen, + status, +}) => { + const { sendEvent } = useLogWindowAnalytics(); + const [open, setOpen] = useState(defaultOpen); + + const statusGlyph = + status === SectionStatus.Pass ? "CheckmarkWithCircle" : "XWithCircle"; + + return ( + + setOpen(!open)} + > + + + + Function: {functionName} + + + + + + ); +}; + +const AnimatedIcon = styled(Icon)<{ open: boolean }>` + transform: ${({ open }): string => (open ? "rotate(90deg)" : "unset")}; + transition-property: transform; + transition-duration: ${transitionDuration.default}ms; +`; + +const SectionHeaderWrapper = styled.div` + display: flex; + align-items: center; + gap: ${size.xs}; + padding: ${size.xxs} 0; + border-bottom: 1px solid ${gray.light2}; +`; + +const ButtonWrapper = styled.div` + display: flex; + align-items: center; + gap: ${size.xs}; +`; + +SectionHeader.displayName = "SectionHeader"; + +export default SectionHeader; diff --git a/apps/parsley/src/components/LogRow/SkippedLinesRow/SkippedLinesRow.test.tsx b/apps/parsley/src/components/LogRow/SkippedLinesRow/SkippedLinesRow.test.tsx index f9dcf9546..8e7361bfb 100644 --- a/apps/parsley/src/components/LogRow/SkippedLinesRow/SkippedLinesRow.test.tsx +++ b/apps/parsley/src/components/LogRow/SkippedLinesRow/SkippedLinesRow.test.tsx @@ -106,4 +106,5 @@ const logLines = [ const skippedLinesProps = { expandLines: vi.fn(), + lineIndex: 0, }; diff --git a/apps/parsley/src/components/LogRow/SkippedLinesRow/index.tsx b/apps/parsley/src/components/LogRow/SkippedLinesRow/index.tsx index ffbfd0645..c6d0efdf7 100644 --- a/apps/parsley/src/components/LogRow/SkippedLinesRow/index.tsx +++ b/apps/parsley/src/components/LogRow/SkippedLinesRow/index.tsx @@ -6,16 +6,19 @@ import { useLogWindowAnalytics } from "analytics"; import Icon from "components/Icon"; import { size } from "constants/tokens"; import { ExpandedLines, Range } from "types/logs"; -import { RootRowProps } from "../types"; +import { Row } from "../types"; const SKIP_NUMBER = 5; -interface Props extends RootRowProps { +interface SkippedLinesRowProps extends Row { expandLines: (expandedLines: ExpandedLines) => void; range: Range; } -const SkippedLinesRow: React.FC = ({ expandLines, range }) => { +const SkippedLinesRow: React.FC = ({ + expandLines, + range, +}) => { const { sendEvent } = useLogWindowAnalytics(); const [, startTransition] = useTransition(); const { end, start } = range; diff --git a/apps/parsley/src/components/LogRow/types.ts b/apps/parsley/src/components/LogRow/types.ts index 80ae5a980..5d53218a6 100644 --- a/apps/parsley/src/components/LogRow/types.ts +++ b/apps/parsley/src/components/LogRow/types.ts @@ -1,14 +1,10 @@ import { WordWrapFormat } from "constants/enums"; -/** - * RootRowProps are the props that are passed to the root LogRow component. - * These props are used to render any type of LogRow and should always be passed to the root LogRow component. - */ -interface RootRowProps { +interface Row { lineIndex: number; } -interface LogRowProps extends RootRowProps { +interface LogLineRow extends Row { getLine: (index: number) => string | undefined; scrollToLine: (lineNumber: number) => void; @@ -25,4 +21,4 @@ interface LogRowProps extends RootRowProps { wrap: boolean; } -export type { RootRowProps, LogRowProps }; +export type { LogLineRow, Row }; diff --git a/apps/parsley/src/constants/logs.ts b/apps/parsley/src/constants/logs.ts index 5276b0fb1..dafbc2211 100644 --- a/apps/parsley/src/constants/logs.ts +++ b/apps/parsley/src/constants/logs.ts @@ -1,4 +1,9 @@ const LOG_FILE_SIZE_LIMIT = 1024 * 1024 * 1024 * 2.5; // 2.5 GB const LOG_LINE_SIZE_LIMIT = 1024 * 1024 * 5; // 5 MB This should be enough to accommodate most lines, but not so large that it will cause performance issues. -export { LOG_FILE_SIZE_LIMIT, LOG_LINE_SIZE_LIMIT }; +enum SectionStatus { + Pass = "pass", + Fail = "fail", +} + +export { LOG_FILE_SIZE_LIMIT, LOG_LINE_SIZE_LIMIT, SectionStatus }; diff --git a/apps/parsley/src/constants/tokens.ts b/apps/parsley/src/constants/tokens.ts index abd6ca280..9d06bac06 100644 --- a/apps/parsley/src/constants/tokens.ts +++ b/apps/parsley/src/constants/tokens.ts @@ -1,4 +1,4 @@ -import { spacing } from "@leafygreen-ui/tokens"; +import { spacing, transitionDuration } from "@leafygreen-ui/tokens"; // Should be used for spacing such as margins and padding. const size = { @@ -49,4 +49,5 @@ export { navbarHeight, subheaderHeight, textInputHeight, + transitionDuration, }; diff --git a/apps/parsley/src/gql/generated/types.ts b/apps/parsley/src/gql/generated/types.ts index 2476c67d2..4d3a1f4c8 100644 --- a/apps/parsley/src/gql/generated/types.ts +++ b/apps/parsley/src/gql/generated/types.ts @@ -1334,9 +1334,7 @@ export type MutationUpdatePublicKeyArgs = { }; export type MutationUpdateSpawnHostStatusArgs = { - action?: InputMaybe; - hostId?: InputMaybe; - updateSpawnHostStatusInput?: InputMaybe; + updateSpawnHostStatusInput: UpdateSpawnHostStatusInput; }; export type MutationUpdateUserSettingsArgs = { diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index 58a5c5940..04b60af63 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -1334,9 +1334,7 @@ export type MutationUpdatePublicKeyArgs = { }; export type MutationUpdateSpawnHostStatusArgs = { - action?: InputMaybe; - hostId?: InputMaybe; - updateSpawnHostStatusInput?: InputMaybe; + updateSpawnHostStatusInput: UpdateSpawnHostStatusInput; }; export type MutationUpdateUserSettingsArgs = {