Skip to content

Commit 62e2dc2

Browse files
committed
feat: Use Store API pagination for serial logs table
1 parent 5194b4a commit 62e2dc2

8 files changed

Lines changed: 150 additions & 38 deletions

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ canonicalwebteam.discourse==7.2.0
66
canonicalwebteam.blog==6.8.4
77
canonicalwebteam.search==2.1.2
88
canonicalwebteam.image-template==1.9.0
9-
canonicalwebteam.store-api==7.8.2
9+
canonicalwebteam.store-api==7.8.3
1010
canonicalwebteam.launchpad==0.9.0
1111
django-openid-auth==0.17
1212
Flask-OpenID==1.3.1

static/js/publisher/hooks/__tests__/useSerialLogs.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ describe("useSerialLogs", () => {
188188
const { result } = renderHook(
189189
() =>
190190
useSerialLogs("test-brand-id", "test-model", {
191-
nextPage: "nextpagecursor",
191+
page: "nextpagecursor",
192192
}),
193193
{
194194
wrapper: createWrapper(),
@@ -235,7 +235,7 @@ describe("useSerialLogs", () => {
235235
endTime: "2026-03-28T04:00:23.875000",
236236
},
237237
pageSize: 10,
238-
nextPage: "nextpagecursor",
238+
page: "nextpagecursor",
239239
}),
240240
{
241241
wrapper: createWrapper(),

static/js/publisher/hooks/useSerialLogs.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ const useSerialLogs = (
55
brandId: string | undefined,
66
modelId: string | undefined,
77
urlSearchParams?: {
8+
page?: string | null;
89
pageSize?: number;
9-
nextPage?: string;
1010
interval?: {
1111
startTime: string;
1212
endTime: string;
@@ -19,7 +19,7 @@ const useSerialLogs = (
1919
);
2020

2121
if (urlSearchParams) {
22-
const { interval, pageSize, nextPage } = urlSearchParams;
22+
const { interval, pageSize, page } = urlSearchParams;
2323

2424
if (interval) {
2525
url.searchParams.set("start-time", interval.startTime);
@@ -30,15 +30,22 @@ const useSerialLogs = (
3030
url.searchParams.set("page-size", pageSize.toString());
3131
}
3232

33-
if (nextPage) {
34-
url.searchParams.set("next-page", nextPage);
33+
if (page) {
34+
url.searchParams.set("next-page", page);
3535
}
3636
}
3737

3838
return useQuery<ApiResponse<SerialLogResponse>, Error>({
39-
queryKey: ["serials", brandId, modelId, urlSearchParams],
39+
queryKey: [
40+
"serials",
41+
brandId,
42+
modelId,
43+
urlSearchParams?.page,
44+
urlSearchParams?.pageSize,
45+
urlSearchParams?.interval,
46+
],
4047
queryFn: async () => {
41-
const response = await fetch(url.toString());
48+
const response = await fetch(url);
4249
const responseData = await response.json();
4350

4451
return responseData;

static/js/publisher/layouts/ModelDetailsPageLayout/ModelNav.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Link, useParams } from "react-router-dom";
22
import { useAtomValue } from "jotai";
33

4-
import { useRemodels } from "../../hooks";
4+
import { useRemodels, useSerialLogs } from "../../hooks";
55
import { brandIdState } from "../../state/brandStoreState";
66

77
function ModelNav({ sectionName }: { sectionName: string }): React.JSX.Element {
88
const { id, modelId } = useParams();
99
const brandId = useAtomValue(brandIdState);
1010
const { data: remodelsData } = useRemodels(brandId);
11+
const { data: serialLogsData } = useSerialLogs(brandId, modelId);
1112

1213
return (
1314
<nav className="p-tabs">
@@ -44,6 +45,18 @@ function ModelNav({ sectionName }: { sectionName: string }): React.JSX.Element {
4445
</Link>
4546
</li>
4647
)}
48+
{serialLogsData?.success && (
49+
<li className="p-tabs__item">
50+
<Link
51+
to={`/admin/${id}/models/${modelId}/serial-log`}
52+
className="p-tabs__link"
53+
aria-selected={sectionName === "serial-log"}
54+
role="tab"
55+
>
56+
Serial log
57+
</Link>
58+
</li>
59+
)}
4760
</ul>
4861
</nav>
4962
);

static/js/publisher/pages/Remodel/Remodel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ function Remodel(): React.JSX.Element {
7070
// because otherwise the cursor history gets out of sync
7171
setCurrentCursor(null);
7272
cursorHistory.current = [];
73-
setSearchParams({ "page-size": newPageSize.toString() });
73+
setSearchParams((params) => {
74+
params.set("page-size", newPageSize.toString());
75+
return params;
76+
});
7477
};
7578

7679
brandStore

static/js/publisher/pages/SerialLog/SerialLog.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useEffect } from "react";
1+
import { useEffect, useState, useRef } from "react";
22
import { useAtomValue, useSetAtom } from "jotai";
3-
import { useParams } from "react-router-dom";
3+
import { useParams, useSearchParams } from "react-router-dom";
44
import { Notification, Icon } from "@canonical/react-components";
55

66
import { useSerialLogs } from "../../hooks";
@@ -15,7 +15,23 @@ import type { SerialLogResponse, ApiResponse } from "../../types/shared";
1515

1616
function SerialLog(): React.JSX.Element {
1717
const { id, modelId } = useParams();
18+
const [searchParams, setSearchParams] = useSearchParams();
1819
const brandId = useAtomValue(brandIdState);
20+
const [currentCursor, setCurrentCursor] = useState<string | null>(null);
21+
const [nextCursor, setNextCursor] = useState<string | null>(null);
22+
const cursorHistory = useRef<Array<string | null>>([]);
23+
24+
const pageSizeParam = searchParams.get("page-size");
25+
const parsedPageSize = pageSizeParam ? parseInt(pageSizeParam) : NaN;
26+
const startTime = searchParams.get("start-time");
27+
const endTime = searchParams.get("end-time");
28+
const pageSize = Number.isInteger(parsedPageSize) ? parsedPageSize : 25;
29+
const params = {
30+
pageSize: pageSize,
31+
page: currentCursor,
32+
...(startTime && endTime && { interval: { startTime, endTime } }),
33+
};
34+
1935
const {
2036
isLoading,
2137
isError,
@@ -24,17 +40,44 @@ function SerialLog(): React.JSX.Element {
2440
}: UseQueryResult<ApiResponse<SerialLogResponse>, Error> = useSerialLogs(
2541
brandId,
2642
modelId,
43+
params,
2744
);
2845
const setSerialLogs = useSetAtom(serialLogsListState);
2946
const brandStore = useAtomValue(brandStoreState(id));
3047

48+
const handlePageForward = () => {
49+
cursorHistory.current.push(currentCursor);
50+
setCurrentCursor(nextCursor);
51+
};
52+
53+
const handlePageBack = () => {
54+
const lastCursor = cursorHistory.current.pop();
55+
setCurrentCursor(lastCursor || null);
56+
};
57+
58+
const handlePageSizeChange = (newPageSize: number) => {
59+
// Need to reset current page when changing page size
60+
// because otherwise the cursor history gets out of sync
61+
setCurrentCursor(null);
62+
cursorHistory.current = [];
63+
setSearchParams((params) => {
64+
params.set("page-size", newPageSize.toString());
65+
return params;
66+
});
67+
};
68+
3169
brandStore
3270
? setPageTitle(`Serial logs in ${brandStore.name}`)
3371
: setPageTitle("Serial logs");
3472

3573
useEffect(() => {
36-
if (!isLoading && !isError && data) {
74+
if (isLoading && isError) {
75+
return;
76+
}
77+
78+
if (data) {
3779
setSerialLogs(data.data?.items || []);
80+
setNextCursor(data.data?.["next-cursor"] || null);
3881
}
3982
}, [isLoading, isError, data]);
4083

@@ -57,7 +100,16 @@ function SerialLog(): React.JSX.Element {
57100
</Notification>
58101
) : (
59102
<div className="u-flex-column u-flex-grow">
60-
<SerialLogTable />
103+
<SerialLogTable
104+
handlePageForward={handlePageForward}
105+
handlePageBack={handlePageBack}
106+
handlePageSizeChange={handlePageSizeChange}
107+
forwardDisabled={!nextCursor}
108+
backDisabled={
109+
cursorHistory.current.length < 1 || currentCursor === null
110+
}
111+
pageSize={pageSize}
112+
/>
61113
</div>
62114
)}
63115
</div>

static/js/publisher/pages/SerialLog/SerialLogTable.tsx

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
import { useAtomValue } from "jotai";
22
import { useParams } from "react-router-dom";
3-
import { MainTable, TablePagination } from "@canonical/react-components";
3+
import {
4+
MainTable,
5+
TablePaginationControls,
6+
} from "@canonical/react-components";
47
import { format } from "date-fns";
58

69
import { brandStoreState } from "../../state/brandStoreState";
710
import { serialLogsListState } from "../../state/serialLogsState";
811

912
import type { SerialLog } from "../../types/shared";
1013

11-
function SerialLogTable(): React.JSX.Element {
14+
type Props = {
15+
handlePageForward: () => void;
16+
handlePageBack: () => void;
17+
handlePageSizeChange: (arg: number) => void;
18+
forwardDisabled: boolean;
19+
backDisabled: boolean;
20+
pageSize: number;
21+
};
22+
23+
function SerialLogTable({
24+
handlePageForward,
25+
handlePageBack,
26+
handlePageSizeChange,
27+
forwardDisabled,
28+
backDisabled,
29+
pageSize,
30+
}: Props): React.JSX.Element {
1231
const { id } = useParams();
1332
const serialLogs = useAtomValue(serialLogsListState);
1433
const brandStore = useAtomValue(brandStoreState(id));
1534

1635
const headers = [
17-
{ content: "Brand" },
18-
{ content: "Model" },
19-
{ content: "Serial" },
36+
{ content: "Brand", className: "u-truncate" },
37+
{ content: "Model", className: "u-truncate" },
38+
{ content: "Serial", className: "u-truncate" },
2039
{
2140
content: "Created date",
22-
className: "u-align--right",
41+
className: "u-align--right u-truncate",
2342
},
2443
];
2544

@@ -38,17 +57,37 @@ function SerialLogTable(): React.JSX.Element {
3857
});
3958

4059
return (
41-
<TablePagination
42-
data={rows}
43-
pageLimits={[25, 50, 100, 200]}
44-
position="below"
45-
>
60+
<>
4661
<MainTable
4762
data-testid="serial-log-table"
4863
emptyStateMsg="No serial logs found"
4964
headers={headers}
65+
rows={rows}
66+
/>
67+
<TablePaginationControls
68+
// Although we don't know the current page as we don't know
69+
// how many pages there are, the `currentPage` value is required, and
70+
// must be > 0, to remove the "of more than x rows" in the visible
71+
// count section. See:
72+
// https://canonical.github.io/react-components/?path=/story/components-tablepagination--controls-with-partially-known-entries
73+
currentPage={1}
74+
itemName="serial log"
75+
nextButtonProps={{
76+
disabled: forwardDisabled,
77+
}}
78+
onNextPage={handlePageForward}
79+
onPageSizeChange={handlePageSizeChange}
80+
onPreviousPage={handlePageBack}
81+
pageLimits={[10, 25, 50, 100, 500]}
82+
pageSize={pageSize}
83+
previousButtonProps={{
84+
disabled: backDisabled,
85+
}}
86+
showPageInput={false}
87+
visibleCount={rows.length}
88+
className="table-pagination-controls"
5089
/>
51-
</TablePagination>
90+
</>
5291
);
5392
}
5493

webapp/endpoints/models.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -473,21 +473,19 @@ def delete_remodel_allowlist(store_id: str):
473473
@login_required
474474
@exchange_required
475475
def get_serial_log(store_id: str, model_name: str):
476-
start_time = flask.request.args.get("start-time")
477-
end_time = flask.request.args.get("end-time")
478-
page_size = flask.request.args.get("page-size")
479-
cursor = flask.request.args.get("next-page")
476+
params = {
477+
# rename `cursor` to `page` on our side for clarity
478+
"cursor": flask.request.args.get("page"),
479+
"start_time": flask.request.args.get("start-time"),
480+
"end_time": flask.request.args.get("end-time"),
481+
"page_size": flask.request.args.get("page-size"),
482+
}
483+
480484
res = {}
481485

482486
try:
483487
logs = publisher_gateway.get_store_model_serial_logs(
484-
flask.session,
485-
store_id,
486-
model_name,
487-
start_time,
488-
end_time,
489-
page_size,
490-
cursor,
488+
flask.session, store_id, model_name, params
491489
)
492490
res["data"] = logs
493491
res["success"] = True

0 commit comments

Comments
 (0)