diff --git a/README.md b/README.md index 68b08d1..e3145c0 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ This npm package provides a collection of reusable Patternfly React components t - ImageComponent - OneCardWrapper - TableWrapper -- Dynamic Component Renderer - - DynamicComponents ## Installation @@ -82,6 +80,48 @@ function App() { } ``` +### Table Component + +```jsx +import { DynamicComponent } from "@rhngui/patternfly-react-renderer"; + +const tableConfig = { + component: "table", + title: "Movie Statistics", + id: "movie-stats-table", + fields: [ + { + name: "Movie Title", + data_path: "movies.title", + data: ["Toy Story", "Finding Nemo", "The Incredibles"], + }, + { + name: "Release Year", + data_path: "movies.year", + data: [1995, 2003, 2004], + }, + { + name: "Genres", + data_path: "movies.genres", + data: [ + ["Animation", "Adventure"], + ["Animation", "Adventure"], + ["Animation", "Action"], + ], + }, + { + name: "Rating", + data_path: "movies.rating", + data: [8.3, 8.1, 8.0], + }, + ], +}; + +function App() { + return ; +} +``` + ## Links - [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/) diff --git a/src/components/TableWrapper.tsx b/src/components/TableWrapper.tsx index e10def5..e562ac7 100644 --- a/src/components/TableWrapper.tsx +++ b/src/components/TableWrapper.tsx @@ -1,9 +1,4 @@ -import { - Chart, - ChartBar, - ChartAxis, - ChartThemeColor, -} from "@patternfly/react-charts/victory"; +import { Card, CardBody } from "@patternfly/react-core"; import { Table, Thead, @@ -13,118 +8,83 @@ import { Td, Caption, } from "@patternfly/react-table"; -import { useState } from "react"; -const TableWrapper = ({ - columns, - rows, - caption, - variant, - graph, - selectable = false, - onRowSelect, - actions, - setCustomData, -}) => { - const [selectedRows, setSelectedRows] = useState([]); +interface FieldData { + name: string; + data_path: string; + data: (string | number | boolean | null | (string | number)[])[]; +} - const toggleRow = (index) => { - const newSelected = selectedRows.includes(index) - ? selectedRows.filter((i) => i !== index) - : [...selectedRows, index]; - setSelectedRows(newSelected); +interface TableWrapperProps { + component: "table"; + title: string; + id: string; + fields: FieldData[]; + className?: string; +} - onRowSelect?.(newSelected.map((i) => rows[i])); - setCustomData(newSelected.map((i) => rows[i])); - }; +const TableWrapper = (props: TableWrapperProps) => { + const { title, id, fields, className } = props; + // Transform fields data into table format + const transformFieldsToTableData = () => { + if (!fields || fields.length === 0) return { columns: [], rows: [] }; + + // Find the maximum number of data items across all fields + const maxDataLength = Math.max(...fields.map((field) => field.data.length)); + + // Create columns from field names + const transformedColumns = fields.map((field) => ({ + key: field.name, + label: field.name, + })); - const toggleAllRows = () => { - const allSelected = selectedRows.length === rows.length; - const newSelected = allSelected ? [] : rows.map((_, i) => i); - setSelectedRows(newSelected); + // Create rows based on the maximum data length + const transformedRows = []; + for (let i = 0; i < maxDataLength; i++) { + const row: Record = {}; + fields.forEach((field) => { + const value = field.data[i]; + if (value === null || value === undefined) { + row[field.name] = ""; + } else if (Array.isArray(value)) { + row[field.name] = value.join(", "); + } else { + row[field.name] = String(value); + } + }); + transformedRows.push(row); + } - onRowSelect?.(newSelected.map((i) => rows[i])); - setCustomData(newSelected.map((i) => rows[i])); + return { columns: transformedColumns, rows: transformedRows }; }; - const graphData = - graph && graph.column - ? rows.map((row) => ({ x: row[columns[0].key], y: row[graph.column] })) - : []; + const { columns, rows } = transformFieldsToTableData(); return ( -
- - {caption && ( - - )} - - - {selectable && ( - - )} - {columns.map((col, index) => ( - - ))} - - - - {rows.map((row, rowIndex) => ( - - {selectable && ( - - )} - {columns.map((col, colIndex) => ( - + + +
-
- {caption} - {actions} -
-
- - {col.label}
- toggleRow(rowIndex)} - /> - {row[col.key]}
+ + + + {columns.map((col, index) => ( + ))} - ))} - -
{title}
{col.label}
- - {graph && ( -
- - - - - -
- )} -
+ + + {rows.map((row, rowIndex) => ( + + {columns.map((col, colIndex) => ( + {row[col.key]} + ))} + + ))} + + + + ); }; export default TableWrapper; - diff --git a/src/test/components/DynamicComponent.test.tsx b/src/test/components/DynamicComponent.test.tsx new file mode 100644 index 0000000..ebf58bf --- /dev/null +++ b/src/test/components/DynamicComponent.test.tsx @@ -0,0 +1,80 @@ +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +import DynamicComponent from "../../components/DynamicComponents"; + +describe("DynamicComponent", () => { + it("should render table component with fields", () => { + const tableConfig = { + component: "table", + title: "Test Table", + id: "test-table-id", + fields: [ + { + name: "Name", + data_path: "user.name", + data: ["John Doe", "Jane Smith"], + }, + { + name: "Age", + data_path: "user.age", + data: [28, 34], + }, + ], + }; + + render(); + + // Check that the title appears in the table caption + const tableCaption = screen.getByRole("grid").querySelector("caption"); + expect(tableCaption).toHaveTextContent("Test Table"); + + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Age")).toBeInTheDocument(); + expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getByText("Jane Smith")).toBeInTheDocument(); + expect(screen.getByText("28")).toBeInTheDocument(); + expect(screen.getByText("34")).toBeInTheDocument(); + }); + + it("should render one-card component", () => { + const oneCardConfig = { + component: "one-card", + title: "Test Card", + fields: [ + { + name: "Field 1", + data_path: "test.field1", + data: ["Value 1"], + }, + ], + }; + + render(); + + expect(screen.getByText("Test Card")).toBeInTheDocument(); + expect(screen.getByText("Field 1")).toBeInTheDocument(); + expect(screen.getByText("Value 1")).toBeInTheDocument(); + }); + + it("should handle empty config", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("should handle null config", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("should handle unknown component", () => { + const unknownConfig = { + component: "unknown-component", + title: "Unknown", + }; + + render(); + // Should render FragmentWrapper (empty fragment) + expect(screen.queryByText("Unknown")).not.toBeInTheDocument(); + }); +}); diff --git a/src/test/components/TableWrapper.test.tsx b/src/test/components/TableWrapper.test.tsx index 5eed556..7871039 100644 --- a/src/test/components/TableWrapper.test.tsx +++ b/src/test/components/TableWrapper.test.tsx @@ -1,181 +1,241 @@ -import { render, fireEvent, screen } from "@testing-library/react"; -import { vi } from "vitest"; +import { render, screen } from "@testing-library/react"; import TableWrapper from "../../components/TableWrapper"; -const columns = [ - { key: "name", label: "Name" }, - { key: "age", label: "Age" }, -]; +describe("TableWrapper Component", () => { + const mockFieldsData = { + component: "table" as const, + title: "Details of Toy Story", + id: "call_5glz9rb6", + fields: [ + { + name: "Title", + data_path: "movie.title", + data: ["Toy Story"], + }, + { + name: "Year", + data_path: "movie.year", + data: [1995], + }, + { + name: "Runtime", + data_path: "movie.runtime", + data: [81], + }, + { + name: "IMDB Rating", + data_path: "movie.imdbRating", + data: [8.3], + }, + { + name: "Revenue", + data_path: "movie.revenue", + data: [373554033], + }, + { + name: "Countries", + data_path: "movie.countries[size:1]", + data: [["USA"]], + }, + ], + }; -const rows = [ - { name: "John Doe", age: 28 }, - { name: "Jane Smith", age: 34 }, -]; + it("should render table with fields prop", () => { + render(); -describe("TableWrapper Component", () => { - it("should render the table correctly", () => { - render(); - expect(screen.getByText("User Table")).toBeInTheDocument(); - expect(screen.getByText("Name")).toBeInTheDocument(); - expect(screen.getByText("Age")).toBeInTheDocument(); + // Check that the title appears in the table caption + const tableCaption = screen.getByRole("grid").querySelector("caption"); + expect(tableCaption).toHaveTextContent("Details of Toy Story"); + + expect(screen.getByText("Title")).toBeInTheDocument(); + expect(screen.getByText("Year")).toBeInTheDocument(); + expect(screen.getByText("Runtime")).toBeInTheDocument(); + expect(screen.getByText("IMDB Rating")).toBeInTheDocument(); + expect(screen.getByText("Revenue")).toBeInTheDocument(); + expect(screen.getByText("Countries")).toBeInTheDocument(); }); - it("should render the table with selectable rows", () => { - render( - - ); - const checkboxes = screen.getAllByRole("checkbox"); - expect(checkboxes).toHaveLength(3); // 1 for "select all", 2 for individual rows + it("should render data values correctly from fields", () => { + render(); + + expect(screen.getByText("Toy Story")).toBeInTheDocument(); + expect(screen.getByText("1995")).toBeInTheDocument(); + expect(screen.getByText("81")).toBeInTheDocument(); + expect(screen.getByText("8.3")).toBeInTheDocument(); + expect(screen.getByText("373554033")).toBeInTheDocument(); + expect(screen.getByText("USA")).toBeInTheDocument(); }); - it('should select and deselect a row', () => { - const setCustomDataMock = vi.fn(); - const onRowSelectMock = vi.fn(); - - const mockColumns = [ - { label: 'Column 1', key: 'test1' }, - { label: 'Column 2', key: 'test2' }, - ]; - - const mockRows = [ - { id: 1, test1: 'test1', test2: 'test2' }, - ]; - - const { getByTestId, getByLabelText } = render( - - ); - - const row = getByTestId('row-1'); - const checkbox = getByLabelText('Select row 0'); - - // Select the row - fireEvent.click(checkbox); - - // After selection, the mock function should be called with the selected row - expect(setCustomDataMock).toHaveBeenCalledWith([ - { - id: 1, - test1: 'test1', - test2: 'test2', - }, - ]); - - // Deselect the row - fireEvent.click(checkbox); // Deselect by clicking the checkbox again - - // After deselection, the mock function should be called with an empty array - expect(setCustomDataMock).toHaveBeenCalledWith([]); + it("should handle array data correctly", () => { + const fieldsWithArray = { + ...mockFieldsData, + fields: [ + { + name: "Countries", + data_path: "movie.countries", + data: [["USA", "Canada", "Mexico"]], + }, + ], + }; + + render(); + + expect(screen.getByText("USA, Canada, Mexico")).toBeInTheDocument(); }); - it("should select all rows", () => { - const mockOnRowSelect = vi.fn(); - const mockSetCustomData = vi.fn(); - - render( - - ); - - // Select all rows - fireEvent.click(screen.getByLabelText("Select all rows")); - expect(mockOnRowSelect).toHaveBeenCalledWith(rows); - - // Deselect all rows - fireEvent.click(screen.getByLabelText("Select all rows")); - expect(mockOnRowSelect).toHaveBeenCalledWith([]); + it("should handle null values correctly", () => { + const fieldsWithNull = { + ...mockFieldsData, + fields: [ + { + name: "Title", + data_path: "movie.title", + data: [null], + }, + { + name: "Year", + data_path: "movie.year", + data: [1995], + }, + ], + }; + + render(); + + expect(screen.getByText("1995")).toBeInTheDocument(); }); - - it("should render the chart if graph data is provided", () => { - const graph = { - column: "age", - title: "Age Chart", + + it("should handle multiple rows of data", () => { + const fieldsWithMultipleRows = { + ...mockFieldsData, + fields: [ + { + name: "Title", + data_path: "movie.title", + data: ["Toy Story", "Toy Story 2", "Toy Story 3"], + }, + { + name: "Year", + data_path: "movie.year", + data: [1995, 1999, 2010], + }, + ], }; + + render(); + + expect(screen.getByText("Toy Story")).toBeInTheDocument(); + expect(screen.getByText("Toy Story 2")).toBeInTheDocument(); + expect(screen.getByText("Toy Story 3")).toBeInTheDocument(); + expect(screen.getByText("1995")).toBeInTheDocument(); + expect(screen.getByText("1999")).toBeInTheDocument(); + expect(screen.getByText("2010")).toBeInTheDocument(); + }); + + it("should apply custom id and className", () => { + const customId = "custom-table-id"; + const customClassName = "custom-table-class"; + render( + {...mockFieldsData} + id={customId} + className={customClassName} + /> ); - expect(screen.getByTitle("Age Chart")).toBeInTheDocument(); + + // Find the Card wrapper that contains the table by looking for the element with the custom id + const cardElement = document.getElementById(customId); + expect(cardElement).toBeInTheDocument(); + expect(cardElement).toHaveClass(customClassName); }); - it("should not render the chart if no graph data is provided", () => { - render( - - ); - expect(screen.queryByTitle("Chart")).toBeNull(); + it("should handle empty fields array", () => { + render(); + + // Check that the title appears in the table caption + const tableCaption = screen.getByRole("grid").querySelector("caption"); + expect(tableCaption).toHaveTextContent("Details of Toy Story"); + // Should not crash with empty fields }); - it("should call setCustomData when a row is selected or deselected", () => { - const mockSetCustomData = vi.fn(); - render( - - ); + it("should handle fields with different data lengths", () => { + const fieldsWithDifferentLengths = { + ...mockFieldsData, + fields: [ + { + name: "Field 1", + data_path: "test.field1", + data: ["value1", "value2"], + }, + { + name: "Field 2", + data_path: "test.field2", + data: ["single value"], + }, + ], + }; - // Select first row - fireEvent.click(screen.getAllByRole("checkbox")[1]); - expect(mockSetCustomData).toHaveBeenCalledWith([rows[0]]); + render(); - // Deselect first row - fireEvent.click(screen.getAllByRole("checkbox")[1]); - expect(mockSetCustomData).toHaveBeenCalledWith([]); + // Should create 2 rows (max length is 2) + expect(screen.getByText("value1")).toBeInTheDocument(); + expect(screen.getByText("value2")).toBeInTheDocument(); + expect(screen.getByText("single value")).toBeInTheDocument(); }); - it("should trigger onRowSelect with correct row data when a row is clicked", () => { - const mockOnRowSelect = vi.fn(); - const mockSetCustomData = vi.fn(); - - render( - - ); - - // Select second row (Jane Smith) - fireEvent.click(screen.getAllByRole("checkbox")[2]); - expect(mockOnRowSelect).toHaveBeenCalledWith([rows[1]]); + it("should render table with correct structure", () => { + render(); + + // Check table structure + const table = screen.getByRole("grid"); + expect(table).toBeInTheDocument(); + + // Check caption + const tableCaption = table.querySelector("caption"); + expect(tableCaption).toHaveTextContent("Details of Toy Story"); + + // Check headers + expect(screen.getByText("Title")).toBeInTheDocument(); + expect(screen.getByText("Year")).toBeInTheDocument(); + expect(screen.getByText("Runtime")).toBeInTheDocument(); + expect(screen.getByText("IMDB Rating")).toBeInTheDocument(); + expect(screen.getByText("Revenue")).toBeInTheDocument(); + expect(screen.getByText("Countries")).toBeInTheDocument(); }); - it("should show the correct row data in the table", () => { - render(); - expect(screen.getByText("John Doe")).toBeInTheDocument(); - expect(screen.getByText("28")).toBeInTheDocument(); - expect(screen.getByText("Jane Smith")).toBeInTheDocument(); - expect(screen.getByText("34")).toBeInTheDocument(); + it("should handle mixed data types", () => { + const fieldsWithMixedTypes = { + ...mockFieldsData, + fields: [ + { + name: "String Field", + data_path: "test.string", + data: ["test string"], + }, + { + name: "Number Field", + data_path: "test.number", + data: [123], + }, + { + name: "Boolean Field", + data_path: "test.boolean", + data: [true], + }, + { + name: "Null Field", + data_path: "test.null", + data: [null], + }, + ], + }; + + render(); + + expect(screen.getByText("test string")).toBeInTheDocument(); + expect(screen.getByText("123")).toBeInTheDocument(); + expect(screen.getByText("true")).toBeInTheDocument(); }); });