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

arshad pushed a commit to branch frontend-refactor
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/frontend-refactor by this push:
     new 9f13a69dbe AMBARI-26548 : Ambari Web React: Hosts Combo Search 
Component (#4071)
9f13a69dbe is described below

commit 9f13a69dbef6dd528e7de11d592ee5adc760844f
Author: Himanshu Maurya <54660538+himanshumaurya09...@users.noreply.github.com>
AuthorDate: Sun Sep 14 23:39:49 2025 +0530

    AMBARI-26548 : Ambari Web React: Hosts Combo Search Component (#4071)
---
 ambari-web/latest/src/constants.ts                 |  21 +
 .../latest/src/screens/Hosts/HostComboSearch.tsx   | 508 +++++++++++++++++++++
 2 files changed, 529 insertions(+)

diff --git a/ambari-web/latest/src/constants.ts 
b/ambari-web/latest/src/constants.ts
index da554606cc..401008d68e 100644
--- a/ambari-web/latest/src/constants.ts
+++ b/ambari-web/latest/src/constants.ts
@@ -27,3 +27,24 @@ export enum ProgressStatus {
   COMPLETED = "COMPLETED",
   FAILED = "FAILED",
 }
+
+
+export const serviceNameDisplayMapping = {
+  HDFS: "HDFS",
+  YARN: "YARN",
+  RANGER: "Ranger",
+  ZOOKEEPER: "Zookeeper",
+  HIVE: "Hive",
+  SPARK: "Spark3",
+  MAPREDUCE2: "MapReduce2",
+  TEZ: "Tez",
+  HBASE: "HBase",
+  KERBEROS: "Kerberos",
+  RANGER_KMS: "Ranger KMS",
+  AMBARI_METRICS: "Ambari Metrics",
+  TRINO: "Trino",
+  SSM: "SSM",
+  SQOOP: "Sqoop",
+  KYUUBI: "Kyuubi",
+  TRINO_GATEWAY: "Trino Gateway",
+};
\ No newline at end of file
diff --git a/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx 
b/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx
new file mode 100644
index 0000000000..ee1a8fefbc
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/HostComboSearch.tsx
@@ -0,0 +1,508 @@
+/**
+ * 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 { useContext, useEffect, useState } from "react";
+import Select from "react-select";
+import { get, isEmpty } from "lodash";
+import { Badge, Button } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faClose } from "@fortawesome/free-solid-svg-icons";
+import { serviceNameDisplayMapping } from "../../constants";
+import { ComponentStatus } from "./enums";
+import HostComponent from "../../models/hostComponent";
+import { IHost } from "../../models/host";
+import HostStackVersion from "../../models/hostStackVersion";
+import {
+  healthClassesForHostFilter,
+  nonRepeatableHostFieldOptions,
+} from "./constants";
+import { HostsApi } from "../../api/hostsApi";
+import { AppContext } from "../../store/context";
+import { translate } from "../../Utils/Utility";
+
+type FilterField = { label: string; value: string; name?: string };
+export type SelectedFilters = {
+  field: FilterField;
+  value: FilterField;
+}[];
+
+type HostComboSearchProps = {
+  showFilters: boolean;
+  allHostModels: IHost[];
+  clusterComponents: any;
+  searchCallback: (data: any) => void;
+  selectedFilters: SelectedFilters;
+  setSelectedFilters: (
+    filters: SelectedFilters | ((prev: SelectedFilters) => SelectedFilters)
+  ) => void;
+};
+
+function HostComboSearch({
+  showFilters,
+  allHostModels,
+  clusterComponents,
+  searchCallback,
+  selectedFilters,
+  setSelectedFilters,
+}: HostComboSearchProps) {
+  const { clusterName } = useContext(AppContext);
+  const [selectedField, setSelectedField] = useState<FilterField | null>(null);
+  const [selectedValue, setSelectedValue] = useState<FilterField | null>(null);
+  const [valueOptions, setValueOptions] = useState<FilterField[]>([]);
+  const [groupedFieldOptions, setGroupedFieldOptions] = useState<any[]>([]);
+
+  useEffect(() => {
+    updateGroupedFieldOptions();
+  }, [clusterComponents]);
+
+  useEffect(() => {
+    setValuesOnFieldChange();
+  }, [selectedField]);
+
+  useEffect(() => {
+    setValuesOnValueChange();
+  }, [selectedValue]);
+
+  useEffect(() => {
+    searchCallback(selectedFilters);
+    updateGroupedFieldOptions();
+  }, [selectedFilters.length]);
+
+  const updateGroupedFieldOptions = () => {
+    const filteredHostOptions = hostOptions.filter((option) => {
+      return !(
+        nonRepeatableHostFieldOptions.includes(option.value) &&
+        selectedFilters.some((filter) => filter.field.value === option.value)
+      );
+    });
+    let groupedFieldOptionsList = [
+      {
+        label: "Host",
+        options: filteredHostOptions,
+      },
+      {
+        label: "Service",
+        options: serviceOptions,
+      },
+    ];
+    if (
+      selectedFilters.length === 0 ||
+      selectedFilters.every(
+        (filter) => !(filter.field.name === "componentState")
+      )
+    ) {
+      groupedFieldOptionsList.push({
+        label: "Component",
+        options: getComponentFieldOptions(),
+      });
+    }
+    setGroupedFieldOptions(groupedFieldOptionsList);
+  };
+
+  const setValuesOnValueChange = async () => {
+    const fieldValue = selectedField?.value || "";
+    if (["hostName", "ip"].includes(fieldValue)) {
+      const correspondingValues = await getCorrespondingValues();
+      setValueOptions(correspondingValues);
+    }
+  };
+
+  const setValuesOnFieldChange = async () => {
+    if (!isEmpty(selectedField)) {
+      const correspondingValues = await getCorrespondingValues();
+      setSelectedValue(null);
+      setValueOptions(correspondingValues);
+    } else {
+      setSelectedValue(null);
+      setValueOptions([]);
+    }
+  };
+
+  const getComponentFieldOptions = () => {
+    return get(clusterComponents, "items", [])
+      .filter(
+        (component: any) => get(component, "host_components", []).length > 0
+      )
+      .map((component: any) => ({
+        label: get(component, "host_components.[0].HostRoles.display_name", 
""),
+        value: get(
+          component,
+          "host_components.[0].HostRoles.component_name",
+          ""
+        ),
+        name: "componentState",
+      }))
+      .sort((a: FilterField, b: FilterField) => 
a.label.localeCompare(b.label));
+  };
+
+  const hostOptions = [
+    { label: "Host Name", value: "hostName", name: "host" },
+    { label: "IP", value: "ip", name: "host" },
+    { label: "Host Status", value: "healthClass", name: "host" },
+    { label: "Cores", value: "cpu", name: "host" },
+    { label: "RAM", value: "memoryFormatted", name: "host" },
+    { label: "Stack Version", value: "version", name: "host" },
+    { label: "Version State", value: "versionState", name: "host" },
+    { label: "Rack", value: "rack", name: "host" },
+  ];
+
+  const serviceOptions = [
+    { label: "Service", value: "services", name: "service" },
+  ];
+
+  async function getCorrespondingValues() {
+    if (!selectedField) return [];
+    let valueOptionsList: FilterField[] = [];
+    const fieldname = selectedField.name;
+    if (fieldname === "service") {
+      valueOptionsList = getServiceValueOptions();
+    } else if (fieldname === "componentState") {
+      valueOptionsList = getComponentValueOptions();
+    } else {
+      valueOptionsList = await getHostValueOptions();
+    }
+    return valueOptionsList;
+  }
+
+  const getHostValueOptions = async () => {
+    if (!selectedField) return [];
+    const fieldValue = selectedField.value;
+    let hostValueOptions: FilterField[] = [];
+    switch (fieldValue) {
+      case "hostName":
+      case "ip":
+        hostValueOptions = await searchByHostname();
+        break;
+      case "rack":
+        hostValueOptions = searchByRack();
+        break;
+      case "version":
+        hostValueOptions = searchByVersion();
+        break;
+      case "versionState":
+        hostValueOptions = searchByVersionState();
+        break;
+      case "healthClass":
+        hostValueOptions = searchByHealthClass();
+        break;
+    }
+    return hostValueOptions;
+  };
+
+  const getServiceValueOptions = () => {
+    if (!selectedField) return [];
+    const fieldValue = selectedField.value;
+    let serviceValueOptions: FilterField[] = [];
+    if (fieldValue === "services") {
+      get(clusterComponents, "items", []).forEach((component: any) => {
+        const serviceName = get(
+          component,
+          "ServiceComponentInfo.service_name",
+          ""
+        );
+        if (
+          serviceName &&
+          get(component, "host_components", []).length > 0 &&
+          !serviceValueOptions.some((option) => option.value === serviceName)
+        ) {
+          serviceValueOptions.push({
+            label: get(serviceNameDisplayMapping, serviceName, serviceName),
+            value: serviceName,
+          });
+        }
+      });
+    }
+    serviceValueOptions.sort((a, b) => a.label.localeCompare(b.label));
+    return serviceValueOptions;
+  };
+
+  const getComponentValueOptions = () => {
+    if (!selectedField) return [];
+    const fieldValue = selectedField.value;
+    let componentValueOptions: FilterField[] = [
+      {
+        label: "All",
+        value: "ALL",
+      },
+    ];
+
+    if (!fieldValue.toLowerCase().includes("client")) {
+      componentValueOptions = componentValueOptions.concat(
+        Object.keys(ComponentStatus)
+          .filter((status: string) => status !== "UPGRADE_FAILED")
+          .map((status: string) => ({
+            label: HostComponent.getTextStatus(
+              ComponentStatus[status as keyof typeof ComponentStatus]
+            ),
+            value: ComponentStatus[status as keyof typeof ComponentStatus],
+          }))
+      );
+      componentValueOptions = componentValueOptions.concat([
+        { label: "Inservice", value: "INSERVICE" },
+        {
+          label: "Decommissioned",
+          value: "DECOMMISSIONED",
+        },
+        {
+          label: "Decommissioning",
+          value: "DECOMMISSIONING",
+        },
+        {
+          label: "RS Decommissioned",
+          value: "RS_DECOMMISSIONED",
+        },
+        { label: "Maintenance Mode On", value: "ON" },
+        { label: "Maintenance Mode Off", value: "OFF" },
+      ]);
+    }
+
+    return componentValueOptions;
+  };
+
+  const getPropertySuggestions = async (fieldValue: string) => {
+    try {
+      const data = {
+        filter: fieldValue,
+        searchTerm: selectedValue?.value || "",
+        pageSize: 10,
+      };
+      const response = await HostsApi.getHostListFilterSuggestions(
+        clusterName,
+        data
+      );
+      return response;
+    } catch (error) {
+      console.error("Error getting property suggestions:", error);
+    }
+    return "";
+  };
+
+  const searchByHostname = async () => {
+    let hostNameValueOptions: FilterField[] = [];
+    let fieldValue = selectedField?.value || "";
+    if (fieldValue === "hostName") fieldValue = "host_name";
+    const suggestions = await getPropertySuggestions(fieldValue);
+    if (suggestions) {
+      get(suggestions, "items", []).forEach((host: any) => {
+        const hostName = get(host, "Hosts." + fieldValue, "");
+        if (
+          hostName &&
+          !hostNameValueOptions.some((option) => option.value === hostName)
+        ) {
+          hostNameValueOptions.push({ label: hostName, value: hostName });
+        }
+      });
+    }
+    return hostNameValueOptions;
+  };
+
+  const searchByRack = () => {
+    let rackValueOptions: FilterField[] = [];
+    allHostModels.forEach((host: IHost) => {
+      const rack = get(host, "rack", "");
+      if (rack && !rackValueOptions.some((option) => option.value === rack)) {
+        rackValueOptions.push({ label: rack, value: rack });
+      }
+    });
+    rackValueOptions.sort((a, b) => a.label.localeCompare(b.label));
+    return rackValueOptions;
+  };
+
+  const searchByVersion = () => {
+    let versionValueOptions: FilterField[] = [];
+    allHostModels.forEach((host: IHost) => {
+      const stackVersion = get(host, "stackVersions", []).find(
+        (version: any) => {
+          return get(version, "status", "") === "CURRENT";
+        }
+      );
+      const versionName = get(stackVersion, "displayName", "");
+      if (
+        versionName &&
+        !versionValueOptions.some((option) => option.value === versionName)
+      ) {
+        versionValueOptions.push({
+          label: versionName,
+          value: versionName,
+        });
+      }
+    });
+    versionValueOptions.sort((a, b) => a.label.localeCompare(b.label));
+    return versionValueOptions;
+  };
+
+  const searchByVersionState = () => {
+    let versionStateValueOptions: FilterField[] = [];
+    HostStackVersion.statusDefinition.forEach((status: string) => {
+      versionStateValueOptions.push({
+        label: HostStackVersion.formatStatus(status),
+        value: status,
+      });
+    });
+    versionStateValueOptions.sort((a, b) => a.label.localeCompare(b.label));
+    return versionStateValueOptions;
+  };
+
+  const searchByHealthClass = () => {
+    let healthClassValueOptions: FilterField[] = [];
+    healthClassesForHostFilter.forEach((healthClass: any) => {
+      healthClassValueOptions.push({
+        label: healthClass.label,
+        value: healthClass.value,
+        name: healthClass.name,
+      });
+    });
+    return healthClassValueOptions;
+  };
+
+  function addFilter(e: any) {
+    e.preventDefault();
+    if (!selectedField || !selectedValue) return;
+    const newFilter = { field: selectedField, value: selectedValue };
+    if (
+      !selectedFilters.some(
+        (filter) =>
+          filter.field.value === newFilter.field.value &&
+          filter.value.value === newFilter.value.value
+      )
+    ) {
+      setSelectedFilters([...selectedFilters, newFilter]);
+      setSelectedField(null as any);
+      setSelectedValue(null as any);
+    }
+  }
+
+  function deleteFilter(filterToDelete: {
+    field: { label: string; value: any };
+    value: { label: string; value: any };
+  }) {
+    setSelectedFilters((prevFilters) => {
+      return prevFilters.filter((filter) => {
+        return !(
+          filter.field.value === filterToDelete.field.value &&
+          filter.value.value === filterToDelete.value.value
+        );
+      });
+    });
+  }
+
+  function resetFilters() {
+    setSelectedField(null as any);
+    setSelectedValue(null as any);
+    setSelectedFilters([]);
+  }
+
+  return (
+    <>
+      {showFilters ? (
+        <div
+          className="d-flex w-100 flex-column ease show ms-2"
+          data-testid="search-filters"
+        >
+          <div className="text-muted">
+            {translate("hosts.combo.search.placebolder")}
+          </div>
+          <div className="d-flex mt-2">
+            <form
+              onSubmit={addFilter}
+              className="d-flex w-100 align-items-center"
+            >
+              <Select
+                value={selectedField}
+                onChange={(value) => {
+                  setSelectedField(value as FilterField);
+                }}
+                options={groupedFieldOptions}
+                placeholder="Select field"
+                className="w-15 me-2"
+                isClearable
+                menuPortalTarget={document.body}
+              />
+              <Select
+                value={selectedValue}
+                options={valueOptions}
+                placeholder="Select Value"
+                className="w-15"
+                isClearable
+                menuPortalTarget={document.body}
+                onChange={(value) => {
+                  if (selectedValue?.value !== value?.value) {
+                    setSelectedValue(value as FilterField);
+                  }
+                }}
+                onInputChange={(inputValue, actionMeta) => {
+                  if (
+                    actionMeta.action === "input-change" &&
+                    !isEmpty(selectedField) &&
+                    selectedValue?.value !== inputValue
+                  ) {
+                    setSelectedValue({
+                      label: inputValue,
+                      value: inputValue,
+                    } as FilterField);
+                  }
+                }}
+              />
+              <Button
+                disabled={!selectedField?.label || !selectedValue?.value}
+                size="sm"
+                variant="outline-secondary"
+                onClick={addFilter}
+                type="submit"
+                className="ms-2"
+              >
+                Add Filter
+              </Button>
+              <Button
+                size="sm"
+                variant="outline-danger"
+                onClick={resetFilters}
+                className="ms-2"
+              >
+                Reset Filters
+              </Button>
+            </form>
+          </div>
+          <div className="mt-2 d-flex flex-wrap">
+            {selectedFilters.map((fil, index) => {
+              return (
+                <Badge
+                  bg={`secondary d-flex mt-2  align-items-center text-white ${
+                    index > 0 ? "ms-2" : ""
+                  }`}
+                >
+                  <div className="text-white">{fil.field.label}:</div>
+                  <div className="ms-2 text-white">{fil.value.label}</div>
+                  <FontAwesomeIcon
+                    icon={faClose}
+                    onClick={() => {
+                      deleteFilter(fil);
+                    }}
+                    className="delete-filter cursot-pointer ms-2"
+                  />
+                </Badge>
+              );
+            })}
+          </div>
+        </div>
+      ) : null}
+    </>
+  );
+}
+
+export default HostComboSearch;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@ambari.apache.org
For additional commands, e-mail: commits-h...@ambari.apache.org

Reply via email to