Skip to content

feat: add search support for Roles entity with scalable typeahead UI (#27540)#27550

Open
rohan911438 wants to merge 3 commits intoopen-metadata:mainfrom
rohan911438:feature/role-search-support
Open

feat: add search support for Roles entity with scalable typeahead UI (#27540)#27550
rohan911438 wants to merge 3 commits intoopen-metadata:mainfrom
rohan911438:feature/role-search-support

Conversation

@rohan911438
Copy link
Copy Markdown

Summary

This PR introduces full search support for the Roles entity, addressing the scalability limitations of the existing paginated API (GET /v1/roles) used across the UI.

Previously, role selection dropdowns relied on pagination, causing roles beyond the first page to be inaccessible and unsearchable. This PR elevates Roles to a first-class searchable entity, aligning it with existing entities like Users and Teams.

Closes #27540


Problem

  • Role dropdowns depended on paginated API calls
  • Roles beyond page limits were not visible/selectable
  • No search or filtering capability existed
  • Workarounds like limit=1000 were not scalable
  • UI tests (e.g., UserCreationWithPersona.spec.ts) were failing due to missing roles

Solution

Implemented end-to-end search support for Roles across backend and frontend:


Backend Changes

  • Added RoleIndex.java to define Role search indexing
  • Updated indexMapping.json with Role index schema
  • Registered ROLE in:
    • SearchIndexFactory
    • SearchRepository
  • Enabled indexing on Role create/update/delete lifecycle
  • Integrated Role support into /v1/search/query API
  • Ensured consistency with existing User/Team search patterns

Frontend Changes

Search Infrastructure

  • Extended SearchIndex enum to include ROLE
  • Updated search interfaces to support role queries

UI Enhancements

Replaced pagination-based dropdowns with typeahead search (AsyncSelect) in:

  • CreateUser.component.tsx
  • UserProfileRoles.component.tsx (including Admin role handling)
  • SsoRolesSelectField.tsx
  • LdapRoleMappingWidget.tsx

Improvements

  • Removed redundant role-fetching API calls
  • Implemented dynamic search-based role loading
  • Improved UX with real-time filtering

Testing

  • Updated UI behavior to work with search-based selection
  • Ensured compatibility with existing workflows
  • Fixes issues with role selection in automated tests

Migration / Deployment Note

After deploying this change, run the Role Search Re-index job from:

Settings → Global Settings → Search Index

This ensures all existing roles are indexed and searchable.


Impact

  • Enables scalable role selection regardless of dataset size
  • Eliminates pagination limitations
  • Improves UX with search-as-you-type functionality
  • Aligns Roles with existing searchable entities (Users, Teams)

Additional Notes

This implementation follows existing search architecture patterns and ensures consistency across entities while improving performance and maintainability.


Please let me know if any refinements or additional test coverage are needed

@rohan911438 rohan911438 requested a review from a team as a code owner April 20, 2026 14:45
Copilot AI review requested due to automatic review settings April 20, 2026 14:45
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@rohan911438
Copy link
Copy Markdown
Author

HI @harshach @fredrik @amiorin @mavimo , I’ve implemented search support for the Roles entity across backend and frontend, replacing pagination with a scalable typeahead approach. This aligns Roles with existing searchable entities like Users and Teams.

Would appreciate your feedback. Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes Roles a first-class searchable entity and updates role-picking UI controls to use search-backed typeahead instead of paginated role listing, improving scalability and fixing cases where roles beyond the first page were unreachable.

Changes:

  • Added backend search index support for Roles (index mapping + RoleIndex + factory wiring).
  • Extended UI search types to include SearchIndex.ROLE / RoleSearchSource.
  • Replaced role dropdowns in multiple UI surfaces with AsyncSelect backed by /v1/search/query.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/RoleIndex.java Adds Role search document builder with excluded heavy fields.
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchIndexFactory.java Wires Entity.ROLE to build a Role search index document.
openmetadata-spec/src/main/resources/elasticsearch/indexMapping.json Registers role index metadata (index name, mapping file, alias).
openmetadata-spec/src/main/resources/elasticsearch/en/role_index_mapping.json Introduces Elasticsearch mapping for role search index.
openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts Adds SearchIndex.ROLE.
openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts Adds RoleSearchSource and maps it to SearchIndex.ROLE.
openmetadata-ui/src/main/resources/ui/src/pages/CreateUserPage/CreateUserPage.component.tsx Removes eager “fetch all roles” and stops passing roles as props.
openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.interface.ts Removes roles prop from CreateUser component contract.
openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx Uses search-backed AsyncSelect for role selection.
openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx Switches roles editor to search-backed AsyncSelect (plus Admin pseudo-option).
openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoRolesSelectField.tsx Uses search-backed AsyncSelect for SSO role selection.
openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/LdapRoleMappingWidget/LdapRoleMappingWidget.tsx Uses search-backed AsyncSelect for LDAP role mapping.

Comment on lines 118 to 121
case Entity.USER -> new UserIndex((User) entity);
case Entity.TEAM -> new TeamIndex((Team) entity);
case Entity.ROLE -> new RoleIndex((Role) entity);
case Entity.METRIC -> new MetricIndex((Metric) entity);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RoleIndex is referenced in the switch but not imported, so this file will not compile. Add the missing import for org.openmetadata.service.search.indexes.RoleIndex (and ensure it’s included in the imports section with the other index classes).

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +360
"role": {
"indexName": "role_search_index",
"indexMappingFile": "/elasticsearch/%s/role_index_mapping.json",
"alias": "role",
"parentAliases": [],
"childAliases": []
},
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexMappingFile is language-scoped (/elasticsearch/%s/role_index_mapping.json), but the PR only adds the mapping under elasticsearch/en/. Add corresponding role_index_mapping.json files for the other supported languages (jp/ru/zh) or switch to a non-localized mapping path to avoid startup/index creation failures when searchIndexMappingLanguage is not en.

Copilot uses AI. Check for mistakes.
Typography as AntDTypography,
} from 'antd';
import { AxiosError } from 'axios';
import { startCase } from 'lodash';
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startCase is imported but not used in this widget, which should fail lint/typecheck with unused imports enabled. Remove the unused import (or use it if intended).

Suggested change
import { startCase } from 'lodash';

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +125
const fetchRoleOptions = async (searchText: string, page?: number) => {
try {
const response = await searchQuery({
query: searchText || '*',
searchIndex: SearchIndex.ROLE,
pageSize: 10,
pageNumber: page ?? 1,
fetchSource: true,
});

fetchRoles();
}, []);
return {
data: response.hits.hits.map((hit) => ({
label: getEntityName(hit._source),
value: hit._source.name,
})),
paging: {
total: response.hits.total.value,
},
};
} catch (error) {
showErrorToast(error as AxiosError);

return { data: [], paging: { total: 0 } };
}
};
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRoleOptions returns { data, paging }, but this AsyncSelect usage does not set enableInfiniteScroll, so AsyncSelect will treat the response as an options array and pass an object to AntD Select (runtime break). Either set enableInfiniteScroll on the AsyncSelect or change fetchRoleOptions to return a plain options array. (Also, if you keep returning plain options, you can drop the now-unused RoleOption type.)

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +84
const fetchRoleOptions = async (searchText: string, page?: number) => {
try {
const response = await searchQuery({
query: searchText || '*',
searchIndex: SearchIndex.ROLE,
pageSize: PAGE_SIZE_LARGE,
pageNumber: page ?? 1,
fetchSource: true,
});
}

return options;
}, [roles, isUserAdmin, getEntityName]);
const options = response.hits.hits.map((hit) => ({
label: getEntityName(hit._source),
value: hit._source.id,
data: hit._source,
}));

