diff --git a/apps/spruce/cypress/integration/image/build_information.ts b/apps/spruce/cypress/integration/image/build_information.ts new file mode 100644 index 000000000..a316fb2c6 --- /dev/null +++ b/apps/spruce/cypress/integration/image/build_information.ts @@ -0,0 +1,9 @@ +describe("build information", () => { + describe("distros", () => { + it("should show the corresponding distros", () => { + cy.visit("/image/ubuntu1804"); + cy.dataCy("distro-table-row").should("have.length", 1); + cy.contains("ubuntu1804-workstation"); + }); + }); +}); diff --git a/apps/spruce/cypress/integration/preferences/notifications.ts b/apps/spruce/cypress/integration/preferences/notifications.ts index fea618ea9..1f49e0323 100644 --- a/apps/spruce/cypress/integration/preferences/notifications.ts +++ b/apps/spruce/cypress/integration/preferences/notifications.ts @@ -31,18 +31,18 @@ describe("user subscriptions table", () => { }); it("shows all of a user's subscriptions and expands with details", () => { - cy.dataCy("leafygreen-table-row").should("have.length", 3); + cy.dataCy("subscription-row").should("have.length", 3); cy.dataCy("regex-selectors").should("not.be.visible"); cy.dataCy("trigger-data").should("not.be.visible"); - cy.dataCy("leafygreen-table-row") + cy.dataCy("subscription-row") .eq(0) .within(() => { cy.get("button").first().click(); }); cy.dataCy("regex-selectors").should("be.visible"); cy.dataCy("trigger-data").should("not.be.visible"); - cy.dataCy("leafygreen-table-row") + cy.dataCy("subscription-row") .eq(2) .within(() => { cy.get("button").first().click(); @@ -52,7 +52,7 @@ describe("user subscriptions table", () => { }); it("Shows the selected count in the 'Delete' button", () => { - cy.dataCy("leafygreen-table-row") + cy.dataCy("subscription-row") .eq(0) .within(() => { cy.get("input[type=checkbox]").check({ force: true }); @@ -77,14 +77,14 @@ describe("user subscriptions table", () => { describe("Deleting subscriptions", () => { it("Deletes a single subscription", () => { - cy.dataCy("leafygreen-table-row") + cy.dataCy("subscription-row") .eq(0) .within(() => { cy.get("input[type=checkbox]").check({ force: true }); }); cy.dataCy("delete-some-button").click(); cy.validateToast("success", "Deleted 1 subscription."); - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("subscription-row").should("have.length", 2); }); }); }); diff --git a/apps/spruce/cypress/integration/version/task_duration.ts b/apps/spruce/cypress/integration/version/task_duration.ts index 38705bf28..50c06a583 100644 --- a/apps/spruce/cypress/integration/version/task_duration.ts +++ b/apps/spruce/cypress/integration/version/task_duration.ts @@ -10,7 +10,7 @@ describe("Task Duration Tab", () => { cy.dataCy("task-name-filter-popover-input-filter").type( `${filterText}{enter}`, ); - cy.dataCy("leafygreen-table-row").should("have.length", 1); + cy.dataCy("task-duration-table-row").should("have.length", 1); cy.location("search").should( "include", `duration=DESC&page=0&taskName=${filterText}`, @@ -28,7 +28,7 @@ describe("Task Duration Tab", () => { cy.dataCy("tree-select-options").within(() => cy.contains("Running").click({ force: true }), ); - cy.dataCy("leafygreen-table-row").should("have.length", 3); + cy.dataCy("task-duration-table-row").should("have.length", 3); cy.location("search").should( "include", "duration=DESC&page=0&statuses=running-umbrella%2Cstarted%2Cdispatched", @@ -48,7 +48,7 @@ describe("Task Duration Tab", () => { cy.dataCy("build-variant-filter-popover-input-filter").type( `${filterText}{enter}`, ); - cy.dataCy("leafygreen-table-row").should("have.length", 2); + cy.dataCy("task-duration-table-row").should("have.length", 2); cy.location("search").should( "include", `duration=DESC&page=0&variant=${filterText}`, @@ -66,14 +66,18 @@ describe("Task Duration Tab", () => { cy.location("search").should("include", "duration=DESC"); const longestTask = "test-thirdparty"; cy.contains(longestTask).should("be.visible"); - cy.dataCy("leafygreen-table-row").first().should("contain", longestTask); + cy.dataCy("task-duration-table-row") + .first() + .should("contain", longestTask); cy.get(durationSortControl).click(); cy.location("search").should("not.include", "duration"); cy.get(durationSortControl).click(); cy.location("search").should("include", "duration=ASC"); const shortestTask = "test-auth"; cy.contains(shortestTask).should("be.visible"); - cy.dataCy("leafygreen-table-row").first().should("contain", shortestTask); + cy.dataCy("task-duration-table-row") + .first() + .should("contain", shortestTask); }); it("clearing all filters resets to the default sort", () => { diff --git a/apps/spruce/src/components/Table/BaseTable.tsx b/apps/spruce/src/components/Table/BaseTable.tsx index 24c01de4a..01c6214a8 100644 --- a/apps/spruce/src/components/Table/BaseTable.tsx +++ b/apps/spruce/src/components/Table/BaseTable.tsx @@ -156,6 +156,7 @@ export const BaseTable = forwardRef( key={row.id} virtualRow={vr} isSelected={selectedRowIndexes.includes(row.index)} + dataCyRow={dataCyRow} /> ); }) @@ -166,6 +167,7 @@ export const BaseTable = forwardRef( // @ts-expect-error: FIXME. This comment was added by an automated script. virtualRow={null} isSelected={selectedRowIndexes.includes(row.index)} + dataCyRow={dataCyRow} /> ))} @@ -183,17 +185,19 @@ export const BaseTable = forwardRef( const cellPaddingStyle = { paddingBottom: size.xxs, paddingTop: size.xxs }; const RenderableRow = ({ + dataCyRow = "leafygreen-table-row", isSelected = false, row, virtualRow, }: { + dataCyRow?: string; row: LeafyGreenTableRow; virtualRow: VirtualItem; isSelected?: boolean; }) => ( div { max-height: unset; diff --git a/apps/spruce/src/constants/externalResources.ts b/apps/spruce/src/constants/externalResources.ts index 2775af93c..f44a238f0 100644 --- a/apps/spruce/src/constants/externalResources.ts +++ b/apps/spruce/src/constants/externalResources.ts @@ -14,6 +14,9 @@ export const wikiUrl = `${wikiBaseUrl}/Home`; const projectSettingsDocumentationUrl = `${wikiBaseUrl}/Project-Configuration`; const hostsDocumentationUrl = `${wikiBaseUrl}/Hosts`; +export const amazonEC2InstanceTypeDocumentationUrl = + "https://aws.amazon.com/ec2/instance-types/"; + export const hostUptimeDocumentationUrl = `${hostsDocumentationUrl}/Spawn-Hosts#unexpirable-host-sleep-schedules`; export const projectDistroSettingsDocumentationUrl = `${projectSettingsDocumentationUrl}/Project-and-Distro-Settings`; diff --git a/apps/spruce/src/constants/hosts.ts b/apps/spruce/src/constants/hosts.ts index 276be36e9..37e7be75f 100644 --- a/apps/spruce/src/constants/hosts.ts +++ b/apps/spruce/src/constants/hosts.ts @@ -51,3 +51,5 @@ export const hostStatuses: Status[] = [ ]; export const MCI_USER = "mci"; + +export const defaultEC2Region = "us-east-1"; diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index b203658b0..7be62460b 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -6272,6 +6272,28 @@ export type HostsQuery = { }; }; +export type ImageDistrosQueryVariables = Exact<{ + imageId: Scalars["String"]["input"]; +}>; + +export type ImageDistrosQuery = { + __typename?: "Query"; + image?: { + __typename?: "Image"; + id: string; + distros: Array<{ + __typename?: "Distro"; + name: string; + provider: Provider; + providerSettingsList: Array; + hostAllocatorSettings: { + __typename?: "HostAllocatorSettings"; + maximumHosts: number; + }; + }>; + } | null; +}; + export type ImagesQueryVariables = Exact<{ [key: string]: never }>; export type ImagesQuery = { __typename?: "Query"; images: Array }; diff --git a/apps/spruce/src/gql/queries/image-distros.graphql b/apps/spruce/src/gql/queries/image-distros.graphql new file mode 100644 index 000000000..2e53f8326 --- /dev/null +++ b/apps/spruce/src/gql/queries/image-distros.graphql @@ -0,0 +1,13 @@ +query ImageDistros($imageId: String!) { + image(imageId: $imageId) { + distros { + hostAllocatorSettings { + maximumHosts + } + name + provider + providerSettingsList + } + id + } +} diff --git a/apps/spruce/src/gql/queries/index.ts b/apps/spruce/src/gql/queries/index.ts index e52c50d3a..1e6e451b8 100644 --- a/apps/spruce/src/gql/queries/index.ts +++ b/apps/spruce/src/gql/queries/index.ts @@ -24,6 +24,7 @@ import HAS_VERSION from "./has-version.graphql"; import HOST_EVENTS from "./host-events.graphql"; import HOST from "./host.graphql"; import HOSTS from "./hosts.graphql"; +import IMAGE_DISTROS from "./image-distros.graphql"; import IMAGES from "./images.graphql"; import INSTANCE_TYPES from "./instance-types.graphql"; import IS_PATCH_CONFIGURED from "./is-patch-configured.graphql"; @@ -110,6 +111,7 @@ export { HOST_EVENTS, HOST, HOSTS, + IMAGE_DISTROS, IMAGES, INSTANCE_TYPES, IS_PATCH_CONFIGURED, diff --git a/apps/spruce/src/pages/image/DistrosTable/DistrosTable.test.tsx b/apps/spruce/src/pages/image/DistrosTable/DistrosTable.test.tsx new file mode 100644 index 000000000..e11cba2e6 --- /dev/null +++ b/apps/spruce/src/pages/image/DistrosTable/DistrosTable.test.tsx @@ -0,0 +1,172 @@ +import { MockedProvider } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + ImageDistrosQuery, + ImageDistrosQueryVariables, + Provider, +} from "gql/generated/types"; +import { IMAGE_DISTROS } from "gql/queries"; +import { + renderWithRouterMatch as render, + screen, + waitFor, + within, +} from "test_utils"; +import { ApolloMock } from "types/gql"; +import { DistrosTable } from "."; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe("distros table", () => { + it("shows correct links to distro pages", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getAllByDataCy("distro-table-row")).toHaveLength(3); + }); + + const ubuntuSmall = screen.getAllByDataCy("distro-table-row")[0]; + expect(within(ubuntuSmall).getAllByRole("link")[0]).toHaveAttribute( + "href", + "/distro/ubuntu2204-small/settings/general", + ); + const ubuntuLarge = screen.getAllByDataCy("distro-table-row")[1]; + expect(within(ubuntuLarge).getAllByRole("link")[0]).toHaveAttribute( + "href", + "/distro/ubuntu2204-large/settings/general", + ); + const ubuntuStatic = screen.getAllByDataCy("distro-table-row")[2]; + expect(within(ubuntuStatic).getAllByRole("link")[0]).toHaveAttribute( + "href", + "/distro/ubuntu2204-static/settings/general", + ); + }); + + it("shows correct instance types", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getAllByDataCy("distro-table-row")).toHaveLength(3); + }); + + const ubuntuSmall = screen.getAllByDataCy("distro-table-row")[0]; + expect(within(ubuntuSmall).getAllByRole("cell")[1]).toHaveTextContent( + "m4.xlarge", + ); + const ubuntuLarge = screen.getAllByDataCy("distro-table-row")[1]; + expect(within(ubuntuLarge).getAllByRole("cell")[1]).toHaveTextContent( + "m6i.2xlarge", + ); + const ubuntuStatic = screen.getAllByDataCy("distro-table-row")[2]; + expect(within(ubuntuStatic).getAllByRole("cell")[1]).toHaveTextContent( + "N/A", + ); + }); + + it("shows correct max host counts", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getAllByDataCy("distro-table-row")).toHaveLength(3); + }); + + const ubuntuSmall = screen.getAllByDataCy("distro-table-row")[0]; + expect(within(ubuntuSmall).getAllByRole("cell")[2]).toHaveTextContent( + "100", + ); + const ubuntuLarge = screen.getAllByDataCy("distro-table-row")[1]; + expect(within(ubuntuLarge).getAllByRole("cell")[2]).toHaveTextContent("30"); + const ubuntuStatic = screen.getAllByDataCy("distro-table-row")[2]; + expect(within(ubuntuStatic).getAllByRole("cell")[2]).toHaveTextContent("2"); + }); + + it("shows correct links to hosts pages", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getAllByDataCy("distro-table-row")).toHaveLength(3); + }); + + const ubuntuSmall = screen.getAllByDataCy("distro-table-row")[0]; + expect(within(ubuntuSmall).getAllByRole("link")[1]).toHaveAttribute( + "href", + "/hosts?distroId=ubuntu2204-small&startedBy=mci", + ); + const ubuntuLarge = screen.getAllByDataCy("distro-table-row")[1]; + expect(within(ubuntuLarge).getAllByRole("link")[1]).toHaveAttribute( + "href", + "/hosts?distroId=ubuntu2204-large&startedBy=mci", + ); + const ubuntuStatic = screen.getAllByDataCy("distro-table-row")[2]; + expect(within(ubuntuStatic).getAllByRole("link")[1]).toHaveAttribute( + "href", + "/hosts?distroId=ubuntu2204-static&startedBy=mci", + ); + }); +}); + +const imageDistrosMock: ApolloMock< + ImageDistrosQuery, + ImageDistrosQueryVariables +> = { + request: { + query: IMAGE_DISTROS, + variables: { imageId: "ubuntu2204" }, + }, + result: { + data: { + image: { + __typename: "Image", + id: "ubuntu2204", + distros: [ + { + __typename: "Distro", + name: "ubuntu2204-small", + provider: Provider.Ec2Fleet, + providerSettingsList: [ + { region: "us-east-1", instance_type: "m4.xlarge" }, + { region: "us-west-1", instance_type: "m4.4xlarge" }, + ], + hostAllocatorSettings: { + __typename: "HostAllocatorSettings", + maximumHosts: 100, + }, + }, + { + __typename: "Distro", + name: "ubuntu2204-large", + provider: Provider.Ec2OnDemand, + providerSettingsList: [ + { region: "us-east-1", instance_type: "m6i.2xlarge" }, + { region: "us-east-1", instance_type: "m5.2xlarge" }, + ], + hostAllocatorSettings: { + __typename: "HostAllocatorSettings", + maximumHosts: 30, + }, + }, + { + __typename: "Distro", + name: "ubuntu2204-static", + provider: Provider.Static, + providerSettingsList: [{ hosts: ["host-1", "host-2"] }], + hostAllocatorSettings: { + __typename: "HostAllocatorSettings", + maximumHosts: 0, + }, + }, + ], + }, + }, + }, +}; diff --git a/apps/spruce/src/pages/image/DistrosTable/index.tsx b/apps/spruce/src/pages/image/DistrosTable/index.tsx new file mode 100644 index 000000000..c19373a4d --- /dev/null +++ b/apps/spruce/src/pages/image/DistrosTable/index.tsx @@ -0,0 +1,163 @@ +import { useMemo, useRef } from "react"; +import { useQuery } from "@apollo/client"; +import styled from "@emotion/styled"; +import IconButton from "@leafygreen-ui/icon-button"; +import { useLeafyGreenTable, LGColumnDef } from "@leafygreen-ui/table"; +import Tooltip, { Align, Justify } from "@leafygreen-ui/tooltip"; +import Icon from "components/Icon"; +import { StyledRouterLink, StyledLink } from "components/styles"; +import { BaseTable } from "components/Table/BaseTable"; +import { amazonEC2InstanceTypeDocumentationUrl } from "constants/externalResources"; +import { defaultEC2Region, MCI_USER } from "constants/hosts"; +import { getAllHostsRoute, getDistroSettingsRoute } from "constants/routes"; +import { size } from "constants/tokens"; +import { useToastContext } from "context/toast"; +import { + ImageDistrosQuery, + ImageDistrosQueryVariables, + Provider, +} from "gql/generated/types"; +import { IMAGE_DISTROS } from "gql/queries"; +import { Unpacked } from "types/utils"; + +type Distro = Unpacked["distros"]>; + +type DistrosTableProps = { + imageId: string; +}; + +export const DistrosTable: React.FC = ({ imageId }) => { + const dispatchToast = useToastContext(); + const { data: imageData, loading } = useQuery< + ImageDistrosQuery, + ImageDistrosQueryVariables + >(IMAGE_DISTROS, { + variables: { imageId }, + onError(err) { + dispatchToast.error( + `There was an error loading image distros: ${err.message}`, + ); + }, + }); + + const distros = useMemo( + () => imageData?.image?.distros ?? [], + [imageData?.image?.distros], + ); + + const tableContainerRef = useRef(null); + const table = useLeafyGreenTable({ + columns, + data: distros ?? [], + containerRef: tableContainerRef, + defaultColumn: { + enableColumnFilter: false, + }, + }); + + return ( + + ); +}; + +const columns: LGColumnDef[] = [ + { + header: "Name", + accessorKey: "name", + size: 200, + cell: ({ getValue }) => { + const distro = getValue() as string; + return ( + + {distro} + + ); + }, + }, + { + header: () => ( + + Instance Type + + + + } + triggerEvent="click" + > + Amazon instance type definitions can be found{" "} + + here + + . + + + ), + accessorKey: "providerSettingsList", + cell: ({ + row: { + original: { provider, providerSettingsList }, + }, + }) => ( + + {provider === Provider.Ec2Fleet || provider === Provider.Ec2OnDemand + ? providerSettingsList.find((e) => e.region === defaultEC2Region) + ?.instance_type + : "N/A"} + + ), + }, + { + header: "Max Hosts", + accessorKey: "hostAllocatorSettings.maximumHosts", + size: 100, + cell: ({ + getValue, + row: { + original: { provider, providerSettingsList }, + }, + }) => ( + + {provider === Provider.Ec2Fleet || provider === Provider.Ec2OnDemand + ? getValue() + : providerSettingsList?.[0]?.hosts?.length ?? 0} + + ), + }, + { + id: "view-hosts-links", + size: 100, + cell: ({ + row: { + original: { name }, + }, + }) => ( + + View hosts + + ), + }, +]; + +const HeaderCell = styled.div` + display: flex; + align-items: center; + gap: ${size.xxs}; +`; diff --git a/apps/spruce/src/pages/image/Header.tsx b/apps/spruce/src/pages/image/Header.tsx new file mode 100644 index 000000000..85f0a091c --- /dev/null +++ b/apps/spruce/src/pages/image/Header.tsx @@ -0,0 +1,32 @@ +import styled from "@emotion/styled"; +import { H2, Overline } from "@leafygreen-ui/typography"; +import { ImageTabRoutes } from "constants/routes"; +import { size } from "constants/tokens"; +import { getTabTitle } from "./getTabTitle"; + +interface HeaderProps { + imageId: string; + tab: ImageTabRoutes; +} + +export const Header: React.FC = ({ imageId, tab }) => { + const { title } = getTabTitle(tab); + + return ( + + {title} + +

{imageId}

+
+
+ ); +}; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const TitleContainer = styled.div` + margin-right: ${size.s}; +`; diff --git a/apps/spruce/src/pages/image/ImageSelect/ImageSelect.test.tsx b/apps/spruce/src/pages/image/ImageSelect/ImageSelect.test.tsx new file mode 100644 index 000000000..c6b1e57c5 --- /dev/null +++ b/apps/spruce/src/pages/image/ImageSelect/ImageSelect.test.tsx @@ -0,0 +1,87 @@ +import { MockedProvider } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { ImagesQuery, ImagesQueryVariables } from "gql/generated/types"; +import { IMAGES } from "gql/queries"; +import { + renderWithRouterMatch as render, + screen, + userEvent, + waitFor, +} from "test_utils"; +import { ApolloMock } from "types/gql"; +import { ImageSelect } from "."; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe("image select", () => { + it("shows image name as dropdown content", async () => { + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getByDataCy("images-select")).toBeInTheDocument(); + }); + await waitFor(() => { + expect(screen.getByDisplayValue("ubuntu2204")).toBeVisible(); + }); + }); + + it("selecting a different image will navigate to the correct URL", async () => { + const user = userEvent.setup(); + const { Component } = RenderFakeToastContext( + , + ); + const { router } = render(, { + wrapper, + route: "/image/ubuntu2204/build-information", + path: "/image/:imageId/:tab", + }); + + await waitFor(() => { + expect(screen.getByDataCy("images-select")).toBeInTheDocument(); + }); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + await user.click(screen.getByLabelText("Images")); + expect(screen.getByRole("listbox")).toBeVisible(); + await user.click(screen.getByText("amazon2")); + expect(router.state.location.pathname).toBe( + "/image/amazon2/build-information", + ); + }); + + it("typing in the text input will narrow down search results", async () => { + const user = userEvent.setup(); + const { Component } = RenderFakeToastContext( + , + ); + render(, { wrapper }); + await waitFor(() => { + expect(screen.getByDataCy("images-select")).toBeInTheDocument(); + }); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + await user.click(screen.getByLabelText("Images")); + expect(screen.getByRole("listbox")).toBeVisible(); + + expect(screen.getAllByRole("option")).toHaveLength(3); + await user.clear(screen.getByPlaceholderText("Select an image")); + await user.type(screen.getByPlaceholderText("Select an image"), "rhel"); + expect(screen.getAllByRole("option")).toHaveLength(1); + }); +}); + +const imagesMock: ApolloMock = { + request: { + query: IMAGES, + variables: {}, + }, + result: { + data: { + images: ["ubuntu2204", "amazon2", "rhel90"], + }, + }, +}; diff --git a/apps/spruce/src/pages/image/Tabs.tsx b/apps/spruce/src/pages/image/Tabs.tsx new file mode 100644 index 000000000..625af71d8 --- /dev/null +++ b/apps/spruce/src/pages/image/Tabs.tsx @@ -0,0 +1,39 @@ +import styled from "@emotion/styled"; +import { Routes, Route, Navigate } from "react-router-dom"; +import { ImageTabRoutes } from "constants/routes"; +import useScrollToAnchor from "hooks/useScrollToAnchor"; +import { Header } from "./Header"; +import { BuildInformationTab } from "./tabs/index"; + +type ImageTabsProps = { + imageId: string; + currentTab: ImageTabRoutes; +}; + +export const ImageTabs: React.FC = ({ + currentTab, + imageId, +}) => { + useScrollToAnchor(); + + return ( + +
+ + } + /> + } + /> + + + ); +}; + +const Container = styled.div` + min-width: 600px; + max-width: 75%; +`; diff --git a/apps/spruce/src/pages/image/index.tsx b/apps/spruce/src/pages/image/index.tsx index b7a2af32f..ad21e2746 100644 --- a/apps/spruce/src/pages/image/index.tsx +++ b/apps/spruce/src/pages/image/index.tsx @@ -1,12 +1,18 @@ import styled from "@emotion/styled"; import { sideNavItemSidePadding } from "@leafygreen-ui/side-nav"; import { Link, useParams, Navigate } from "react-router-dom"; -import { SideNav, SideNavGroup, SideNavItem } from "components/styles"; +import { + SideNav, + SideNavGroup, + SideNavItem, + PageWrapper, +} from "components/styles"; import { ImageTabRoutes, getImageRoute, slugs } from "constants/routes"; import { size } from "constants/tokens"; import { useFirstImage } from "hooks"; import { getTabTitle } from "./getTabTitle"; import { ImageSelect } from "./ImageSelect"; +import { ImageTabs } from "./Tabs"; const Image: React.FC = () => { const { [slugs.imageId]: imageId, [slugs.tab]: currentTab } = useParams<{ @@ -18,7 +24,10 @@ const Image: React.FC = () => { const selectedImage = imageId ?? firstImage; - if (!Object.values(ImageTabRoutes).includes(currentTab as ImageTabRoutes)) { + if ( + currentTab === undefined || + !Object.values(ImageTabRoutes).includes(currentTab as ImageTabRoutes) + ) { return ( { } return ( - - - - - - {Object.values(ImageTabRoutes).map((tab) => ( - - {getTabTitle(tab).title} - - ))} - - + <> + + + + + + {Object.values(ImageTabRoutes).map((tab) => ( + + {getTabTitle(tab).title} + + ))} + + + + + + ); }; diff --git a/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx b/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx new file mode 100644 index 000000000..48cd05481 --- /dev/null +++ b/apps/spruce/src/pages/image/tabs/BuildInformationTab/index.tsx @@ -0,0 +1,14 @@ +import { SpruceFormContainer } from "components/SpruceForm"; +import { DistrosTable } from "pages/image/DistrosTable"; + +type BuildInformationTabProps = { + imageId: string; +}; + +export const BuildInformationTab: React.FC = ({ + imageId, +}) => ( + + + +); diff --git a/apps/spruce/src/pages/image/tabs/index.tsx b/apps/spruce/src/pages/image/tabs/index.tsx new file mode 100644 index 000000000..c11546104 --- /dev/null +++ b/apps/spruce/src/pages/image/tabs/index.tsx @@ -0,0 +1 @@ +export { BuildInformationTab } from "./BuildInformationTab/index"; diff --git a/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_MultipleExecutions.storyshot b/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_MultipleExecutions.storyshot index 18ef5371e..5b53cecef 100644 --- a/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_MultipleExecutions.storyshot +++ b/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_MultipleExecutions.storyshot @@ -165,7 +165,7 @@ @@ -248,7 +248,7 @@ diff --git a/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_SingleExecution.storyshot b/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_SingleExecution.storyshot index 7bf7cbe71..f1423a024 100644 --- a/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_SingleExecution.storyshot +++ b/apps/spruce/src/pages/task/taskTabs/__snapshots__/ExecutionTasksTable_SingleExecution.storyshot @@ -165,7 +165,7 @@ @@ -242,7 +242,7 @@ diff --git a/apps/spruce/src/pages/version/taskDuration/TaskDurationTable.test.tsx b/apps/spruce/src/pages/version/taskDuration/TaskDurationTable.test.tsx index 390d28088..5ac2542eb 100644 --- a/apps/spruce/src/pages/version/taskDuration/TaskDurationTable.test.tsx +++ b/apps/spruce/src/pages/version/taskDuration/TaskDurationTable.test.tsx @@ -15,7 +15,7 @@ describe("taskDurationTable", () => { , ); - expect(screen.queryAllByDataCy("leafygreen-table-row")).toHaveLength(2); + expect(screen.queryAllByDataCy("task-duration-table-row")).toHaveLength(2); }); it("opens nested row on click", async () => { @@ -29,7 +29,7 @@ describe("taskDurationTable", () => { screen.queryByText("check_codegen_execution_task"), ).not.toBeVisible(); const expandRowButton = within( - screen.queryAllByDataCy("leafygreen-table-row")[0], + screen.queryAllByDataCy("task-duration-table-row")[0], ).queryByRole("button"); // @ts-expect-error: FIXME. This comment was added by an automated script. await user.click(expandRowButton); diff --git a/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_Default.storyshot b/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_Default.storyshot index 7a74a8882..29320fc35 100644 --- a/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_Default.storyshot +++ b/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_Default.storyshot @@ -192,7 +192,7 @@ @@ -463,7 +463,7 @@ @@ -553,7 +553,7 @@ @@ -643,7 +643,7 @@ @@ -733,7 +733,7 @@ @@ -823,7 +823,7 @@ @@ -913,7 +913,7 @@ @@ -1003,7 +1003,7 @@ @@ -1093,7 +1093,7 @@ @@ -1183,7 +1183,7 @@ diff --git a/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_LongContent.storyshot b/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_LongContent.storyshot index dae6b5b1d..98fef649a 100644 --- a/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_LongContent.storyshot +++ b/apps/spruce/src/pages/version/taskDuration/__snapshots__/TaskDurationTable_LongContent.storyshot @@ -192,7 +192,7 @@