sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #08288
[Merge] ~petermakowski/maas-site-manager:display-request-errors-MAASENG-1629 into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:display-request-errors-MAASENG-1629 into maas-site-manager:main.
Commit message:
display request errors
- add vanilla animation utilities
- update TableCaption width on mobile
- rely on automatic Sentry error capturing instead of manual captureException
- remove redundant isFetchedAfterMount
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/442646
# QA Steps
Edit `resolvers.ts` file by replacing lines `46`, `62`,`72` with `return res(ctx.status(400));`
Go to /sites
Verify that the error message (after a number of retries) has been displayed
Go to /settings/tokens
Verify that the error message (after a number of retries) has been displayed
Go to /settings/requests
Verify that the error message (after a number of retries) has been displayed
# Screenshots
https://share.cleanshot.com/SVdcb51c
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:display-request-errors-MAASENG-1629 into maas-site-manager:main.
diff --git a/frontend/src/App.scss b/frontend/src/App.scss
index b904532..cad8a9a 100644
--- a/frontend/src/App.scss
+++ b/frontend/src/App.scss
@@ -45,6 +45,7 @@
@include vf-u-align;
@include vf-u-margin-collapse;
@include vf-u-show;
+@include vf-u-animations;
// layout
@include vf-l-application;
diff --git a/frontend/src/api/handlers.ts b/frontend/src/api/handlers.ts
index 70d4775..3b5c995 100644
--- a/frontend/src/api/handlers.ts
+++ b/frontend/src/api/handlers.ts
@@ -1,5 +1,3 @@
-import * as Sentry from "@sentry/browser";
-
import api from "./api";
import type { Token } from "./types";
import urls from "./urls";
@@ -44,7 +42,7 @@ export const getSites = async (params: GetSitesQueryParams, queryText?: string)
});
return response.data;
} catch (error) {
- Sentry.captureException(new Error("getSites failed", { cause: error }));
+ throw error;
}
};
@@ -71,7 +69,7 @@ export const getTokens = async (params: GetTokensQueryParams) => {
const response = await api.get(urls.tokens, { params });
return response.data;
} catch (error) {
- Sentry.captureException(new Error("getTokens failed", { cause: error }));
+ throw error;
}
};
@@ -92,7 +90,7 @@ export const getEnrollmentRequests = async (params: GetEnrollmentRequestsQueryPa
const response = await api.get(urls.enrollmentRequests, { params });
return response.data;
} catch (error) {
- Sentry.captureException(new Error("getEnrollmentRequests failed", { cause: error }));
+ throw error;
}
};
diff --git a/frontend/src/components/ErrorMessage/ErrorMessage.test.tsx b/frontend/src/components/ErrorMessage/ErrorMessage.test.tsx
new file mode 100644
index 0000000..6900e42
--- /dev/null
+++ b/frontend/src/components/ErrorMessage/ErrorMessage.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from "@testing-library/react";
+
+import ErrorMessage from "./ErrorMessage";
+
+it("renders the error message if error is an instance of Error", () => {
+ const testError = new Error("Test error message");
+ render(<ErrorMessage error={testError} />);
+ expect(screen.getByText("Test error message")).toBeInTheDocument();
+});
+
+it("renders provided error string", () => {
+ const error = "This is a string error";
+ render(<ErrorMessage error={error} />);
+ expect(screen.getByText("This is a string error")).toBeInTheDocument();
+});
+
+it("renders a default error message if no error is provided", () => {
+ render(<ErrorMessage error={undefined} />);
+ expect(screen.getByText("An unknown error has occured")).toBeInTheDocument();
+});
diff --git a/frontend/src/components/ErrorMessage/ErrorMessage.tsx b/frontend/src/components/ErrorMessage/ErrorMessage.tsx
new file mode 100644
index 0000000..6f99272
--- /dev/null
+++ b/frontend/src/components/ErrorMessage/ErrorMessage.tsx
@@ -0,0 +1,5 @@
+const ErrorMessage = ({ error }: { error: unknown }) => (
+ <>{error instanceof Error ? error.message : typeof error === "string" ? error : "An unknown error has occured"}</>
+);
+
+export default ErrorMessage;
diff --git a/frontend/src/components/ErrorMessage/index.ts b/frontend/src/components/ErrorMessage/index.ts
new file mode 100644
index 0000000..3b2ac0c
--- /dev/null
+++ b/frontend/src/components/ErrorMessage/index.ts
@@ -0,0 +1 @@
+export { default } from "./ErrorMessage";
diff --git a/frontend/src/components/RequestsList/RequestsList.tsx b/frontend/src/components/RequestsList/RequestsList.tsx
index c3a3e8c..c86f595 100644
--- a/frontend/src/components/RequestsList/RequestsList.tsx
+++ b/frontend/src/components/RequestsList/RequestsList.tsx
@@ -13,7 +13,7 @@ const Requests: React.FC = () => {
const [totalDataCount, setTotalDataCount] = useState(0);
const { page, debouncedPage, size, handleNextClick, handlePreviousClick, handlePageSizeChange, setPage } =
usePagination(DEFAULT_PAGE_SIZE, totalDataCount);
- const { data, isLoading, isFetchedAfterMount } = useRequestsQuery({
+ const { error, data, isLoading } = useRequestsQuery({
page: `${debouncedPage}`,
size: `${size}`,
});
@@ -42,7 +42,7 @@ const Requests: React.FC = () => {
/>
</Col>
<Col size={12}>
- <RequestsTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
+ <RequestsTable data={data} error={error} isLoading={isLoading} />
</Col>
</Row>
</section>
diff --git a/frontend/src/components/RequestsTable/RequestsTable.test.tsx b/frontend/src/components/RequestsTable/RequestsTable.test.tsx
index 1d2ff3e..90f4700 100644
--- a/frontend/src/components/RequestsTable/RequestsTable.test.tsx
+++ b/frontend/src/components/RequestsTable/RequestsTable.test.tsx
@@ -6,23 +6,21 @@ import { formatUTCDateString } from "@/utils";
it("displays a loading text", () => {
const { rerender } = renderWithMemoryRouter(
- <RequestsTable data={enrollmentRequestQueryResultFactory.build()} isFetchedAfterMount={false} isLoading={true} />,
+ <RequestsTable data={enrollmentRequestQueryResultFactory.build()} error={undefined} isLoading={true} />,
);
const table = screen.getByRole("table", { name: /enrollment requests/i });
expect(table).toBeInTheDocument();
expect(within(table).getByText(/Loading/i)).toBeInTheDocument();
- rerender(
- <RequestsTable data={enrollmentRequestQueryResultFactory.build()} isFetchedAfterMount={true} isLoading={false} />,
- );
+ rerender(<RequestsTable data={enrollmentRequestQueryResultFactory.build()} error={undefined} isLoading={false} />);
expect(within(table).queryByText(/Loading/i)).not.toBeInTheDocument();
});
it("should show a message if there are no open enrolment requests", () => {
renderWithMemoryRouter(
- <RequestsTable data={enrollmentRequestQueryResultFactory.build()} isFetchedAfterMount={true} isLoading={false} />,
+ <RequestsTable data={enrollmentRequestQueryResultFactory.build()} error={undefined} isLoading={false} />,
);
const table = screen.getByRole("table", { name: /enrollment requests/i });
@@ -33,11 +31,7 @@ it("should show a message if there are no open enrolment requests", () => {
it("displays enrollment request in each table row correctly", () => {
const items = enrollmentRequestFactory.buildList(1);
renderWithMemoryRouter(
- <RequestsTable
- data={enrollmentRequestQueryResultFactory.build({ items })}
- isFetchedAfterMount={true}
- isLoading={false}
- />,
+ <RequestsTable data={enrollmentRequestQueryResultFactory.build({ items })} error={undefined} isLoading={false} />,
);
const tableBody = screen.getAllByRole("rowgroup")[1];
diff --git a/frontend/src/components/RequestsTable/RequestsTable.tsx b/frontend/src/components/RequestsTable/RequestsTable.tsx
index 20887c4..34e109f 100644
--- a/frontend/src/components/RequestsTable/RequestsTable.tsx
+++ b/frontend/src/components/RequestsTable/RequestsTable.tsx
@@ -20,9 +20,9 @@ const columnHelper = createColumnHelper<EnrollmentRequest>();
const RequestsTable = ({
data,
- isFetchedAfterMount,
+ error,
isLoading,
-}: Pick<UseEnrollmentRequestsQueryResult, "data" | "isLoading" | "isFetchedAfterMount">) => {
+}: Pick<UseEnrollmentRequestsQueryResult, "data" | "error" | "isLoading">) => {
const { rowSelection, setRowSelection } = useAppContext();
// clear selection on unmount
@@ -106,8 +106,14 @@ const RequestsTable = ({
</tr>
))}
</thead>
- {isLoading && !isFetchedAfterMount ? (
- <caption>Loading...</caption>
+ {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>
diff --git a/frontend/src/components/SitesList/SitesList.tsx b/frontend/src/components/SitesList/SitesList.tsx
index 937bf1d..dde06b2 100644
--- a/frontend/src/components/SitesList/SitesList.tsx
+++ b/frontend/src/components/SitesList/SitesList.tsx
@@ -16,7 +16,7 @@ const SitesList = () => {
const [searchText, setSearchText] = useState("");
const debounceSearchText = useDebounce(searchText);
- const { data, isLoading, isFetchedAfterMount } = useSitesQuery(
+ const { error, data, isLoading } = useSitesQuery(
{ page: `${debouncedPage}`, size: `${size}` },
parseSearchTextToQueryParams(debounceSearchText),
);
@@ -35,7 +35,7 @@ const SitesList = () => {
<div>
<SitesTable
data={data}
- isFetchedAfterMount={isFetchedAfterMount}
+ error={error}
isLoading={isLoading}
paginationProps={{
currentPage: page,
diff --git a/frontend/src/components/SitesList/SitesTable/SitesTable.test.tsx b/frontend/src/components/SitesList/SitesTable/SitesTable.test.tsx
index 068bac2..40a5c6b 100644
--- a/frontend/src/components/SitesList/SitesTable/SitesTable.test.tsx
+++ b/frontend/src/components/SitesList/SitesTable/SitesTable.test.tsx
@@ -22,6 +22,14 @@ const paginationProps = {
setCurrentPage: vi.fn,
totalItems: 1,
};
+const commonProps = {
+ error: undefined,
+ isLoading: false,
+ paginationProps: {
+ ...paginationProps,
+ },
+ setSearchText: vi.fn(),
+};
beforeEach(() => {
vi.useFakeTimers();
@@ -40,14 +48,12 @@ afterAll(() => {
it("displays an empty sites table", () => {
renderWithMemoryRouter(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build()}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
totalItems: 0,
}}
- setSearchText={vi.fn()}
/>,
);
@@ -58,13 +64,11 @@ it("displays rows with details for each site", () => {
const items = siteFactory.buildList(1);
renderWithMemoryRouter(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build({ items, total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
}}
- setSearchText={vi.fn()}
/>,
);
@@ -83,7 +87,7 @@ it("displays correctly paginated results", () => {
renderWithMemoryRouter(
<SitesTable
data={sitesQueryResultFactory.build({ items, total: 100, page: 1, size: pageLength })}
- isFetchedAfterMount={true}
+ error={undefined}
isLoading={false}
paginationProps={{
...paginationProps,
@@ -106,7 +110,7 @@ it("displays correct local time", () => {
renderWithMemoryRouter(
<SitesTable
data={sitesQueryResultFactory.build({ items: [item], total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
+ error={undefined}
isLoading={false}
paginationProps={{
...paginationProps,
@@ -124,7 +128,7 @@ it("displays full name of the country", () => {
renderWithMemoryRouter(
<SitesTable
data={sitesQueryResultFactory.build({ items: [item], total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
+ error={undefined}
isLoading={false}
paginationProps={{
...paginationProps,
@@ -147,13 +151,11 @@ it("displays correct number of deployed machines", () => {
});
renderWithMemoryRouter(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build({ items: [item], total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
}}
- setSearchText={vi.fn()}
/>,
);
@@ -166,13 +168,11 @@ it("if name is not unique a warning is displayed.", async () => {
});
const { rerender } = renderWithMemoryRouter(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build({ items: [itemUnique], total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
}}
- setSearchText={vi.fn()}
/>,
);
@@ -183,13 +183,11 @@ it("if name is not unique a warning is displayed.", async () => {
});
rerender(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build({ items: [itemNonUnique], total: 1, page: 1, size: 1 })}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
}}
- setSearchText={vi.fn()}
/>,
);
@@ -200,15 +198,13 @@ it("displays a pagination bar with the table", () => {
const items = siteFactory.buildList(2);
renderWithMemoryRouter(
<SitesTable
+ {...commonProps}
data={sitesQueryResultFactory.build({ items, total: 2, page: 1, size: 10 })}
- isFetchedAfterMount={true}
- isLoading={false}
paginationProps={{
...paginationProps,
itemsPerPage: 10,
totalItems: 2,
}}
- setSearchText={vi.fn()}
/>,
);
diff --git a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
index 32439cd..f368905 100644
--- a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
+++ b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
@@ -14,6 +14,7 @@ import type { SitesQueryResult } from "@/api/types";
import ExternalLink from "@/components/ExternalLink";
import NoRegions from "@/components/NoRegions";
import SelectAllCheckbox from "@/components/SelectAllCheckbox";
+import TableCaption from "@/components/TableCaption/TableCaption";
import type { PaginationBarProps } from "@/components/base/PaginationBar/PaginationBar";
import PaginationBar from "@/components/base/PaginationBar/PaginationBar";
import TooltipButton from "@/components/base/TooltipButton/TooltipButton";
@@ -33,11 +34,11 @@ export type SitesColumn = Column<Site, unknown>;
const SitesTable = ({
data,
- isFetchedAfterMount,
isLoading,
+ error,
setSearchText,
paginationProps,
-}: Pick<UseSitesQueryResult, "data" | "isLoading" | "isFetchedAfterMount"> & {
+}: Pick<UseSitesQueryResult, "data" | "isLoading" | "error"> & {
setSearchText: (text: string) => void;
paginationProps: PaginationBarProps;
}) => {
@@ -229,17 +230,7 @@ const SitesTable = ({
isLoading={isLoading}
setSearchText={setSearchText}
/>
- <PaginationBar
- currentPage={paginationProps.currentPage}
- dataContext={paginationProps.dataContext}
- handlePageSizeChange={paginationProps.handlePageSizeChange}
- isLoading={paginationProps.isLoading}
- itemsPerPage={paginationProps.itemsPerPage}
- onNextClick={paginationProps.onNextClick}
- onPreviousClick={paginationProps.onPreviousClick}
- setCurrentPage={paginationProps.setCurrentPage}
- totalItems={paginationProps.totalItems}
- />
+ <PaginationBar {...paginationProps} />
<table aria-label="sites" className="sites-table">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
@@ -254,8 +245,14 @@ const SitesTable = ({
</tr>
))}
</thead>
- {isLoading && !isFetchedAfterMount ? (
- <caption>Loading...</caption>
+ {error ? (
+ <TableCaption>
+ <TableCaption.Error error={error} />
+ </TableCaption>
+ ) : isLoading ? (
+ <TableCaption>
+ <TableCaption.Loading />
+ </TableCaption>
) : table.getRowModel().rows.length < 1 ? (
<NoRegions />
) : (
diff --git a/frontend/src/components/TableCaption/TableCaption.test.tsx b/frontend/src/components/TableCaption/TableCaption.test.tsx
new file mode 100644
index 0000000..79bd80e
--- /dev/null
+++ b/frontend/src/components/TableCaption/TableCaption.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from "@testing-library/react";
+
+import TableCaption from "./TableCaption";
+
+test("TableCaption displays children as caption", () => {
+ render(
+ <table>
+ <TableCaption>caption text</TableCaption>
+ </table>,
+ );
+ expect(screen.getByText("caption text")).toBeInTheDocument();
+ expect(screen.getByRole("table", { name: "caption text" })).toBeInTheDocument();
+});
+
+test("TableCaption.Title displays title", () => {
+ render(<TableCaption.Title>Test Title</TableCaption.Title>);
+ expect(screen.getByText("Test Title")).toBeInTheDocument();
+});
+
+test("TableCaption.Title displays title", () => {
+ render(<TableCaption.Description>Test description</TableCaption.Description>);
+ expect(screen.getByText("Test description")).toBeInTheDocument();
+});
+
+test("TableCaption.Error renders error message", () => {
+ const error = new Error("Test error message");
+ render(<TableCaption.Error error={error} />);
+ expect(screen.getByText("Test error message")).toBeInTheDocument();
+});
+
+test("TableCaption.Loading renders spinner with text", () => {
+ render(<TableCaption.Loading />);
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+});
diff --git a/frontend/src/components/TableCaption/TableCaption.tsx b/frontend/src/components/TableCaption/TableCaption.tsx
index e9d1c44..49a297c 100644
--- a/frontend/src/components/TableCaption/TableCaption.tsx
+++ b/frontend/src/components/TableCaption/TableCaption.tsx
@@ -1,5 +1,9 @@
import type { PropsWithChildren } from "react";
+import { Notification, Spinner } from "@canonical/react-components";
+
+import ErrorMessage from "@/components/ErrorMessage";
+
type TableCationProps = PropsWithChildren;
const TableCaption = ({ children }: TableCationProps) => (
@@ -10,7 +14,7 @@ const TableCaption = ({ children }: TableCationProps) => (
const Title = ({ children }: TableCationProps) => (
<div className="row">
- <div className="col-start-large-4 u-align--left col-8 col-medium-4 col-small-3">
+ <div className="col-start-large-4 u-align--left col-8 col-medium-4">
<p className="p-heading--4 u-no-margin--bottom">{children}</p>
</div>
</div>
@@ -18,13 +22,25 @@ const Title = ({ children }: TableCationProps) => (
const Description = ({ children }: TableCationProps) => (
<div className="row">
- <div className="u-align--left col-start-large-4 col-8 col-medium-4 col-small-3">
+ <div className="u-align--left col-start-large-4 col-8 col-medium-4">
<p>{children}</p>
</div>
</div>
);
+const Error = ({ error }: React.ComponentProps<typeof ErrorMessage>) => (
+ <div className="row">
+ <div className="u-align--left col-start-large-4 col-8 col-medium-4">
+ <Notification severity="negative" title={<ErrorMessage error={error} />} />
+ </div>
+ </div>
+);
+
+const Loading = () => <Spinner text="Loading..." />;
+
TableCaption.Title = Title;
TableCaption.Description = Description;
+TableCaption.Loading = Loading;
+TableCaption.Error = Error;
export default TableCaption;
diff --git a/frontend/src/components/TableError/index.ts b/frontend/src/components/TableError/index.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frontend/src/components/TableError/index.ts
diff --git a/frontend/src/components/TokensList/TokensList.tsx b/frontend/src/components/TokensList/TokensList.tsx
index b805a11..12d0f09 100644
--- a/frontend/src/components/TokensList/TokensList.tsx
+++ b/frontend/src/components/TokensList/TokensList.tsx
@@ -21,7 +21,7 @@ const TokensList = () => {
const { page, debouncedPage, size, handleNextClick, handlePreviousClick, handlePageSizeChange, setPage } =
usePagination(DEFAULT_PAGE_SIZE, totalDataCount);
- const { data, isLoading, isFetchedAfterMount, isSuccess } = useTokensQuery({
+ const { error, data, isLoading, isSuccess } = useTokensQuery({
page: `${debouncedPage}`,
size: `${size}`,
});
@@ -109,7 +109,7 @@ const TokensList = () => {
totalItems={data?.total || 0}
/>
</header>
- <TokensTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
+ <TokensTable data={data} error={error} isLoading={isLoading} />
</section>
);
};
diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.test.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.test.tsx
index 289267c..6f983e8 100644
--- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.test.tsx
+++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.test.tsx
@@ -13,14 +13,14 @@ afterEach(() => {
});
it("displays the tokens table", () => {
- render(<TokensTable data={{ items: [], total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items: [], total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
expect(screen.getByRole("table", { name: /tokens/i })).toBeInTheDocument();
});
it("displays rows for each token", () => {
const items = tokenFactory.buildList(1);
- render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
const tableBody = screen.getAllByRole("rowgroup")[1];
expect(within(tableBody).getAllByRole("row")).toHaveLength(items.length);
@@ -31,7 +31,7 @@ it("displays rows for each token", () => {
it("displays a copy button in each row", () => {
const items = tokenFactory.buildList(1);
- render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
const tableBody = screen.getAllByRole("rowgroup")[1];
within(tableBody)
@@ -43,7 +43,7 @@ it("displays a copy button in each row", () => {
it("should display a no-tokens caption if there are no tokens", () => {
const items: Token[] = [];
- render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
expect(screen.getByText(/No tokens available/i)).toBeInTheDocument();
});
@@ -53,7 +53,7 @@ it("displays created date in UTC", () => {
vi.setSystemTime(date);
const items = [tokenFactory.build({ created: "2023-04-21T11:30:00.000Z" })];
- render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
expect(screen.getByText(/2023-04-21 11:30/i)).toBeInTheDocument();
});
@@ -63,7 +63,7 @@ it("displays time until expiration in UTC", () => {
vi.setSystemTime(date);
const items = [tokenFactory.build({ expired: "2023-04-21T14:00:00.000Z" })];
- render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
+ render(<TokensTable data={{ items, total: 0, page: 1, size: 0 }} error={undefined} isLoading={false} />);
expect(screen.getByText(/in 2 hours/i)).toBeInTheDocument();
});
diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
index 3c4d33a..42cbf90 100644
--- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
+++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
@@ -6,6 +6,7 @@ import pick from "lodash/fp/pick";
import type { Token } from "@/api/types";
import SelectAllCheckbox from "@/components/SelectAllCheckbox";
+import TableCaption from "@/components/TableCaption";
import CopyButton from "@/components/base/CopyButton";
import TooltipButton from "@/components/base/TooltipButton";
import { useAppContext } from "@/context";
@@ -20,11 +21,7 @@ const createAccessor =
export type TokenColumnDef = ColumnDef<Token, Partial<Token>>;
export type TokenColumn = Column<Token, unknown>;
-const TokensTable = ({
- data,
- isFetchedAfterMount,
- isLoading,
-}: Pick<useTokensQueryResult, "data" | "isLoading" | "isFetchedAfterMount">) => {
+const TokensTable = ({ data, error, isLoading }: Pick<useTokensQueryResult, "data" | "error" | "isLoading">) => {
const [copiedText, setCopiedText] = useState("");
const { rowSelection, setRowSelection } = useAppContext();
const isTokenCopied = useCallback((token: string) => token === copiedText, [copiedText]);
@@ -135,8 +132,21 @@ const TokensTable = ({
</tr>
))}
</thead>
- {isLoading && !isFetchedAfterMount ? (
- <caption>Loading...</caption>
+ {error ? (
+ <TableCaption>
+ <TableCaption.Error error={error} />
+ </TableCaption>
+ ) : isLoading ? (
+ <TableCaption>
+ <TableCaption.Loading />
+ </TableCaption>
+ ) : tokenTable.getRowModel().rows.length < 1 ? (
+ <TableCaption>
+ <TableCaption.Title>No tokens available</TableCaption.Title>
+ <TableCaption.Description>
+ Generate new tokens and follow the instructions above to enrol MAAS regions.
+ </TableCaption.Description>
+ </TableCaption>
) : (
<tbody>
{tokenTable.getRowModel().rows.map((row) => (
@@ -148,14 +158,6 @@ const TokensTable = ({
))}
</tbody>
)}
- {!isLoading && data?.items.length === 0 ? (
- <caption className="empty-table-caption">
- <div className="empty-table-caption__body">
- <h2>No tokens available</h2>
- <p className="p-heading--4">Generate new tokens and follow the instructions above to enrol MAAS regions.</p>
- </div>
- </caption>
- ) : null}
</table>
);
};
Follow ups