diff --git a/docs/docs-content/integrations/deprecated-packs.md b/docs/docs-content/integrations/deprecated-packs.md index b38cad7cab..8187f10fc5 100644 --- a/docs/docs-content/integrations/deprecated-packs.md +++ b/docs/docs-content/integrations/deprecated-packs.md @@ -3,7 +3,8 @@ sidebar_label: "Deprecated Packs" title: "Deprecated Packs" description: "Deprecated Packs" icon: "" -hide_table_of_contents: false +hide_table_of_contents: true +sidebar_position: 40 tags: ["packs", "deprecation"] --- diff --git a/src/components/PacksTable/PackTable.test.tsx b/src/components/PacksTable/PackTable.test.tsx index 6064f5f37f..1848cb44e8 100644 --- a/src/components/PacksTable/PackTable.test.tsx +++ b/src/components/PacksTable/PackTable.test.tsx @@ -3,9 +3,36 @@ import { render, waitFor, screen, fireEvent } from "@testing-library/react"; import fetchMock from "jest-fetch-mock"; import FilteredTable from "./PacksTable"; import { toTitleCase } from "./PacksTable"; + +// Mock the Docusaurus dependencies +jest.mock("@docusaurus/theme-common", () => ({ + useColorMode: () => ({ + colorMode: "light", + setColorMode: jest.fn(), + }), +})); + +jest.mock("@theme/Admonition"); + // Enable fetch mocking fetchMock.enableMocks(); +beforeAll(() => { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); +}); + describe("FilteredTable Tests", () => { const mockPacks = [ { @@ -23,6 +50,7 @@ describe("FilteredTable Tests", () => { releaseType: "Experimental", contributor: "", docsURL: "", + hash: "mock-hash-1", }, { name: "amazon-linux-eks", @@ -39,6 +67,7 @@ describe("FilteredTable Tests", () => { releaseType: "Stable", contributor: "", docsURL: "", + hash: "mock-hash-2", }, ]; @@ -49,14 +78,14 @@ describe("FilteredTable Tests", () => { it("should show loader initially", () => { const { container } = render(); - expect(container.querySelector(".loader")).toBeInTheDocument(); + expect(container.querySelector(".ant-spin")).toBeInTheDocument(); }); it("should hide loader and display packs after API call", async () => { fetchMock.mockResponseOnce(JSON.stringify({ dateCreated: "2022-08-25", Packs: mockPacks })); const { container } = render(); - await waitFor(() => expect(container.querySelector(".loader")).not.toBeInTheDocument()); + await waitFor(() => expect(container.querySelector(".ant-spin")).not.toBeInTheDocument()); expect(screen.getByText("Alpine")).toBeInTheDocument(); expect(screen.getByText("Amazon EKS optimized Linux")).toBeInTheDocument(); }); @@ -102,6 +131,18 @@ describe("FilteredTable Tests", () => { expect(screen.getByText("EKS, vSphere")).toBeInTheDocument(); }); + + it("should have unique row keys", async () => { + fetchMock.mockResponseOnce(JSON.stringify({ dateCreated: "2022-08-25", Packs: mockPacks })); + const { container } = render(); + + await waitFor(() => { + const rows = container.querySelectorAll(".ant-table-row"); + const keys = Array.from(rows).map((row) => row.getAttribute("data-row-key")); + const uniqueKeys = new Set(keys); + expect(keys.length).toBe(uniqueKeys.size); + }); + }); }); describe("toTitleCase", () => { diff --git a/src/components/PacksTable/PacksTable.module.scss b/src/components/PacksTable/PacksTable.module.scss index a399dc309c..f296f04d45 100644 --- a/src/components/PacksTable/PacksTable.module.scss +++ b/src/components/PacksTable/PacksTable.module.scss @@ -46,6 +46,31 @@ } } +.tableContainer { + margin: 16px 0; + overflow: auto; + + @media screen and (max-width: 768px) { + display: none; + } + + :global { + .ant-input-search { + margin-bottom: 16px; + } + } +} + +.tableWrapper { + border: 1px solid var(--ifm-toc-border-color); + border-radius: 4px; + padding: 16px; + + @media screen and (max-width: 768px) { + padding: 8px; + } +} + .green { background-color: var(--ifm-color-success-contrast-background); color: var(--ifm-color-success-contrast-foreground); @@ -60,32 +85,27 @@ border-radius: 16px; } -.tableWrapper { - border: 1px solid var(--ifm-toc-border-color); - padding: 16px; +.disabled, +.deleted, +.deprecated { padding: 4px 8px; - border-radius: 4px; + border-radius: 16px; + display: inline-block; } .disabled { background-color: var(--ifm-color-warning-contrast-background); color: var(--ifm-color-warning-contrast-foreground); - padding: 4px 8px; - border-radius: 16px; } .deleted { background-color: var(--ifm-color-danger-contrast-background); color: var(--ifm-color-danger-contrast-foreground); - padding: 4px 8px; - border-radius: 16px; } .deprecated { background-color: var(--ifm-color-info-contrast-background); color: var(--ifm-color-info-contrast-foreground); - padding: 4px 8px; - border-radius: 16px; } .error { @@ -93,5 +113,18 @@ justify-content: center; align-items: center; min-height: 400px; - color: red; + color: var(--ifm-color-danger); +} + +.unsupportedMessage { + display: none; + margin: 16px 0; + + @media screen and (max-width: 768px) { + display: block; + } +} + +.searchContainer { + margin-bottom: 16px; } diff --git a/src/components/PacksTable/PacksTable.tsx b/src/components/PacksTable/PacksTable.tsx index 685daaa994..f95568b56c 100644 --- a/src/components/PacksTable/PacksTable.tsx +++ b/src/components/PacksTable/PacksTable.tsx @@ -1,7 +1,10 @@ -import React, { useEffect, useState, useCallback } from "react"; -import CustomTable from "../CustomTable/CustomTable"; +import React, { useEffect, useState, useMemo } from "react"; +import { Table, ConfigProvider, theme } from "antd"; import styles from "./PacksTable.module.scss"; import Search from "../Technologies/Search"; +import { useColorMode } from "@docusaurus/theme-common"; +import type { ColumnsType } from "antd/es/table"; +import Admonition from "@theme/Admonition"; type Pack = { name: string; @@ -19,6 +22,7 @@ type Pack = { releaseType: string; contributor: string; docsURL: string; + hash?: string; }; const statusClassNames: Record = { @@ -35,21 +39,23 @@ const formatCloudType = (type: string): string => { vsphere: "vSphere", maas: "MaaS", gcp: "GCP", - libvirt: "libvirt", + edge: "Edge", openstack: "OpenStack", "edge-native": "Edge", tke: "TKE", aks: "AKS", - coxedge: "Cox Edge", gke: "GKE", all: "All", azure: "Azure", + tencent: "Tencent", // ... add other special cases as needed }; return type .split(",") - .map((part) => cloudTypeMapping[part.trim()] || capitalizeWord(part)) + .map((part) => part.trim()) + .filter((part) => part !== "nested" && part !== "libvirt" && part !== "baremetal" && part !== "coxedge") + .map((part) => cloudTypeMapping[part] || capitalizeWord(part)) .join(", "); }; @@ -58,71 +64,56 @@ const capitalizeWord = (string: string): string => { return string.toUpperCase(); }; -interface PacksColumn { - title: string; - dataIndex: keyof Pack; - key: string; - sorter?: (a: Pack, b: Pack) => number; - render?: (value: string, row: Pack) => React.ReactNode; - width: number; -} - -const columns: PacksColumn[] = [ +const columns: ColumnsType = [ { title: "Name", dataIndex: "displayName", key: "displayName", - sorter: (a: Pack, b: Pack) => a.displayName.localeCompare(b.displayName), - width: 200, + sorter: (a, b) => a.displayName.localeCompare(b.displayName), }, { title: "Cloud Types", dataIndex: "cloudTypesFormatted", key: "cloudTypesFormatted", - sorter: (a: Pack, b: Pack) => a.cloudTypesFormatted.localeCompare(b.cloudTypesFormatted), + sorter: (a, b) => a.cloudTypesFormatted.localeCompare(b.cloudTypesFormatted), render: (value: string) => formatCloudType(value), - width: 200, }, { title: "Version", dataIndex: "version", key: "version", - sorter: (a: Pack, b: Pack) => a.version.localeCompare(b.version), - width: 120, + sorter: (a, b) => a.version.localeCompare(b.version), }, { title: "Status", dataIndex: "prodStatus", key: "prodStatus", - sorter: (a: Pack, b: Pack) => a.prodStatus.localeCompare(b.prodStatus), + sorter: (a, b) => a.prodStatus.localeCompare(b.prodStatus), render: (status: string) => { const className = statusClassNames[status]; return {status}; }, - width: 150, }, { title: "Last Updated", dataIndex: "packLastModifiedDate", key: "packLastModifiedDate", - sorter: (a: Pack, b: Pack) => - new Date(a.packLastModifiedDate).getTime() - new Date(b.packLastModifiedDate).getTime(), - width: 150, + sorter: (a, b) => new Date(a.packLastModifiedDate).getTime() - new Date(b.packLastModifiedDate).getTime(), + render: (date: string) => (date === "-" ? date : new Date(date).toLocaleDateString()), }, { title: "Last Modified", dataIndex: "timeLastUpdated", key: "timeLastUpdated", - sorter: (a: Pack, b: Pack) => - new Date(a.packLastModifiedDate).getTime() - new Date(b.packLastModifiedDate).getTime(), + sorter: (a, b) => new Date(a.packLastModifiedDate).getTime() - new Date(b.packLastModifiedDate).getTime(), render: (date: string, pack: Pack) => { + if (date === "-") return date; const dateObject = new Date(pack.packLastModifiedDate); const oneMonthAgo = new Date(); oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); const isWithinAMonth = dateObject >= oneMonthAgo; return {date}; }, - width: 150, }, ]; @@ -137,22 +128,20 @@ const FilteredTable: React.FC = () => { const [deprecatedPacks, setDeprecatedPacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [timer, setTimer] = useState(null); + const { colorMode } = useColorMode(); + const { defaultAlgorithm, darkAlgorithm } = theme; useEffect(() => { fetch("/packs-data/packs_report.json") .then((response) => response.json()) .then((packData: PacksData) => { const deprecatedPackData = packData.Packs.filter((pack) => { - // Handle the case where the pack name is empty. - // This is applicable when the API returns a pack with no name. - // The API also does not include the last modified date for these packs. - if (pack.displayName == "") { + if (pack.displayName === "") { pack.displayName = toTitleCase(pack.name); pack.timeLastUpdated = "-"; pack.packLastModifiedDate = "-"; } - + pack.hash = Math.random().toString(36).substring(2, 15); return pack.prodStatus !== "active" && pack.prodStatus !== "unknown"; }); setDeprecatedPacks(deprecatedPackData); @@ -164,37 +153,50 @@ const FilteredTable: React.FC = () => { }); }, []); - const handleSearch = useCallback( - (searchString: string) => { - if (timer) { - clearTimeout(timer); - } + // Simplified search filtering + const filteredPacks = useMemo(() => { + const searchTerm = searchValue.trim().toLowerCase(); + if (!searchTerm) return deprecatedPacks; - const newTimer = setTimeout(() => { - setSearchValue(searchString); - }, 300); + return deprecatedPacks.filter((pack) => pack.displayName.toLowerCase().includes(searchTerm)); + }, [deprecatedPacks, searchValue]); - setTimer(newTimer); - }, - [timer] - ); - - const filteredPacks = searchValue - ? deprecatedPacks.filter((pack) => pack.displayName.toLowerCase().includes(searchValue.toLowerCase())) - : deprecatedPacks; + // Single handler for both onChange and onSearch + const handleSearchChange = (value: string) => { + setSearchValue(value); + }; return ( -
- - - className={styles.packsTable} - columns={columns} - dataSource={filteredPacks} - loading={loading} - scrollY={250} - pagination={{ pageSize: 250 }} - /> - {error &&
Failed to load Deprecated Packs
} +
+ +
+ + The current screen size is not supported. Use a larger display to access the Packs table. + +
+ +
+
+ +
+ `${record.name}-${record.version}-${record.hash}`} + pagination={{ + pageSizeOptions: ["500", "1000", "2500", "5000"], + defaultPageSize: 1000, + showSizeChanger: true, + }} + scroll={{ y: 800 }} + bordered + tableLayout="fixed" + sticky + /> + {error &&
Failed to load Deprecated Packs
} + + ); }; diff --git a/src/components/PacksTable/__mocks__/@docusaurus/theme-common.ts b/src/components/PacksTable/__mocks__/@docusaurus/theme-common.ts new file mode 100644 index 0000000000..9cc4a7910c --- /dev/null +++ b/src/components/PacksTable/__mocks__/@docusaurus/theme-common.ts @@ -0,0 +1,4 @@ +export const useColorMode = () => ({ + colorMode: "light", + setColorMode: jest.fn(), +}); diff --git a/src/components/PacksTable/__mocks__/@theme/Admonition.tsx b/src/components/PacksTable/__mocks__/@theme/Admonition.tsx new file mode 100644 index 0000000000..bf8c7c295e --- /dev/null +++ b/src/components/PacksTable/__mocks__/@theme/Admonition.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function MockAdmonition({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/utils/helpers/affected-table.test.js b/utils/helpers/affected-table.test.js index a0e27b9896..1dd6ebe2af 100644 --- a/utils/helpers/affected-table.test.js +++ b/utils/helpers/affected-table.test.js @@ -11,8 +11,8 @@ describe("generateMarkdownTable", () => { const expectedTable = `| Version | Palette Enterprise | Palette Enterprise Airgap | VerteX | VerteX Airgap | | - | -------- | -------- | -------- | -------- | -| 4.5.3 | Impacted | No Impact | No Impact | No Impact | -| 4.4.20 | Impacted | No Impact | No Impact | No Impact |`; +| 4.5.3 | ⚠️ Impacted | ✅ No Impact | ✅ No Impact | ✅ No Impact | +| 4.4.20 | ⚠️ Impacted | ✅ No Impact | ✅ No Impact | ✅ No Impact |`; expect(generateMarkdownTable(cveImpactMap).replace(/\s+/g, "")).toBe(expectedTable.replace(/\s+/g, "")); });