Skip to content

Commit e0b5d0f

Browse files
Merge pull request #5 from RedHat-UX/NGUI-382
fix(NGUI-382): Fixed table component
2 parents 1ba3c4c + 0270881 commit e0b5d0f

File tree

4 files changed

+401
-261
lines changed

4 files changed

+401
-261
lines changed

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ This npm package provides a collection of reusable Patternfly React components t
1313
- ImageComponent
1414
- OneCardWrapper
1515
- TableWrapper
16-
- Dynamic Component Renderer
17-
- DynamicComponents
1816

1917
## Installation
2018

@@ -82,6 +80,48 @@ function App() {
8280
}
8381
```
8482

83+
### Table Component
84+
85+
```jsx
86+
import { DynamicComponent } from "@rhngui/patternfly-react-renderer";
87+
88+
const tableConfig = {
89+
component: "table",
90+
title: "Movie Statistics",
91+
id: "movie-stats-table",
92+
fields: [
93+
{
94+
name: "Movie Title",
95+
data_path: "movies.title",
96+
data: ["Toy Story", "Finding Nemo", "The Incredibles"],
97+
},
98+
{
99+
name: "Release Year",
100+
data_path: "movies.year",
101+
data: [1995, 2003, 2004],
102+
},
103+
{
104+
name: "Genres",
105+
data_path: "movies.genres",
106+
data: [
107+
["Animation", "Adventure"],
108+
["Animation", "Adventure"],
109+
["Animation", "Action"],
110+
],
111+
},
112+
{
113+
name: "Rating",
114+
data_path: "movies.rating",
115+
data: [8.3, 8.1, 8.0],
116+
},
117+
],
118+
};
119+
120+
function App() {
121+
return <DynamicComponent config={tableConfig} />;
122+
}
123+
```
124+
85125
## Links
86126

87127
- [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/)

src/components/TableWrapper.tsx

Lines changed: 66 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
Chart,
3-
ChartBar,
4-
ChartAxis,
5-
ChartThemeColor,
6-
} from "@patternfly/react-charts/victory";
1+
import { Card, CardBody } from "@patternfly/react-core";
72
import {
83
Table,
94
Thead,
@@ -13,118 +8,83 @@ import {
138
Td,
149
Caption,
1510
} from "@patternfly/react-table";
16-
import { useState } from "react";
1711

18-
const TableWrapper = ({
19-
columns,
20-
rows,
21-
caption,
22-
variant,
23-
graph,
24-
selectable = false,
25-
onRowSelect,
26-
actions,
27-
setCustomData,
28-
}) => {
29-
const [selectedRows, setSelectedRows] = useState([]);
12+
interface FieldData {
13+
name: string;
14+
data_path: string;
15+
data: (string | number | boolean | null | (string | number)[])[];
16+
}
3017

31-
const toggleRow = (index) => {
32-
const newSelected = selectedRows.includes(index)
33-
? selectedRows.filter((i) => i !== index)
34-
: [...selectedRows, index];
35-
setSelectedRows(newSelected);
18+
interface TableWrapperProps {
19+
component: "table";
20+
title: string;
21+
id: string;
22+
fields: FieldData[];
23+
className?: string;
24+
}
3625

37-
onRowSelect?.(newSelected.map((i) => rows[i]));
38-
setCustomData(newSelected.map((i) => rows[i]));
39-
};
26+
const TableWrapper = (props: TableWrapperProps) => {
27+
const { title, id, fields, className } = props;
28+
// Transform fields data into table format
29+
const transformFieldsToTableData = () => {
30+
if (!fields || fields.length === 0) return { columns: [], rows: [] };
31+
32+
// Find the maximum number of data items across all fields
33+
const maxDataLength = Math.max(...fields.map((field) => field.data.length));
34+
35+
// Create columns from field names
36+
const transformedColumns = fields.map((field) => ({
37+
key: field.name,
38+
label: field.name,
39+
}));
4040

41-
const toggleAllRows = () => {
42-
const allSelected = selectedRows.length === rows.length;
43-
const newSelected = allSelected ? [] : rows.map((_, i) => i);
44-
setSelectedRows(newSelected);
41+
// Create rows based on the maximum data length
42+
const transformedRows = [];
43+
for (let i = 0; i < maxDataLength; i++) {
44+
const row: Record<string, string | number | null> = {};
45+
fields.forEach((field) => {
46+
const value = field.data[i];
47+
if (value === null || value === undefined) {
48+
row[field.name] = "";
49+
} else if (Array.isArray(value)) {
50+
row[field.name] = value.join(", ");
51+
} else {
52+
row[field.name] = String(value);
53+
}
54+
});
55+
transformedRows.push(row);
56+
}
4557

46-
onRowSelect?.(newSelected.map((i) => rows[i]));
47-
setCustomData(newSelected.map((i) => rows[i]));
58+
return { columns: transformedColumns, rows: transformedRows };
4859
};
4960

50-
const graphData =
51-
graph && graph.column
52-
? rows.map((row) => ({ x: row[columns[0].key], y: row[graph.column] }))
53-
: [];
61+
const { columns, rows } = transformFieldsToTableData();
5462

5563
return (
56-
<div>
57-
<Table variant={variant} borders={variant !== "compactBorderless"}>
58-
{caption && (
59-
<Caption>
60-
<div
61-
style={{
62-
display: "flex",
63-
justifyContent: "space-between",
64-
alignItems: "center",
65-
}}
66-
>
67-
<span>{caption}</span>
68-
{actions}
69-
</div>
70-
</Caption>
71-
)}
72-
<Thead>
73-
<Tr>
74-
{selectable && (
75-
<Th>
76-
<input
77-
type="checkbox"
78-
aria-label="Select all rows"
79-
checked={selectedRows.length === rows.length}
80-
onChange={toggleAllRows}
81-
/>
82-
</Th>
83-
)}
84-
{columns.map((col, index) => (
85-
<Th key={index}>{col.label}</Th>
86-
))}
87-
</Tr>
88-
</Thead>
89-
<Tbody>
90-
{rows.map((row, rowIndex) => (
91-
<Tr key={rowIndex} data-testid={`row-${row.id ?? rowIndex}`}>
92-
{selectable && (
93-
<Td>
94-
<input
95-
type="checkbox"
96-
aria-label={`Select row ${rowIndex}`}
97-
checked={selectedRows.includes(rowIndex)}
98-
onChange={() => toggleRow(rowIndex)}
99-
/>
100-
</Td>
101-
)}
102-
{columns.map((col, colIndex) => (
103-
<Td key={colIndex}>{row[col.key]}</Td>
64+
<Card id={id} className={className}>
65+
<CardBody>
66+
<Table variant="compact" borders>
67+
<Caption>{title}</Caption>
68+
<Thead>
69+
<Tr>
70+
{columns.map((col, index) => (
71+
<Th key={index}>{col.label}</Th>
10472
))}
10573
</Tr>
106-
))}
107-
</Tbody>
108-
</Table>
109-
110-
{graph && (
111-
<div style={{ height: "300px" }}>
112-
<Chart
113-
ariaTitle={graph.title || "Chart"}
114-
domainPadding={{ x: [30, 25] }}
115-
height={300}
116-
width={600}
117-
themeColor={ChartThemeColor.multiUnordered}
118-
>
119-
<ChartAxis />
120-
<ChartAxis dependentAxis />
121-
<ChartBar data={graphData} barWidth={30} />
122-
</Chart>
123-
</div>
124-
)}
125-
</div>
74+
</Thead>
75+
<Tbody>
76+
{rows.map((row, rowIndex) => (
77+
<Tr key={rowIndex} data-testid={`row-${rowIndex}`}>
78+
{columns.map((col, colIndex) => (
79+
<Td key={colIndex}>{row[col.key]}</Td>
80+
))}
81+
</Tr>
82+
))}
83+
</Tbody>
84+
</Table>
85+
</CardBody>
86+
</Card>
12687
);
12788
};
12889

12990
export default TableWrapper;
130-
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render, screen } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
3+
4+
import DynamicComponent from "../../components/DynamicComponents";
5+
6+
describe("DynamicComponent", () => {
7+
it("should render table component with fields", () => {
8+
const tableConfig = {
9+
component: "table",
10+
title: "Test Table",
11+
id: "test-table-id",
12+
fields: [
13+
{
14+
name: "Name",
15+
data_path: "user.name",
16+
data: ["John Doe", "Jane Smith"],
17+
},
18+
{
19+
name: "Age",
20+
data_path: "user.age",
21+
data: [28, 34],
22+
},
23+
],
24+
};
25+
26+
render(<DynamicComponent config={tableConfig} />);
27+
28+
// Check that the title appears in the table caption
29+
const tableCaption = screen.getByRole("grid").querySelector("caption");
30+
expect(tableCaption).toHaveTextContent("Test Table");
31+
32+
expect(screen.getByText("Name")).toBeInTheDocument();
33+
expect(screen.getByText("Age")).toBeInTheDocument();
34+
expect(screen.getByText("John Doe")).toBeInTheDocument();
35+
expect(screen.getByText("Jane Smith")).toBeInTheDocument();
36+
expect(screen.getByText("28")).toBeInTheDocument();
37+
expect(screen.getByText("34")).toBeInTheDocument();
38+
});
39+
40+
it("should render one-card component", () => {
41+
const oneCardConfig = {
42+
component: "one-card",
43+
title: "Test Card",
44+
fields: [
45+
{
46+
name: "Field 1",
47+
data_path: "test.field1",
48+
data: ["Value 1"],
49+
},
50+
],
51+
};
52+
53+
render(<DynamicComponent config={oneCardConfig} />);
54+
55+
expect(screen.getByText("Test Card")).toBeInTheDocument();
56+
expect(screen.getByText("Field 1")).toBeInTheDocument();
57+
expect(screen.getByText("Value 1")).toBeInTheDocument();
58+
});
59+
60+
it("should handle empty config", () => {
61+
const { container } = render(<DynamicComponent config={{}} />);
62+
expect(container.firstChild).toBeNull();
63+
});
64+
65+
it("should handle null config", () => {
66+
const { container } = render(<DynamicComponent config={null} />);
67+
expect(container.firstChild).toBeNull();
68+
});
69+
70+
it("should handle unknown component", () => {
71+
const unknownConfig = {
72+
component: "unknown-component",
73+
title: "Unknown",
74+
};
75+
76+
render(<DynamicComponent config={unknownConfig} />);
77+
// Should render FragmentWrapper (empty fragment)
78+
expect(screen.queryByText("Unknown")).not.toBeInTheDocument();
79+
});
80+
});

0 commit comments

Comments
 (0)