pawarprasad123 commented on code in PR #688:
URL: https://github.com/apache/atlas/pull/688#discussion_r3859809243
##########
dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx:
##########
@@ -27,6 +27,7 @@ import {
useRef,
useState,
useMemo,
+ useCallback,
Review Comment:
Mixed == / === in SideBarTree.tsx (many pre-existing, some in new code)
1077
Use strict equality (===) per project coding standards.
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -21,11 +21,13 @@ import {
useCallback,
useEffect,
Review Comment:
1) Inconsistent quote style in SideBarBody.tsx ('calc(100vh - 100px)' vs
double quotes elsewhere)
2) Lines 296, 314, 393, 429, 538, 554
Several sx={{...}} props remain in this file. The PR description states
inline/sx styling was fully removed. Either migrate these remaining styles to
SCSS or update the PR description to reflect partial migration.
##########
dashboard/src/views/SideBar/__tests__/SideBarBody.test.tsx:
##########
@@ -15,6 +15,7 @@
* limitations under the License.
*/
+import '@testing-library/jest-dom';
Review Comment:
Consider adding a test that verifies <Outlet /> content persists (does not
unmount) when toggling the sidebar or opening/closing popovers — this validates
the core performance fix.
##########
dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx:
##########
@@ -287,1027 +323,1067 @@ const BarTreeView: FC<{
sideBarOpen,
searchTerm,
loader,
+ isPopover,
}) => {
- const dispatch = useAppDispatch();
- const { savedSearchData }: any = useAppSelector(
- (state: any) => state.savedSearch
- );
- const { bmguid } = useParams();
- const location = useLocation();
- const navigate = useNavigate();
- const searchParams = new URLSearchParams(location.search);
- const [expand, setExpand] = useState<null | HTMLElement>(null);
- const [selectedNode, setSelectedNode] = useState<{
- type: string | null;
- tag: string | null;
- relationship: string | null;
- businessMetadata: string | null;
- }>({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: null,
- });
-
- const [openModal, setOpenModal] = useState<boolean>(false);
- const toastId: any = useRef(null);
- const open = Boolean(expand);
- const [expandedItems, setExpandedItems] = useState<string[]>([]);
- const [tagModal, setTagModal] = useState<boolean>(false);
- const [glossaryModal, setGlossaryModal] = useState<boolean>(false);
- const { businessMetaData }: any = useAppSelector(
- (state: any) => state.businessMetaData
- );
-
- const filteredData = useMemo(() => {
- return treeData.filter((node) => {
- return (
- node.label?.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (node.children &&
- node.children.some((child) =>
- child.label?.toLowerCase().includes(searchTerm.toLowerCase())
- ))
- );
+ const { savedSearchData }: any = useAppSelector(
+ (state: any) => state.savedSearch
+ );
+ const { bmguid } = useParams();
+ const dispatch = useAppDispatch();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const searchParams = new URLSearchParams(location.search);
+ const [expand, setExpand] = useState<null | HTMLElement>(null);
+ const [selectedNode, setSelectedNode] = useState<SelectedNode>({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
});
- }, [treeData, searchTerm]);
- const displayTreeName = useMemo(() => {
- return treeName === "CustomFilters" ? "Custom Filters" : treeName
- }, [treeName]);
+ const [openModal, setOpenModal] = useState<boolean>(false);
+ const toastId: any = useRef(null);
+ const open = Boolean(expand);
+ const [expandedItems, setExpandedItems] = useState<string[]>([]);
+ const [tagModal, setTagModal] = useState<boolean>(false);
+ const [glossaryModal, setGlossaryModal] = useState<boolean>(false);
+ const { businessMetaData }: any = useAppSelector(
+ (state: any) => state.businessMetaData
+ );
- const highlightText = useMemo(() => {
- return (text: string) => {
- if (!searchTerm) return text;
+ const filteredData = useMemo(() => {
+ if (!searchTerm) return treeData;
+ const lowerSearch = searchTerm.toLowerCase();
+ return treeData.reduce((acc: any[], node: any) => {
+ const nodeMatches = node.label?.toLowerCase().includes(lowerSearch);
+ let filteredChildren = node.children;
+ if (!nodeMatches && node.children) {
+ filteredChildren = node.children.filter((child: any) =>
+ child.label?.toLowerCase().includes(lowerSearch)
+ );
+ }
+ if (nodeMatches || (filteredChildren && filteredChildren.length > 0)) {
+ acc.push({ ...node, children: filteredChildren });
+ }
+ return acc;
+ }, []);
+ }, [treeData, searchTerm]);
- const parts = text.split(new RegExp(`(${searchTerm})`, "gi"));
- return parts.map((part, index) =>
- part.toLowerCase() === searchTerm.toLowerCase() ? (
- <span key={index} style={{ color: "#D3D3D3", fontWeight: "600" }}>
- {part}
- </span>
- ) : (
- part
- )
- );
- };
- }, [searchTerm]);
-
- const expandedItemsMemo = useMemo(() => {
- const allNodeIds = filteredData.flatMap((node) => {
- return [
- node.id,
- ...(node.children ? node.children.map((child) => child.id) : []),
- ];
- });
- return [...allNodeIds, ...[treeName]];
- }, [filteredData, treeName]);
+ const displayTreeName = useMemo(() => {
+ return treeName === "CustomFilters" ? "Custom Filters" : treeName
+ }, [treeName]);
- useEffect(() => {
- setExpandedItems(expandedItemsMemo);
- }, [expandedItemsMemo]);
+ const highlightText = useMemo(() => {
+ return (text: string) => {
+ if (!searchTerm) return text;
- useEffect(() => {
- const searchParams = new URLSearchParams(location.search);
- const nodeIdFromParamsType = searchParams.get("type");
- const nodeIdFromParamsTag = searchParams.get("tag");
- const nodeIdFromParamsRelationshipName =
- searchParams.get("relationshipName");
- const nodeIdFromBMName = location.pathname.includes(
- "/administrator/businessMetadata"
- );
+ const escapeRegExp = (string: string) => {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means
the whole matched string
+ };
- const bmObj = !isEmpty(businessMetaData?.businessMetadataDefs)
- ? businessMetaData?.businessMetadataDefs?.find((obj: EnumTypeDefData) =>
{
+ const escapedSearchTerm = escapeRegExp(searchTerm);
+ const parts = text.split(new RegExp(`(${escapedSearchTerm})`, "gi"));
+ return parts.map((part, index) =>
+ part.toLowerCase() === searchTerm.toLowerCase() ? (
+ <span key={index} className="sidebar-tree-highlight">
+ {part}
+ </span>
+ ) : (
+ part
+ )
+ );
+ };
+ }, [searchTerm]);
+
+ const getNodeId = useCallback((node: TreeNode) => {
+ if (treeName == "Classifications" && node.types == "parent") {
+ return node.label;
+ } else if (treeName == "Classifications" && node.types == "child") {
+ return `${node.id}@${node.label}`;
+ }
+ return !isEmpty(node?.parent) ? `${node.id}@${node?.parent}` : node.id;
+ }, [treeName]);
+
+ const expandedItemsMemo = useMemo(() => {
+ const parentNodeIds = filteredData.map((node) => getNodeId(node));
+ return [...parentNodeIds, treeName];
+ }, [filteredData, treeName, getNodeId]);
+
+ useEffect(() => {
+ setExpandedItems(expandedItemsMemo);
+ }, [expandedItemsMemo]);
+
+ useEffect(() => {
+ const searchParams = new URLSearchParams(location.search);
+ const nodeIdFromParamsType = searchParams.get("type");
+ const nodeIdFromParamsTag = searchParams.get("tag");
+ const nodeIdFromParamsRelationshipName =
+ searchParams.get("relationshipName");
+ const nodeIdFromBMName = location.pathname.includes(
+ "/administrator/businessMetadata"
+ );
+ const nodeIdFromParamsTerm = searchParams.get("term") ||
searchParams.get("category") || searchParams.get("gtype") ||
location.pathname.split("/glossary/")[1];
+ const nodeIdFromCustomFilter = searchParams.get("customFilter");
+
+ const bmObj = !isEmpty(businessMetaData?.businessMetadataDefs)
+ ? businessMetaData?.businessMetadataDefs?.find((obj: EnumTypeDefData)
=> {
if (bmguid == obj.guid) {
return obj;
}
})
- : {};
- const { name = "" } = bmObj || {};
-
- setSelectedNode({
- type: nodeIdFromParamsType,
- tag: nodeIdFromParamsTag,
- relationship: nodeIdFromParamsRelationshipName,
- businessMetadata: nodeIdFromBMName ? name : null,
- });
+ : {};
+ const { name = "" } = bmObj || {};
- if (
- !nodeIdFromParamsType &&
- !nodeIdFromParamsTag &&
- !nodeIdFromParamsRelationshipName &&
- !nodeIdFromBMName
- ) {
setSelectedNode({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: null,
+ type: nodeIdFromParamsType,
+ tag: nodeIdFromParamsTag,
+ relationship: nodeIdFromParamsRelationshipName,
+ businessMetadata: nodeIdFromBMName ? name : null,
+ term: nodeIdFromParamsTerm || null,
+ customFilter: nodeIdFromCustomFilter || null,
});
- }
- }, [location.search]);
- const getEmptyTypesTitle = () => {
- switch (treeName) {
- case "Entities":
- return `${isEmptyServicetype ? "Hide" : "Show"} empty service types`;
+ if (nodeIdFromParamsTerm && typeof nodeIdFromParamsTerm === "string" &&
nodeIdFromParamsTerm.includes("@") && treeName === "Glossary") {
+ const glossaryName = nodeIdFromParamsTerm.split("@")[1];
+ if (glossaryName) {
+ const parentNode = treeData.find((n) => n.label === glossaryName);
+ if (parentNode) {
+ const nodeIdToExpand = getNodeId(parentNode);
+ setExpandedItems((prev) => {
+ if (!prev.includes(nodeIdToExpand)) {
+ return [...prev, nodeIdToExpand];
+ }
+ return prev;
+ });
+ }
+ }
+ }
- case "Classifications":
- return `${isEmptyServicetype ? "Show" : "Hide"} unused
classifications`;
+ if (
+ !nodeIdFromParamsType &&
+ !nodeIdFromParamsTag &&
+ !nodeIdFromParamsRelationshipName &&
+ !nodeIdFromBMName &&
+ !nodeIdFromParamsTerm &&
+ !nodeIdFromCustomFilter
+ ) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ }, [location.pathname, location.search, treeData, treeName,
businessMetaData, bmguid, getNodeId]);
- case "Glossary":
- return `Show ${isEmptyServicetype ? "Category" : "Term"}`;
+ const getEmptyTypesTitle = () => {
+ switch (treeName) {
+ case "Entities":
+ return `${isEmptyServicetype ? "Hide" : "Show"} empty service types`;
- case "CustomFilters":
- return `Show ${isEmptyServicetype ? "all" : "Type"}`;
+ case "Classifications":
+ return `${isEmptyServicetype ? "Show" : "Hide"} unused
classifications`;
- default:
- return "";
- }
- };
+ case "Glossary":
+ return `Show ${isEmptyServicetype ? "Category" : "Term"}`;
- const handleExpandedItemsChange = (
- _event: SyntheticEvent,
- newExpandedItems: string[]
- ) => {
- setExpandedItems(newExpandedItems);
- };
+ case "CustomFilters":
+ return `Show ${isEmptyServicetype ? "all" : "Type"}`;
- const handleOpenModal = () => {
- setOpenModal(true);
- };
- const handleCloseModal = () => {
- setOpenModal(false);
- };
+ default:
+ return "";
+ }
+ };
- const handleClickMenu = (event: MouseEvent<HTMLElement>) => {
- event.stopPropagation();
- setExpand(event.currentTarget);
- };
+ const handleExpandedItemsChange = (
+ _event: SyntheticEvent,
+ newExpandedItems: string[]
+ ) => {
+ setExpandedItems(newExpandedItems);
+ };
- const handleClose = () => {
- setExpand(null);
- };
+ const handleOpenModal = () => {
+ setOpenModal(true);
+ };
+ const handleCloseModal = () => {
+ setOpenModal(false);
+ };
- const handleCloseTagModal = () => {
- setTagModal(false);
- };
- const handleCloseGlossaryModal = () => {
- setGlossaryModal(false);
- };
+ const handleClickMenu = (event: MouseEvent<HTMLElement>) => {
+ event.stopPropagation();
+ setExpand(event.currentTarget);
+ };
- const handleClickNode = (nodeId: string) => {
- const searchParams = new URLSearchParams(location.search);
- const isTypeMatch = searchParams.get("type") === nodeId;
- const isTagMatch = searchParams.get("tag") === nodeId;
- const isRelationshipMatch = searchParams.get("relationshipName") ===
nodeId;
- const isBusinessMetadataMatch = location.pathname.includes(
- "/administrator/businessMetadata"
- );
+ const handleClose = () => {
+ setExpand(null);
+ };
- if (isTypeMatch) {
- setSelectedNode({
- type: nodeId,
- tag: null,
- relationship: null,
- businessMetadata: null,
- });
- }
- if (isTagMatch) {
- setSelectedNode({
- type: null,
- tag: nodeId,
- relationship: null,
- businessMetadata: null,
- });
- }
- if (isRelationshipMatch) {
- setSelectedNode({
- type: null,
- tag: null,
- relationship: nodeId,
- businessMetadata: null,
- });
- }
- if (isBusinessMetadataMatch) {
- setSelectedNode({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: nodeId,
- });
- }
- };
+ const handleCloseTagModal = () => {
+ setTagModal(false);
+ };
+ const handleCloseGlossaryModal = () => {
+ setGlossaryModal(false);
+ };
- const getNodeId = (node: TreeNode) => {
- if (treeName == "Classifications" && node.types == "parent") {
- return node.label;
- } else if (treeName == "Classifications" && node.types == "child") {
- return `${node.id}@${node.label}`;
- }
- return !isEmpty(node?.parent) ? `${node.id}@${node?.parent}` : node.id;
- };
+ const handleClickNode = (nodeId: string) => {
+ const searchParams = new URLSearchParams(location.search);
+ const isTypeMatch = searchParams.get("type") === nodeId;
+ const isTagMatch = searchParams.get("tag") === nodeId;
+ const isRelationshipMatch = searchParams.get("relationshipName") ===
nodeId;
+ const isBusinessMetadataMatch = location.pathname.includes(
+ "/administrator/businessMetadata"
+ );
+
+ if (isTypeMatch) {
+ setSelectedNode({
+ type: nodeId,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isTagMatch) {
+ setSelectedNode({
+ type: null,
+ tag: nodeId,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isRelationshipMatch) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: nodeId,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isBusinessMetadataMatch) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: nodeId,
+ term: null,
+ customFilter: null,
+ });
+ }
+ };
- const handleNodeClick = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- navigate: NavigateFunction,
- isEmptyServicetype: boolean | undefined,
- savedSearchData: any,
- toastId: any
- ) => {
- globalSearchFilterInitialQuery.setQuery({});
- searchParams.delete("tabActive");
-
- if (treeName === "Classifications") {
- handleClickNode(node.id);
- } else {
- handleClickNode(node.id);
- }
-
- if (node.id === "No Records Found") {
- if (typeof event !== "undefined" && event.stopPropagation) {
- event.stopPropagation();
+ const handleNodeClick = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ navigate: NavigateFunction,
+ isEmptyServicetype: boolean | undefined,
+ savedSearchData: any,
+ toastId: any
+ ) => {
+ globalSearchFilterInitialQuery.setQuery({});
+ searchParams.delete("tabActive");
+
+ if (treeName === "Classifications") {
+ handleClickNode(node.id);
+ } else {
+ handleClickNode(node.id);
}
- return;
- }
-
- if (shouldSetSearchParams(node, treeName)) {
- setSearchParams(
- node,
- treeName,
- searchParams,
- isEmptyServicetype,
- savedSearchData
+
+ if (node.id === "No Records Found") {
+ if (typeof event !== "undefined" && event.stopPropagation) {
+ event.stopPropagation();
+ }
+ return;
+ }
+
+ if (shouldSetSearchParams(node, treeName)) {
+ setSearchParams(
+ node,
+ treeName,
+ searchParams,
+ isEmptyServicetype,
+ savedSearchData
+ );
+ navigateToPath(
+ node,
+ treeName,
+ searchParams,
+ navigate,
+ isEmptyServicetype,
+ toastId
+ );
+ }
+ };
+
+ const shouldSetSearchParams = (node: TreeNode, treeName: string) => {
+ if (treeName === "CustomFilters" && node.types === "parent") {
+ return false;
+ }
+ return (
+ node.children === undefined ||
+ isEmpty(node.children) ||
+ treeName === "Classifications" ||
+ (treeName === "Glossary" && node.types === "child")
);
- navigateToPath(
- node,
- treeName,
- searchParams,
- navigate,
- isEmptyServicetype,
- toastId
+ };
+
+ const setSearchParams = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ isEmptyServicetype: boolean | undefined,
+ savedSearchData: any
+ ) => {
+ searchParams.set(
+ "searchType",
+ node.parent === "ADVANCED" ? "dsl" : "basic"
);
- }
- };
- const shouldSetSearchParams = (node: TreeNode, treeName: string) => {
- if (treeName === "CustomFilters" && node.types === "parent") {
- return false;
- }
- return (
- node.children === undefined ||
- isEmpty(node.children) ||
- treeName === "Classifications" ||
- (treeName === "Glossary" && node.types === "child")
- );
- };
+ switch (treeName) {
+ case "Entities":
+ searchParams.delete("relationshipName");
+ searchParams.set("type", node.id);
+ break;
+ case "Classifications": {
+ searchParams.delete("relationshipName");
+ const id = node.label.split(" (")[0];
+ searchParams.set("tag", id);
+ break;
+ }
+ case "Glossary":
+ setGlossarySearchParams(node, searchParams, isEmptyServicetype);
+ break;
+ case "Relationships":
+ case "CustomFilters":
+ setCustomFiltersSearchParams(node, searchParams, savedSearchData);
+ break;
+ default:
+ break;
+ }
- const setSearchParams = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- isEmptyServicetype: boolean | undefined,
- savedSearchData: any
- ) => {
- searchParams.set(
- "searchType",
- node.parent === "ADVANCED" ? "dsl" : "basic"
- );
+ if (treeName !== "CustomFilters") {
+ searchParams.delete("attributes");
+ searchParams.delete("entityFilters");
+ searchParams.delete("tagFilters");
+ searchParams.delete("relationshipFilters");
+ searchParams.set("pageLimit", "25");
+ searchParams.set("pageOffset", "0");
+ }
+ };
- switch (treeName) {
- case "Entities":
- searchParams.delete("relationshipName");
- searchParams.set("type", node.id);
- break;
- case "Classifications": {
- searchParams.delete("relationshipName");
- const id = node.label.split(" (")[0];
- searchParams.set("tag", id);
- break;
+ const setGlossarySearchParams = (
+ node: TreeNode,
+ searchParams: URLSearchParams,
+ isEmptyServicetype: boolean | undefined
+ ) => {
+ const guid =
+ !isEmptyServicetype && node.cGuid !== undefined
+ ? node.cGuid
+ : node.guid || "";
+ searchParams.delete("relationshipName");
+
+ if (isEmptyServicetype) {
+ searchParams.set("term", `${node.id}@${node.parent}`);
+ } else {
+ searchParams.delete("type");
+ searchParams.delete("tag");
+ searchParams.set("gid", node.guid as string);
}
- case "Glossary":
- setGlossarySearchParams(node, searchParams, isEmptyServicetype);
- break;
- case "Relationships":
- case "CustomFilters":
- setCustomFiltersSearchParams(node, searchParams, savedSearchData);
- break;
- default:
- break;
- }
-
- if (treeName !== "CustomFilters") {
- searchParams.delete("attributes");
- searchParams.delete("entityFilters");
- searchParams.delete("tagFilters");
- searchParams.delete("relationshipFilters");
- searchParams.set("pageLimit", "25");
- searchParams.set("pageOffset", "0");
- }
- };
- const setGlossarySearchParams = (
- node: TreeNode,
- searchParams: URLSearchParams,
- isEmptyServicetype: boolean | undefined
- ) => {
- const guid =
- !isEmptyServicetype && node.cGuid !== undefined
- ? node.cGuid
- : node.guid || "";
- searchParams.delete("relationshipName");
-
- if (isEmptyServicetype) {
- searchParams.set("term", `${node.id}@${node.parent}`);
- } else {
- searchParams.delete("type");
- searchParams.delete("tag");
- searchParams.set("gid", node.guid as string);
- }
-
- searchParams.set("gtype", `${isEmptyServicetype ? "term" : "category"}`);
- searchParams.set("viewType", `${isEmptyServicetype ? "term" :
"category"}`);
- searchParams.set("guid", guid);
- };
+ searchParams.set("gtype", `${isEmptyServicetype ? "term" : "category"}`);
+ searchParams.set("viewType", `${isEmptyServicetype ? "term" :
"category"}`);
+ searchParams.set("guid", guid);
+ };
- const setCustomFiltersSearchParams = (
- node: TreeNode,
- searchParams: URLSearchParams,
- savedSearchData: any[]
- ) => {
- // Clear all existing params except searchType
- const keys = Array.from(searchParams.keys());
- for (let i = 0; i < keys.length; i++) {
- if (keys[i] !== "searchType") {
- searchParams.delete(keys[i]);
- }
- }
-
- // Clear globalSearchFilterInitialQuery when applying new saved search
- globalSearchFilterInitialQuery.setQuery({});
-
- if (treeName === "CustomFilters") {
- const params = savedSearchData.find((obj) => obj.name === node.id);
- if (params) {
- const searchParamsObj = params?.searchParameters || {};
-
- // Step 1: Set searchType based on saved search type
- if (params.searchType) {
- const searchTypeValue = params.searchType === "ADVANCED" ? "dsl" :
"basic";
- searchParams.set("searchType", searchTypeValue);
+ const setCustomFiltersSearchParams = (
+ node: TreeNode,
+ searchParams: URLSearchParams,
+ savedSearchData: any[]
+ ) => {
+ // Clear all existing params except searchType
+ const keys = Array.from(searchParams.keys());
+ for (let i = 0; i < keys.length; i++) {
+ if (keys[i] !== "searchType") {
+ searchParams.delete(keys[i]);
}
-
- // Step 2: Apply basic search parameters (excluding filters which are
handled separately)
- for (const key in searchParamsObj) {
- if (shouldSetCustomFilterParam(node, key) &&
+ }
+
+ // Clear globalSearchFilterInitialQuery when applying new saved search
+ globalSearchFilterInitialQuery.setQuery({});
+
+ if (treeName === "CustomFilters") {
+ const params = savedSearchData.find((obj) => obj.name === node.id);
+ if (params) {
+ const searchParamsObj = params?.searchParameters || {};
+
+ // Step 1: Set searchType based on saved search type
+ if (params.searchType) {
+ const searchTypeValue = params.searchType === "ADVANCED" ? "dsl" :
"basic";
+ searchParams.set("searchType", searchTypeValue);
+ }
+
+ // Step 2: Apply basic search parameters (excluding filters which
are handled separately)
+ for (const key in searchParamsObj) {
+ if (shouldSetCustomFilterParam(node, key) &&
!["entityFilters", "tagFilters",
"relationshipFilters"].includes(key)) {
- setCustomFilterParam(searchParams, key, searchParamsObj[key]);
+ setCustomFilterParam(searchParams, key, searchParamsObj[key]);
+ }
}
- }
-
- // Step 3: Convert and apply entityFilters from API format to URL
string format
- if (searchParamsObj.entityFilters &&
!isEmpty(searchParamsObj.entityFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.entityFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("entityFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters
component UI
- const qbFilter =
convertApiToQueryBuilder(searchParamsObj.entityFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- entityFilters: qbFilter
- });
+
+ // Step 3: Convert and apply entityFilters from API format to URL
string format
+ if (searchParamsObj.entityFilters &&
!isEmpty(searchParamsObj.entityFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.entityFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("entityFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.entityFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ entityFilters: qbFilter
+ });
+ }
}
}
- }
-
- // Step 4: Convert and apply tagFilters from API format to URL string
format
- if (searchParamsObj.tagFilters &&
!isEmpty(searchParamsObj.tagFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.tagFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("tagFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters
component UI
- const qbFilter =
convertApiToQueryBuilder(searchParamsObj.tagFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- tagFilters: qbFilter
- });
+
+ // Step 4: Convert and apply tagFilters from API format to URL
string format
+ if (searchParamsObj.tagFilters &&
!isEmpty(searchParamsObj.tagFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.tagFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("tagFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.tagFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ tagFilters: qbFilter
+ });
+ }
}
}
- }
-
- // Step 5: Convert and apply relationshipFilters from API format to
URL string format
- if (searchParamsObj.relationshipFilters &&
!isEmpty(searchParamsObj.relationshipFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.relationshipFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("relationshipFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters
component UI
- const qbFilter =
convertApiToQueryBuilder(searchParamsObj.relationshipFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- relationshipFilters: qbFilter
- });
+
+ // Step 5: Convert and apply relationshipFilters from API format to
URL string format
+ if (searchParamsObj.relationshipFilters &&
!isEmpty(searchParamsObj.relationshipFilters)) {
+ const clonedFilter =
cloneDeep(searchParamsObj.relationshipFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("relationshipFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.relationshipFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ relationshipFilters: qbFilter
+ });
+ }
}
}
+
+ searchParams.set("isCF", "true");
+ searchParams.set("customFilter", node.id);
}
-
- searchParams.set("isCF", "true");
+ } else {
+ searchParams.set("relationshipName", node.id);
}
- } else {
- searchParams.set("relationshipName", node.id);
- }
- };
-
- // Helper function to convert API format filter (criterion/condition) to
query builder format (rules/combinator)
- const convertApiToQueryBuilder = (apiFilter: any): any => {
- if (!apiFilter || typeof apiFilter !== "object") {
- return null;
- }
-
- const result: any = {};
-
- // Convert condition to combinator
- if (apiFilter.condition) {
- result.combinator = apiFilter.condition.toLowerCase();
- } else {
- result.combinator = "and"; // default
- }
-
- // Convert criterion to rules
- if (apiFilter.criterion && Array.isArray(apiFilter.criterion)) {
- result.rules = apiFilter.criterion.map((rule: any) => {
- // If nested condition, recurse
- if (rule.condition || rule.criterion) {
- return convertApiToQueryBuilder(rule);
- }
- // Convert API rule format to query builder format
- return {
- field: rule.attributeName || rule.id,
- operator: rule.operator,
- value: rule.attributeValue || rule.value,
- type: rule.type || rule.attributeType
- };
- });
- } else if (apiFilter.rules && Array.isArray(apiFilter.rules)) {
- // Already in query builder format
- result.rules = apiFilter.rules.map((rule: any) =>
- rule.condition || rule.criterion ? convertApiToQueryBuilder(rule) :
rule
- );
- }
-
- return Object.keys(result).length > 0 ? result : null;
- };
+ };
- const shouldSetCustomFilterParam = (node: TreeNode, key: string) => {
- return (
- node.parent === "BASIC" ||
- node.parent === "ADVANCED" ||
- (node.parent === "BASIC_RELATIONSHIP" &&
- (key === "relationshipName" || key === "limit" || key === "offset"))
- );
- };
+ // Helper function to convert API format filter (criterion/condition) to
query builder format (rules/combinator)
+ const convertApiToQueryBuilder = (apiFilter: any): any => {
+ if (!apiFilter || typeof apiFilter !== "object") {
+ return null;
+ }
- const setCustomFilterParam = (
- searchParams: URLSearchParams,
- key: string,
- value: any
- ) => {
- if (key === "limit") {
- searchParams.set("pageLimit", value || 25);
- } else if (key === "offset") {
- searchParams.set("pageOffset", value);
- } else if (key === "typeName") {
- searchParams.set("type", value);
- } else if (key === "classification") {
- // Map classification to tag parameter for URL (matching classic UI)
- searchParams.set("tag", value);
- } else if (key === "termName") {
- // Map termName (API format) to term parameter for URL (matching classic
UI)
- searchParams.set("term", value);
- } else if (value !== null && value !== undefined && value !== "") {
- // Only set parameter if value is not null, undefined, or empty string
- searchParams.set(key, value);
- }
- };
+ const result: any = {};
- const navigateToPath = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- navigate: NavigateFunction,
- isEmptyServicetype: boolean | undefined,
- toastId: any
- ) => {
- switch (treeName) {
- case "Business MetaData":
- searchParams.delete("relationshipName");
- navigate(
- { pathname: `administrator/businessMetadata/${node.guid}` },
- { replace: true }
+ // Convert condition to combinator
+ if (apiFilter.condition) {
+ result.combinator = apiFilter.condition.toLowerCase();
+ } else {
+ result.combinator = "and"; // default
+ }
+
+ // Convert criterion to rules
+ if (apiFilter.criterion && Array.isArray(apiFilter.criterion)) {
+ result.rules = apiFilter.criterion.map((rule: any) => {
+ // If nested condition, recurse
+ if (rule.condition || rule.criterion) {
+ return convertApiToQueryBuilder(rule);
+ }
+ // Convert API rule format to query builder format
+ return {
+ field: rule.attributeName || rule.id,
+ operator: rule.operator,
+ value: rule.attributeValue || rule.value,
+ type: rule.type || rule.attributeType
+ };
+ });
+ } else if (apiFilter.rules && Array.isArray(apiFilter.rules)) {
+ // Already in query builder format
+ result.rules = apiFilter.rules.map((rule: any) =>
+ rule.condition || rule.criterion ? convertApiToQueryBuilder(rule) :
rule
);
- break;
- case "Glossary":
- if (!isEmptyServicetype) {
- searchParams.delete("relationshipName");
- navigate(
- {
- pathname: `glossary/${
- node.cGuid !== undefined ? node.cGuid : node.guid
- }`,
- search: searchParams.toString(),
- },
- { replace: true }
- );
- } else if (node.types === "parent") {
- toast.dismiss(toastId.current);
- toastId.current = toast.warning("Create a Term or Category");
- } else {
+ }
+
+ return Object.keys(result).length > 0 ? result : null;
+ };
+
+ const shouldSetCustomFilterParam = (node: TreeNode, key: string) => {
+ return (
+ node.parent === "BASIC" ||
+ node.parent === "ADVANCED" ||
+ (node.parent === "BASIC_RELATIONSHIP" &&
+ (key === "relationshipName" || key === "limit" || key === "offset"))
+ );
+ };
+
+ const setCustomFilterParam = (
+ searchParams: URLSearchParams,
+ key: string,
+ value: any
+ ) => {
+ if (key === "limit") {
+ searchParams.set("pageLimit", value || 25);
+ } else if (key === "offset") {
+ searchParams.set("pageOffset", value);
+ } else if (key === "typeName") {
+ searchParams.set("type", value);
+ } else if (key === "classification") {
+ // Map classification to tag parameter for URL (matching classic UI)
+ searchParams.set("tag", value);
+ } else if (key === "termName") {
+ // Map termName (API format) to term parameter for URL (matching
classic UI)
+ searchParams.set("term", value);
+ } else if (value !== null && value !== undefined && value !== "") {
+ // Only set parameter if value is not null, undefined, or empty string
+ searchParams.set(key, value);
+ }
+ };
+
+ const navigateToPath = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ navigate: NavigateFunction,
+ isEmptyServicetype: boolean | undefined,
+ toastId: any
+ ) => {
+ switch (treeName) {
+ case "Business MetaData":
searchParams.delete("relationshipName");
navigate(
- {
- pathname: "/search/searchResult",
- search: searchParams.toString(),
- },
+ { pathname: `administrator/businessMetadata/${node.guid}` },
{ replace: true }
);
- }
- break;
- case "Relationships":
- case "CustomFilters":
- if (
- treeName == "Relationships" ||
- (treeName == "CustomFilters" && node.parent == "BASIC_RELATIONSHIP")
- ) {
- navigate(
- {
- pathname: `relationship/relationshipSearchresult`,
- search: searchParams.toString(),
- },
- { replace: true }
- );
- } else {
+ break;
+ case "Glossary":
+ if (!isEmptyServicetype) {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: `glossary/${node.cGuid !== undefined ? node.cGuid :
node.guid
+ }`,
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ } else if (node.types === "parent") {
+ toast.dismiss(toastId.current);
+ toastId.current = toast.warning("Create a Term or Category");
+ } else {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: "/search/searchResult",
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ }
+ break;
+ case "Relationships":
+ case "CustomFilters":
+ if (
+ treeName == "Relationships" ||
+ (treeName == "CustomFilters" && node.parent ==
"BASIC_RELATIONSHIP")
+ ) {
+ navigate(
+ {
+ pathname: `relationship/relationshipSearchresult`,
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ } else {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: "/search/searchResult",
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ }
+ break;
+ default:
searchParams.delete("relationshipName");
navigate(
- {
- pathname: "/search/searchResult",
- search: searchParams.toString(),
- },
+ { pathname: "/search/searchResult", search:
searchParams.toString() },
{ replace: true }
);
- }
- break;
- default:
- searchParams.delete("relationshipName");
- navigate(
- { pathname: "/search/searchResult", search: searchParams.toString()
},
- { replace: true }
- );
- break;
- }
- };
-
- const TreeLabelWithTooltip: React.FC<{ label: string }> = ({ label }) => {
- const labelRef = useRef<HTMLSpanElement>(null);
- const [isOverflown, setIsOverflown] = useState(false);
-
- useEffect(() => {
- const el = labelRef.current;
- if (el) {
- setIsOverflown(el.scrollWidth > el.clientWidth);
+ break;
}
- }, [label, searchTerm]);
+ };
- return (
- <LightTooltip title={label} disableHoverListener={!isOverflown}>
- <span
- ref={labelRef}
- className="tree-item-label"
- style={{
- display: "inline-block",
- maxWidth: "100%",
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "nowrap",
- }}
- >
- {highlightText(label)}
- </span>
- </LightTooltip>
- );
- };
+ const TreeLabelWithTooltip: React.FC<{ label: string }> = ({ label }) => {
+ const labelRef = useRef<HTMLSpanElement>(null);
+ const [isOverflown, setIsOverflown] = useState(false);
- const renderTreeItem = (node: TreeNode) =>
- node?.id && (
- <CustomTreeItem
- key={node.id}
- itemId={getNodeId(node)}
- label={
- <div
- {...({
- selectedNodeType: selectedNode.type,
- selectedNodeTag: selectedNode.tag,
- selectedNodeRelationship: selectedNode.relationship,
- selectedNodeBM: selectedNode.businessMetadata,
- node: node.id,
- onClick: (_event: MouseEvent<HTMLElement>) => {
- handleNodeClick(
- node,
- treeName,
- searchParams,
- navigate,
- isEmptyServicetype,
- savedSearchData,
- toastId
- );
- },
- className: "custom-treeitem-label",
- } as any)}
+ useEffect(() => {
+ const el = labelRef.current;
+ if (el) {
+ setIsOverflown(el.scrollWidth > el.clientWidth);
+ }
+ }, [label]);
+
+ return (
+ <LightTooltip title={label} disableHoverListener={!isOverflown}>
+ <span
+ ref={labelRef}
+ className="tree-item-label"
>
- {node.id != "No Records Found" && (
- <TreeIcons
- node={node}
- treeName={treeName}
- isEmptyServicetype={isEmptyServicetype ?? false}
- />
- )}
- <TreeLabelWithTooltip label={node.label} />
- {(treeName == "Entities" ||
- treeName == "Classifications" ||
- treeName == "CustomFilters" ||
- treeName == "Glossary") &&
- node.id != "No Records Found" && (
- <TreeNodeIcons
- node={node}
- treeName={treeName}
- updatedData={refreshData}
- isEmptyServicetype={isEmptyServicetype}
- />
+ {highlightText(label)}
+ </span>
+ </LightTooltip>
+ );
+ };
+
+ const renderTreeItem = (node: TreeNode) =>
+ node?.id && (
+ <CustomTreeItem
+ key={node.id}
+ itemId={getNodeId(node)}
+ label={
+ <HoverableTreeItemContainer
+ {...({
+ selectedNodeType: selectedNode.type,
+ selectedNodeTag: selectedNode.tag,
+ selectedNodeRelationship: selectedNode.relationship,
+ selectedNodeBM: selectedNode.businessMetadata,
+ selectedNodeTerm: selectedNode.term ?
selectedNode.term.split("@")[0] : null,
+ selectedNodeCustomFilter: selectedNode.customFilter,
+ node: treeName === "Classifications" && node.label ?
node.label.split(" (")[0] : node.id,
+ onClick: (_event: MouseEvent<HTMLElement>) => {
+ handleNodeClick(
+ node,
+ treeName,
+ searchParams,
+ navigate,
+ isEmptyServicetype,
+ savedSearchData,
+ toastId
+ );
+ },
+ className: "custom-treeitem-label",
+ } as any)}
+ >
+ {(isHovered: boolean) => (
+ <>
+ {node.id != "No Records Found" && (
+ <TreeIcons
+ node={node}
+ treeName={treeName}
+ isEmptyServicetype={isEmptyServicetype ?? false}
+ />
+ )}
+ <TreeLabelWithTooltip label={node.label} />
+ {(treeName == "Entities" ||
+ treeName == "Classifications" ||
+ treeName == "CustomFilters" ||
+ treeName == "Glossary") &&
+ node.id != "No Records Found" && (
+ <TreeNodeIcons
+ node={node}
+ treeName={treeName}
+ updatedData={refreshData}
+ isEmptyServicetype={isEmptyServicetype}
+ isHovered={isHovered}
+ />
+ )}
+ </>
)}
- </div>
- }
- >
- {node.children && node.children.map((child) => renderTreeItem(child))}
- </CustomTreeItem>
- );
+ </HoverableTreeItemContainer>
+ }
+ >
+ {node.children && node.children.map((child) =>
renderTreeItem(child))}
+ </CustomTreeItem>
+ );
- const downloadFile = async () => {
- try {
- if (treeName == "Glossary") {
- await downloadGlossaryImportTemplate();
- return;
- }
- const apiResp: any = await getBusinessMetadataImportTmpl({});
- const text: string = apiResp ? apiResp.data : "";
- const blob = new Blob([text], { type: "text/plain" });
+ const downloadFile = async () => {
+ try {
+ if (treeName == "Glossary") {
+ await downloadGlossaryImportTemplate();
+ return;
+ }
+ const apiResp: any = await getBusinessMetadataImportTmpl({});
+ const text: string = apiResp ? apiResp.data : "";
+ const blob = new Blob([text], { type: "text/plain" });
- const url = window.URL.createObjectURL(blob);
+ const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", "template_business_metadata");
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "template_business_metadata");
- document.body.appendChild(link);
+ document.body.appendChild(link);
- link.click();
+ link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- } catch {
- /* ignore download error */
- }
- };
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch {
+ /* ignore download error */
+ }
+ };
- const label = { inputProps: { "aria-label": "Switch demo" } };
- return (
- <>
- <Stack
- className="sidebar-tree-box"
- sx={{
- ...(sideBarOpen == false && {
- visibility: "hidden !important",
- top: "62px !important",
- }),
- minWidth: "30px",
- width: `100% !important`,
- overflowX: "auto",
- }}
- >
- <SimpleTreeView
- expandedItems={expandedItems}
- onExpandedItemsChange={handleExpandedItemsChange}
- aria-label="customized"
- className="sidebar-treeview"
+ const label = { inputProps: { "aria-label": "Switch demo" } };
+ return (
+ <>
+ <Stack
+ className="sidebar-tree-box"
+ sx={{
+ ...(sideBarOpen == false && {
+ visibility: "hidden !important",
+ top: "62px !important",
+ }),
+ minWidth: "30px",
+ width: `100% !important`,
+ }}
>
- <TreeItem
- itemId={treeName}
- label={
- <Stack
- display="flex"
- alignItems="center"
- flexDirection="row"
- className="tree-item-parent-label"
- >
- <Stack flexGrow={1}>
- <span>{displayTreeName}</span>
- </Stack>
- <Stack direction="row" alignItems="center" gap="0.375rem">
- <LightTooltip title="Refresh">
- <IconButton
- size="small"
- data-cy="refreshTree"
- onClick={(e) => {
- e.stopPropagation();
- refreshData();
- }}
- disabled={loader}
- >
- <RefreshIcon />
- </IconButton>
- </LightTooltip>
-
+ <SimpleTreeView
+ expandedItems={expandedItems}
+ onExpandedItemsChange={handleExpandedItemsChange}
+ aria-label="customized"
+ className="sidebar-treeview"
+ >
+ <TreeItem
+ itemId={treeName}
+ label={
+ <Stack
+ display="flex"
+ alignItems="center"
+ flexDirection="row"
+ className="tree-item-parent-label"
+ >
+ <Stack flexGrow={1} direction="row" alignItems="center"
gap="12px">
+ {treeName === "Entities" && <img
src="/img/sidebar-icons/icon-entities.svg" style={{ width: "20px", height:
"20px", opacity: 1 }} alt="" />}
Review Comment:
Inline styles remain here despite the SCSS migration goal. Please reuse the
existing .sidebar-tree-icon class from sidebar.scss (lines 320–324) for
consistency.
##########
dashboard/src/components/GlobalSearch/QuickSearch.tsx:
##########
@@ -85,6 +85,8 @@ const SCOPE_LABELS: Record<QuickSearchScope, string> = {
businessMetadata: "Business Metadata"
};
+
Review Comment:
Extra blank lines in QuickSearch.tsx (lines 85–87)
##########
dashboard/src/components/SidebarSearchInput.tsx:
##########
@@ -0,0 +1,76 @@
+/*
+ * 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, { ChangeEvent } from "react";
+import { Paper, InputBase, Stack } from "@mui/material";
+import ClearIcon from "@mui/icons-material/Clear";
+import { IconButton } from "@components/muiComponents";
+
+interface SidebarSearchInputProps {
+ searchTerm: string;
+ onChange: (value: string) => void;
+ dataCy?: string;
+}
+
+export const SidebarSearchInput: React.FC<SidebarSearchInputProps> = ({
+ searchTerm,
+ onChange,
+ dataCy
+}) => (
+ <Paper
+ sx={{
Review Comment:
sx props for width, color, padding remain. Move to .sidebar-searchbar SCSS
or document as intentional MUI overrides.
##########
dashboard/src/components/EntityDisplayImage.tsx:
##########
@@ -15,89 +15,59 @@
* limitations under the License.
*/
-import { useEffect, useState } from "react";
-import { Avatar, Skeleton } from "@mui/material";
+import type { SyntheticEvent } from "react";
+import { Avatar } from "@mui/material";
import { getEntityIconPath } from "../utils/Utils";
-import axios from "axios";
+
+interface DisplayImageProps {
+ entity: Record<string, unknown>;
+ width?: string | number;
+ height?: string | number;
+ avatarDisplay?: boolean;
+ isProcess?: boolean;
+}
const DisplayImage = ({
entity,
width,
height,
avatarDisplay,
isProcess
-}: any) => {
- const [imageUrl, setImageUrl] = useState<any>(null);
- const [checkEntityImage, setCheckEntityImage] = useState<any>({
- [entity.guid]: false
- });
-
- useEffect(() => {
- const fetchImagePath = async () => {
- let entityData = { ...entity, ...{ isProcess: isProcess } };
- let imagePath: any = getEntityIconPath({ entityData: entityData });
- try {
- const response = await axios.get(imagePath, {
- responseType: "blob"
- });
- const contentType: any = response.headers["content-type"];
-
- if (contentType && contentType.startsWith("image/")) {
- let cache = { [entityData.guid]: imagePath };
- setCheckEntityImage(cache);
- setImageUrl(getEntityIconPath({ entityData: entityData }));
- } else {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- } catch (_error) {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- };
+}: DisplayImageProps) => {
+ const entityData = { ...entity, isProcess };
+
+ const primaryUrl = getEntityIconPath({ entityData }) || "";
+ const fallbackUrl = getEntityIconPath({ entityData, errorUrl: primaryUrl })
|| "";
- fetchImagePath();
- }, []);
+ const handleError = (e: SyntheticEvent<HTMLImageElement, Event>) => {
+ const target = e.currentTarget;
+ if (!target.src.endsWith(fallbackUrl)) {
Review Comment:
endsWith can fail when target.src is an absolute URL and fallbackUrl is
relative (or includes query params). Consider comparing pathname segments or
tracking fallback state with a ref/data attribute instead.
##########
dashboard/src/redux/slice/sessionSlice.ts:
##########
@@ -77,6 +96,30 @@ const sessionSlice = createSlice({
data: null,
error: (action.payload as string) || action.error?.message || 'An
error occurred'
};
+ }),
+ builder.addCase(fetchVersionData.pending, (state) => {
Review Comment:
Clearing data on pending causes the sidebar version footer to flash empty
during refetch. Consider keeping previous data while loading: true, similar to
a stale-while-revalidate pattern.
- verify this properly, if changes needed update test case also
##########
dashboard/src/components/__tests__/TreeSkeletonLoader.test.tsx:
##########
@@ -0,0 +1,47 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import TreeSkeletonLoader from '../TreeSkeletonLoader';
+
+describe('TreeSkeletonLoader', () => {
+ it('renders default number of skeletons when count is not provided', () => {
+ const { container } = render(<TreeSkeletonLoader />);
+
+ // By default count is 7
+ const skeletons = container.querySelectorAll('.MuiSkeleton-root');
+ // Each row has 1 arrow, 1 text = 2 skeletons per row
+ // 7 rows * 2 = 14 skeletons
+ expect(skeletons.length).toBe(14);
+ });
+
+ it('renders specific number of skeletons based on count prop', () => {
+ const { container } = render(<TreeSkeletonLoader count={2} />);
+
+ const skeletons = container.querySelectorAll('.MuiSkeleton-root');
+ // 2 rows * 2 = 4 skeletons
+ expect(skeletons.length).toBe(4);
+ });
+
+ it('renders correctly with 0 count', () => {
+ const { container } = render(<TreeSkeletonLoader count={0} />);
+
+ const skeletons = container.querySelectorAll('.MuiSkeleton-root');
+ expect(skeletons.length).toBe(0);
+ });
Review Comment:
Add edge-case tests for negative count or undefined behavior to match the
thoroughness of other new test files.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]