Skip to content

Commit

Permalink
feat(zones): Migrate zones list to generic table MAASENG-4306 (#5588)
Browse files Browse the repository at this point in the history
  • Loading branch information
abuyukyi101198 authored Jan 23, 2025
1 parent 78b4551 commit e953942
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 218 deletions.
4 changes: 0 additions & 4 deletions src/app/base/components/GenericTable/GenericTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ describe("GenericTable", () => {

const mockFilterCells = vi.fn(() => true);
const mockFilterHeaders = vi.fn(() => true);
const mockGetRowId = vi.fn((row) => row.id.toString());

it("renders table with headers and rows", () => {
render(
Expand All @@ -76,7 +75,6 @@ describe("GenericTable", () => {
data={data}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
rowSelection={{}}
setRowSelection={vi.fn}
/>
Expand All @@ -96,7 +94,6 @@ describe("GenericTable", () => {
data={[]}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
noData={<span>No data</span>}
rowSelection={{}}
setRowSelection={vi.fn}
Expand All @@ -113,7 +110,6 @@ describe("GenericTable", () => {
data={data}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
rowSelection={{}}
setRowSelection={vi.fn}
/>
Expand Down
135 changes: 83 additions & 52 deletions src/app/base/components/GenericTable/GenericTable.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,124 @@
import type { Dispatch, ReactNode, SetStateAction } from "react";
import { useMemo, useState } from "react";
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useMemo,
useState,
} from "react";

import { DynamicTable } from "@canonical/maas-react-components";
import { Button } from "@canonical/react-components";
import type {
Column,
Row,
ColumnDef,
ColumnSort,
ExpandedState,
GroupingState,
ExpandedState,
SortingState,
Header,
Row,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getExpandedRowModel,
getGroupedRowModel,
useReactTable,
flexRender,
} from "@tanstack/react-table";
import classNames from "classnames";

import TableCheckbox from "@/app/base/components/GenericTable/TableCheckbox";
import TableHeader from "@/app/base/components/GenericTable/TableHeader";

import "./_index.scss";
import SortingIndicator from "./SortingIndicator";

type GenericTableProps<T> = {
ariaLabel?: string;
canSelect?: boolean;
columns: ColumnDef<T, Partial<T>>[];
data: T[];
filterCells: (row: Row<T>, column: Column<T>) => boolean;
filterHeaders: (header: Header<T, unknown>) => boolean;
getRowId: (
originalRow: T,
index: number,
parent?: Row<T> | undefined
) => string;
filterCells?: (row: Row<T>, column: Column<T>) => boolean;
filterHeaders?: (header: Header<T, unknown>) => boolean;
groupBy?: string[];
noData?: ReactNode;
pin?: { value: string; isTop: boolean }[];
sortBy?: ColumnSort[];
rowSelection: RowSelectionState;
rowSelection?: RowSelectionState;
setRowSelection?: Dispatch<SetStateAction<RowSelectionState>>;
};

const GenericTable = <T,>({
ariaLabel,
canSelect = false,
columns,
data,
filterCells,
filterHeaders,
getRowId,
filterCells = () => true,
filterHeaders = () => true,
groupBy,
sortBy,
noData,
pin,
sortBy,
rowSelection,
setRowSelection,
}: GenericTableProps<T>) => {
const [grouping, setGrouping] = useState<GroupingState>(groupBy ?? []);
const [expanded, setExpanded] = useState<ExpandedState>(true);
const [sorting, setSorting] = useState<SortingState>(sortBy ?? []);

const sortedData = useMemo(() => {
if (canSelect) {
columns = [
{
id: "select",
accessorKey: "id",
enableSorting: false,
header: "",
cell: ({ row }) =>
!row.getIsGrouped() ? <TableCheckbox row={row} /> : null,
},
...columns,
];

if (groupBy) {
columns = [
{
id: "group-select",
accessorKey: "id",
enableSorting: false,
header: ({ table }) => <TableCheckbox.All table={table} />,
cell: ({ row }) =>
row.getIsGrouped() ? <TableCheckbox.Group row={row} /> : null,
},
...columns,
];
}
}

data = useMemo(() => {
return [...data].sort((a, b) => {
if (pin && pin.length > 0 && grouping.length > 0) {
for (const { value, isTop } of pin) {
const groupId = grouping[0];
const aValue = a[groupId as keyof typeof a];
const bValue = b[groupId as keyof typeof b];

if (aValue === value && bValue !== value) {
return isTop ? -1 : 1;
}
if (bValue === value && aValue !== value) {
return isTop ? 1 : -1;
}
}
}

for (const groupId of grouping) {
const aGroupValue = a[groupId as keyof typeof a];
const bGroupValue = b[groupId as keyof typeof b];
if (aGroupValue < bGroupValue) {
return -1;
}
if (aGroupValue > bGroupValue) {
return 1;
}
}

for (const { id, desc } of sorting) {
const aValue = a[id as keyof typeof a];
const bValue = b[id as keyof typeof b];
Expand All @@ -75,10 +131,10 @@ const GenericTable = <T,>({
}
return 0;
});
}, [data, sorting]);
}, [data, sorting, grouping, pin]);

const table = useReactTable<T>({
data: sortedData,
data,
columns,
state: {
grouping,
Expand All @@ -101,40 +157,15 @@ const GenericTable = <T,>({
groupedColumnMode: false,
enableRowSelection: true,
enableMultiRowSelection: true,
getRowId,
});

return (
<DynamicTable
aria-label={ariaLabel}
className="p-table-dynamic--with-select generic-table"
variant={"full-height"}
>
<DynamicTable className="p-generic-table" variant="full-height">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.filter(filterHeaders).map((header) => (
<th className={classNames(`${header.column.id}`)} key={header.id}>
{header.column.getCanSort() ? (
<Button
appearance="link"
className="p-button--table-header"
onClick={header.column.getToggleSortingHandler()}
type="button"
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
<SortingIndicator header={header} />
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</th>
<TableHeader header={header} key={header.id} />
))}
</tr>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const TableAllCheckbox = <T,>({ table, ...props }: TableCheckboxProps<T>) => {
}

return (
<label className="p-checkbox--inline">
<label className="p-checkbox--inline p-table-checkbox--all">
<input
aria-checked={checked}
aria-label="select all"
Expand Down Expand Up @@ -54,7 +54,7 @@ const TableGroupCheckbox = <T,>({ row, ...props }: TableCheckboxProps<T>) => {
const isSomeSubRowsSelected =
!row.getIsAllSubRowsSelected() && row.getIsSomeSelected();
return (
<label className="p-checkbox--inline">
<label className="p-checkbox--inline p-table-checkbox--group">
<input
aria-checked={isSomeSubRowsSelected ? "mixed" : undefined}
className="p-checkbox__input"
Expand Down Expand Up @@ -84,7 +84,7 @@ const TableCheckbox = <T,>({ row, ...props }: TableCheckboxProps<T>) => {
return null;
}
return (
<label className="p-checkbox--inline">
<label className="p-checkbox--inline p-table-checkbox">
<input
className="p-checkbox__input"
type="checkbox"
Expand Down
32 changes: 32 additions & 0 deletions src/app/base/components/GenericTable/TableHeader/TableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Button } from "@canonical/react-components";
import type { Header } from "@tanstack/react-table";
import { flexRender } from "@tanstack/react-table";
import classNames from "classnames";

import SortingIndicator from "@/app/base/components/GenericTable/SortingIndicator";

type TableHeaderProps<T> = {
header: Header<T, unknown>;
};

const TableHeader = <T,>({ header }: TableHeaderProps<T>) => {
return (
<th className={classNames(`${header.column.id}`)} key={header.id}>
{header.column.getCanSort() ? (
<Button
appearance="link"
className="p-button--table-header"
onClick={header.column.getToggleSortingHandler()}
type="button"
>
{flexRender(header.column.columnDef.header, header.getContext())}
<SortingIndicator header={header} />
</Button>
) : (
flexRender(header.column.columnDef.header, header.getContext())
)}
</th>
);
};

export default TableHeader;
1 change: 1 addition & 0 deletions src/app/base/components/GenericTable/TableHeader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./TableHeader";
15 changes: 7 additions & 8 deletions src/app/base/components/GenericTable/_index.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
.generic-table {
table-layout: auto;
th:not(th:nth-of-type(1), th:nth-of-type(2), th:last-of-type) {
padding-left: 1.5rem;
.p-generic-table {
thead, tbody {
display: table-row-group;
width: 100%;
}

.individual-row {
width: calc(100% - 1rem);
margin-left: 1rem;
.group-select, .select {
width: 2rem;
}
}
}
14 changes: 9 additions & 5 deletions src/app/images/components/SMImagesTable/SMImagesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import type { RowSelectionState } from "@tanstack/react-table";
import { useSelector } from "react-redux";

import ImagesTableHeader from "./ImagesTableHeader";
import useImageTableColumns, {
filterCells,
filterHeaders,
} from "./useImageTableColumns/useImageTableColumns";

import GenericTable from "@/app/base/components/GenericTable";
import { useSidePanel } from "@/app/base/side-panel-context";
import useImageTableColumns, {
filterCells,
filterHeaders,
} from "@/app/images/components/SMImagesTable/useImageTableColumns/useImageTableColumns";
import { ImageSidePanelViews } from "@/app/images/constants";
import type { Image } from "@/app/images/types";
import bootResourceSelectors from "@/app/store/bootresource/selectors";
Expand Down Expand Up @@ -80,11 +80,11 @@ export const SMImagesTable: React.FC = () => {
/>
)}
<GenericTable
canSelect
columns={columns}
data={images}
filterCells={filterCells}
filterHeaders={filterHeaders}
getRowId={(row) => `${row.id}`}
groupBy={["name"]}
noData={
<TableCaption>
Expand All @@ -96,6 +96,10 @@ export const SMImagesTable: React.FC = () => {
</TableCaption.Description>
</TableCaption>
}
pin={[
{ value: "Ubuntu", isTop: true },
{ value: "Other", isTop: false },
]}
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
sortBy={[{ id: "release", desc: true }]}
Expand Down
2 changes: 1 addition & 1 deletion src/app/images/components/SMImagesTable/_index.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.generic-table {
.p-generic-table {
.status {
width: 15rem;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { vi } from "vitest";

import useImageTableColumns from "@/app/images/components/SMImagesTable/useImageTableColumns/useImageTableColumns";
import type { Image } from "@/app/images/types";
import { screen, renderHook, render } from "@/testing/utils";
import { renderHook } from "@/testing/utils";

vi.mock("@/context", async () => {
const actual = await vi.importActual("@/context");
Expand Down Expand Up @@ -41,7 +41,6 @@ it("returns the correct number of columns", () => {
const { result } = setupTestCase();
expect(result.current).toBeInstanceOf(Array);
expect(result.current.map((column) => column.id)).toStrictEqual([
"select",
"name",
"release",
"architecture",
Expand All @@ -53,15 +52,3 @@ it("returns the correct number of columns", () => {
"action",
]);
});

it("input has correct accessible label", () => {
const { result, props } = setupTestCase("Ubuntu");

const selectColumn = result.current.find((column) => column.id === "select");
// @ts-ignore-next-line
const cellValue = selectColumn.cell(props);
render(cellValue);

const inputElement = screen.getByRole("checkbox");
expect(inputElement).toHaveAccessibleName("Ubuntu");
});
Loading

0 comments on commit e953942

Please sign in to comment.