sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #08410
[Merge] ~petermakowski/maas-site-manager:header-height-MAASENG-1609 into maas-site-manager:main
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;
}
}
Follow ups