diff --git a/packages/app/src/components/catalog/ChoreoEntityKindPicker.tsx b/packages/app/src/components/catalog/ChoreoEntityKindPicker.tsx
index 4c20843cf..a741884a6 100644
--- a/packages/app/src/components/catalog/ChoreoEntityKindPicker.tsx
+++ b/packages/app/src/components/catalog/ChoreoEntityKindPicker.tsx
@@ -1,12 +1,16 @@
import { alertApiRef, useApi, useApp } from '@backstage/core-plugin-api';
import Box from '@material-ui/core/Box';
+import CircularProgress from '@material-ui/core/CircularProgress';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListSubheader from '@material-ui/core/ListSubheader';
-import MenuItem from '@material-ui/core/MenuItem';
-import Select from '@material-ui/core/Select';
+import Popper, { PopperProps } from '@material-ui/core/Popper';
+import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
-import { type ReactNode, useEffect, useMemo, useState } from 'react';
+import Autocomplete, {
+ AutocompleteRenderGroupParams,
+} from '@material-ui/lab/Autocomplete';
+import { useEffect, useMemo, useState } from 'react';
import {
EntityKindFilter,
useEntityList,
@@ -98,19 +102,14 @@ const useStyles = makeStyles((theme: Theme) =>
color: theme.palette.text.primary,
whiteSpace: 'nowrap',
},
- select: {
+ autocomplete: {
minWidth: 180,
},
- renderValue: {
+ option: {
display: 'flex',
alignItems: 'center',
- gap: theme.spacing(1),
- '& svg': {
- fontSize: '1.2rem',
- color: theme.palette.text.secondary,
- },
},
- subheader: {
+ groupHeader: {
color: theme.palette.text.secondary,
fontSize: theme.typography.caption.fontSize,
fontWeight: theme.typography.fontWeightBold as number,
@@ -126,9 +125,38 @@ const useStyles = makeStyles((theme: Theme) =>
fontSize: '1.2rem',
},
},
+ listbox: {
+ // Allow the dropdown to expand without early scrolling;
+ // only scroll when it approaches viewport height in short windows.
+ maxHeight: 'calc(100vh - 170px)',
+ },
}),
);
+const UNGROUPED_KIND_CATEGORY = '__ungrouped__';
+
+interface KindOption {
+ category: string;
+ kind: string;
+ label: string;
+}
+
+const BottomPinnedPopper = (props: PopperProps) => (
+
+);
+
// Hook to fetch all available Choreo entity kinds from the catalog
function useAllKinds(): {
allKinds: Map;
@@ -264,7 +292,7 @@ export const ChoreoEntityKindPicker = (props: ChoreoEntityKindPickerProps) => {
const alertApi = useApi(alertApiRef);
- const { error, allKinds, selectedKind, setSelectedKind } =
+ const { loading, error, allKinds, selectedKind, setSelectedKind } =
useEntityKindFilter({
initialFilter: initialFilter,
});
@@ -297,94 +325,137 @@ export const ChoreoEntityKindPicker = (props: ChoreoEntityKindPickerProps) => {
return available;
}, [allKinds, allowedKinds]);
- // Build grouped menu items
- const menuItems = useMemo(() => {
- if (error) return [];
+ const kindOptions = useMemo(() => {
+ if (error) return [] as KindOption[];
- const items: ReactNode[] = [];
+ const options: KindOption[] = [];
- // Add Namespace (domain) as a standalone top-level item
+ // Keep Namespace as the first, ungrouped option.
if (availableKinds.has('domain')) {
- const DomainIcon = app.getSystemIcon('kind:domain');
- items.push(
- ,
- );
+ options.push({
+ category: UNGROUPED_KIND_CATEGORY,
+ kind: 'domain',
+ label: kindDisplayNames.domain,
+ });
}
for (const category of kindCategories) {
- // Skip platform-only categories for non-platform engineers
if (category.platformOnly && !isPlatformEngineer) continue;
- // Filter to only kinds that exist in the catalog
const visibleKinds = category.kinds.filter(k => availableKinds.has(k));
-
- // Skip category if no kinds are available
if (visibleKinds.length === 0) continue;
- items.push(
-
- {category.label}
- ,
- );
-
for (const kind of visibleKinds) {
- const KindIcon = app.getSystemIcon(`kind:${kind}`);
- items.push(
- ,
- );
+ options.push({
+ category: category.label,
+ kind,
+ label: kindDisplayNames[kind] || kind,
+ });
}
}
- return items;
- }, [availableKinds, isPlatformEngineer, error, classes, app]);
+ return options;
+ }, [availableKinds, isPlatformEngineer, error]);
+
+ const selectedOption = useMemo(() => {
+ const normalizedKind = selectedKind.toLowerCase();
+ const matchedOption = kindOptions.find(
+ option => option.kind === normalizedKind,
+ );
+
+ if (matchedOption) {
+ return matchedOption;
+ }
+
+ return {
+ category: UNGROUPED_KIND_CATEGORY,
+ kind: normalizedKind,
+ label: kindDisplayNames[normalizedKind] || selectedKind,
+ };
+ }, [kindOptions, selectedKind]);
+
+ const autocompleteOptions = useMemo(() => {
+ const hasSelectedOption = kindOptions.some(
+ option => option.kind === selectedOption.kind,
+ );
+
+ if (hasSelectedOption) {
+ return kindOptions;
+ }
+
+ return [selectedOption, ...kindOptions];
+ }, [kindOptions, selectedOption]);
+
+ const renderOption = (option: KindOption) => {
+ const KindIcon = app.getSystemIcon(`kind:${option.kind}`);
+
+ return (
+
+ {KindIcon && (
+
+
+
+ )}
+ {option.label}
+
+ );
+ };
+
+ const renderGroup = (params: AutocompleteRenderGroupParams) => {
+ if (params.group === UNGROUPED_KIND_CATEGORY) {
+ return {params.children};
+ }
+
+ return (
+
+
+ {params.group}
+
+ {params.children}
+
+ );
+ };
if (error) return null;
return hidden ? null : (
Kind
-
+ renderInput={params => (
+
+ {loading ? : null}
+ {params.InputProps.endAdornment}
+ >
+ ),
+ }}
+ />
+ )}
+ />
);
};