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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]