Przeglądaj źródła

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>
Mihaela Balutoiu 1 miesiąc temu
rodzic
commit
3ae0512dda

+ 2 - 2
cypress/support/routeSelectors.ts

@@ -4,11 +4,11 @@ export const routeSelectors = {
   AUTH_TOKENS: "**/identity/auth/tokens",
   AUTH_TOKENS: "**/identity/auth/tokens",
   CONN_SCHEMA_OPENSTACK: "**/coriolis/**/providers/openstack/schemas/16",
   CONN_SCHEMA_OPENSTACK: "**/coriolis/**/providers/openstack/schemas/16",
   ENDPOINTS: "**/coriolis/**/endpoints",
   ENDPOINTS: "**/coriolis/**/endpoints",
-  DEPLOYMENTS: "**/coriolis/**/deployments",
+  DEPLOYMENTS: "**/coriolis/**/deployments*",
   PROJECTS: "**/identity/auth/projects",
   PROJECTS: "**/identity/auth/projects",
   PROVIDERS: "**/coriolis/**/providers",
   PROVIDERS: "**/coriolis/**/providers",
   REGIONS: "**/coriolis/**/regions",
   REGIONS: "**/coriolis/**/regions",
-  TRANSFERS: "**/coriolis/**/transfers",
+  TRANSFERS: "**/coriolis/**/transfers*",
   ROLE_ASSIGNMENTS: "**/identity/role_assignments*",
   ROLE_ASSIGNMENTS: "**/identity/role_assignments*",
   SCHEDULES: "**/coriolis/**/transfers/**/schedules",
   SCHEDULES: "**/coriolis/**/transfers/**/schedules",
   SECRETS: "**/barbican/**/secrets",
   SECRETS: "**/barbican/**/secrets",

+ 130 - 0
src/components/modules/TransferModule/Executions/Executions.spec.tsx

@@ -231,6 +231,136 @@ describe("Executions", () => {
     expect(TestUtils.select("Executions__LoadingWrapper")).toBeTruthy();
     expect(TestUtils.select("Executions__LoadingWrapper")).toBeTruthy();
   });
   });
 
 
