Peter Makowski has proposed merging ~petermakowski/maas-site-manager:header-height-MAASENG-1609 into maas-site-manager:main.
Requested reviews: MAAS Committers (maas-committers) For more details, see: https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/442849 -- Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:header-height-MAASENG-1609 into maas-site-manager:main.
diff --git a/frontend/src/App.scss b/frontend/src/App.scss index cad8a9a..2c1dbd9 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -73,3 +73,4 @@ @import "@/components/base/TablePagination/TablePagination"; @import "@/components/base/PaginationBar/PaginationBar"; @import "@/components/base/TooltipButton/TooltipButton"; +@import "@/components/DynamicTable/DynamicTable"; diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss index d82c0ab..cf65341 100644 --- a/frontend/src/_utils.scss +++ b/frontend/src/_utils.scss @@ -75,3 +75,4 @@ clip: rect(1px, 1px, 1px, 1px) !important; white-space: nowrap !important; } + diff --git a/frontend/src/components/DynamicTable/DynamicTable.test.tsx b/frontend/src/components/DynamicTable/DynamicTable.test.tsx new file mode 100644 index 0000000..5ff7851 --- /dev/null +++ b/frontend/src/components/DynamicTable/DynamicTable.test.tsx @@ -0,0 +1,37 @@ +import { render, fireEvent, waitFor } from "@testing-library/react"; + +import DynamicTable from "./DynamicTable"; + +import BREAKPOINTS from "@/base/breakpoints"; + +const offset = 100; + +beforeAll(() => { + // simulate top offset as JSDOM doesn't support getBoundingClientRect + // - equivalent of another element of height 100px being displayed above the table + vi.spyOn(window.HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({ + bottom: 0, + height: 0, + left: 0, + right: 0, + top: offset, + width: 0, + } as DOMRect); +}); + +it("sets a fixed table body height based on top offset on large screens", async () => { + vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.xSmall); + await fireEvent(window, new Event("resize")); + + const { container } = render(<DynamicTable.Body className="test-class">Test content</DynamicTable.Body>); + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const tbody = container.querySelector("tbody"); + fireEvent(window, new Event("resize")); + + // does not alter the height on small screens + expect(tbody).toHaveStyle("height: undefined"); + + vi.spyOn(window, "innerWidth", "get").mockReturnValue(BREAKPOINTS.large); + await fireEvent(window, new Event("resize")); + await waitFor(() => expect(tbody).toHaveStyle(`height: calc(100vh - ${offset + 1}px)`)); +}); diff --git a/frontend/src/components/DynamicTable/DynamicTable.tsx b/frontend/src/components/DynamicTable/DynamicTable.tsx new file mode 100644 index 0000000..b214fbf --- /dev/null +++ b/frontend/src/components/DynamicTable/DynamicTable.tsx @@ -0,0 +1,50 @@ +import type { PropsWithChildren, RefObject } from "react"; +import { useState, useEffect, useLayoutEffect } from "react"; + +import classNames from "classnames"; + +import BREAKPOINTS from "@/base/breakpoints"; + +const DynamicTable = ({ className, children }: PropsWithChildren<{ className?: string }>) => { + return <table className={classNames("p-table--dynamic", className)}>{children}</table>; +}; + +/** + * sets a fixed height for the table body + * allowing it to be scrolled independently of the page + */ +const DynamicTableBody = ({ className, children }: PropsWithChildren<{ className?: string }>) => { + const tableBodyRef: RefObject<HTMLTableSectionElement> = useRef(null); + const [offset, setOffset] = useState<number | null>(null); + + const handleResize = useCallback(() => { + if (window.innerWidth > BREAKPOINTS.small) { + const top = tableBodyRef.current?.getBoundingClientRect?.().top; + if (top) setOffset(top + 1); + } else { + setOffset(null); + } + }, []); + + useLayoutEffect(() => { + handleResize(); + }, [handleResize]); + + useEffect(() => { + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [handleResize]); + + return ( + <tbody + className={className} + ref={tableBodyRef} + style={offset ? { height: `calc(100vh - ${offset}px)`, minHeight: `calc(100vh - ${offset}px)` } : undefined} + > + {children} + </tbody> + ); +}; +DynamicTable.Body = DynamicTableBody; + +export default DynamicTable; diff --git a/frontend/src/components/DynamicTable/_DynamicTable.scss b/frontend/src/components/DynamicTable/_DynamicTable.scss new file mode 100644 index 0000000..9b60ac2 --- /dev/null +++ b/frontend/src/components/DynamicTable/_DynamicTable.scss @@ -0,0 +1,23 @@ +.p-table--dynamic { + margin-bottom: 0; + + + thead, tbody { + display: block; + overflow-x: hidden; + overflow-y: auto; + scrollbar-gutter: stable; +} + + tbody { + height: auto; + min-height: auto; + } + + thead tr, + tbody tr { + display: table; + table-layout: fixed; + width: 100%; + } +} diff --git a/frontend/src/components/DynamicTable/index.ts b/frontend/src/components/DynamicTable/index.ts new file mode 100644 index 0000000..51b2226 --- /dev/null +++ b/frontend/src/components/DynamicTable/index.ts @@ -0,0 +1 @@ +export { default } from "./DynamicTable"; diff --git a/frontend/src/components/RequestsTable/RequestsTable.tsx b/frontend/src/components/RequestsTable/RequestsTable.tsx index 34e109f..c9ade99 100644 --- a/frontend/src/components/RequestsTable/RequestsTable.tsx +++ b/frontend/src/components/RequestsTable/RequestsTable.tsx @@ -6,6 +6,7 @@ import type { Column, ColumnDef } from "@tanstack/react-table"; import type { EnrollmentRequest } from "@/api/types"; import docsUrls from "@/base/docsUrls"; import DateTime from "@/components/DateTime"; +import DynamicTable from "@/components/DynamicTable/DynamicTable"; import ExternalLink from "@/components/ExternalLink"; import SelectAllCheckbox from "@/components/SelectAllCheckbox"; import TableCaption from "@/components/TableCaption"; @@ -91,57 +92,55 @@ const RequestsTable = ({ }); return ( - <> - <table aria-label="enrollment requests" className="sites-table"> - <thead> - {table.getHeaderGroups().map((headerGroup) => ( - <tr key={headerGroup.id}> - {headerGroup.headers.map((header) => { - return ( - <th className={`${header.column.id}`} colSpan={header.colSpan} key={header.id}> - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - </th> - ); - })} - </tr> - ))} - </thead> - {error ? ( - <TableCaption> - <TableCaption.Error error={error} /> - </TableCaption> - ) : isLoading ? ( - <TableCaption> - <TableCaption.Loading /> - </TableCaption> - ) : table.getRowModel().rows.length < 1 ? ( - <TableCaption> - <TableCaption.Title>No outstanding requests</TableCaption.Title> - <TableCaption.Description> - You have to request an enrolment in the site-manager-agent. - <br /> - <ExternalLink to={docsUrls.enrollmentRequest}>Read more about it in the documentation.</ExternalLink> - </TableCaption.Description> - </TableCaption> - ) : ( - <tbody> - {table.getRowModel().rows.map((row) => { + <DynamicTable aria-label="enrollment requests" className="sites-table"> + <thead> + {table.getHeaderGroups().map((headerGroup) => ( + <tr key={headerGroup.id}> + {headerGroup.headers.map((header) => { return ( - <tr key={row.id}> - {row.getVisibleCells().map((cell) => { - return ( - <td className={`${cell.column.id}`} key={cell.id}> - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </td> - ); - })} - </tr> + <th className={`${header.column.id}`} colSpan={header.colSpan} key={header.id}> + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + </th> ); })} - </tbody> - )} - </table> - </> + </tr> + ))} + </thead> + {error ? ( + <TableCaption> + <TableCaption.Error error={error} /> + </TableCaption> + ) : isLoading ? ( + <TableCaption> + <TableCaption.Loading /> + </TableCaption> + ) : table.getRowModel().rows.length < 1 ? ( + <TableCaption> + <TableCaption.Title>No outstanding requests</TableCaption.Title> + <TableCaption.Description> + You have to request an enrolment in the site-manager-agent. + <br /> + <ExternalLink to={docsUrls.enrollmentRequest}>Read more about it in the documentation.</ExternalLink> + </TableCaption.Description> + </TableCaption> + ) : ( + <DynamicTable.Body> + {table.getRowModel().rows.map((row) => { + return ( + <tr key={row.id}> + {row.getVisibleCells().map((cell) => { + return ( + <td className={`${cell.column.id}`} key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </td> + ); + })} + </tr> + ); + })} + </DynamicTable.Body> + )} + </DynamicTable> ); }; diff --git a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx index f368905..c211493 100644 --- a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx +++ b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx @@ -11,6 +11,7 @@ import ConnectionInfo from "./ConnectionInfo"; import SitesTableControls from "./SitesTableControls/SitesTableControls"; import type { SitesQueryResult } from "@/api/types"; +import DynamicTable from "@/components/DynamicTable/DynamicTable"; import ExternalLink from "@/components/ExternalLink"; import NoRegions from "@/components/NoRegions"; import SelectAllCheckbox from "@/components/SelectAllCheckbox"; @@ -231,7 +232,7 @@ const SitesTable = ({ setSearchText={setSearchText} /> <PaginationBar {...paginationProps} /> - <table aria-label="sites" className="sites-table"> + <DynamicTable aria-label="sites" className="sites-table" id="sites-table"> <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> @@ -256,7 +257,7 @@ const SitesTable = ({ ) : table.getRowModel().rows.length < 1 ? ( <NoRegions /> ) : ( - <tbody> + <DynamicTable.Body> {table.getRowModel().rows.map((row) => { return ( <tr @@ -273,9 +274,9 @@ const SitesTable = ({ </tr> ); })} - </tbody> + </DynamicTable.Body> )} - </table> + </DynamicTable> </> ); }; diff --git a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss index 03a559d..dacb224 100644 --- a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss +++ b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss @@ -1,7 +1,8 @@ $connection-status-icon-width: 1.5rem; .sites-table { - thead th:first-child { + thead th:first-child, + tbody td:first-child { width: 3rem; } diff --git a/frontend/src/components/TokensList/_TokensList.scss b/frontend/src/components/TokensList/_TokensList.scss index 3d0d8d5..a3593d1 100644 --- a/frontend/src/components/TokensList/_TokensList.scss +++ b/frontend/src/components/TokensList/_TokensList.scss @@ -6,17 +6,11 @@ $instructions-height-medium: 10.9375rem; display: grid; @media only screen and (min-width: $breakpoint-small) { - height: $header-height-medium; - position: sticky; - top: -#{$spv--medium}; background-color: white; z-index: 1; padding-top: $spv--medium; } - @media only screen and (min-width: $breakpoint-large) { - height: $header-height-large; - } .tokens-list-certificate { display: grid; diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx index 42cbf90..a250c3b 100644 --- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx +++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx @@ -1,10 +1,13 @@ +import type { PropsWithChildren } from "react"; import { useCallback, useMemo, useState } from "react"; import type { ColumnDef, Column, Row, Getter } from "@tanstack/react-table"; import { flexRender, useReactTable, getCoreRowModel } from "@tanstack/react-table"; +import classNames from "classnames"; import pick from "lodash/fp/pick"; import type { Token } from "@/api/types"; +import DynamicTable from "@/components/DynamicTable"; import SelectAllCheckbox from "@/components/SelectAllCheckbox"; import TableCaption from "@/components/TableCaption"; import CopyButton from "@/components/base/CopyButton"; @@ -119,13 +122,14 @@ const TokensTable = ({ data, error, isLoading }: Pick<useTokensQueryResult, "dat enableMultiRowSelection: true, onRowSelectionChange: setRowSelection, }); + return ( - <table aria-label="tokens" className="tokens-table"> + <DynamicTable aria-label="tokens" className="tokens-table u-no-margin--bottom"> <thead> {tokenTable.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( - <th colSpan={header.colSpan} key={header.id}> + <th className={`tokens-table__col-${header.column.id}`} colSpan={header.colSpan} key={header.id}> {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} </th> ))} @@ -148,17 +152,21 @@ const TokensTable = ({ data, error, isLoading }: Pick<useTokensQueryResult, "dat </TableCaption.Description> </TableCaption> ) : ( - <tbody> + <DynamicTable.Body> {tokenTable.getRowModel().rows.map((row) => ( <tr key={row.id}> {row.getVisibleCells().map((cell) => { - return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>; + return ( + <td className={`tokens-table__col-${cell.column.id}`} key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </td> + ); })} </tr> ))} - </tbody> + </DynamicTable.Body> )} - </table> + </DynamicTable> ); }; diff --git a/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss b/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss index 7835c4c..2c8587c 100644 --- a/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss +++ b/frontend/src/components/TokensList/components/TokensTable/_TokensTable.scss @@ -5,49 +5,45 @@ } thead th { - position: sticky; top: -#{$spv--medium}; - background-color: white; - z-index: 1; padding-top: $spv--large; - - @media only screen and (min-width: $breakpoint-small){ - top: calc($header-height-medium - $spv--medium); - } - - @media only screen and (min-width: $breakpoint-large) { - top: calc($header-height-large - $spv--medium); - } } - thead th:first-child { + thead th:first-child, + tbody td:first-child { width: 3rem; } - thead th:last-child { + thead th:last-child, + tbody td:last-child { width: 15rem; } - thead th:nth-child(3) { + thead th:nth-child(3), + tbody td:nth-child(3) { width: 15rem; } @media screen and (min-width: $breakpoint-small) and (max-width: $breakpoint-large) { - thead th:last-child { + thead th:last-child, + tbody td:last-child { width: 10rem; } - thead th:nth-child(3) { + thead th:nth-child(3), + tbody td:nth-child(3) { width: 10rem; } } @media screen and (max-width: $breakpoint-small) { - thead th:last-child { + thead th:last-child, + tbody td:last-child { width: auto; } - thead th:nth-child(3) { + thead th:nth-child(3), + tbody td:nth-child(3) { width: auto; } }
-- Mailing list: https://launchpad.net/~sts-sponsors Post to : sts-sponsors@lists.launchpad.net Unsubscribe : https://launchpad.net/~sts-sponsors More help : https://help.launchpad.net/ListHelp