diff --git a/apps/spruce/cypress/integration/image/build_information.ts b/apps/spruce/cypress/integration/image/build_information.ts index ef71b01b6..9711f8b8c 100644 --- a/apps/spruce/cypress/integration/image/build_information.ts +++ b/apps/spruce/cypress/integration/image/build_information.ts @@ -26,6 +26,53 @@ describe("build information", () => { }); }); + describe("os", () => { + it("should show the corresponding OS info", () => { + cy.visit("/image/ubuntu2204"); + cy.dataCy("os-table-row").should("have.length", 10); + }); + + it("should show different OS info on different pages", () => { + cy.visit("/image/ubuntu2204"); + cy.dataCy("os-table-row").should("have.length", 10); + + // First OS info name on first page. + cy.dataCy("os-table-row") + .first() + .find("td") + .first() + .invoke("text") + .as("firstOSName", { type: "static" }); + + cy.dataCy("os-card").within(() => { + cy.get("[data-testid=lg-pagination-next-button]").click(); + }); + + // First OS info name on second page. + cy.dataCy("os-table-row") + .first() + .find("td") + .first() + .invoke("text") + .as("nextOSName", { type: "static" }); + + // OS info names should not be equal. + cy.get("@firstOSName").then((firstOSName) => { + cy.get("@nextOSName").then((nextOSName) => { + expect(nextOSName).not.to.equal(firstOSName); + }); + }); + }); + + it("should show no OS info when filtering for nonexistent item", () => { + cy.visit("/image/ubuntu2204"); + cy.dataCy("os-table-row").should("have.length", 10); + cy.dataCy("os-name-filter").click(); + cy.get('input[placeholder="Name regex"]').type("fakeOS{enter}"); + cy.dataCy("os-table-row").should("have.length", 0); + }); + }); + describe("packages", () => { it("should show the corresponding packages", () => { cy.visit("/image/ubuntu2204"); diff --git a/apps/spruce/src/components/Banners/SiteBanner.tsx b/apps/spruce/src/components/Banners/SiteBanner.tsx index 485508f72..0f8b38a28 100644 --- a/apps/spruce/src/components/Banners/SiteBanner.tsx +++ b/apps/spruce/src/components/Banners/SiteBanner.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import Banner, { Variant } from "@leafygreen-ui/banner"; import Cookies from "js-cookie"; import { useSpruceConfig } from "hooks"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { jiraLinkify } from "utils/string"; export interface SiteBannerProps { text: string; diff --git a/apps/spruce/src/components/CommitChartLabel/index.tsx b/apps/spruce/src/components/CommitChartLabel/index.tsx index 354e60020..c68ee9349 100644 --- a/apps/spruce/src/components/CommitChartLabel/index.tsx +++ b/apps/spruce/src/components/CommitChartLabel/index.tsx @@ -8,8 +8,7 @@ import { getVersionRoute, getTriggerRoute } from "constants/routes"; import { size, zIndex } from "constants/tokens"; import { UpstreamProjectFragment, GitTag } from "gql/generated/types"; import { useSpruceConfig, useDateFormat } from "hooks"; -import { shortenGithash } from "utils/string"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { shortenGithash, jiraLinkify } from "utils/string"; const { gray } = palette; const MAX_CHAR = 40; diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index 5776d1e67..a48a0103a 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -6355,6 +6355,25 @@ export type ImageGeneralQuery = { } | null; }; +export type ImageOperatingSystemQueryVariables = Exact<{ + imageId: Scalars["String"]["input"]; + opts: OperatingSystemOpts; +}>; + +export type ImageOperatingSystemQuery = { + __typename?: "Query"; + image?: { + __typename?: "Image"; + id: string; + operatingSystem: { + __typename?: "ImageOperatingSystemPayload"; + filteredCount: number; + totalCount: number; + data: Array<{ __typename?: "OSInfo"; name: string; version: string }>; + }; + } | null; +}; + export type ImagePackagesQueryVariables = Exact<{ imageId: Scalars["String"]["input"]; opts: PackageOpts; diff --git a/apps/spruce/src/gql/queries/image-operating-system.graphql b/apps/spruce/src/gql/queries/image-operating-system.graphql new file mode 100644 index 000000000..f07d47bae --- /dev/null +++ b/apps/spruce/src/gql/queries/image-operating-system.graphql @@ -0,0 +1,13 @@ +query ImageOperatingSystem($imageId: String!, $opts: OperatingSystemOpts!) { + image(imageId: $imageId) { + id + operatingSystem(opts: $opts) { + data { + name + version + } + filteredCount + totalCount + } + } +} diff --git a/apps/spruce/src/gql/queries/index.ts b/apps/spruce/src/gql/queries/index.ts index b0c09f190..68fef50e6 100644 --- a/apps/spruce/src/gql/queries/index.ts +++ b/apps/spruce/src/gql/queries/index.ts @@ -25,6 +25,7 @@ import HOSTS from "./hosts.graphql"; import IMAGE_DISTROS from "./image-distros.graphql"; import IMAGE_EVENTS from "./image-events.graphql"; import IMAGE_GENERAL from "./image-general.graphql"; +import IMAGE_OPERATING_SYSTEM from "./image-operating-system.graphql"; import IMAGE_PACKAGES from "./image-packages.graphql"; import IMAGE_TOOLCHAINS from "./image-toolchains.graphql"; import IMAGES from "./images.graphql"; @@ -114,6 +115,7 @@ export { IMAGE_DISTROS, IMAGE_EVENTS, IMAGE_GENERAL, + IMAGE_OPERATING_SYSTEM, IMAGE_PACKAGES, IMAGE_TOOLCHAINS, IMAGES, diff --git a/apps/spruce/src/pages/Version.tsx b/apps/spruce/src/pages/Version.tsx index 638a4ec7e..882869790 100644 --- a/apps/spruce/src/pages/Version.tsx +++ b/apps/spruce/src/pages/Version.tsx @@ -16,8 +16,7 @@ import { VersionQuery, VersionQueryVariables } from "gql/generated/types"; import { VERSION } from "gql/queries"; import { useSpruceConfig } from "hooks"; import { PageDoesNotExist } from "pages/NotFound"; -import { shortenGithash, githubPRLinkify } from "utils/string"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { shortenGithash, githubPRLinkify, jiraLinkify } from "utils/string"; import { WarningBanner, ErrorBanner, IgnoredBanner } from "./version/Banners"; import VersionPageBreadcrumbs from "./version/Breadcrumbs"; import BuildVariantCard from "./version/BuildVariantCard"; diff --git a/apps/spruce/src/pages/commits/InactiveCommits/index.tsx b/apps/spruce/src/pages/commits/InactiveCommits/index.tsx index c21bc9639..f5543a2bf 100644 --- a/apps/spruce/src/pages/commits/InactiveCommits/index.tsx +++ b/apps/spruce/src/pages/commits/InactiveCommits/index.tsx @@ -13,7 +13,7 @@ import { size, zIndex, fontSize } from "constants/tokens"; import { useSpruceConfig, useDateFormat } from "hooks"; import { CommitRolledUpVersions } from "types/commits"; import { string } from "utils"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { jiraLinkify } from "utils/string"; import { commitChartHeight } from "../constants"; const { shortenGithash, trimStringFromMiddle } = string; diff --git a/apps/spruce/src/pages/image/DistrosTable/index.tsx b/apps/spruce/src/pages/image/DistrosTable/index.tsx index a18dc1e0e..8877b3946 100644 --- a/apps/spruce/src/pages/image/DistrosTable/index.tsx +++ b/apps/spruce/src/pages/image/DistrosTable/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef } from "react"; +import { useRef } from "react"; import { useQuery } from "@apollo/client"; import styled from "@emotion/styled"; import IconButton from "@leafygreen-ui/icon-button"; @@ -33,22 +33,19 @@ export const DistrosTable: React.FC = ({ imageId }) => { ImageDistrosQueryVariables >(IMAGE_DISTROS, { variables: { imageId }, - onError(err) { + onError: (err) => { dispatchToast.error( `There was an error loading image distros: ${err.message}`, ); }, }); - const distros = useMemo( - () => imageData?.image?.distros ?? [], - [imageData?.image?.distros], - ); + const distros = imageData?.image?.distros ?? []; const tableContainerRef = useRef(null); const table = useLeafyGreenTable({ columns, - data: distros ?? [], + data: distros, containerRef: tableContainerRef, defaultColumn: { enableColumnFilter: false, diff --git a/apps/spruce/src/pages/image/ImageEventLog/ImageEventLogTable.tsx b/apps/spruce/src/pages/image/ImageEventLog/ImageEventLogTable.tsx index bac4ceb8e..8709ad14a 100644 --- a/apps/spruce/src/pages/image/ImageEventLog/ImageEventLogTable.tsx +++ b/apps/spruce/src/pages/image/ImageEventLog/ImageEventLogTable.tsx @@ -9,7 +9,6 @@ import { getFilteredRowModel, getFacetedUniqueValues, } from "@leafygreen-ui/table"; -import { toSentenceCase } from "@evg-ui/lib/utils/string"; import { BaseTable } from "components/Table/BaseTable"; import { tableColumnOffset } from "constants/tokens"; import { @@ -48,12 +47,18 @@ const imageEventTypeTreeData = [ key: ImageEventType.Toolchain, }, { - title: "Operating System", + title: "OS", value: ImageEventType.OperatingSystem, key: ImageEventType.OperatingSystem, }, ]; +const eventTypeToLabel = { + [ImageEventType.Package]: "Package", + [ImageEventType.Toolchain]: "Toolchain", + [ImageEventType.OperatingSystem]: "OS", +}; + interface ImageEventLogTableProps { entries: ImageEventEntry[]; globalFilter: string; @@ -87,7 +92,7 @@ export const ImageEventLogTable: React.FC = ({ const emptyMessage = hasFilters ? "No data to display" - : "No changes detected within the scope. The scope can be expanded upon request from the runtime environments team."; + : "No changes detected within the scope. The scope can be expanded upon request to the Runtime Environments team."; return ( [] = [ accessorKey: "type", cell: ({ getValue }) => { const value = getValue() as ImageEventType; - return toSentenceCase(value); + return eventTypeToLabel[value]; }, enableColumnFilter: true, filterFn: filterFns.arrIncludesSome, diff --git a/apps/spruce/src/pages/image/OperatingSystemTable/OperatingSystemTable.test.tsx b/apps/spruce/src/pages/image/OperatingSystemTable/OperatingSystemTable.test.tsx new file mode 100644 index 000000000..a2e05eff8 --- /dev/null +++ b/apps/spruce/src/pages/image/OperatingSystemTable/OperatingSystemTable.test.tsx @@ -0,0 +1,245 @@ +import { MockedProvider } from "@apollo/client/testing"; +import { + renderWithRouterMatch as render, + screen, + waitFor, + within, + userEvent, +} from "@evg-ui/lib/test_utils"; +import { ApolloMock } from "@evg-ui/lib/types/gql"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables, +} from "gql/generated/types"; +import { IMAGE_OPERATING_SYSTEM } from "gql/queries"; +import { OperatingSystemTable } from "."; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe("operating system table", () => { + it("shows name field data", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(10); + }); + const expectedNames = ( + imageOperatingSystemMock.result?.data?.image?.operatingSystem.data || [] + ).map(({ name }) => name); + + const rows = screen.getAllByDataCy("os-table-row"); + expectedNames.forEach((expectedName, i) => { + expect(within(rows[i]).getAllByRole("cell")[0]).toHaveTextContent( + expectedName, + ); + }); + }); + + it("shows version field data", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(10); + }); + const expectedVersions = ( + imageOperatingSystemMock.result?.data?.image?.operatingSystem.data || [] + ).map(({ version }) => version); + + const rows = screen.getAllByDataCy("os-table-row"); + expectedVersions.forEach((expectedVersion, i) => { + expect(within(rows[i]).getAllByRole("cell")[1]).toHaveTextContent( + expectedVersion, + ); + }); + }); + + it("supports name filter", async () => { + const user = userEvent.setup(); + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(10); + }); + await user.click(screen.getByDataCy("os-name-filter")); + await user.type( + screen.getByPlaceholderText("Name regex"), + "^Kernel{enter}", + ); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(1); + }); + expect(screen.getByText("1 - 1 of 1 items")).toBeInTheDocument(); + }); + + it("supports pagination", async () => { + const user = userEvent.setup(); + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(10); + }); + await user.click(screen.getByTestId("lg-pagination-next-button")); + await waitFor(() => { + expect(screen.queryAllByDataCy("os-table-row")).toHaveLength(1); + }); + expect(screen.getByText("11 - 11 of 11 items")).toBeInTheDocument(); + }); +}); + +const imageOperatingSystemMock: ApolloMock< + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables +> = { + request: { + query: IMAGE_OPERATING_SYSTEM, + variables: { imageId: "ubuntu2204", opts: { page: 0, limit: 10 } }, + }, + result: { + data: { + image: { + __typename: "Image", + id: "ubuntu2204", + operatingSystem: { + __typename: "ImageOperatingSystemPayload", + data: [ + { + __typename: "OSInfo", + name: "NAME", + version: "Ubuntu", + }, + { + __typename: "OSInfo", + + name: "PRETTY_NAME", + version: "Ubuntu 22.04", + }, + { + __typename: "OSInfo", + name: "VERSION", + version: "22.04", + }, + { + __typename: "OSInfo", + name: "VERSION_ID", + version: "22.04", + }, + { + __typename: "OSInfo", + name: "ID", + version: "ubuntu", + }, + { + __typename: "OSInfo", + name: "ID_LIKE", + version: "ubuntu", + }, + { + __typename: "OSInfo", + name: "Kernel", + version: "123457.89.0", + }, + { + __typename: "OSInfo", + name: "LOGO", + version: "ubuntu-icon", + }, + { + __typename: "OSInfo", + name: "HOME_URL", + version: "home-url", + }, + { + __typename: "OSInfo", + name: "DOCUMENTATION_URL", + version: "documentation-url", + }, + ], + filteredCount: 11, + totalCount: 11, + }, + }, + }, + }, +}; + +const imageOperatingSystemNextPageMock: ApolloMock< + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables +> = { + request: { + query: IMAGE_OPERATING_SYSTEM, + variables: { imageId: "ubuntu2204", opts: { page: 1, limit: 10 } }, + }, + result: { + data: { + image: { + __typename: "Image", + id: "ubuntu2204", + operatingSystem: { + __typename: "ImageOperatingSystemPayload", + data: [ + { + __typename: "OSInfo", + name: "BUG_REPORT_URL", + version: "bug-report-url", + }, + ], + filteredCount: 11, + totalCount: 11, + }, + }, + }, + }, +}; + +const imageOperatingSystemNameFilterMock: ApolloMock< + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables +> = { + request: { + query: IMAGE_OPERATING_SYSTEM, + variables: { + imageId: "ubuntu2204", + opts: { page: 0, limit: 10, name: "^Kernel" }, + }, + }, + result: { + data: { + image: { + __typename: "Image", + id: "ubuntu2204", + operatingSystem: { + __typename: "ImageOperatingSystemPayload", + data: [ + { + __typename: "OSInfo", + name: "Kernel", + version: "123457.89.0", + }, + ], + filteredCount: 1, + totalCount: 11, + }, + }, + }, + }, +}; diff --git a/apps/spruce/src/pages/image/OperatingSystemTable/index.tsx b/apps/spruce/src/pages/image/OperatingSystemTable/index.tsx new file mode 100644 index 000000000..8702506c0 --- /dev/null +++ b/apps/spruce/src/pages/image/OperatingSystemTable/index.tsx @@ -0,0 +1,108 @@ +import { useRef, useState } from "react"; +import { useQuery } from "@apollo/client"; +import { + useLeafyGreenTable, + LGColumnDef, + ColumnFiltersState, + PaginationState, +} from "@leafygreen-ui/table"; +import { BaseTable } from "components/Table/BaseTable"; +import { DEFAULT_PAGE_SIZE } from "constants/index"; +import { useToastContext } from "context/toast"; +import { + OsInfo, + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables, +} from "gql/generated/types"; +import { IMAGE_OPERATING_SYSTEM } from "gql/queries"; + +type OperatingSystemTableProps = { + imageId: string; +}; + +export const OperatingSystemTable: React.FC = ({ + imageId, +}) => { + const dispatchToast = useToastContext(); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [columnFilters, setColumnFilters] = useState([]); + const { data: osData, loading } = useQuery< + ImageOperatingSystemQuery, + ImageOperatingSystemQueryVariables + >(IMAGE_OPERATING_SYSTEM, { + variables: { + imageId, + opts: { + page: pagination.pageIndex, + limit: pagination.pageSize, + name: columnFilters.find((filter) => filter.id === "name") + ?.value as string, + }, + }, + onError: (err) => { + dispatchToast.error( + `There was an error loading operating system information: ${err.message}`, + ); + }, + }); + + const operatingSystemInfo = osData?.image?.operatingSystem.data ?? []; + + const numTotalItems = + osData?.image?.operatingSystem.filteredCount ?? + osData?.image?.operatingSystem.totalCount; + + const tableContainerRef = useRef(null); + const table = useLeafyGreenTable({ + columns, + data: operatingSystemInfo, + containerRef: tableContainerRef, + defaultColumn: { + enableColumnFilter: false, + }, + manualPagination: true, + manualFiltering: true, + rowCount: numTotalItems, + state: { + pagination, + columnFilters, + }, + onColumnFiltersChange: setColumnFilters, + onPaginationChange: setPagination, + }); + + return ( + + ); +}; + +const columns: LGColumnDef[] = [ + { + header: "Name", + accessorKey: "name", + enableColumnFilter: true, + meta: { + search: { + "data-cy": "os-name-filter", + placeholder: "Name regex", + }, + }, + }, + { + header: "Version", + accessorKey: "version", + cell: ({ getValue }) => (getValue() as string).replace(/"/g, ""), + }, +]; diff --git a/apps/spruce/src/pages/image/PackagesTable/index.tsx b/apps/spruce/src/pages/image/PackagesTable/index.tsx index ab0647e5c..2b3ca159b 100644 --- a/apps/spruce/src/pages/image/PackagesTable/index.tsx +++ b/apps/spruce/src/pages/image/PackagesTable/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { useQuery } from "@apollo/client"; import { useLeafyGreenTable, @@ -40,27 +40,18 @@ export const PackagesTable: React.FC = ({ imageId }) => { ?.value as string, }, }, - onError(err) { + onError: (err) => { dispatchToast.error( `There was an error loading image packages: ${err.message}`, ); }, }); - const packages = useMemo( - () => packagesData?.image?.packages.data ?? [], - [packagesData?.image?.packages.data], - ); + const packages = packagesData?.image?.packages.data ?? []; - const numPackages = useMemo( - () => - packagesData?.image?.packages.filteredCount ?? - packagesData?.image?.packages.totalCount, - [ - packagesData?.image?.packages.filteredCount, - packagesData?.image?.packages.totalCount, - ], - ); + const numPackages = + packagesData?.image?.packages.filteredCount ?? + packagesData?.image?.packages.totalCount; const tableContainerRef = useRef(null); const table = useLeafyGreenTable({ @@ -99,7 +90,6 @@ const columns: LGColumnDef[] = [ header: "Name", accessorKey: "name", enableColumnFilter: true, - filterFn: "includesString", meta: { search: { "data-cy": "package-name-filter", diff --git a/apps/spruce/src/pages/image/Tabs.tsx b/apps/spruce/src/pages/image/Tabs.tsx index 735f4cbbc..3cb9c2209 100644 --- a/apps/spruce/src/pages/image/Tabs.tsx +++ b/apps/spruce/src/pages/image/Tabs.tsx @@ -25,11 +25,11 @@ export const ImageTabs: React.FC = ({ path="*" /> } + element={} path={ImageTabRoutes.BuildInformation} /> } + element={} path={ImageTabRoutes.EventLog} /> diff --git a/apps/spruce/src/pages/image/ToolchainsTable/index.tsx b/apps/spruce/src/pages/image/ToolchainsTable/index.tsx index bc01f1400..e0670323c 100644 --- a/apps/spruce/src/pages/image/ToolchainsTable/index.tsx +++ b/apps/spruce/src/pages/image/ToolchainsTable/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { useQuery } from "@apollo/client"; import { useLeafyGreenTable, @@ -44,17 +44,14 @@ export const ToolchainsTable: React.FC = ({ ?.value as string) ?? undefined, }, }, - onError(err) { + onError: (err) => { dispatchToast.error( `There was an error loading image toolchains: ${err.message}`, ); }, }); - const toolchains = useMemo( - () => imageData?.image?.toolchains?.data ?? [], - [imageData?.image?.toolchains?.data], - ); + const toolchains = imageData?.image?.toolchains?.data ?? []; const numTotalItems = imageData?.image?.toolchains?.filteredCount ?? diff --git a/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx b/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx index d4cb1d0de..07909818b 100644 --- a/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx +++ b/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx @@ -1,6 +1,7 @@ import { SpruceFormContainer } from "components/SpruceForm"; import { DistrosTable } from "pages/image/DistrosTable"; import { GeneralTable } from "pages/image/GeneralTable"; +import { OperatingSystemTable } from "pages/image/OperatingSystemTable"; import { PackagesTable } from "pages/image/PackagesTable"; import { ToolchainsTable } from "pages/image/ToolchainsTable"; @@ -12,12 +13,15 @@ export const BuildInformationTab: React.FC = ({ imageId, }) => ( <> - + + + + diff --git a/apps/spruce/src/pages/image/tabs/EventLogTab/EventLogTab.test.tsx b/apps/spruce/src/pages/image/tabs/EventLogTab/EventLogTab.test.tsx index e08c1865e..dd63e8d55 100644 --- a/apps/spruce/src/pages/image/tabs/EventLogTab/EventLogTab.test.tsx +++ b/apps/spruce/src/pages/image/tabs/EventLogTab/EventLogTab.test.tsx @@ -106,7 +106,7 @@ describe("image event log page", async () => { }); const expectedEmptyMessage = - "No changes detected within the scope. The scope can be expanded upon request from the runtime environments team."; + "No changes detected within the scope. The scope can be expanded upon request to the Runtime Environments team."; const cards = screen.getAllByDataCy("image-event-log-card"); expect( diff --git a/apps/spruce/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx b/apps/spruce/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx index 9d6f97790..c02f65bcc 100644 --- a/apps/spruce/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx +++ b/apps/spruce/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx @@ -35,7 +35,7 @@ import { NotificationMethods, notificationMethodToCopy, } from "types/subscription"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { jiraLinkify } from "utils/string"; import { ClearSubscriptions } from "./ClearSubscriptions"; import { useSubscriptionData } from "./useSubscriptionData"; import { getResourceRoute } from "./utils"; diff --git a/apps/spruce/src/pages/waterfall/VersionLabel.tsx b/apps/spruce/src/pages/waterfall/VersionLabel.tsx index 48527e2f6..60fb4eaca 100644 --- a/apps/spruce/src/pages/waterfall/VersionLabel.tsx +++ b/apps/spruce/src/pages/waterfall/VersionLabel.tsx @@ -7,8 +7,7 @@ import { StyledRouterLink } from "components/styles"; import { getVersionRoute, getTriggerRoute } from "constants/routes"; import { WaterfallQuery } from "gql/generated/types"; import { useSpruceConfig, useDateFormat } from "hooks"; -import { shortenGithash } from "utils/string"; -import { jiraLinkify } from "utils/string/jiraLinkify"; +import { shortenGithash, jiraLinkify } from "utils/string"; type VersionFields = NonNullable< Unpacked["version"] diff --git a/apps/spruce/src/utils/string/Linkify.test.tsx b/apps/spruce/src/utils/string/Linkify.test.tsx new file mode 100644 index 000000000..9c4d0bb23 --- /dev/null +++ b/apps/spruce/src/utils/string/Linkify.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from "@evg-ui/lib/test_utils"; +import { githubPRLinkify, jiraLinkify } from "./Linkify"; + +describe("githubPRLinkify", () => { + it("linkifies a GitHub pull request link", () => { + render( + + {githubPRLinkify("https://github.com/evergreen-ci/ui/pull/413")} + , + ); + expect(screen.getByRole("link")).toBeInTheDocument(); + }); + + it("does not linkify if not a GitHub pull request", () => { + render({githubPRLinkify("https://evergreen.mongodb.com")}); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + }); +}); + +describe("jiraLinkify", () => { + it("linkifies a JIRA ticket", () => { + render({jiraLinkify("DEVPROD-1234", "jira")}); + expect(screen.getByRole("link")).toBeInTheDocument(); + }); + + it("does not linkify if not a JIRA ticket", () => { + render({jiraLinkify("devprod-1234", "jira")}); + expect(screen.queryByRole("link")).not.toBeInTheDocument(); + }); +}); diff --git a/apps/spruce/src/utils/string/jiraLinkify.tsx b/apps/spruce/src/utils/string/Linkify.tsx similarity index 61% rename from apps/spruce/src/utils/string/jiraLinkify.tsx rename to apps/spruce/src/utils/string/Linkify.tsx index 037f27525..cfeb48c81 100644 --- a/apps/spruce/src/utils/string/jiraLinkify.tsx +++ b/apps/spruce/src/utils/string/Linkify.tsx @@ -2,7 +2,18 @@ import reactStringReplace from "react-string-replace"; import { StyledLink } from "components/styles"; import { getJiraTicketUrl } from "constants/externalResources"; -export const jiraLinkify = ( +const githubPRLinkify = (unlinkified: string | React.ReactNode[]) => + reactStringReplace( + unlinkified, + /(https:\/\/github.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/pull\/\d+)/g, + (match, i) => ( + + {match} + + ), + ); + +const jiraLinkify = ( unlinkified: string | React.ReactNode[], jiraHost: string, // @ts-expect-error: FIXME. This comment was added by an automated script. @@ -18,3 +29,5 @@ export const jiraLinkify = ( {match} )); + +export { githubPRLinkify, jiraLinkify }; diff --git a/apps/spruce/src/utils/string/githubPRLinkify.tsx b/apps/spruce/src/utils/string/githubPRLinkify.tsx deleted file mode 100644 index 4cee3f8a4..000000000 --- a/apps/spruce/src/utils/string/githubPRLinkify.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import reactStringReplace from "react-string-replace"; -import { StyledLink } from "components/styles"; - -export const githubPRLinkify = (unlinkified: string | React.ReactNode[]) => - reactStringReplace( - unlinkified, - /(https:\/\/github.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/pull\/\d+)/g, - (match, i) => ( - - {match} - - ), - ); diff --git a/apps/spruce/src/utils/string/index.ts b/apps/spruce/src/utils/string/index.ts index 3020ebc51..de20b3b1d 100644 --- a/apps/spruce/src/utils/string/index.ts +++ b/apps/spruce/src/utils/string/index.ts @@ -2,7 +2,7 @@ import { format, toZonedTime } from "date-fns-tz"; import get from "lodash/get"; import { TimeFormat } from "constants/fieldMaps"; -export { githubPRLinkify } from "./githubPRLinkify"; +export { githubPRLinkify, jiraLinkify } from "./Linkify"; /** * `msToDuration` converts a number of milliseconds to a string representing the duration