+  it("triggers onLoadOlderExecutions when left arrow is clicked at the first execution", async () => {
+    const onLoadOlderExecutions = jest.fn();
+    render(
+      <Executions
+        {...defaultProps}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    const previousArrow = TestUtils.selectAll(
+      "Arrow__Wrapper",
+      TestUtils.select("Timeline__Wrapper")!,
+    )[0];
+    await act(async () => {
+      previousArrow.click();
+    });
+    expect(onLoadOlderExecutions).toHaveBeenCalled();
+  });
+
+  it("does not trigger onLoadOlderExecutions when not at the first execution", async () => {
+    const onLoadOlderExecutions = jest.fn();
+    render(
+      <Executions
+        {...defaultProps}
+        executions={[
+          EXECUTION_MOCK,
+          { ...EXECUTION_MOCK, id: "second-id", status: "COMPLETED" },
+        ]}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    const nextArrow = TestUtils.selectAll(
+      "Arrow__Wrapper",
+      TestUtils.select("Timeline__Wrapper")!,
+    )[1];
+    await act(async () => {
+      nextArrow.click();
+    });
+    const previousArrow = TestUtils.selectAll(
+      "Arrow__Wrapper",
+      TestUtils.select("Timeline__Wrapper")!,
+    )[0];
+    await act(async () => {
+      previousArrow.click();
+    });
+    expect(onLoadOlderExecutions).not.toHaveBeenCalled();
+  });
+
+  it("triggers onLoadOlderExecutions when the oldest bullet is clicked", async () => {
+    const onLoadOlderExecutions = jest.fn();
+    render(
+      <Executions
+        {...defaultProps}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    const timelineItem = TestUtils.select("Timeline__Item-");
+    expect(timelineItem).toBeTruthy();
+    await act(async () => {
+      timelineItem!.click();
+    });
+    expect(onLoadOlderExecutions).toHaveBeenCalled();
+  });
+
+  it("navigates to newest of prepended older executions after previous arrow click", async () => {
+    const onLoadOlderExecutions = jest.fn();
+    const { rerender } = render(
+      <Executions
+        {...defaultProps}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    const previousArrow = TestUtils.selectAll(
+      "Arrow__Wrapper",
+      TestUtils.select("Timeline__Wrapper")!,
+    )[0];
+    await act(async () => {
+      previousArrow.click();
+    });
+    expect(onLoadOlderExecutions).toHaveBeenCalled();
+
+    const olderExec = { ...EXECUTION_MOCK, id: "older-id", number: 0 };
+    rerender(
+      <Executions
+        {...defaultProps}
+        executions={[olderExec, EXECUTION_MOCK]}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    expect(defaultProps.onChange).toHaveBeenLastCalledWith(olderExec.id);
+  });
+
+  it("keeps the clicked execution selected when older executions are prepended", async () => {
+    const onLoadOlderExecutions = jest.fn();
+    const newerExec = { ...EXECUTION_MOCK, id: "newer-id", number: 2 };
+    const { rerender } = render(
+      <Executions
+        {...defaultProps}
+        executions={[EXECUTION_MOCK, newerExec]}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    const oldestBullet = TestUtils.select("Timeline__Item-");
+    expect(oldestBullet).toBeTruthy();
+    await act(async () => {
+      oldestBullet!.click();
+    });
+    expect(onLoadOlderExecutions).toHaveBeenCalled();
+    expect(defaultProps.onChange).toHaveBeenLastCalledWith(EXECUTION_MOCK.id);
+
+    const olderExecs = [
+      { ...EXECUTION_MOCK, id: "older-id-1", number: -1 },
+      { ...EXECUTION_MOCK, id: "older-id-2", number: 0 },
+    ];
+    rerender(
+      <Executions
+        {...defaultProps}
+        executions={[...olderExecs, EXECUTION_MOCK, newerExec]}
+        hasOlderExecutions
+        onLoadOlderExecutions={onLoadOlderExecutions}
+      />,
+    );
+    expect(defaultProps.onChange).toHaveBeenLastCalledWith(EXECUTION_MOCK.id);
+  });
+
   it("deletes execution", async () => {
   it("deletes execution", async () => {
     const deleteExecution = jest.fn();
     const deleteExecution = jest.fn();
     render(
     render(

+ 37 - 0
src/components/modules/TransferModule/Executions/Executions.tsx

@@ -89,6 +89,8 @@ type Props = {
   loading: boolean;
   loading: boolean;
   tasksLoading: boolean;
   tasksLoading: boolean;
   instancesDetails: Instance[];
   instancesDetails: Instance[];
+  hasOlderExecutions?: boolean;
+  onLoadOlderExecutions?: () => void;
   onChange: (executionId: string) => void;
   onChange: (executionId: string) => void;
   onCancelExecutionClick: (
   onCancelExecutionClick: (
     execution: Execution | null,
     execution: Execution | null,
@@ -106,6 +108,8 @@ class Executions extends React.Component<Props, State> {
     selectedExecution: null,
     selectedExecution: null,
   };
   };
 
 
+  selectPreviousOnOlderLoad = false;
+
   UNSAFE_componentWillMount() {
   UNSAFE_componentWillMount() {
     this.setSelectedExecution(this.props);
     this.setSelectedExecution(this.props);
   }
   }
@@ -146,6 +150,26 @@ class Executions extends React.Component<Props, State> {
           }
           }
         }
         }
       }
       }
+
+      const prevFirstId =
+        this.props.executions.length > 0
+          ? this.props.executions[0].id
+          : undefined;
+      const newFirstId =
+        props.executions.length > 0 ? props.executions[0].id : undefined;
+      if (
+        props.executions.length > this.props.executions.length &&
+        prevFirstId !== undefined &&
+        prevFirstId !== newFirstId &&
+        !(lastExecution && lastExecution.status === "RUNNING")
+      ) {
+        if (this.selectPreviousOnOlderLoad) {
+          const newCount =
+            props.executions.length - this.props.executions.length;
+          selectExecution = props.executions[newCount - 1];
+        }
+        this.selectPreviousOnOlderLoad = false;
+      }
     }
     }
     const currentSelectedExecution = this.state.selectedExecution;
     const currentSelectedExecution = this.state.selectedExecution;
     if (!currentSelectedExecution) {
     if (!currentSelectedExecution) {
@@ -213,6 +237,10 @@ class Executions extends React.Component<Props, State> {
     );
     );
 
 
     if (selectedIndex === 0) {
     if (selectedIndex === 0) {
+      if (this.props.hasOlderExecutions && this.props.onLoadOlderExecutions) {
+        this.selectPreviousOnOlderLoad = true;
+        this.props.onLoadOlderExecutions();
+      }
       return;
       return;
     }
     }
 
 
@@ -251,6 +279,14 @@ class Executions extends React.Component<Props, State> {
     this.setState({ selectedExecution: item }, () => {
     this.setState({ selectedExecution: item }, () => {
       this.handleChange(item);
       this.handleChange(item);
     });
     });
+
+    if (
+      item.id === this.props.executions[0]?.id &&
+      this.props.hasOlderExecutions &&
+      this.props.onLoadOlderExecutions
+    ) {
+      this.props.onLoadOlderExecutions();
+    }
   }
   }
 
 
   handleCancelExecutionClick() {
   handleCancelExecutionClick() {
@@ -283,6 +319,7 @@ class Executions extends React.Component<Props, State> {
       <Timeline
       <Timeline
         items={this.props.executions}
         items={this.props.executions}
         selectedItem={this.state.selectedExecution}
         selectedItem={this.state.selectedExecution}
+        hasOlderItems={this.props.hasOlderExecutions}
         onPreviousClick={() => {
         onPreviousClick={() => {
           this.handlePreviousExecutionClick();
           this.handlePreviousExecutionClick();
         }}
         }}

+ 6 - 1
src/components/modules/TransferModule/Timeline/Timeline.tsx

@@ -84,6 +84,7 @@ const ItemLabel = styled.div<any>`
 type Props = {
 type Props = {
   items?: Execution[] | null;
   items?: Execution[] | null;
   selectedItem?: Execution | null;
   selectedItem?: Execution | null;
+  hasOlderItems?: boolean;
   onPreviousClick?: () => void;
   onPreviousClick?: () => void;
   onNextClick?: () => void;
   onNextClick?: () => void;
   onItemClick?: (item: Execution) => void;
   onItemClick?: (item: Execution) => void;
@@ -216,7 +217,11 @@ class Timeline extends React.Component<Props> {
       >
       >
         <ArrowStyled
         <ArrowStyled
           orientation="left"
           orientation="left"
-          forceShow={!this.props.items || !this.props.items.length}
+          forceShow={
+            !this.props.items ||
+            !this.props.items.length ||
+            this.props.hasOlderItems
+          }
           primary={Boolean(this.props.items && this.props.items.length)}
           primary={Boolean(this.props.items && this.props.items.length)}
           onClick={this.props.onPreviousClick}
           onClick={this.props.onPreviousClick}
         />
         />

+ 4 - 0
src/components/modules/TransferModule/TransferDetailsContent/TransferDetailsContent.tsx

@@ -93,6 +93,8 @@ type Props = {
   executionsTasks: ExecutionTasks[];
   executionsTasks: ExecutionTasks[];
   minionPools: MinionPool[];
   minionPools: MinionPool[];
   storageBackends: StorageBackend[];
   storageBackends: StorageBackend[];
+  hasOlderExecutions?: boolean;
+  onLoadOlderExecutions?: () => void;
   onExecutionChange: (executionId: string) => void;
   onExecutionChange: (executionId: string) => void;
   onCancelExecutionClick: (
   onCancelExecutionClick: (
     execution: Execution | null,
     execution: Execution | null,
@@ -212,6 +214,8 @@ class TransferDetailsContent extends React.Component<Props, State> {
         onChange={this.props.onExecutionChange}
         onChange={this.props.onExecutionChange}
         tasksLoading={this.props.executionsTasksLoading}
         tasksLoading={this.props.executionsTasksLoading}
         instancesDetails={this.props.instancesDetails}
         instancesDetails={this.props.instancesDetails}
+        hasOlderExecutions={this.props.hasOlderExecutions}
+        onLoadOlderExecutions={this.props.onLoadOlderExecutions}
       />
       />
     );
     );
   }
   }

+ 40 - 6
src/components/smart/TransferDetailsPage/TransferDetailsPage.tsx

@@ -103,6 +103,7 @@ class TransferDetailsPage extends React.Component<Props, State> {
 
 
   componentDidMount() {
   componentDidMount() {
     document.title = "Transfer Details";
     document.title = "Transfer Details";
+    transferStore.resetExecutionsPagination();
 
 
     const loadTransfer = async () => {
     const loadTransfer = async () => {
       await endpointStore.getEndpoints({ showLoading: true });
       await endpointStore.getEndpoints({ showLoading: true });
@@ -114,6 +115,9 @@ class TransferDetailsPage extends React.Component<Props, State> {
           if (!this.transfer) {
           if (!this.transfer) {
             return;
             return;
           }
           }
+          if (this.props.match.params.page === "executions") {
+            transferStore.getTransferExecutions({ showLoading: true });
+          }
           const sourceEndpoint = endpointStore.endpoints.find(
           const sourceEndpoint = endpointStore.endpoints.find(
             e => e.id === this.transfer!.origin_endpoint_id,
             e => e.id === this.transfer!.origin_endpoint_id,
           );
           );
@@ -177,17 +181,30 @@ class TransferDetailsPage extends React.Component<Props, State> {
 
 
   UNSAFE_componentWillReceiveProps(newProps: Props) {
   UNSAFE_componentWillReceiveProps(newProps: Props) {
     if (newProps.match.params.id !== this.props.match.params.id) {
     if (newProps.match.params.id !== this.props.match.params.id) {
+      transferStore.resetExecutionsPagination();
       this.loadTransferWithInstances({
       this.loadTransferWithInstances({
         cache: true,
         cache: true,
         transferId: newProps.match.params.id,
         transferId: newProps.match.params.id,
+        onDetailsLoaded: () => {
+          if (newProps.match.params.page === "executions") {
+            transferStore.getTransferExecutions({ showLoading: true });
+          }
+        },
       });
       });
       scheduleStore.getSchedules(newProps.match.params.id);
       scheduleStore.getSchedules(newProps.match.params.id);
+    } else if (
+      newProps.match.params.page === "executions" &&
+      this.props.match.params.page !== "executions"
+    ) {
+      transferStore.resetExecutionsPagination();
+      transferStore.getTransferExecutions({ showLoading: true });
     }
     }
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
     transferStore.cancelTransferDetails();
     transferStore.cancelTransferDetails();
     transferStore.clearDetails();
     transferStore.clearDetails();
+    transferStore.resetExecutionsPagination();
     scheduleStore.clearUnsavedSchedules();
     scheduleStore.clearUnsavedSchedules();
     this.stopPolling = true;
     this.stopPolling = true;
   }
   }
@@ -626,10 +643,22 @@ class TransferDetailsPage extends React.Component<Props, State> {
       }),
       }),
       (async () => {
       (async () => {
         if (window.location.pathname.indexOf("executions") > -1) {
         if (window.location.pathname.indexOf("executions") > -1) {
-          await transferStore.getExecutionTasks({
-            transferId: this.transferId,
-            polling: true,
-          });
+          const currentId = transferStore.currentlyLoadingExecution;
+          const currentExec = currentId
+            ? transferStore.executionsList.find(e => e.id === currentId)
+            : null;
+          // Only poll tasks for active executions — completed/cancelled tasks never change
+          if (
+            currentExec &&
+            (currentExec.status === "RUNNING" ||
+              currentExec.status === "CANCELLING" ||
+              currentExec.status === "AWAITING_MINION_ALLOCATIONS")
+          ) {
+            await transferStore.getExecutionTasks({
+              transferId: this.transferId,
+              polling: true,
+            });
+          }
         }
         }
       })(),
       })(),
     ]);
     ]);
@@ -830,12 +859,17 @@ class TransferDetailsPage extends React.Component<Props, State> {
               }
               }
               executionsLoading={
               executionsLoading={
                 transferStore.startingExecution ||
                 transferStore.startingExecution ||
-                transferStore.transferDetailsLoading
+                transferStore.transferDetailsLoading ||
+                transferStore.executionsLoading
               }
               }
               onExecutionChange={id => {
               onExecutionChange={id => {
                 this.handleExecutionChange(id);
                 this.handleExecutionChange(id);
               }}
               }}
-              executions={transferStore.transferDetails?.executions || []}
+              executions={transferStore.executionsList}
+              hasOlderExecutions={transferStore.executionsHasOlderPage}
+              onLoadOlderExecutions={() => {
+                transferStore.loadOlderExecutions();
+              }}
               executionsTasksLoading={
               executionsTasksLoading={
                 transferStore.executionsTasksLoading ||
                 transferStore.executionsTasksLoading ||
                 transferStore.transferDetailsLoading ||
                 transferStore.transferDetailsLoading ||

+ 22 - 0
src/sources/TransferSource.ts

@@ -145,6 +145,28 @@ class TransferSource {
     return transfer;
     return transfer;
   }
   }
 
 
+  async getExecutions(
+    transferId: string,
+    options?: {
+      limit?: number;
+      marker?: string | null;
+    },
+  ): Promise<Execution[]> {
+    const params: string[] = [];
+    if (options?.marker) {
+      params.push(`marker=${encodeURIComponent(options.marker)}`);
+    }
+    if (options?.limit !== undefined) {
+      params.push(`limit=${options.limit}`);
+    }
+    const queryString = params.length > 0 ? `?${params.join("&")}` : "";
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/transfers/${transferId}/executions${queryString}`,
+    });
+    const executions: Execution[] = response.data.executions;
+    return executions;
+  }
+
   async getExecutionTasks(options: {
   async getExecutionTasks(options: {
     transferId: string;
     transferId: string;
     executionId?: string;
     executionId?: string;

+ 180 - 4
src/stores/TransferStore.ts

@@ -14,7 +14,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import { observable, action, runInAction } from "mobx";
 import { observable, action, runInAction } from "mobx";
 
 
-import TransferSource from "@src/sources/TransferSource";
+import TransferSource, {
+  TransferSourceUtils,
+  sortTasks,
+} from "@src/sources/TransferSource";
 import type {
 import type {
   UpdateData,
   UpdateData,
   TransferItem,
   TransferItem,
@@ -79,7 +82,13 @@ class TransferStore {
 
 
   private transferPageMarkers: (string | null)[] = [null];
   private transferPageMarkers: (string | null)[] = [null];
 
 
-  addExecution: { transferId: string; execution: Execution } | null = null;
+  @observable executionsList: Execution[] = [];
+
+  @observable executionsHasOlderPage = false;
+
+  @observable executionsLoading = false;
+
+  executionsPageSize = 10;
 
 
   @action resetTransferPagination(): void {
   @action resetTransferPagination(): void {
     this.transfersPage = 1;
     this.transfersPage = 1;
@@ -87,6 +96,83 @@ class TransferStore {
     this.transferPageMarkers = [null];
     this.transferPageMarkers = [null];
   }
   }
 
 
+  @action resetExecutionsPagination(): void {
+    this.executionsList = [];
+    this.executionsHasOlderPage = false;
+    this.executionsLoading = false;
+  }
+
+  @action async getTransferExecutions(options?: {
+    showLoading?: boolean;
+    polling?: boolean;
+  }): Promise<void> {
+    const transferId = this.transferDetails?.id;
+    if (!transferId) {
+      return;
+    }
+
+    if (options?.showLoading) {
+      this.executionsLoading = true;
+    }
+
+    try {
+      const raw = await TransferSource.getExecutions(transferId, {
+        limit: this.executionsPageSize + 1,
+      });
+      const hasOlderPage = raw.length > this.executionsPageSize;
+      const executions = hasOlderPage
+        ? raw.slice(0, this.executionsPageSize)
+        : raw;
+      TransferSourceUtils.sortExecutions(executions);
+      runInAction(() => {
+        this.executionsList = executions;
+        this.executionsHasOlderPage = hasOlderPage;
+        this.executionsLoading = false;
+      });
+    } catch (err) {
+      runInAction(() => {
+        this.executionsLoading = false;
+      });
+      console.error(err);
+    }
+  }
+
+  @action async loadOlderExecutions(): Promise<void> {
+    const transferId = this.transferDetails?.id;
+    if (!transferId || !this.executionsHasOlderPage || this.executionsLoading) {
+      return;
+    }
+
+    const marker = this.executionsList[0]?.id;
+    if (!marker) {
+      return;
+    }
+
+    this.executionsLoading = true;
+
+    try {
+      const raw = await TransferSource.getExecutions(transferId, {
+        limit: this.executionsPageSize + 1,
+        marker,
+      });
+      const hasOlderPage = raw.length > this.executionsPageSize;
+      const executions = hasOlderPage
+        ? raw.slice(0, this.executionsPageSize)
+        : raw;
+      TransferSourceUtils.sortExecutions(executions);
+      runInAction(() => {
+        this.executionsList = [...executions, ...this.executionsList];
+        this.executionsHasOlderPage = hasOlderPage;
+        this.executionsLoading = false;
+      });
+    } catch (err) {
+      runInAction(() => {
+        this.executionsLoading = false;
+      });
+      console.error(err);
+    }
+  }
+
   @action async setTransfersPage(page: number): Promise<void> {
   @action async setTransfersPage(page: number): Promise<void> {
     this.transfersPage = page;
     this.transfersPage = page;
     await this.getTransfers({ showLoading: true });
     await this.getTransfers({ showLoading: true });
@@ -164,6 +250,44 @@ class TransferStore {
 
 
       runInAction(() => {
       runInAction(() => {
         this.transferDetails = transfer;
         this.transferDetails = transfer;
+        let statusChanged = false;
+        const updatedList = this.executionsList.map(e => {
+          const fresh = transfer.executions?.find(te => te.id === e.id);
+          if (fresh && fresh.status !== e.status) {
+            statusChanged = true;
+            return { ...e, status: fresh.status };
+          }
+          return e;
+        });
+        if (statusChanged) {
+          this.executionsList = updatedList;
+        }
+
+        if (this.executionsList.length > 0 && transfer.executions?.length) {
+          const newestNumber = Math.max(
+            ...this.executionsList.map(e => e.number),
+          );
+          const incoming = transfer.executions.filter(
+            e =>
+              e.number > newestNumber &&
+              !this.executionsList.find(l => l.id === e.id),
+          );
+          if (incoming.length > 0) {
+            TransferSourceUtils.sortExecutions(incoming);
+            this.executionsList = [...this.executionsList, ...incoming];
+          }
+        }
+
+        transfer.executions?.forEach(exec => {
+          const withTasks = exec as ExecutionTasks;
+          if (
+            Array.isArray(withTasks.tasks) &&
+            !this.executionsTasks.find(et => et.id === exec.id)
+          ) {
+            sortTasks(withTasks.tasks, TransferSourceUtils.sortTaskUpdates);
+            this.executionsTasks = [...this.executionsTasks, withTasks];
+          }
+        });
       });
       });
     } finally {
     } finally {
       runInAction(() => {
       runInAction(() => {
@@ -175,6 +299,7 @@ class TransferStore {
   @action clearDetails() {
   @action clearDetails() {
     this.transferDetails = null;
     this.transferDetails = null;
     this.currentlyLoadingExecution = "";
     this.currentlyLoadingExecution = "";
+    this.executionsTasks = [];
   }
   }
 
 
   @action getTransfersSuccess(
   @action getTransfersSuccess(
@@ -195,7 +320,7 @@ class TransferStore {
     this.backgroundLoading = false;
     this.backgroundLoading = false;
   }
   }
 
 
-  private currentlyLoadingExecution = "";
+  currentlyLoadingExecution = "";
 
 
   @action async getExecutionTasks(options: {
   @action async getExecutionTasks(options: {
     transferId: string;
     transferId: string;
@@ -215,8 +340,13 @@ class TransferStore {
     }
     }
 
 
     if (
     if (
-      !this.executionsTasks.find(e => e.id === this.currentlyLoadingExecution)
+      !polling &&
+      this.executionsTasks.find(e => e.id === this.currentlyLoadingExecution)
     ) {
     ) {
+      return;
+    }
+
+    if (!polling) {
       this.executionsTasksLoading = true;
       this.executionsTasksLoading = true;
     }
     }
 
 
@@ -257,6 +387,18 @@ class TransferStore {
         execution,
         execution,
       );
       );
       this.transferDetails = updatedTransfer;
       this.transferDetails = updatedTransfer;
+
+      if (!this.executionsList.find(e => e.id === execution.id)) {
+        this.executionsList = [...this.executionsList, execution];
+      }
+
+      const withTasks = execution as ExecutionTasks;
+      if (Array.isArray(withTasks.tasks)) {
+        this.executionsTasks = [
+          ...this.executionsTasks.filter(e => e.id !== execution.id),
+          withTasks,
+        ];
+      }
     }
     }
     this.getExecutionTasks({
     this.getExecutionTasks({
       transferId: transferId,
       transferId: transferId,
@@ -272,6 +414,13 @@ class TransferStore {
     force?: boolean;
     force?: boolean;
   }): Promise<void> {
   }): Promise<void> {
     await TransferSource.cancelExecution(options);
     await TransferSource.cancelExecution(options);
+    runInAction(() => {
+      if (options.executionId) {
+        this.executionsList = this.executionsList.map(e =>
+          e.id === options.executionId ? { ...e, status: "CANCELLING" } : e,
+        );
+      }
+    });
     if (options.force) {
     if (options.force) {
       notificationStore.alert("Force cancelled", "success");
       notificationStore.alert("Force cancelled", "success");
     } else {
     } else {
@@ -285,6 +434,13 @@ class TransferStore {
   ): Promise<void> {
   ): Promise<void> {
     await TransferSource.deleteExecution(transferId, executionId);
     await TransferSource.deleteExecution(transferId, executionId);
     this.deleteExecutionSuccess(transferId, executionId);
     this.deleteExecutionSuccess(transferId, executionId);
+    if (
+      this.executionsList.length === 0 &&
+      this.transferDetails?.id === transferId
+    ) {
+      this.resetExecutionsPagination();
+      await this.getTransferExecutions({ showLoading: true });
+    }
   }
   }
 
 
   @action deleteExecutionSuccess(transferId: string, executionId: string) {
   @action deleteExecutionSuccess(transferId: string, executionId: string) {
@@ -296,6 +452,10 @@ class TransferStore {
       ];
       ];
       this.transferDetails.executions = executions;
       this.transferDetails.executions = executions;
     }
     }
+    this.executionsList = this.executionsList.filter(e => e.id !== executionId);
+    this.executionsTasks = this.executionsTasks.filter(
+      e => e.id !== executionId,
+    );
     if (executionId === this.currentlyLoadingExecution) {
     if (executionId === this.currentlyLoadingExecution) {
       this.currentlyLoadingExecution = "";
       this.currentlyLoadingExecution = "";
     }
     }
@@ -320,7 +480,23 @@ class TransferStore {
         execution,
         execution,
       );
       );
       this.transferDetails = updatedTransfer;
       this.transferDetails = updatedTransfer;
+
+      if (!this.executionsList.find(e => e.id === execution.id)) {
+        this.executionsList = [...this.executionsList, execution];
+      }
+
+      const withTasks = execution as ExecutionTasks;
+      if (Array.isArray(withTasks.tasks)) {
+        this.executionsTasks = [
+          ...this.executionsTasks.filter(e => e.id !== execution.id),
+          withTasks,
+        ];
+      }
     }
     }
+    this.getExecutionTasks({
+      transferId,
+      executionId: execution.id,
+    });
   }
   }
 
 
   async update(options: {
   async update(options: {