This is an automated email from the ASF dual-hosted git repository.

sandeepk318 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 356530cc95 AMBARI-26645: Persist Hosts List filters and selection 
across navigation; fix pagination display when empty (#4202)
356530cc95 is described below

commit 356530cc95ab187f8353e94814eb15a9ad9c310c
Author: Himanshu Maurya <[email protected]>
AuthorDate: Fri Sep 4 17:10:26 2026 +0530

    AMBARI-26645: Persist Hosts List filters and selection across navigation; 
fix pagination display when empty (#4202)
---
 ambari-web/latest/src/AppLoader.tsx                |   9 +-
 ambari-web/latest/src/Utils/db.ts                  |  20 ++++
 ambari-web/latest/src/components/Paginator.tsx     |   3 +-
 .../latest/src/screens/Hosts/HostComboSearch.tsx   |   8 +-
 ambari-web/latest/src/screens/Hosts/HostsList.tsx  |  70 ++++++++++++--
 .../latest/src/store/HostsListStateContext.tsx     | 105 +++++++++++++++++++++
 6 files changed, 203 insertions(+), 12 deletions(-)

diff --git a/ambari-web/latest/src/AppLoader.tsx 
b/ambari-web/latest/src/AppLoader.tsx
index f08df1d10e..bee8dae352 100644
--- a/ambari-web/latest/src/AppLoader.tsx
+++ b/ambari-web/latest/src/AppLoader.tsx
@@ -21,6 +21,7 @@ import { Navigate, Outlet, useLocation, useNavigate } from 
"react-router-dom";
 import { Alert, Button, ProgressBar } from "react-bootstrap";
 import { AppContext, AppProvider } from "./store/context";
 import { AlertsProvider } from "./store/AlertsContext";
+import { HostsListStateProvider } from "./store/HostsListStateContext";
 import { ModalProvider } from "./store/ModalContext";
 import { useAuth } from "./hooks/useAuth";
 import useAuthorizationPolicy from "./hooks/useAuthorizationPolicy";
@@ -109,9 +110,11 @@ export function AuthenticatedApplication() {
   return (
     <AppProvider>
       <AlertsProvider>
-        <ModalProvider>
-          <ApplicationLoader />
-        </ModalProvider>
+        <HostsListStateProvider>
+          <ModalProvider>
+            <ApplicationLoader />
+          </ModalProvider>
+        </HostsListStateProvider>
       </AlertsProvider>
     </AppProvider>
   );
diff --git a/ambari-web/latest/src/Utils/db.ts 
b/ambari-web/latest/src/Utils/db.ts
index 85934fc0d6..dc2e2b3eba 100644
--- a/ambari-web/latest/src/Utils/db.ts
+++ b/ambari-web/latest/src/Utils/db.ts
@@ -213,6 +213,26 @@ interface DbData {
       return JSON.parse(JSON.stringify(InitialData));
     }
 
+    // Ember stores host selection at 
app.tables.selectedItems.mainHostController.
+    getSelectedHosts(): string[] {
+      const selectedHosts = this.get('app', 
'tables.selectedItems.mainHostController');
+      return Array.isArray(selectedHosts)
+        ? selectedHosts.filter((hostName: any) => typeof hostName === 'string')
+        : [];
+    }
+
+    setSelectedHosts(selectedHosts: string[]): void {
+      this.set('app', 'tables.selectedItems.mainHostController', 
selectedHosts);
+    }
+
+    unselectHosts(hostsToUnselect: string[] = []): void {
+      this.setSelectedHosts(
+        this.getSelectedHosts().filter(
+          (hostName) => !hostsToUnselect.includes(hostName)
+        )
+      );
+    }
+
   }
 
   export const db = new Database()
diff --git a/ambari-web/latest/src/components/Paginator.tsx 
b/ambari-web/latest/src/components/Paginator.tsx
index c23822109b..e32dd3de3d 100644
--- a/ambari-web/latest/src/components/Paginator.tsx
+++ b/ambari-web/latest/src/components/Paginator.tsx
@@ -59,7 +59,8 @@ const Paginator = ({
       items.push(<Pagination.Ellipsis key={`ellipsis-${number}`}/>);
     }
   }
-  const firstItemIndex = (currentPage - 1) * itemsPerPage + 1;
+  const firstItemIndex =
+    totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
   const lastItemIndex = Math.min(currentPage * itemsPerPage, totalItems);
   return (
       <div className="mt-4 p-3 py-0" data-testid="pagination">
diff --git a/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx 
b/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx
index ee1a8fefbc..c146de797c 100644
--- a/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx
@@ -50,6 +50,7 @@ type HostComboSearchProps = {
   setSelectedFilters: (
     filters: SelectedFilters | ((prev: SelectedFilters) => SelectedFilters)
   ) => void;
+  onResetFilters?: () => void;
 };
 
 function HostComboSearch({
@@ -59,6 +60,7 @@ function HostComboSearch({
   searchCallback,
   selectedFilters,
   setSelectedFilters,
+  onResetFilters,
 }: HostComboSearchProps) {
   const { clusterName } = useContext(AppContext);
   const [selectedField, setSelectedField] = useState<FilterField | null>(null);
@@ -404,7 +406,11 @@ function HostComboSearch({
   function resetFilters() {
     setSelectedField(null as any);
     setSelectedValue(null as any);
-    setSelectedFilters([]);
+    if (onResetFilters) {
+      onResetFilters();
+    } else {
+      setSelectedFilters([]);
+    }
   }
 
   return (
diff --git a/ambari-web/latest/src/screens/Hosts/HostsList.tsx 
b/ambari-web/latest/src/screens/Hosts/HostsList.tsx
index 1c8101766e..1b43017ede 100644
--- a/ambari-web/latest/src/screens/Hosts/HostsList.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostsList.tsx
@@ -25,7 +25,7 @@ import {
   useRef,
   useState,
 } from "react";
-import { useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
 import { Alert, Button, Card, Form, ProgressBar } from "react-bootstrap";
 import { cloneDeep, get, isEmpty, startCase } from "lodash";
 import DefaultButton from "../../components/DefaultButton";
@@ -58,6 +58,7 @@ import {
 import { AppContext } from "../../store/context";
 import useStackVersion from "../../hooks/useStackVersion";
 import { ServiceContext } from "../../store/ServiceContext";
+import { useHostsListState } from "../../store/HostsListStateContext";
 import { IHostComponent } from "../../models/hostComponent";
 import { IHostStackVersion } from "../../models/hostStackVersion";
 import Host, { IHost } from "../../models/host";
@@ -92,17 +93,47 @@ export default function HostsList() {
     versionName?: string;
     versionStatus?: string;
   }>();
+  const navigate = useNavigate();
   const { clusterName, serviceComponentInfo } = useContext(AppContext);
   const { allServiceModels: serviceModels, polledHostComponentsData } = 
useContext(ServiceContext);
   const [loading, setLoading] = useState(true);
   const [paginationLoading, setPaginationLoading] = useState(false);
-  const [showFilters, setShowFilters] = useState(false);
   const [showModal, setShowModal] = useState(false);
   const [currentHostModels, setCurrentHostModels] = useState<Host[]>([]);
   const [allHostCount, setAllHostCount] = useState(0);
-  const [selectedHosts, setSelectedHosts] = useState<string[]>([]);
-  const [selectedFilters, setSelectedFilters] = useState<any>([]);
-  const [filterString, setFilterString] = useState<string>("");
+  const {
+    selectedFilters,
+    setSelectedFilters,
+    selectedHosts,
+    setSelectedHosts,
+  } = useHostsListState();
+  // Open the filter bar when filters are already applied, so restored filters
+  // stay visible instead of being hidden behind a collapsed bar.
+  const [showFilters, setShowFilters] = useState(
+    () => selectedFilters.length > 0
+  );
+  // Derive from the restored filters up front: the query effect runs on 
mount, so
+  // starting empty would fire an unfiltered request and show unfiltered hosts 
even
+  // though the filters are shown as applied.
+  const [filterString, setFilterString] = useState<string>(() =>
+    selectedFilters.length > 0
+      ? computeParameters(getQueryParameters(selectedFilters))
+      : ""
+  );
+  // A component/version deep link seeds the filters from the URL, so clearing 
the
+  // filters has to drop those params too or the effect just re-applies them.
+  const clearFilters = useCallback(() => {
+    setSelectedFilters([]);
+    if (params.componentName || params.versionName || params.versionStatus) {
+      navigate("/main/hosts", { replace: true });
+    }
+  }, [
+    navigate,
+    params.componentName,
+    params.versionName,
+    params.versionStatus,
+    setSelectedFilters,
+  ]);
   const [hostApiQueryParams, setHostApiQueryParams] = useState<any>({
     pageSize: 10,
     startFrom: 0,
@@ -154,6 +185,14 @@ export default function HostsList() {
     order: "asc",
   });
 
+  // Filters can also arrive after mount (for example from a component or 
version
+  // deep link), so reveal the bar whenever any filter becomes active.
+  useEffect(() => {
+    if (selectedFilters.length > 0) {
+      setShowFilters(true);
+    }
+  }, [selectedFilters.length]);
+
   // Reuse centralized polled data from CachedServiceApi instead of making a 
separate API call
   // This eliminates the duplicate /components/ call that was previously 
polled independently
   useEffect(() => {
@@ -172,6 +211,12 @@ export default function HostsList() {
 
   useEffect(() => {
     if (params.componentName && !isEmpty(clusterComponents)) {
+      // clusterComponents comes from a poll, so this effect re-runs regularly.
+      // Seed only while no filter is set: that still applies the component 
from
+      // the URL on landing, but a later re-run leaves the user's filters 
alone.
+      if (selectedFilters.length > 0) {
+        return;
+      }
       const component = get(clusterComponents, "items", []).find(
         (component: any) =>
           get(component, "ServiceComponentInfo.component_name", "") ===
@@ -198,7 +243,7 @@ export default function HostsList() {
         setSelectedFilters(newFilter);
       }
     }
-  }, [params.componentName, clusterComponents]);
+  }, [params.componentName, clusterComponents, selectedFilters.length]);
 
   useEffect(() => {
     if (
@@ -206,6 +251,11 @@ export default function HostsList() {
       params.versionStatus &&
       !isEmpty(stackVersionList)
     ) {
+      // Seed only while no filter is set, so a refreshed stackVersionList does
+      // not discard filters the user added on top of the ones from the URL.
+      if (selectedFilters.length > 0) {
+        return;
+      }
       const versionExists = stackVersionList.some(
         (version: any) =>
           version.displayName === params.versionName &&
@@ -243,7 +293,12 @@ export default function HostsList() {
         setSelectedFilters(newFilter);
       }
     }
-  }, [params.versionName, params.versionStatus, stackVersionList]);
+  }, [
+    params.versionName,
+    params.versionStatus,
+    stackVersionList,
+    selectedFilters.length,
+  ]);
 
   useEffect(() => {
     if (
@@ -1380,6 +1435,7 @@ export default function HostsList() {
                 searchCallback={setSelectedFilters}
                 selectedFilters={selectedFilters}
                 setSelectedFilters={setSelectedFilters}
+                onResetFilters={clearFilters}
               />
               {paginationLoading ? (
                 <Spinner />
diff --git a/ambari-web/latest/src/store/HostsListStateContext.tsx 
b/ambari-web/latest/src/store/HostsListStateContext.tsx
new file mode 100644
index 0000000000..bfa44da675
--- /dev/null
+++ b/ambari-web/latest/src/store/HostsListStateContext.tsx
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, {
+  createContext,
+  useCallback,
+  useContext,
+  useEffect,
+  useRef,
+  useState,
+} from "react";
+import { useLocation } from "react-router-dom";
+import { db } from "../Utils/db";
+
+interface HostsListStateContextType {
+  selectedFilters: any;
+  setSelectedFilters: (filters: any | ((prev: any) => any)) => void;
+  selectedHosts: string[];
+  setSelectedHosts: (
+    hosts: string[] | ((prev: string[]) => string[])
+  ) => void;
+}
+
+const HostsListStateContext = createContext<
+  HostsListStateContextType | undefined
+>(undefined);
+
+/**
+ * Hosts List page filters and host selection are held here rather than inside
+ * HostsList so they survive the unmount that happens when navigating to a Host
+ * page. Filters are in memory and reset once the user leaves the Hosts pages;
+ * the host selection is mirrored into the local db (the same
+ * app.tables.selectedItems.mainHostController slot Ember uses) so it also
+ * survives a page refresh.
+ */
+export const HostsListStateProvider: React.FC<{
+  children: React.ReactNode;
+}> = ({ children }) => {
+  const location = useLocation();
+  const [selectedFilters, setSelectedFilters] = useState<any>([]);
+  const [selectedHosts, setSelectedHostsState] = useState<string[]>(() =>
+    db.getSelectedHosts()
+  );
+  const wasOnHostsPages = useRef(false);
+
+  const isOnHostsPages = /^\/main\/hosts(\/|$)/.test(location.pathname);
+
+  useEffect(() => {
+    if (wasOnHostsPages.current && !isOnHostsPages) {
+      setSelectedFilters([]);
+    }
+    wasOnHostsPages.current = isOnHostsPages;
+  }, [isOnHostsPages]);
+
+  // Persist as part of the setter rather than in an effect: an effect also 
runs
+  // on mount/remount, which could write an empty selection over the stored one
+  // before the restore had been applied.
+  const setSelectedHosts = useCallback(
+    (hosts: string[] | ((prev: string[]) => string[])) => {
+      const next =
+        typeof hosts === "function" ? hosts(db.getSelectedHosts()) : hosts;
+      db.setSelectedHosts(next);
+      setSelectedHostsState(next);
+    },
+    []
+  );
+
+  return (
+    <HostsListStateContext.Provider
+      value={{
+        selectedFilters,
+        setSelectedFilters,
+        selectedHosts,
+        setSelectedHosts,
+      }}
+    >
+      {children}
+    </HostsListStateContext.Provider>
+  );
+};
+
+export const useHostsListState = (): HostsListStateContextType => {
+  const context = useContext(HostsListStateContext);
+  if (!context) {
+    throw new Error(
+      "useHostsListState must be used within a HostsListStateProvider"
+    );
+  }
+  return context;
+};


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to