const fetchRoles = async () => {
try {
const response = await getRoles(
'',
undefined,
undefined,
false,
PAGE_SIZE_LARGE
);
setRoles(response.data);
} catch (err) {
if (!isUserAdmin && (isEmpty(searchText) || toLower(TERM_ADMIN).includes(toLower(searchText)))) {
options.push({
label: TERM_ADMIN,
value: toLower(TERM_ADMIN),
data: undefined,
} as any);
}

return {
data: options,
paging: {
total: response.hits.total.value + (!isUserAdmin ? 1 : 0),
},
};
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRoleOptions returns a paginated { data, paging } object, but the AsyncSelect below does not set enableInfiniteScroll. With infinite scroll disabled, AsyncSelect expects the api to return a plain options array; otherwise it will pass an object to AntD Select as options and break the dropdown. Fix by either enabling infinite scroll or returning an array. Also, paging.total currently adds +1 even when the Admin option is not added; only increment when that option is actually included.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +76
if (!isUserAdmin && (isEmpty(searchText) || toLower(TERM_ADMIN).includes(toLower(searchText)))) {
options.push({
label: TERM_ADMIN,
value: toLower(TERM_ADMIN),
data: undefined,
} as any);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid as any here. AsyncSelect options are DefaultOptionType, so create a properly typed option object (or define a local option type) instead of casting to any when adding the Admin pseudo-option.

Copilot generated this review using guidance from repository custom instructions.
open={isDropdownOpen}
options={useRolesOption}
popupClassName="roles-custom-dropdown-class"
ref={dropdownRef as any}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ref={dropdownRef as any} is an any cast and will fail the project’s TypeScript type-safety standards. Prefer typing the ref to the correct Select ref type (or wrapping element type) so the cast isn’t needed.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +467 to 473
<AsyncSelect
enableInfiniteScroll
showSearch
api={fetchRoleOptions}
data-testid="roles-dropdown"
disabled={isEmpty(roles)}
filterOption={handleSearchFilterOption}
getPopupContainer={(triggerNode) => triggerNode.parentElement}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncSelect is designed for server-side searching and sets filterOption={false} internally. Passing filterOption={handleSearchFilterOption} here overrides that and can re-filter remote results unexpectedly. Remove the filterOption prop when using AsyncSelect with api={fetchRoleOptions}.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +55
const fetchRoleOptions = async (searchText: string, page?: number) => {
try {
const response = await searchQuery({
query: searchText || '*',
searchIndex: SearchIndex.ROLE,
pageSize: 10,
pageNumber: page ?? 1,
fetchSource: true,
});

useEffect(() => {
getRoles('*', undefined, undefined, true, 1000)
.then((response) => {
setRoleOptions(
(response.data || []).map((role) => ({
label: role.displayName || role.name,
value: role.name,
}))
);
})
.catch((error: AxiosError) => showErrorToast(error));
}, []);
return {
data: response.hits.hits.map((hit) => ({
label: getEntityName(hit._source),
value: hit._source.name,
})),
paging: {
total: response.hits.total.value,
},
};
} catch (error) {
showErrorToast(error as AxiosError);

return { data: [], paging: { total: 0 } };
}
};
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRoleOptions returns { data, paging }, but the AsyncSelect here doesn’t enable infinite scroll. When enableInfiniteScroll is false, AsyncSelect expects the api to return a plain options array; otherwise it will pass an object as options to AntD Select. Either set enableInfiniteScroll or return an array from fetchRoleOptions.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copilot AI review requested due to automatic review settings April 20, 2026 16:35
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Apr 20, 2026

Code Review ✅ Approved 3 resolved / 3 findings

Implements role search with scalable typeahead UI, resolving issues with unsafe type casting, incorrect import paths, and missing index mappings.

✅ 3 resolved
Quality: Unsafe as any cast for Admin option in role list

📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx:74-76
At line 76, as any is used to force the Admin pseudo-option (which has data: undefined) into the typed options array. This bypasses TypeScript's type safety and could mask issues if the AsyncSelect component tries to access data properties on the option.

Consider defining a proper union type or using a sentinel value instead of undefined.

Quality: Import path changed from '../../users.less' to incorrect depth

📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx:31
The import of users.less was changed from '../../users.less' to '../../../../users.less'. Given the file is at UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx, the original relative path ../../users.less (resolving to Users/users.less) seems correct while ../../../../users.less would resolve outside the Settings directory. This may cause a build failure or missing styles.

Bug: Role index mapping missing 'description' field for search

📄 openmetadata-spec/src/main/resources/elasticsearch/jp/role_index_mapping.json:76-90
The role_index_mapping.json does not include a description field mapping. Roles in OpenMetadata have descriptions (e.g., 'Data Consumer role'), and users will likely want to search/filter roles by description text. The comparable team_index_mapping.json includes a description field with proper text analysis. Without this field, search queries that match on description content will not return relevant roles, degrading the typeahead UX.

Consider adding at minimum:

"description": {
  "type": "text",
  "analyzer": "om_analyzer"
}

Also consider adding policies (keyword/nested) if filtering roles by attached policies is desired.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment on lines 55 to 58
const { value, onChange, id, disabled, readonly } = props;

const [mappings, setMappings] = useState<RoleMappingEntry[]>([]);
const [availableRoles, setAvailableRoles] = useState<RoleOption[]>([]);
const [isLoadingRoles, setIsLoadingRoles] = useState(false);
const [errors, setErrors] = useState<MappingError>({});
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After removing the availableRoles/isLoadingRoles state, the RoleOption type defined above is no longer referenced anywhere, which can trigger unused-locals lint/typecheck failures. Please remove the unused type (or reintroduce a use for it).

Copilot uses AI. Check for mistakes.
Comment on lines 248 to 254
@@ -250,7 +254,6 @@ const UserProfileRoles = ({
)}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxTagPlaceholder is explicitly typed as any[], which weakens type-safety and can mask real issues. Please remove the any annotation (let TS infer it) or use the proper AntD Select omitted-values type.

Copilot uses AI. Check for mistakes.
open={isDropdownOpen}
options={useRolesOption}
popupClassName="roles-custom-dropdown-class"
ref={dropdownRef as any}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncSelect is a function component (not forwardRef), so passing ref={... as any} will not work and will generate a runtime warning (function components cannot receive refs). Please remove this ref usage or update AsyncSelect to forwardRef and type the ref properly.

Suggested change
ref={dropdownRef as any}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add search support for Roles entity to enable scalable role selection

2 participants