diff --git a/src/features/metadata/components/BuildDropdown.tsx b/src/features/metadata/components/BuildDropdown.tsx index b8d8f485..2603d6aa 100644 --- a/src/features/metadata/components/BuildDropdown.tsx +++ b/src/features/metadata/components/BuildDropdown.tsx @@ -6,22 +6,25 @@ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import IconButton from "@mui/material/IconButton"; import { useAppDispatch } from "../../../hooks"; import { currentBuildIdChanged } from ".."; +import { Build } from "../../../common/models"; +import { buildDatetimeStatus } from "../../../utils/helpers/buildMapper"; interface IBuildProps { /** * @param builds list of builds * @param currentBuildId id of the current build - * @param onChangeStatus update the build status + * @param selectedBuildId id of the build selected from the dropdown */ - builds: { - id: number; - name: string; - status: string; - }[]; + builds: Build[]; + currentBuildId: number; selectedBuildId: number; } -export const Build = ({ builds, selectedBuildId }: IBuildProps) => { +export const BuildDropdown = ({ + builds, + currentBuildId, + selectedBuildId +}: IBuildProps) => { const dispatch = useAppDispatch(); const { palette } = useTheme(); const [open, setOpen] = useState(false); @@ -92,7 +95,7 @@ export const Build = ({ builds, selectedBuildId }: IBuildProps) => { {builds ? builds.map(build => ( - {build.name} + {buildDatetimeStatus(build, currentBuildId)} )) : null} diff --git a/src/features/metadata/components/EnvBuildStatus.tsx b/src/features/metadata/components/EnvBuildStatus.tsx new file mode 100644 index 00000000..e05f2943 --- /dev/null +++ b/src/features/metadata/components/EnvBuildStatus.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { CircularProgress, Typography } from "@mui/material"; +import Link from "@mui/material/Link"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import { Artifact, Build } from "../../../common/models"; +import { PrefContext } from "../../../preferences"; +import { StyledMetadataItem } from "../../../styles/StyledMetadataItem"; +import artifactList from "../../../utils/helpers/artifact"; +import { artifactBaseUrl } from "../../../utils/helpers/parseArtifactList"; +import { buildStatus } from "../../../utils/helpers/buildMapper"; + +const LogLink = ({ logArtifact }: { logArtifact: Artifact }) => { + const pref = React.useContext(PrefContext); + const url = new URL( + logArtifact.route, + artifactBaseUrl(pref.apiUrl, window.location.origin) + ); + return ( + + + Log + + ); +}; + +interface IEnvBuildStatusProps { + build: Build; +} + +export const EnvBuildStatus = ({ build }: IEnvBuildStatusProps) => { + const logArtifact: Artifact | void = artifactList(build.id, ["LOGS"])[0]; + + return ( + + Status: {""} + + {buildStatus(build)} + {build.status_info && ` (${build.status_info})`} + {build.status === "BUILDING" || build.status === "QUEUED" ? ( + + ) : ( + // If the selected build is a failed build, render the link to the build log. + build.status === "FAILED" && + logArtifact && ( + <> + . + + ) + )} + + + ); +}; diff --git a/src/features/metadata/components/EnvBuilds.tsx b/src/features/metadata/components/EnvBuilds.tsx index ce06497e..780f08f7 100644 --- a/src/features/metadata/components/EnvBuilds.tsx +++ b/src/features/metadata/components/EnvBuilds.tsx @@ -1,15 +1,9 @@ import React from "react"; -import { CircularProgress, Typography } from "@mui/material"; +import { CircularProgress } from "@mui/material"; import { StyledMetadataItem } from "../../../styles/StyledMetadataItem"; import { Build as IBuild } from "../../../common/models"; -import { Build } from "../../../features/metadata/components"; -import { buildMapper } from "../../../utils/helpers/buildMapper"; -import Link from "@mui/material/Link"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import { artifactBaseUrl } from "../../../utils/helpers"; -import { PrefContext } from "../../../preferences"; -import artifactList from "../../../utils/helpers/artifact"; -import { Artifact } from "../../../common/models"; +import { BuildDropdown } from "../../../features/metadata/components"; +import { EnvBuildStatus } from "./EnvBuildStatus"; export interface IData { currentBuildId: number; @@ -24,37 +18,7 @@ export const EnvBuilds = ({ builds, mode }: IData) => { - const envBuilds = builds.length ? buildMapper(builds, currentBuildId) : []; - const currentBuild = envBuilds.find(build => build.id === selectedBuildId); - - // If the selected build is a failed build, we will render the link to the build log. - let logLink; - const showLogLink = currentBuild?.status === "Failed"; - const logArtifact: Artifact | never = artifactList(currentBuild?.id, [ - "LOGS" - ])[0]; - if (showLogLink && logArtifact) { - const pref = React.useContext(PrefContext); - const url = new URL( - logArtifact.route, - artifactBaseUrl(pref.apiUrl, window.location.origin) - ); - logLink = ( - - - Log - - ); - } - + const selectedBuild = builds.find(build => build.id === selectedBuildId); return ( <> {mode === "edit" ? "Change active environment version:" : "Builds:"} - {currentBuild ? ( + {selectedBuild ? ( <> - - - Status: {""} - - {currentBuild.status} - {currentBuild.status_info && ` (${currentBuild.status_info})`} - {((currentBuild.status === "Building" || - currentBuild.status === "Queued") && ( - - )) || - // If the selected build is a failed build, render the link to the build log. - (showLogLink && <>. {logLink})} - - + + ) : ( { - const BUILD_STATUS = ["BUILDING"]; - return BUILD_STATUS.includes(status); -}; - -const isQueued = (status: string) => { - const BUILD_STATUS = ["QUEUED"]; - return BUILD_STATUS.includes(status); -}; - -const isCompleted = (status: string, duration: number) => { - if (status === "COMPLETED") { - if (duration > 0) { - return `Completed in ${duration} min`; - } - return "Completed"; - } - return STATUS_OPTIONS[status]; -}; - const dateToTimezone = (date: string) => { if (!date) { return ""; @@ -40,51 +20,41 @@ const dateToTimezone = (date: string) => { }); }; -export const buildMapper = (data: Build[], currentBuildId: number) => { - return data.map( - ({ id, status, status_info, ended_on, scheduled_on }: Build) => { - let duration = 0; +export const buildDatetimeStatus = ( + { id, status, ended_on, scheduled_on }: Build, + currentBuildId: number +): string => { + if (id === currentBuildId) { + return `${dateToTimezone(ended_on ?? scheduled_on)} - Active`; + } else if (status === "BUILDING") { + return `${dateToTimezone(scheduled_on)} - Building`; + } else if (status === "QUEUED") { + return `${dateToTimezone(scheduled_on)} - Queued`; + } else { + return `${dateToTimezone(ended_on ?? scheduled_on)} - ${ + STATUS_OPTIONS[status] + }`; + } +}; + +export const buildStatus = ({ + status, + ended_on, + scheduled_on +}: Build): string => { + switch (status) { + case "COMPLETED": if (ended_on && scheduled_on) { const startTime = new Date(scheduled_on); const endTime = new Date(ended_on); - duration = (endTime.valueOf() - startTime.valueOf()) / 60000; + let duration = (endTime.valueOf() - startTime.valueOf()) / 60000; duration = Math.round(duration); + if (duration > 0) { + return `Completed in ${duration} min`; + } } - if (id === currentBuildId) { - return { - id, - name: `${dateToTimezone(ended_on ?? scheduled_on)} - Active`, - status: isCompleted(status, duration), - status_info - }; - } - - if (isBuilding(status)) { - return { - id, - name: `${dateToTimezone(scheduled_on)} - Building`, - status: "Building", - status_info - }; - } - - if (isQueued(status)) { - return { - id, - name: `${dateToTimezone(scheduled_on)} - Queued`, - status: "Building", - status_info - }; - } - - return { - id, - name: `${dateToTimezone(ended_on ?? scheduled_on)} - ${ - STATUS_OPTIONS[status] - }`, - status: isCompleted(status, duration), - status_info - }; - } - ); + return "Completed"; + default: + return STATUS_OPTIONS[status]; + } }; diff --git a/test/helpers/BuildMapper.test.tsx b/test/helpers/BuildMapper.test.tsx index 336b0a98..83bddcda 100644 --- a/test/helpers/BuildMapper.test.tsx +++ b/test/helpers/BuildMapper.test.tsx @@ -1,54 +1,46 @@ -import { buildMapper } from "../../src/utils/helpers"; +import { buildDatetimeStatus } from "../../src/utils/helpers"; import { BUILD } from "../testutils"; -const generateBuild = (status: string) => [ - { - ...BUILD, - status - } -]; -describe("buildMapper", () => { +const generateBuild = (status: string) => ({ + ...BUILD, + status +}); + +describe("buildDatetimeStatus", () => { it("should return an active build", () => { - const builds = generateBuild("COMPLETED"); - const [mappedBuild] = buildMapper(builds, 1); - expect(mappedBuild.name).toContain("Active"); + const build = generateBuild("COMPLETED"); + expect(buildDatetimeStatus(build, 1)).toMatch(/Active$/); }); it("should return build", () => { - const builds = generateBuild("BUILDING"); - const [mappedBuild] = buildMapper(builds, 2); - expect(mappedBuild.name).toContain("Building"); + const build = generateBuild("BUILDING"); + expect(buildDatetimeStatus(build, 2)).toMatch(/Building$/); }); it("should return queued build", () => { - const builds = generateBuild("QUEUED"); - const [mappedBuild] = buildMapper(builds, 2); - expect(mappedBuild.name).toContain("Queued"); + const build = generateBuild("QUEUED"); + expect(buildDatetimeStatus(build, 2)).toMatch(/Queued$/); }); it("should return completed build", () => { - const builds = generateBuild("COMPLETED"); - const [mappedBuild] = buildMapper(builds, 2); - expect(mappedBuild.name).toContain("Available"); + const build = generateBuild("COMPLETED"); + expect(buildDatetimeStatus(build, 2)).toMatch(/Available$/); }); it("should return failed build", () => { - const builds = generateBuild("FAILED"); - const [mappedBuild] = buildMapper(builds, 2); - expect(mappedBuild.name).toContain("Failed"); + const build = generateBuild("FAILED"); + expect(buildDatetimeStatus(build, 2)).toMatch(/Failed$/); }); it("should use the scheduled_on date if the ended_on prop is null", () => { - const [mappedBuild] = buildMapper( - [ - { - ...BUILD, - status: "FAILED", - ended_on: null - } - ], + const datetimeStatus = buildDatetimeStatus( + { + ...BUILD, + status: "FAILED", + ended_on: null + }, 2 ); - expect(mappedBuild.name).toContain("November 8th, 2022"); + expect(datetimeStatus).toContain("November 8th, 2022"); }); }); diff --git a/test/metadata/BuildDropdown.test.tsx b/test/metadata/BuildDropdown.test.tsx index d8f548fa..709b192c 100644 --- a/test/metadata/BuildDropdown.test.tsx +++ b/test/metadata/BuildDropdown.test.tsx @@ -3,33 +3,23 @@ import { Provider } from "react-redux"; import { render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { store } from "../../src/store"; -import { Build } from "../../src/features/metadata/components/BuildDropdown"; +import { BuildDropdown } from "../../src/features/metadata/components/BuildDropdown"; +import { mockBuilds } from "../../src/features/metadata/mocks/mockBuilds"; import { mockTheme } from "../testutils"; - -const builds = [ - { - id: 0, - name: "August 5th, 2022 - 4:04 - Available", - status: "Building" - }, - { - id: 1, - name: "August 5th, 2022 - 3:57 - Available", - status: "Building" - } -]; +import { buildDatetimeStatus } from "../../src/utils/helpers/buildMapper"; describe("", () => { let component: RenderResult; + const currentBuildId = 1; beforeEach(() => { component = render( mockTheme( - ) @@ -37,18 +27,25 @@ describe("", () => { }); it("should render component", () => { - expect(component.container).toHaveTextContent(builds[0].name); + expect(component.container).toHaveTextContent( + buildDatetimeStatus(mockBuilds[0], currentBuildId) + ); }); it("should display builds in the dropdown", async () => { + // This is what the dropdown should display for the build + const dropdownOptionName = buildDatetimeStatus( + mockBuilds[1], + currentBuildId + ); const [dropdownButton] = screen.getAllByRole("button"); userEvent.click(dropdownButton); const dropdownItem = await screen.findByRole("option", { - name: builds[1].name + name: dropdownOptionName }); userEvent.click(dropdownItem); - const buildName = await screen.findByText(builds[1].name); + const buildName = await screen.findByText(dropdownOptionName); expect(buildName).toBeInTheDocument(); }); }); diff --git a/test/metadata/EnvBuildStatus.test.tsx b/test/metadata/EnvBuildStatus.test.tsx new file mode 100644 index 00000000..573f80c5 --- /dev/null +++ b/test/metadata/EnvBuildStatus.test.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; +import { EnvBuildStatus } from "../../src/features/metadata/components"; +import { BUILD } from "../testutils"; +import { store } from "../../src/store"; + +describe("", () => { + it("should render link to log for failed build", () => { + const { getByTestId, getByRole } = render( + + + + ); + expect(getByRole("link", { name: "Log" })).toBeInTheDocument(); + expect(getByTestId("build-status")).toHaveTextContent( + /^Status: Failed\. Log$/ + ); + }); + + it("should not render link to log for normal build", () => { + const { getByTestId, queryByRole } = render( + + + + ); + expect(queryByRole("link", { name: "Log" })).not.toBeInTheDocument(); + expect(getByTestId("build-status")).toHaveTextContent( + /^Status: Completed$/ + ); + }); +});