Skip to content

Commit 5bb31ef

Browse files
Add API-side pagination for transfer executions timeline
Replaces client-side pagination with marker-based infinite loading for the executions date bullet view. Tasks are cached from the transfer details response to avoid redundant fetches. Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
1 parent 45fe07f commit 5bb31ef

8 files changed

Lines changed: 324 additions & 13 deletions

File tree

cypress/support/routeSelectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ export const routeSelectors = {
44
AUTH_TOKENS: "**/identity/auth/tokens",
55
CONN_SCHEMA_OPENSTACK: "**/coriolis/**/providers/openstack/schemas/16",
66
ENDPOINTS: "**/coriolis/**/endpoints",
7-
DEPLOYMENTS: "**/coriolis/**/deployments",
7+
DEPLOYMENTS: "**/coriolis/**/deployments*",
88
PROJECTS: "**/identity/auth/projects",
99
PROVIDERS: "**/coriolis/**/providers",
1010
REGIONS: "**/coriolis/**/regions",
11-
TRANSFERS: "**/coriolis/**/transfers",
11+
TRANSFERS: "**/coriolis/**/transfers*",
1212
ROLE_ASSIGNMENTS: "**/identity/role_assignments*",
1313
SCHEDULES: "**/coriolis/**/transfers/**/schedules",
1414
SECRETS: "**/barbican/**/secrets",

src/components/modules/TransferModule/Executions/Executions.spec.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,81 @@ describe("Executions", () => {
231231
expect(TestUtils.select("Executions__LoadingWrapper")).toBeTruthy();
232232
});
233233

234+
it("triggers onLoadOlderExecutions when left arrow is clicked at the first execution", async () => {
235+
const onLoadOlderExecutions = jest.fn();
236+
render(
237+
<Executions
238+
{...defaultProps}
239+
hasOlderExecutions
240+
onLoadOlderExecutions={onLoadOlderExecutions}
241+
/>,
242+
);
243+
const previousArrow = TestUtils.selectAll(
244+
"Arrow__Wrapper",
245+
TestUtils.select("Timeline__Wrapper")!,
246+
)[0];
247+
await act(async () => {
248+
previousArrow.click();
249+
});
250+
expect(onLoadOlderExecutions).toHaveBeenCalled();
251+
});
252+
253+
it("does not trigger onLoadOlderExecutions when not at the first execution", async () => {
254+
const onLoadOlderExecutions = jest.fn();
255+
render(
256+
<Executions
257+
{...defaultProps}
258+
executions={[
259+
EXECUTION_MOCK,
260+
{ ...EXECUTION_MOCK, id: "second-id", status: "COMPLETED" },
261+
]}
262+
hasOlderExecutions
263+
onLoadOlderExecutions={onLoadOlderExecutions}
264+
/>,
265+
);
266+
const nextArrow = TestUtils.selectAll(
267+
"Arrow__Wrapper",
268+
TestUtils.select("Timeline__Wrapper")!,
269+
)[1];
270+
await act(async () => {
271+
nextArrow.click();
272+
});
273+
const previousArrow = TestUtils.selectAll(
274+
"Arrow__Wrapper",
275+
TestUtils.select("Timeline__Wrapper")!,
276+
)[0];
277+
await act(async () => {
278+
previousArrow.click();
279+
});
280+
expect(onLoadOlderExecutions).not.toHaveBeenCalled();
281+
});
282+
283+
it("triggers onLoadOlderExecutions when the oldest bullet is clicked", async () => {
284+
const onLoadOlderExecutions = jest.fn();
285+
render(
286+
<Executions
287+
{...defaultProps}
288+
hasOlderExecutions
289+
onLoadOlderExecutions={onLoadOlderExecutions}
290+
/>,
291+
);
292+
const timelineItem = TestUtils.select("Timeline__Item-");
293+
expect(timelineItem).toBeTruthy();
294+
await act(async () => {
295+
timelineItem!.click();
296+
});
297+
expect(onLoadOlderExecutions).toHaveBeenCalled();
298+
});
299+
300+
it("navigates to newest of prepended older executions after load", () => {
301+
const { rerender } = render(<Executions {...defaultProps} />);
302+
const olderExec = { ...EXECUTION_MOCK, id: "older-id", number: 0 };
303+
rerender(
304+
<Executions {...defaultProps} executions={[olderExec, EXECUTION_MOCK]} />,
305+
);
306+
expect(defaultProps.onChange).toHaveBeenLastCalledWith(olderExec.id);
307+
});
308+
234309
it("deletes execution", async () => {
235310
const deleteExecution = jest.fn();
236311
render(

src/components/modules/TransferModule/Executions/Executions.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ type Props = {
8989
loading: boolean;
9090
tasksLoading: boolean;
9191
instancesDetails: Instance[];
92+
hasOlderExecutions?: boolean;
93+
onLoadOlderExecutions?: () => void;
9294
onChange: (executionId: string) => void;
9395
onCancelExecutionClick: (
9496
execution: Execution | null,
@@ -146,6 +148,18 @@ class Executions extends React.Component<Props, State> {
146148
}
147149
}
148150
}
151+
152+
const prevFirstId = this.props.executions[0]?.id;
153+
const newFirstId = props.executions[0]?.id;
154+
if (
155+
props.executions.length > this.props.executions.length &&
156+
prevFirstId !== undefined &&
157+
prevFirstId !== newFirstId &&
158+
!(lastExecution && lastExecution.status === "RUNNING")
159+
) {
160+
const newCount = props.executions.length - this.props.executions.length;
161+
selectExecution = props.executions[newCount - 1];
162+
}
149163
}
150164
const currentSelectedExecution = this.state.selectedExecution;
151165
if (!currentSelectedExecution) {
@@ -213,6 +227,9 @@ class Executions extends React.Component<Props, State> {
213227
);
214228

215229
if (selectedIndex === 0) {
230+
if (this.props.hasOlderExecutions && this.props.onLoadOlderExecutions) {
231+
this.props.onLoadOlderExecutions();
232+
}
216233
return;
217234
}
218235

@@ -251,6 +268,14 @@ class Executions extends React.Component<Props, State> {
251268
this.setState({ selectedExecution: item }, () => {
252269
this.handleChange(item);
253270
});
271+
272+
if (
273+
item.id === this.props.executions[0]?.id &&
274+
this.props.hasOlderExecutions &&
275+
this.props.onLoadOlderExecutions
276+
) {
277+
this.props.onLoadOlderExecutions();
278+
}
254279
}
255280

256281
handleCancelExecutionClick() {
@@ -283,6 +308,7 @@ class Executions extends React.Component<Props, State> {
283308
<Timeline
284309
items={this.props.executions}
285310
selectedItem={this.state.selectedExecution}
311+
hasOlderItems={this.props.hasOlderExecutions}
286312
onPreviousClick={() => {
287313
this.handlePreviousExecutionClick();
288314
}}

src/components/modules/TransferModule/Timeline/Timeline.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const ItemLabel = styled.div<any>`
8484
type Props = {
8585
items?: Execution[] | null;
8686
selectedItem?: Execution | null;
87+
hasOlderItems?: boolean;
8788
onPreviousClick?: () => void;
8889
onNextClick?: () => void;
8990
onItemClick?: (item: Execution) => void;
@@ -216,7 +217,11 @@ class Timeline extends React.Component<Props> {
216217
>
217218
<ArrowStyled
218219
orientation="left"
219-
forceShow={!this.props.items || !this.props.items.length}
220+
forceShow={
221+
!this.props.items ||
222+
!this.props.items.length ||
223+
this.props.hasOlderItems
224+
}
220225
primary={Boolean(this.props.items && this.props.items.length)}
221226
onClick={this.props.onPreviousClick}
222227
/>

src/components/modules/TransferModule/TransferDetailsContent/TransferDetailsContent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ type Props = {
9393
executionsTasks: ExecutionTasks[];
9494
minionPools: MinionPool[];
9595
storageBackends: StorageBackend[];
96+
hasOlderExecutions?: boolean;
97+
onLoadOlderExecutions?: () => void;
9698
onExecutionChange: (executionId: string) => void;
9799
onCancelExecutionClick: (
98100
execution: Execution | null,
@@ -212,6 +214,8 @@ class TransferDetailsContent extends React.Component<Props, State> {
212214
onChange={this.props.onExecutionChange}
213215
tasksLoading={this.props.executionsTasksLoading}
214216
instancesDetails={this.props.instancesDetails}
217+
hasOlderExecutions={this.props.hasOlderExecutions}
218+
onLoadOlderExecutions={this.props.onLoadOlderExecutions}
215219
/>
216220
);
217221
}

src/components/smart/TransferDetailsPage/TransferDetailsPage.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class TransferDetailsPage extends React.Component<Props, State> {
103103

104104
componentDidMount() {
105105
document.title = "Transfer Details";
106+
transferStore.resetExecutionsPagination();
106107

107108
const loadTransfer = async () => {
108109
await endpointStore.getEndpoints({ showLoading: true });
@@ -114,6 +115,9 @@ class TransferDetailsPage extends React.Component<Props, State> {
114115
if (!this.transfer) {
115116
return;
116117
}
118+
if (this.props.match.params.page === "executions") {
119+
transferStore.getTransferExecutions({ showLoading: true });
120+
}
117121
const sourceEndpoint = endpointStore.endpoints.find(
118122
e => e.id === this.transfer!.origin_endpoint_id,
119123
);
@@ -177,17 +181,30 @@ class TransferDetailsPage extends React.Component<Props, State> {
177181

178182
UNSAFE_componentWillReceiveProps(newProps: Props) {
179183
if (newProps.match.params.id !== this.props.match.params.id) {
184+
transferStore.resetExecutionsPagination();
180185
this.loadTransferWithInstances({
181186
cache: true,
182187
transferId: newProps.match.params.id,
188+
onDetailsLoaded: () => {
189+
if (newProps.match.params.page === "executions") {
190+
transferStore.getTransferExecutions({ showLoading: true });
191+
}
192+
},
183193
});
184194
scheduleStore.getSchedules(newProps.match.params.id);
195+
} else if (
196+
newProps.match.params.page === "executions" &&
197+
this.props.match.params.page !== "executions"
198+
) {
199+
transferStore.resetExecutionsPagination();
200+
transferStore.getTransferExecutions({ showLoading: true });
185201
}
186202
}
187203

188204
componentWillUnmount() {
189205
transferStore.cancelTransferDetails();
190206
transferStore.clearDetails();
207+
transferStore.resetExecutionsPagination();
191208
scheduleStore.clearUnsavedSchedules();
192209
this.stopPolling = true;
193210
}
@@ -626,10 +643,22 @@ class TransferDetailsPage extends React.Component<Props, State> {
626643
}),
627644
(async () => {
628645
if (window.location.pathname.indexOf("executions") > -1) {
629-
await transferStore.getExecutionTasks({
630-
transferId: this.transferId,
631-
polling: true,
632-
});
646+
const currentId = transferStore.currentlyLoadingExecution;
647+
const currentExec = currentId
648+
? transferStore.executionsList.find(e => e.id === currentId)
649+
: null;
650+
// Only poll tasks for active executions — completed/cancelled tasks never change
651+
if (
652+
currentExec &&
653+
(currentExec.status === "RUNNING" ||
654+
currentExec.status === "CANCELLING" ||
655+
currentExec.status === "AWAITING_MINION_ALLOCATIONS")
656+
) {
657+
await transferStore.getExecutionTasks({
658+
transferId: this.transferId,
659+
polling: true,
660+
});
661+
}
633662
}
634663
})(),
635664
]);
@@ -830,12 +859,17 @@ class TransferDetailsPage extends React.Component<Props, State> {
830859
}
831860
executionsLoading={
832861
transferStore.startingExecution ||
833-
transferStore.transferDetailsLoading
862+
transferStore.transferDetailsLoading ||
863+
transferStore.executionsLoading
834864
}
835865
onExecutionChange={id => {
836866
this.handleExecutionChange(id);
837867
}}
838-
executions={transferStore.transferDetails?.executions || []}
868+
executions={transferStore.executionsList}
869+
hasOlderExecutions={transferStore.executionsHasOlderPage}
870+
onLoadOlderExecutions={() => {
871+
transferStore.loadOlderExecutions();
872+
}}
839873
executionsTasksLoading={
840874
transferStore.executionsTasksLoading ||
841875
transferStore.transferDetailsLoading ||

src/sources/TransferSource.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,28 @@ class TransferSource {
156156
return transfer;
157157
}
158158

159+
async getExecutions(
160+
transferId: string,
161+
options?: {
162+
limit?: number;
163+
marker?: string | null;
164+
},
165+
): Promise<Execution[]> {
166+
const params: string[] = [];
167+
if (options?.marker) {
168+
params.push(`marker=${encodeURIComponent(options.marker)}`);
169+
}
170+
if (options?.limit !== undefined) {
171+
params.push(`limit=${options.limit}`);
172+
}
173+
const queryString = params.length > 0 ? `?${params.join("&")}` : "";
174+
const response = await Api.send({
175+
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/transfers/${transferId}/executions${queryString}`,
176+
});
177+
const executions: Execution[] = response.data.executions;
178+
return TransferSourceUtils.filterDeletedExecutions(executions);
179+
}
180+
159181
async getExecutionTasks(options: {
160182
transferId: string;
161183
executionId?: string;

0 commit comments

Comments
 (0)