import type { FC, ReactElement } from 'react'; import { useState, useCallback, useEffect, useMemo } from 'react'; import { Alert } from '@patternfly/react-core'; import * as fuzzy from 'fuzzysearch'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import type { K8sResourceCommon, K8sModel, K8sResourceKind, WatchK8sResource, } from '@console/dynamic-plugin-sdk/src'; import type { ActionItem } from '@console/internal/components/utils/console-select'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { NamespaceModel, ProjectModel } from '@console/internal/models'; import { FLAGS } from '@console/shared/src/constants/common'; import { useCreateNamespaceModal } from '@console/shared/src/hooks/useCreateNamespaceModal'; import { useCreateProjectModal } from '@console/shared/src/hooks/useCreateProjectModal'; import { useFlag } from '@console/shared/src/hooks/useFlag'; import { flagPending } from '../../reducers/features'; import { ResourceName } from './resource-icon'; import { LoadingInline } from './status-box'; const getKey = (key, keyKind) => (keyKind ? `${key}-${keyKind}` : key); interface ListDropdownResource extends Partial { data?: K8sResourceCommon[]; } export interface ListDropdownProps { dataFilter?: (resource: K8sResourceCommon) => boolean; desc?: string; // specify both key/kind selectedKey?: string; selectedKeyKind?: string; fixed?: boolean; resources?: ListDropdownResource[]; placeholder?: string; onChange: ( selectedKey: string, selectedKeyKind?: string, selectedResource?: K8sResourceKind, ) => void; id?: string; disabled?: boolean; dataTest?: string; actionItems?: ActionItem[]; loaded?: boolean; loadError?: boolean; } interface ListDropdownInternalProps extends Omit { resources?: Record; } const InnerListDropdown: FC = ({ desc, placeholder, loaded, loadError, resources, dataFilter, fixed, id, disabled, actionItems, dataTest, onChange, ...props }) => { const { t } = useTranslation('public'); const [items, setItems] = useState<{ [key: string]: { kindLabel: string; name: string; resource: K8sResourceKind }; }>({}); const [selectedKey, setSelectedKey] = useState( props.selectedKey ? getKey(props.selectedKey, props.selectedKeyKind) : undefined, ); const [title, setTitle] = useState(loaded ? placeholder : ); const autocompleteFilter = (text: string, item: ReactElement) => fuzzy(text, item.props.name); const handleOnChange = useCallback( (key: string) => { if (_.find(actionItems, { actionKey: key })) { onChange(key); return; } const { name, kindLabel, resource } = items[key]; setSelectedKey(key); setTitle(); onChange(name, kindLabel, resource); }, [actionItems, items, onChange], ); useEffect(() => { if (loadError) { setTitle(
{t('Error loading {{desc}}', { desc })}
); return; } const unsortedList = {}; _.each(resources, ({ data }, kindLabel) => { (data || []).forEach((resource) => { if (!dataFilter || dataFilter(resource)) { unsortedList[`${resource.metadata.name}-${kindLabel}`] = { kindLabel, name: resource.metadata.name, resource, }; } }); }); const sortedList = Object.fromEntries( Object.keys(unsortedList) .sort() .map((key) => [key, unsortedList[key]]), ); setItems(sortedList); // Only update selectedKey if props.selectedKey is defined const newSelectedKey = props.selectedKey !== undefined ? getKey(props.selectedKey, props.selectedKeyKind) : selectedKey; setSelectedKey(newSelectedKey); const selectedItem = sortedList[newSelectedKey]; setTitle( selectedItem ? ( ) : ( placeholder ), ); }, [ loaded, loadError, resources, dataFilter, props.selectedKey, props.selectedKeyKind, placeholder, desc, t, selectedKey, ]); const renderedItems = useMemo(() => { const result = {}; _.keys(items).forEach((key) => { const item = items[key]; result[key] = ; }); return result; }, [items]); return (
{fixed ? ( renderedItems[selectedKey] ) : ( } alwaysShowTitle onChange={handleOnChange} id={id} isFullWidth menuClassName="dropdown-menu--text-wrap" disabled={disabled} dataTest={dataTest} /> )} {loaded && _.isEmpty(renderedItems) && (desc || props.selectedKeyKind) && ( )}
); }; export const ListDropdown: FC = (props) => { const watchResources = useMemo(() => { if (!props.resources || props.resources.length === 0) { return {}; } return props.resources.reduce( (acc, resource) => { // Use prop as key if provided, otherwise fallback to kind const key = resource.prop || resource.kind; acc[key] = { kind: resource.kind, isList: true, namespace: resource.namespace, selector: resource.selector, fieldSelector: resource.fieldSelector, limit: resource.limit, namespaced: resource.namespaced, optional: resource.optional, }; return acc; }, {} as Record, ); }, [props.resources]); const watchedResources = useK8sWatchResources>(watchResources); const loaded = useMemo( () => Object.values(watchedResources) .filter((r) => !r.loadError) .every((r) => r.loaded), [watchedResources], ); const loadError = useMemo( () => Object.values(watchedResources).some((r) => r.loadError), [watchedResources], ); return ( ); }; export const useProjectOrNamespaceModel = (): [K8sModel, boolean] | [] => { const openshiftFlag = useFlag(FLAGS.OPENSHIFT); const canCreateNs = useFlag(FLAGS.CAN_CREATE_NS); const canCreateProject = useFlag(FLAGS.CAN_CREATE_PROJECT); if (flagPending(openshiftFlag) || flagPending(canCreateNs) || flagPending(canCreateProject)) { return []; } // NamespaceModal is used when not on an openshift cluster const model = openshiftFlag ? ProjectModel : NamespaceModel; const canCreate = openshiftFlag ? canCreateProject : canCreateNs; return [model, canCreate]; }; export const NsDropdown: FC = (props) => { const { t } = useTranslation('public'); const createNamespaceModal = useCreateNamespaceModal(); const createProjectModal = useCreateProjectModal(); const [selectedKey, setSelectedKey] = useState(props.selectedKey); const [model, canCreate] = useProjectOrNamespaceModel(); const actionItems = model && canCreate ? [ { actionTitle: t('Create {{resourceKindLabel}}', { resourceKindLabel: model.labelKey ? t(model.labelKey) : model.label, }), actionKey: `Create_${model.label}`, }, ] : []; const onChange: ListDropdownProps['onChange'] = (actionKey, kindLabel, resource) => { switch (actionKey) { case 'Create_Namespace': createNamespaceModal({ onSubmit: (newNamespace) => { setSelectedKey(newNamespace.metadata.name); props.onChange(newNamespace.metadata.name, newNamespace.kind, newNamespace); }, }); break; case 'Create_Project': createProjectModal({ onSubmit: (newProject) => { setSelectedKey(newProject.metadata.name); props.onChange(newProject.metadata.name, newProject.kind, newProject); }, }); break; default: setSelectedKey(actionKey); props.onChange(actionKey, kindLabel, resource); break; } }; const getPlaceholder = (placeholderModel) => { if (!placeholderModel.kind) { return t('Select item'); } return t('Select {{kindLabel}}', { kindLabel: placeholderModel.labelKey ? t(placeholderModel.labelKey) : placeholderModel.label, }); }; return model ? ( ) : null; };