This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch ATLAS-5246-v1
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/ATLAS-5246-v1 by this push:
new 334161b4f ATLAS-5287: ATLAS UI: Dashboard- search bar with dropdown
(#616)
334161b4f is described below
commit 334161b4fc8d059dcff68e120d2193eec4b7e2b6
Author: Prasad Pawar <[email protected]>
AuthorDate: Fri May 8 12:53:43 2026 +0530
ATLAS-5287: ATLAS UI: Dashboard- search bar with dropdown (#616)
---
.../src/components/GlobalSearch/QuickSearch.tsx | 1185 ++++++++++++--------
dashboard/src/utils/scopedQuickSearchUtils.ts | 466 ++++++++
.../src/views/SideBar/SideBarTree/SideBarTree.tsx | 23 +-
3 files changed, 1165 insertions(+), 509 deletions(-)
diff --git a/dashboard/src/components/GlobalSearch/QuickSearch.tsx
b/dashboard/src/components/GlobalSearch/QuickSearch.tsx
index 272ee107d..c8eba9490 100644
--- a/dashboard/src/components/GlobalSearch/QuickSearch.tsx
+++ b/dashboard/src/components/GlobalSearch/QuickSearch.tsx
@@ -16,518 +16,715 @@
*/
import {
- Autocomplete,
- CircularProgress,
- InputAdornment,
- Stack,
- TextField,
- Typography
+ Autocomplete,
+ CircularProgress,
+ FormControl,
+ InputAdornment,
+ MenuItem,
+ Select,
+ Stack,
+ TextField,
+ Typography,
+ type SelectChangeEvent
} from "@mui/material";
-import { useRef, useState } from "react";
+import { useCallback, useMemo, useRef, useState } from "react";
import { getGlobalSearchResult } from "../../api/apiMethods/searchApiMethod";
import DisplayImage from "../EntityDisplayImage";
import SearchIcon from "@mui/icons-material/Search";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { entityStateReadOnly } from "../../utils/Enum";
import {
- extractKeyValueFromEntity,
- isEmpty,
- serverError
+ extractKeyValueFromEntity,
+ isEmpty,
+ serverError
} from "../../utils/Utils";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import AdvancedSearch from "./AdvancedSearch";
import {
- HandleValuesType,
- QuickSearchOptionListType,
- SuggestionDataType
+ HandleValuesType,
+ QuickSearchOptionListType,
+ SuggestionDataType
} from "../../models/globalSearchType";
import { AxiosResponse } from "axios";
import { CustomButton } from "@components/muiComponents";
+import { useAppSelector } from "@hooks/reducerHook";
+import {
+ buildBusinessMetadataScopedOptions,
+ buildClassificationScopedOptions,
+ buildEntitiesTreeForQuickSearch,
+ buildGlossaryTermsTreeForQuickSearch,
+ entityTreeToScopedOptions,
+ glossaryTreeToScopedOptions,
+ type QuickSearchScope,
+ type ScopedQuickSearchOption
+} from "@utils/scopedQuickSearchUtils";
+import {
+ navigateToBasicTextQuery,
+ navigateToBusinessMetadataDetailPage,
+ navigateToClassificationDetailPage,
+ navigateToGlossaryTermDetailPage,
+ navigateToServiceTypeEntitySearch
+} from "@utils/dashboardSearchUtils";
+
+interface GlobalOptionRow {
+ title: string;
+ types: string;
+ entityObj?: any;
+ scoped?: ScopedQuickSearchOption;
+}
+
+const SCOPE_SELECT_ID = "quick-search-scope-select";
+
+const SCOPE_LABELS: Record<QuickSearchScope, string> = {
+ default: "Select All",
+ entity: "Entity",
+ classification: "Classification",
+ glossary: "Glossary / Terms",
+ businessMetadata: "Business Metadata"
+};
const QuickSearch = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const toastId = useRef(null);
- const searchParams = new URLSearchParams(location.search);
- const [options, setOptions] = useState<any>([]);
- const [open, setOpen] = useState<boolean>(false);
- const [openAdvanceSearch, setOpenAdvanceSearch] = useState<boolean>(false);
- const [loading, setLoading] = useState<boolean>(false);
- const [value, setValue] = useState<string>("");
-
- const getData = async (searchTerm: string) => {
- let entities: QuickSearchOptionListType = [];
- let suggestionNames: QuickSearchOptionListType = [];
- let quickSearchResp: AxiosResponse | null = null;
- let suggestionSearchResp: AxiosResponse | null = null;
- try {
- setLoading(true);
- quickSearchResp = await getGlobalSearchResult("quick", {
- params: { query: searchTerm, limit: 5, offset: 0 }
- });
- setLoading(false);
- } catch (error) {
- setLoading(false);
- console.error("Error fetching quick search results:", error);
- serverError(error, toastId);
- }
-
- try {
- setLoading(true);
- suggestionSearchResp = await getGlobalSearchResult("suggestions", {
- params: { prefixString: searchTerm }
- });
- setLoading(false);
- } catch (error) {
- setLoading(false);
- console.error("Error fetching suggestion search results:", error);
- serverError(error, toastId);
- }
- const { searchResults = [] } = quickSearchResp?.data || {};
- const { suggestions = [] }: SuggestionDataType =
- suggestionSearchResp?.data || {};
-
- entities = !isEmpty(searchResults?.entities)
- ? searchResults?.entities?.map((entityDef: any) => {
- const { name }: { name: string; found: boolean; key: any } =
- extractKeyValueFromEntity(entityDef);
- return {
- title: `${name}`,
- parent: entityDef.typeName,
- types: "Entities",
- entityObj: entityDef
- };
- })
- : [{ title: "No Entities Found", types: "Entities" }];
-
- suggestionNames = !isEmpty(suggestions)
- ? suggestions.map((suggestion: any) => {
- return {
- title: `${suggestion}`,
- types: "Suggestions"
- };
- })
- : [{ title: "No Suggestions Found", types: "Suggestions" }];
-
- setOptions([...entities, ...suggestionNames]);
- };
-
- const onInputChange = (_event: any, value: string) => {
- // Sanitize input: remove any potential script tags and validate
- const sanitizedValue = value ? value.trim() : "";
-
- if (sanitizedValue) {
- setOpen(true);
- getData(sanitizedValue);
- } else {
- setOptions([]);
- setValue("");
- setOpen(false);
- }
- };
-
- const handleValues = (option: HandleValuesType | string | null) => {
- // Handle case when option is a string (direct search query)
- if (typeof option === "string") {
- const queryValue = option.trim();
- if (queryValue) {
- setOpen(false);
- setOptions([]);
- // URLSearchParams automatically encodes the value, preventing XSS
- // Additional validation: ensure query is not empty after trim
- const sanitizedQuery = queryValue || "*";
- searchParams.set("query", sanitizedQuery);
- searchParams.set("searchType", "basic");
- navigate(
- {
- pathname: `/search/searchResult`,
- search: searchParams.toString()
- },
- { replace: true }
- );
- }
- return;
- }
-
- // Handle case when option is null or undefined
- if (!option || typeof option !== "object") {
- return;
- }
-
- const { entityObj, title, types } = option;
-
- // Validate that title exists and is not undefined
- if (!title || title === "undefined" || typeof title !== "string") {
- return;
- }
-
- // Sanitize title: trim and validate
- const sanitizedTitle = title.trim();
- if (!sanitizedTitle) {
- return;
- }
-
- setOpen(false);
- setOptions([]);
- // URLSearchParams automatically encodes the value, preventing XSS
- searchParams.set("query", sanitizedTitle);
- searchParams.set("searchType", "basic");
-
- if (types === "Entities" && entityObj && entityObj.guid) {
- navigate(
- {
- pathname: `/detailPage/${entityObj.guid}`
- },
- { replace: true }
- );
- } else {
- navigate(
- {
- pathname: `/search/searchResult`,
- search: searchParams.toString()
- },
- { replace: true }
- );
- }
- };
-
- const handleClickAway = () => {
- setOpen(false);
- };
-
- const handleCloseModal = () => {
- setOpenAdvanceSearch(false);
- };
-
- return (
- <>
- <Stack
- direction="row"
- className="global-search-stack"
- alignItems="center"
- gap="0.5rem"
- >
- <ClickAwayListener onClickAway={handleClickAway}>
- <Autocomplete
- open={open}
- loading={loading}
- onKeyDown={(e) => {
- const code = e.keyCode || e.which;
-
- switch (code) {
- case 13: { // Enter key
- e.preventDefault();
- const inputValue = (e.target as
HTMLInputElement).value.trim();
-
- // If input is empty, use "*" for wildcard search (matching
classic UI behavior)
- const searchQuery = inputValue === "" ? "*" : inputValue;
-
- // Try to find exact match in options
- const activeOption = options.find(
- (option: { title: any }) =>
- typeof option !== "string" &&
- option.title === searchQuery
- );
-
- if (activeOption) {
- // If exact match found, use that option
- handleValues(activeOption as HandleValuesType);
- } else {
- // If no match found, trigger basic search with typed
value (matching classic UI behavior)
- handleValues(searchQuery);
- }
- break;
- }
- case 9: // Tab key
- case 27: // Escape key
- setOpen(false);
- break;
- default:
- break;
- }
- }}
- freeSolo
- id="global-search"
- disablePortal
- className="global-search-autocomplete"
- sx={{
- "& + .MuiAutocomplete-popper .MuiAutocomplete-option": {
- backgroundColor: "white"
- },
- "& + .MuiAutocomplete-popper .MuiAutocomplete-option:hover": {
- backgroundColor: "#c7e3ff"
- }
- }}
- value={value}
- onChange={(_event: any, newValue: any) => {
- if (newValue) {
- // Only update options if newValue is a valid option object
- if (typeof newValue === "object" && newValue.title) {
- setOptions([newValue, ...options]);
- }
- setValue(newValue);
- // Only call handleValues if newValue is a valid option
- if (typeof newValue === "object" && newValue.title &&
newValue.title !== "undefined") {
- handleValues(newValue);
- }
- } else {
- setValue("");
- }
- }}
- clearOnBlur={false}
- autoComplete={true}
- includeInputInList
- noOptionsText={"No Entities"}
- disableClearable
- onInputChange={onInputChange}
- getOptionLabel={(option: string | QuickSearchOptionListType) => {
- if (typeof option === "string") {
- return option;
- }
- // Safely extract title, defaulting to empty string if undefined
- const title = (option as any)?.title;
- return title && typeof title === "string" ? title : "";
- }}
- renderOption={(props, option, { inputValue }) => {
- const { entityObj, types, parent } =
- typeof option !== "string" &&
- "entityObj" in option &&
- "types" in option &&
- "parent" in option
- ? (option as {
- entityObj: { status?: string; guid?: string };
- types: string;
- parent: string;
- })
- : { entityObj: null, types: "", parent: "" };
- typeof option !== "string" &&
- "entityObj" in option &&
- "types" in option
- ? option
- : { entityObj: null, types: "", parent: "" };
- const title =
- typeof option !== "string" && "title" in option
- ? option.title
- : option;
-
- // Validate and sanitize title to prevent XSS
- const safeTitle = typeof title === "string" ? title : "";
-
- // Validate guid to prevent XSS in URL
- const guid = (entityObj as { guid?: string })?.guid;
- const safeGuid = guid && typeof guid === "string" ? guid : "";
- const href = safeGuid ? `/detailPage/${safeGuid}` : "#";
-
- const { name }: { name: string; found: boolean; key: any } =
- extractKeyValueFromEntity(entityObj);
-
- // Safely handle name extraction
- const safeName = name && typeof name === "string" ? name : "";
-
- const matches = match(
- types === "Entities" ? safeName : safeTitle,
- inputValue || "",
- {
- findAllOccurrences: true,
- insideWords: true
- }
- );
- const parts = parse(
- types === "Entities" ? safeName : safeTitle,
- matches
- );
- return (
- <Stack
- flexDirection="row"
- component="li"
- className="global-search-options"
- sx={{
- "& > span": {
- mr: 2,
- flexShrink: 0
- }
- }}
- {...props}
- onClick={() => {
- if (typeof option !== "string") {
- handleValues(option as unknown as HandleValuesType);
- }
- }}
- >
- {types === "Entities" && !isEmpty(entityObj) ? (
- <Link
- className="entity-name text-decoration-none"
- style={{
- maxWidth: "100%",
- width: "100%",
- color: "black",
- textDecoration: "none",
- display: "inline-flex",
- alignItems: "center",
- flexWrap: "wrap"
- }}
- to={{
- pathname: href
- }}
- color={
- entityObj?.status &&
- entityStateReadOnly[entityObj.status]
- ? "error"
- : "primary"
- }
- >
- {" "}
- {types === "Entities" && !isEmpty(entityObj) && (
- <DisplayImage entity={entityObj} />
- )}{" "}
- {types === "Entities" && !isEmpty(entityObj)
- ? parts.map((part, index) => (
- <Stack
- flexDirection="row"
- key={index}
- style={{
- fontWeight: part.highlight ? "bold" : "regular"
- }}
- >
- {entityObj?.guid !== "-1" && !part.highlight ? (
- <Link
- className="entity-name text-blue
text-decoration-none"
- style={{
- color: "black",
- textDecoration: "none",
- maxWidth: "100%",
- width: "100%"
- }}
- to={{
- pathname: href
- }}
- color={
- entityObj?.status &&
- entityStateReadOnly[entityObj.status]
- ? "error"
- : "primary"
- }
- >
- {part.text}
- </Link>
- ) : (
- part.text
- )}
- </Stack>
- ))
- : parts.map((part, index) => (
- <Stack
- flexDirection="row"
- key={index}
- style={{
- fontWeight: part.highlight ? "bold" : "regular"
- }}
- >
- {part.text}
- </Stack>
- ))}
- {types === "Entities" &&
- !isEmpty(entityObj) &&
- ` (${parent})`}
- </Link>
- ) : (
- parts.map((part, index) => (
- <Typography
- component="p"
- key={index}
- className="global-search-options-text"
- sx={{
- fontWeight: part.highlight ? "bold" : "regular"
- }}
- >
- {" "}
- {part.text}
- </Typography>
- ))
- )}
- </Stack>
- );
- }}
- renderInput={(params) => (
- <TextField
- {...params}
- placeholder="Search Entities..."
- fullWidth
- onClick={() => {
- setOpen(true);
- }}
- className="text-black-default"
- InputProps={{
- style: {
- padding: "1px 10px",
- borderRadius: "4px",
- color: "white !important",
- opacity: 1
- },
- ...params.InputProps,
- type: "search",
- endAdornment: (
- <InputAdornment position="start">
- {loading ? (
- <CircularProgress sx={{ color: "gray" }} size={15} />
- ) : (
- <SearchIcon fontSize="small" />
- )}
- </InputAdornment>
- )
- }}
- />
- )}
- groupBy={(option) =>
- typeof option !== "string" && "types" in option
- ? String(option.types)
- : ""
- }
- options={options}
- filterOptions={(options) => options}
- />
- </ClickAwayListener>
-
- <CustomButton
- variant="outlined"
- size="small"
- sx={{
- backgroundColor: "white !important",
- color: "#4a90e2 !important",
- borderColor: "#dddddd !important",
- "&:hover": {
- backgroundColor: "rgba(74, 144, 226, 0.08) !important",
- // borderColor: "#4a90e2 !important",
- color: "#4a90e2 !important"
- }
- }}
- onClick={() => {
- setOpenAdvanceSearch(true);
- }}
- >
- <Typography
- sx={{
- color: "#4a90e2 !important",
- fontWeight: "600 !important",
- fontSize: "0.875rem !important"
- }}
- display="inline"
- >
- Advanced
- </Typography>
- </CustomButton>
- </Stack>
-
- {openAdvanceSearch && (
- <AdvancedSearch
- openAdvanceSearch={openAdvanceSearch}
- handleCloseModal={handleCloseModal}
- />
- )}
- </>
- );
+ const navigate = useNavigate();
+ const location = useLocation();
+ const toastId = useRef(null);
+ const searchParams = new URLSearchParams(location.search);
+ const [options, setOptions] = useState<GlobalOptionRow[]>([]);
+ const [open, setOpen] = useState<boolean>(false);
+ const [openAdvanceSearch, setOpenAdvanceSearch] =
useState<boolean>(false);
+ const [loading, setLoading] = useState<boolean>(false);
+ const [inputText, setInputText] = useState<string>("");
+ const [scope, setScope] = useState<QuickSearchScope>("default");
+
+ const { typeHeaderData } = useAppSelector((state: any) =>
state.typeHeader);
+ const { metricsData } = useAppSelector((state: any) => state.metrics);
+ const { allEntityTypesData } = useAppSelector((state: any) =>
state.allEntityTypes);
+ const { classificationData } = useAppSelector((state: any) =>
state.classification);
+ const { glossaryData } = useAppSelector((state: any) => state.glossary);
+ const { businessMetaData } = useAppSelector((state: any) =>
state.businessMetaData);
+
+ const entityTree = useMemo(
+ () =>
+ buildEntitiesTreeForQuickSearch(
+ typeHeaderData,
+ metricsData?.data?.entity,
+ allEntityTypesData?.category
+ ),
+ [typeHeaderData, metricsData, allEntityTypesData]
+ );
+
+ const rebuildScopedOptions = useCallback(
+ (term: string): GlobalOptionRow[] => {
+ if (scope === "entity") {
+ return entityTreeToScopedOptions(entityTree,
term).map((o) => ({
+ title: o.title,
+ types: o.group,
+ scoped: o
+ }));
+ }
+ if (scope === "classification") {
+ return buildClassificationScopedOptions(
+ classificationData,
+ metricsData?.data?.tag?.tagEntities,
+ term
+ ).map((o) => ({
+ title: o.title,
+ types: o.group,
+ scoped: o
+ }));
+ }
+ if (scope === "glossary") {
+ const gTree =
buildGlossaryTermsTreeForQuickSearch(glossaryData);
+ return glossaryTreeToScopedOptions(gTree,
term).map((o) => ({
+ title: o.title,
+ types: o.group,
+ scoped: o
+ }));
+ }
+ if (scope === "businessMetadata") {
+ return buildBusinessMetadataScopedOptions(
+ businessMetaData?.businessMetadataDefs,
+ term
+ ).map((o) => ({
+ title: o.title,
+ types: o.group,
+ scoped: o
+ }));
+ }
+ return [];
+ },
+ [
+ scope,
+ entityTree,
+ classificationData,
+ metricsData,
+ glossaryData,
+ businessMetaData
+ ]
+ );
+
+ const getGlobalSearchData = async (searchTerm: string) => {
+ let entities: QuickSearchOptionListType = [];
+ let suggestionNames: QuickSearchOptionListType = [];
+ let quickSearchResp: AxiosResponse | null = null;
+ let suggestionSearchResp: AxiosResponse | null = null;
+ try {
+ setLoading(true);
+ quickSearchResp = await getGlobalSearchResult("quick", {
+ params: { query: searchTerm, limit: 5, offset:
0 }
+ });
+ setLoading(false);
+ } catch (error) {
+ setLoading(false);
+ console.error("Error fetching quick search results:",
error);
+ serverError(error, toastId);
+ }
+
+ try {
+ setLoading(true);
+ suggestionSearchResp = await
getGlobalSearchResult("suggestions", {
+ params: { prefixString: searchTerm }
+ });
+ setLoading(false);
+ } catch (error) {
+ setLoading(false);
+ console.error("Error fetching suggestion search
results:", error);
+ serverError(error, toastId);
+ }
+ const { searchResults = [] } = quickSearchResp?.data || {};
+ const { suggestions = [] }: SuggestionDataType =
+ suggestionSearchResp?.data || {};
+
+ entities = !isEmpty(searchResults?.entities)
+ ? searchResults?.entities?.map((entityDef: any) => {
+ const { name }: { name: string; found:
boolean; key: any } =
+
extractKeyValueFromEntity(entityDef);
+ return {
+ title: `${name}`,
+ parent: entityDef.typeName,
+ types: "Entities",
+ entityObj: entityDef
+ };
+ })
+ : [{ title: "No Entities Found", types: "Entities" }];
+
+ suggestionNames = !isEmpty(suggestions)
+ ? suggestions.map((suggestion: any) => {
+ return {
+ title: `${suggestion}`,
+ types: "Suggestions"
+ };
+ })
+ : [{ title: "No Suggestions Found", types:
"Suggestions" }];
+
+ setOptions([...entities, ...suggestionNames] as
GlobalOptionRow[]);
+ };
+
+ const onInputChange = (_event: unknown, value: string) => {
+ const sanitizedValue = value ? value.trim() : "";
+ setInputText(value ?? "");
+ if (!sanitizedValue) {
+ setOptions([]);
+ setOpen(false);
+ return;
+ }
+ setOpen(true);
+ if (scope === "default") {
+ void getGlobalSearchData(sanitizedValue);
+ } else {
+ setOptions(rebuildScopedOptions(sanitizedValue));
+ }
+ };
+
+ const handleScopedSelection = (opt: ScopedQuickSearchOption) => {
+ setOpen(false);
+ setOptions([]);
+ setInputText("");
+ if (opt.kind === "entity-type" && opt.entityTypeName) {
+ navigateToBasicTextQuery(navigate, opt.entityTypeName);
+ return;
+ }
+ if (opt.kind === "entity-service" &&
opt.serviceUnderlyingTypeNames?.length) {
+ navigateToServiceTypeEntitySearch(
+ navigate,
+ opt.serviceUnderlyingTypeNames,
+ false
+ );
+ return;
+ }
+ if (opt.kind === "classification" && opt.classificationName) {
+ navigateToClassificationDetailPage(navigate,
opt.classificationName);
+ return;
+ }
+ if (
+ opt.kind === "glossary-term" &&
+ opt.termGuid &&
+ opt.glossaryGuid &&
+ opt.termId &&
+ opt.termParent
+ ) {
+ navigateToGlossaryTermDetailPage(navigate, {
+ termGuid: opt.termGuid,
+ termId: opt.termId,
+ glossaryGuid: opt.glossaryGuid,
+ parentName: opt.termParent
+ });
+ return;
+ }
+ if (opt.kind === "business-metadata" && opt.bmGuid) {
+ navigateToBusinessMetadataDetailPage(navigate,
opt.bmGuid);
+ }
+ };
+
+ const handleValues = (option: HandleValuesType | GlobalOptionRow |
string | null) => {
+ if (typeof option === "string") {
+ const queryValue = option.trim();
+ if (!queryValue) return;
+ setOpen(false);
+ setOptions([]);
+ setInputText("");
+ if (scope !== "default") {
+ return;
+ }
+ const sanitizedQuery = queryValue || "*";
+ searchParams.set("query", sanitizedQuery);
+ searchParams.set("searchType", "basic");
+ navigate(
+ {
+ pathname: `/search/searchResult`,
+ search: searchParams.toString()
+ },
+ { replace: true }
+ );
+ return;
+ }
+
+ if (!option || typeof option !== "object") return;
+
+ const row = option as GlobalOptionRow;
+ if (row.scoped) {
+ handleScopedSelection(row.scoped);
+ return;
+ }
+
+ const { entityObj, title, types } = row as HandleValuesType;
+ if (!title || title === "undefined" || typeof title !==
"string") return;
+
+ const sanitizedTitle = title.trim();
+ if (!sanitizedTitle) return;
+
+ setOpen(false);
+ setOptions([]);
+ setInputText("");
+
+ searchParams.set("query", sanitizedTitle);
+ searchParams.set("searchType", "basic");
+
+ if (types === "Entities" && entityObj && entityObj.guid) {
+ navigate(
+ {
+ pathname:
`/detailPage/${entityObj.guid}`
+ },
+ { replace: true }
+ );
+ } else {
+ navigate(
+ {
+ pathname: `/search/searchResult`,
+ search: searchParams.toString()
+ },
+ { replace: true }
+ );
+ }
+ };
+
+ const handleSubmitSearch = () => {
+ const q = inputText.trim();
+ if (scope !== "default") {
+ const activeOption = options.find((o) => o.title === q);
+ if (activeOption?.scoped) {
+ handleScopedSelection(activeOption.scoped);
+ }
+ return;
+ }
+ if (!q) {
+ handleValues("*");
+ return;
+ }
+ const activeOption = options.find(
+ (o) => typeof o !== "string" && o.title === q
+ );
+ if (activeOption) {
+ handleValues(activeOption as HandleValuesType);
+ } else {
+ handleValues(q);
+ }
+ };
+
+ const handleScopeChange = (e: SelectChangeEvent<QuickSearchScope>) => {
+ const v = e.target.value as QuickSearchScope;
+ setScope(v);
+ setOptions([]);
+ setInputText("");
+ setOpen(false);
+ };
+
+ const handleClickAway = () => {
+ setOpen(false);
+ };
+
+ const handleCloseModal = () => {
+ setOpenAdvanceSearch(false);
+ };
+
+ const inputPlaceholder =
+ scope === "default" ? "Search Entities..." : "Contains text...";
+
+ return (
+ <>
+ <Stack
+ direction="row"
+ className="global-search-stack"
+ alignItems="center"
+ gap="0.5rem"
+ >
+ <FormControl
+ size="small"
+ sx={{ minWidth: 160, backgroundColor:
"white", borderRadius: 1 }}
+ >
+ <Select<QuickSearchScope>
+ id={SCOPE_SELECT_ID}
+ value={scope}
+ onChange={handleScopeChange}
+ aria-label="Search scope"
+ displayEmpty
+ renderValue={(v) =>
SCOPE_LABELS[v as QuickSearchScope]}
+ >
+ <MenuItem
value="default">Select All</MenuItem>
+ <MenuItem
value="entity">Entity</MenuItem>
+ <MenuItem
value="classification">Classification</MenuItem>
+ <MenuItem
value="glossary">Glossary / Terms</MenuItem>
+ <MenuItem
value="businessMetadata">Business Metadata</MenuItem>
+ </Select>
+ </FormControl>
+
+ <ClickAwayListener
onClickAway={handleClickAway}>
+ <Autocomplete
+ open={open}
+ loading={loading}
+ inputValue={inputText}
+ onInputChange={onInputChange}
+ onKeyDown={(e) => {
+ const code = e.keyCode
|| e.which;
+ switch (code) {
+ case 13: {
+
e.preventDefault();
+
handleSubmitSearch();
+ break;
+ }
+ case 9:
+ case 27:
+
setOpen(false);
+ break;
+ default:
+ break;
+ }
+ }}
+ freeSolo
+ id="global-search"
+ disablePortal
+
className="global-search-autocomplete"
+ sx={{
+ minWidth: 280,
+ flex: 1,
+ "& +
.MuiAutocomplete-popper .MuiAutocomplete-option": {
+
backgroundColor: "white"
+ },
+ "& +
.MuiAutocomplete-popper .MuiAutocomplete-option:hover": {
+
backgroundColor: "#c7e3ff"
+ }
+ }}
+ value={null}
+ onChange={(_event: unknown,
newValue: unknown) => {
+ if (newValue && typeof
newValue === "object") {
+ const o =
newValue as GlobalOptionRow;
+ if (o.title) {
+
handleValues(o);
+ }
+ }
+ }}
+ clearOnBlur={false}
+ autoComplete
+ includeInputInList
+ noOptionsText={scope ===
"default" ? "No Entities" : "No matches"}
+ getOptionLabel={(option: string
| GlobalOptionRow) => {
+ if (typeof option ===
"string") return option;
+ return option?.title &&
typeof option.title === "string"
+ ? option.title
+ : "";
+ }}
+ renderOption={(props, option, {
inputValue }) => {
+ const row = option as
GlobalOptionRow;
+ if (row.scoped) {
+ const title =
row.title;
+ const matches =
match(title, inputValue || "", {
+
findAllOccurrences: true,
+
insideWords: true
+ });
+ const parts =
parse(title, matches);
+ return (
+ <li
{...props} key={row.scoped.id}>
+
<Typography component="span" variant="body2">
+
{parts.map((part, index) => (
+
<span
+
key={index}
+
style={{
+
fontWeight: part.highlight ? 700 : 400
+
}}
+
>
+
{part.text}
+
</span>
+
))}
+
</Typography>
+ </li>
+ );
+ }
+
+ const { entityObj,
types, parent } =
+ typeof option
!== "string" &&
+ "entityObj" in
option &&
+ "types" in
option &&
+ "parent" in
option
+ ?
(option as {
+
entityObj: { status?: string; guid?: string };
+
types: string;
+
parent: string;
+
})
+ : {
entityObj: null, types: "", parent: "" };
+ const title =
+ typeof option
!== "string" && "title" in option
+ ?
option.title
+ :
option;
+ const safeTitle =
typeof title === "string" ? title : "";
+ const guid = (entityObj
as { guid?: string })?.guid;
+ const safeGuid = guid
&& typeof guid === "string" ? guid : "";
+ const href = safeGuid ?
`/detailPage/${safeGuid}` : "#";
+ const { name }: { name:
string; found: boolean; key: any } =
+
extractKeyValueFromEntity(entityObj);
+ const safeName = name
&& typeof name === "string" ? name : "";
+ const matches = match(
+ types ===
"Entities" ? safeName : safeTitle,
+ inputValue ||
"",
+ {
+
findAllOccurrences: true,
+
insideWords: true
+ }
+ );
+ const parts = parse(
+ types ===
"Entities" ? safeName : safeTitle,
+ matches
+ );
+ return (
+ <Stack
+
flexDirection="row"
+
component="li"
+
className="global-search-options"
+ sx={{
+
"& > span": {
+
mr: 2,
+
flexShrink: 0
+
}
+ }}
+
{...props}
+
onClick={() => {
+
handleValues(option as GlobalOptionRow);
+ }}
+ >
+ {types
=== "Entities" && !isEmpty(entityObj) ? (
+
<Link
+
className="entity-name text-decoration-none"
+
style={{
+
maxWidth: "100%",
+
width: "100%",
+
color: "black",
+
textDecoration: "none",
+
display: "inline-flex",
+
alignItems: "center",
+
flexWrap: "wrap"
+
}}
+
to={{ pathname: href }}
+
color={
+
entityObj?.status &&
+
entityStateReadOnly[entityObj.status]
+
? "error"
+
: "primary"
+
}
+
>
+
{types === "Entities" && !isEmpty(entityObj) && (
+
<DisplayImage entity={entityObj} />
+
)}
+
{types === "Entities" && !isEmpty(entityObj)
+
? parts.map((part, index) => (
+
<Stack
+
flexDirection="row"
+
key={index}
+
style={{
+
fontWeight: part.highlight ?
"bold" : "regular"
+
}}
+
>
+
{entityObj?.guid !== "-1" &&
!part.highlight ? (
+
<Link
+
className="entity-name
text-blue text-decoration-none"
+
style={{
+
color: "black",
+
textDecoration:
"none",
+
maxWidth:
"100%",
+
width: "100%"
+
}}
+
to={{ pathname: href }}
+
color={
+
entityObj?.status &&
+
entityStateReadOnly[entityObj.status]
+
?
"error"
+
:
"primary"
+
}
+
>
+
{part.text}
+
</Link>
+
) : (
+
part.text
+
)}
+
</Stack>
+
))
+
: parts.map((part, index) => (
+
<Stack
+
flexDirection="row"
+
key={index}
+
style={{
+
fontWeight: part.highlight ?
"bold" : "regular"
+
}}
+
>
+
{part.text}
+
</Stack>
+
))}
+
{types === "Entities" &&
+
!isEmpty(entityObj) &&
+
` (${parent})`}
+
</Link>
+ ) : (
+
parts.map((part, index) => (
+
<Typography
+
component="p"
+
key={index}
+
className="global-search-options-text"
+
sx={{
+
fontWeight: part.highlight ? "bold" : "regular"
+
}}
+
>
+
{part.text}
+
</Typography>
+
))
+ )}
+ </Stack>
+ );
+ }}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+
placeholder={inputPlaceholder}
+ fullWidth
+ onClick={() => {
+
setOpen(true);
+ }}
+
className="text-black-default"
+ InputProps={{
+ style: {
+
padding: "1px 10px",
+
borderRadius: "4px",
+
color: "#1a1a1a",
+
backgroundColor: "white"
+ },
+
...params.InputProps,
+ type:
"search",
+
endAdornment: (
+
<InputAdornment position="end">
+
{loading ? (
+
<CircularProgress sx={{ color: "gray" }} size={15} />
+
) : (
+
<SearchIcon fontSize="small" sx={{ color: "gray" }} />
+
)}
+
</InputAdornment>
+ )
+ }}
+ inputProps={{
+
...params.inputProps,
+
"aria-label": "Global search"
+ }}
+ />
+ )}
+ groupBy={(option) =>
+ typeof option !==
"string" && "types" in option
+ ?
String((option as GlobalOptionRow).types)
+ : ""
+ }
+ options={options}
+ filterOptions={(x) => x}
+ />
+ </ClickAwayListener>
+
+ <CustomButton
+ variant="contained"
+ size="small"
+ sx={{
+ backgroundColor: "#4a90e2
!important",
+ color: "#fff !important",
+ textTransform: "none",
+ fontWeight: 600
+ }}
+ onClick={handleSubmitSearch}
+ aria-label="Run search"
+ >
+ Search
+ </CustomButton>
+
+ <CustomButton
+ variant="outlined"
+ size="small"
+ sx={{
+ backgroundColor: "white
!important",
+ color: "#4a90e2 !important",
+ borderColor: "#dddddd
!important",
+ "&:hover": {
+ backgroundColor:
"rgba(74, 144, 226, 0.08) !important",
+ color: "#4a90e2
!important"
+ }
+ }}
+ onClick={() => {
+ setOpenAdvanceSearch(true);
+ }}
+ >
+ <Typography
+ sx={{
+ color: "#4a90e2
!important",
+ fontWeight: "600
!important",
+ fontSize: "0.875rem
!important"
+ }}
+ display="inline"
+ >
+ Advanced
+ </Typography>
+ </CustomButton>
+ </Stack>
+
+ {openAdvanceSearch && (
+ <AdvancedSearch
+ openAdvanceSearch={openAdvanceSearch}
+ handleCloseModal={handleCloseModal}
+ />
+ )}
+ </>
+ );
};
export default QuickSearch;
diff --git a/dashboard/src/utils/scopedQuickSearchUtils.ts
b/dashboard/src/utils/scopedQuickSearchUtils.ts
new file mode 100644
index 000000000..81a97c44e
--- /dev/null
+++ b/dashboard/src/utils/scopedQuickSearchUtils.ts
@@ -0,0 +1,466 @@
+/*
+ * 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 type { TreeNode } from "@models/treeStructureType";
+import type {
+ ChildrenInterface,
+ ServiceTypeInterface,
+ TypeHeaderInterface
+} from "@models/entityTreeType";
+import { addOnEntities } from "./Enum";
+import { customSortBy, customSortByObjectKeys, isEmpty } from "./Utils";
+
+export type QuickSearchScope =
+ | "default"
+ | "entity"
+ | "classification"
+ | "glossary"
+ | "businessMetadata";
+
+export type ScopedOptionKind =
+ | "entity-service"
+ | "entity-type"
+ | "classification"
+ | "glossary-term"
+ | "business-metadata";
+
+export interface ScopedQuickSearchOption {
+ id: string;
+ title: string;
+ group: string;
+ kind: ScopedOptionKind;
+ entityTypeName?: string;
+ serviceUnderlyingTypeNames?: string[];
+ classificationName?: string;
+ termGuid?: string;
+ glossaryGuid?: string;
+ termId?: string;
+ termParent?: string;
+ bmGuid?: string;
+}
+
+interface MetricsEntitySnapshot {
+ entityActive?: Record<string, number>;
+ entityDeleted?: Record<string, number>;
+}
+
+interface GlossaryTermCategoryRow {
+ parentCategoryGuid?: string;
+ categoryGuid?: string;
+ displayText?: string;
+ termGuid?: string;
+}
+
+interface GlossaryApiRow {
+ name: string;
+ guid: string;
+ categories?: GlossaryTermCategoryRow[];
+ terms?: GlossaryTermCategoryRow[];
+}
+
+const DEFAULT_SERVICE = "other_types";
+
+const generateServiceTypeArr = (
+ entityCountArr: ServiceTypeInterface[],
+ serviceType: string,
+ children: ChildrenInterface,
+ entityCount: number
+) => {
+ const existing = entityCountArr.find(
+ (obj): obj is ServiceTypeInterface =>
+ typeof obj === "object" && obj !== null && serviceType
in obj
+ );
+ if (existing) {
+ const bucket = (existing as Record<string, {
+ children: ChildrenInterface[];
+ totalCount: number;
+ }>)[serviceType];
+ if (bucket) {
+ bucket.children.push(children);
+ bucket.totalCount += entityCount;
+ }
+ } else {
+ entityCountArr.push({
+ [serviceType]: {
+ children: [children],
+ name: serviceType,
+ totalCount: entityCount
+ }
+ } as ServiceTypeInterface);
+ }
+};
+
+const pushRootEntityToTree = (
+ entities: ServiceTypeInterface[],
+ allEntityCategory: string | undefined
+) => {
+ const rootEntityChildren: ChildrenInterface = {
+ gType: "Entity",
+ guid: addOnEntities[0],
+ id: addOnEntities[0],
+ name: addOnEntities[0],
+ type: allEntityCategory,
+ text: addOnEntities[0]
+ };
+ const hasOther = entities.some((obj) => obj["other_types"] !==
undefined);
+ if (hasOther) {
+ const idx = entities.findIndex((obj) => "other_types" in obj);
+ (entities[idx] as
ServiceTypeInterface)["other_types"].children.push(
+ rootEntityChildren
+ );
+ } else {
+ entities.push({
+ other_types: {
+ name: "other_types",
+ children: [rootEntityChildren],
+ totalCount: 0
+ }
+ } as ServiceTypeInterface);
+ }
+ return entities;
+};
+
+/**
+ * Mirrors {@link EntitiesTree} group view (service type → entity typedefs).
+ */
+export const buildEntitiesTreeForQuickSearch = (
+ typeHeaderData: TypeHeaderInterface[] | null | undefined,
+ metricsEntity: MetricsEntitySnapshot | null | undefined,
+ allEntityCategory: string | undefined
+): TreeNode[] => {
+ if (!Array.isArray(typeHeaderData) || !typeHeaderData.length ||
!metricsEntity) {
+ return [];
+ }
+ const active = metricsEntity.entityActive || {};
+ const deleted = metricsEntity.entityDeleted || {};
+ const newArr: ServiceTypeInterface[] = [];
+
+ typeHeaderData.forEach((entity) => {
+ let { serviceType = DEFAULT_SERVICE, category, name, guid } =
entity;
+ if (category !== "ENTITY") return;
+ const entityCount =
+ Number(active[name] ?? 0) + Number(deleted[name] ?? 0);
+ const modelName = entityCount ? `${name} (${entityCount})` :
name;
+ const children: ChildrenInterface = {
+ text: modelName,
+ name,
+ type: category,
+ gType: "Entity",
+ guid,
+ id: guid
+ };
+ generateServiceTypeArr(newArr, serviceType, children,
entityCount);
+ });
+
+ pushRootEntityToTree(newArr, allEntityCategory);
+
+ const child = (childs: ChildrenInterface[]) => {
+ if (!childs?.length) return [];
+ return customSortBy(
+ childs.map((obj) => ({
+ id: obj.name as string,
+ label: obj.text as string,
+ types: "child"
+ })),
+ ["label"]
+ );
+ };
+
+ const sorted = customSortByObjectKeys(newArr);
+ return sorted.map((entity: ServiceTypeInterface) => {
+ const key = Object.keys(entity)[0];
+ const entityData = entity[key];
+ return {
+ id: entityData.name,
+ label:
+ entityData.totalCount === 0
+ ? entityData.name
+ : `${entityData.name}
(${entityData.totalCount})`,
+ children: child(entityData.children),
+ types: "parent"
+ };
+ });
+};
+
+const filterTreeNodes = (treeData: TreeNode[], searchTerm: string): TreeNode[]
=> {
+ const q = searchTerm.trim().toLowerCase();
+ if (!q) return treeData;
+ return treeData
+ .filter(
+ (node) =>
+ node.label?.toLowerCase().includes(q) ||
+ node.children?.some((child) =>
child.label?.toLowerCase().includes(q))
+ )
+ .map((node) => {
+ const parentMatches =
node.label?.toLowerCase().includes(q) ?? false;
+ const kids = node.children?.length
+ ? parentMatches
+ ? node.children
+ : node.children.filter((c) =>
c.label?.toLowerCase().includes(q))
+ : undefined;
+ return { ...node, children: kids };
+ });
+};
+
+export const entityTreeToScopedOptions = (
+ tree: TreeNode[],
+ searchTerm: string
+): ScopedQuickSearchOption[] => {
+ const filtered = filterTreeNodes(tree, searchTerm);
+ const out: ScopedQuickSearchOption[] = [];
+ for (const parent of filtered) {
+ const underlying =
+ parent.children?.map((c) => c.id).filter(Boolean) ?? [];
+ out.push({
+ id: `svc:${parent.id}`,
+ title: parent.label,
+ group: "Entity",
+ kind: "entity-service",
+ serviceUnderlyingTypeNames: underlying
+ });
+ for (const child of parent.children ?? []) {
+ out.push({
+ id: `type:${child.id}`,
+ title: child.label,
+ group: "Entity",
+ kind: "entity-type",
+ entityTypeName: child.id
+ });
+ }
+ }
+ return out;
+};
+
+interface ClassificationDefRow {
+ name: string;
+ subTypes: string[];
+ guid: string;
+ superTypes: string[];
+}
+
+/** Depth-first classification list (same traversal as sidebar tree). */
+export const buildClassificationScopedOptions = (
+ classificationData: { classificationDefs: ClassificationDefRow[] } |
null,
+ tagEntities: Record<string, number> | undefined,
+ searchTerm: string
+): ScopedQuickSearchOption[] => {
+ if (!classificationData?.classificationDefs?.length) return [];
+ const defs = classificationData.classificationDefs;
+ const byName = new Map(defs.map((d) => [d.name, d]));
+ const q = searchTerm.trim().toLowerCase();
+ const out: ScopedQuickSearchOption[] = [];
+ const seen = new Set<string>();
+
+ const visit = (typeName: string) => {
+ if (seen.has(typeName)) return;
+ seen.add(typeName);
+ const def = byName.get(typeName);
+ if (!def) return;
+ const count = tagEntities?.[typeName];
+ const label =
+ count !== undefined ? `${typeName} (${count})` :
typeName;
+ if (!q || label.toLowerCase().includes(q) ||
typeName.toLowerCase().includes(q)) {
+ out.push({
+ id: `cls:${def.guid}:${typeName}`,
+ title: label,
+ group: "Classification",
+ kind: "classification",
+ classificationName: typeName
+ });
+ }
+ (def.subTypes ?? []).forEach((st) => visit(st));
+ };
+
+ defs
+ .filter((d) => isEmpty(d.superTypes))
+ .forEach((d) => visit(d.name));
+
+ return customSortBy(out, ["title"]);
+};
+
+/** Build glossary tree for “terms” mode (see {@link GlossaryTree}
glossaryType true). */
+export const buildGlossaryTermsTreeForQuickSearch = (
+ glossaryData: GlossaryApiRow[] | null | undefined
+): TreeNode[] => {
+ if (!glossaryData?.length) return [];
+ const glossaryType = true;
+
+ const toServiceRows: Record<string, {
+ name: string;
+ children: ChildrenInterface[];
+ id: string;
+ types: string;
+ parent: string;
+ guid: string;
+ }>[] = glossaryData.map((glossary) => {
+ const categoryRelation =
+ glossary.categories?.filter((o) => o.parentCategoryGuid
!== undefined) ??
+ [];
+
+ const getChildren = (glossaries: {
+ children: NonNullable<GlossaryApiRow["terms"]>;
+ parent: string;
+ }) => {
+ if (isEmpty(glossaries.children)) return [];
+ return glossaries.children
+ .map((glossariesType) => {
+ const getChild = () =>
+ categoryRelation
+ .map((obj) => {
+ if
(obj.parentCategoryGuid === glossariesType.categoryGuid) {
+ return {
+
name: obj.displayText,
+
id: obj.displayText,
+
children: [] as ChildrenInterface[],
+
types: "child",
+
parent: glossaries.parent,
+
cGuid: glossaryType ? obj.termGuid : obj.categoryGuid,
+
guid: glossary.guid
+ };
+ }
+ return
undefined;
+ })
+ .filter(Boolean) as
ChildrenInterface[];
+ if (glossariesType.parentCategoryGuid
=== undefined) {
+ return {
+ name:
glossariesType.displayText,
+ id:
glossariesType.displayText,
+ children: getChild(),
+ types: "child",
+ parent:
glossaries.parent,
+ cGuid: glossaryType
+ ?
glossariesType.termGuid
+ :
glossariesType.categoryGuid,
+ guid: glossary.guid
+ } as ChildrenInterface;
+ }
+ return undefined;
+ })
+ .filter(Boolean) as ChildrenInterface[];
+ };
+
+ const children = getChildren({
+ children: glossary?.terms ?? [],
+ parent: glossary.name
+ });
+
+ return {
+ [glossary.name]: {
+ name: glossary.name,
+ children: children || [],
+ id: glossary.guid,
+ types: "parent",
+ parent: glossary.name,
+ guid: glossary.guid
+ }
+ };
+ });
+
+ const child = (childs: ChildrenInterface[]): TreeNode[] => {
+ if (!childs?.length) return [];
+ return customSortBy(
+ childs.map((obj) => ({
+ id: obj?.name as string,
+ label: obj?.name as string,
+ children:
+ obj?.children !== undefined
+ ?
child(obj.children.filter(Boolean) as ChildrenInterface[])
+ : [],
+ types: obj?.types,
+ parent: obj?.parent,
+ guid: obj?.guid,
+ cGuid: obj?.cGuid
+ })),
+ ["label"]
+ );
+ };
+
+ const sorted = customSortByObjectKeys(toServiceRows as
ServiceTypeInterface[]);
+ return sorted.map((entity: Record<string, {
+ name: string;
+ children: ChildrenInterface[];
+ types: string;
+ parent: string;
+ guid: string;
+ }>) => {
+ const g = entity[Object.keys(entity)[0]];
+ return {
+ id: g.name,
+ label: g.name,
+ children: child((g.children ?? []) as
ChildrenInterface[]),
+ types: g.types,
+ parent: g.parent,
+ guid: g.guid
+ };
+ });
+};
+
+const collectGlossaryTermOptions = (
+ nodes: TreeNode[],
+ glossaryLabel: string,
+ out: ScopedQuickSearchOption[]
+) => {
+ for (const n of nodes) {
+ if (n.cGuid && n.types === "child" && n.id) {
+ out.push({
+ id: `term:${n.cGuid}`,
+ title: `${n.label} (${glossaryLabel})`,
+ group: "Glossary / Terms",
+ kind: "glossary-term",
+ termGuid: n.cGuid as string,
+ glossaryGuid: (n.guid as string) ?? "",
+ termId: n.id,
+ termParent: (n.parent as string) ??
glossaryLabel
+ });
+ }
+ if (n.children?.length) {
+ collectGlossaryTermOptions(n.children, glossaryLabel,
out);
+ }
+ }
+};
+
+export const glossaryTreeToScopedOptions = (
+ tree: TreeNode[],
+ searchTerm: string
+): ScopedQuickSearchOption[] => {
+ const filtered = filterTreeNodes(tree, searchTerm);
+ const out: ScopedQuickSearchOption[] = [];
+ for (const parent of filtered) {
+ collectGlossaryTermOptions(parent.children ?? [], parent.label,
out);
+ }
+ return customSortBy(out, ["title"]);
+};
+
+export const buildBusinessMetadataScopedOptions = (
+ businessMetadataDefs: Array<{ name: string; guid: string }> | undefined,
+ searchTerm: string
+): ScopedQuickSearchOption[] => {
+ if (!businessMetadataDefs?.length) return [];
+ const q = searchTerm.trim().toLowerCase();
+ return customSortBy(
+ businessMetadataDefs
+ .filter((d) => !q || d.name.toLowerCase().includes(q))
+ .map((d) => ({
+ id: `bm:${d.guid}`,
+ title: d.name,
+ group: "Business Metadata",
+ kind: "business-metadata" as const,
+ bmGuid: d.guid
+ })),
+ ["title"]
+ );
+};
diff --git a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
index 02f5e27b6..bc9e444c2 100644
--- a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
@@ -64,7 +64,7 @@ import { globalSearchFilterInitialQuery, isEmpty } from
"@utils/Utils";
import { attributeFilter } from "@utils/CommonViewFunction";
import { cloneDeep } from "@utils/Helper";
import LaunchOutlinedIcon from "@mui/icons-material/LaunchOutlined";
-import { getGlossaryImportTmpl } from "@api/apiMethods/glossaryApiMethod";
+import { downloadGlossaryImportTemplate } from "@utils/glossaryImportFlow";
import { toast } from "react-toastify";
import { EnumTypeDefData, TreeNode } from "@models/treeStructureType";
import ImportDialog from "@components/ImportDialog";
@@ -969,33 +969,26 @@ const BarTreeView: FC<{
const downloadFile = async () => {
try {
- let apiResp: any = {};
- if (treeName == "Entities") {
- apiResp = await getBusinessMetadataImportTmpl({});
- } else if (treeName == "Glossary") {
- apiResp = await getGlossaryImportTmpl({});
- }
- let text: string = "";
- if (apiResp) {
- text = apiResp.data;
+ 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 link = document.createElement("a");
link.href = url;
- if (treeName == "Entities") {
- link.setAttribute("download", "template_business_metadata");
- } else if (treeName == "Glossary") {
- link.setAttribute("download", "template");
- }
+ link.setAttribute("download", "template_business_metadata");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
} catch {
/* ignore download error */
}