diff --git a/frontend/src/concepts/connectionTypes/ConnectionTypeForm.tsx b/frontend/src/concepts/connectionTypes/ConnectionTypeForm.tsx index 939fa9ce70..35f44f9342 100644 --- a/frontend/src/concepts/connectionTypes/ConnectionTypeForm.tsx +++ b/frontend/src/concepts/connectionTypes/ConnectionTypeForm.tsx @@ -27,44 +27,76 @@ import { } from '~/concepts/k8s/K8sNameDescriptionField/types'; import { ConnectionTypeDetailsHelperText } from './ConnectionTypeDetailsHelperText'; +const createSelectOption = ( + connectionType: ConnectionTypeConfigMapObj, + isSelected: boolean, +): TypeaheadSelectOption => { + const description = getDescriptionFromK8sResource(connectionType); + return { + value: getResourceNameFromK8sResource(connectionType), + content: getDisplayNameFromK8sResource(connectionType), + description: ( + + {description && ( + + + + )} + {connectionType.data?.category?.length && ( + + + + )} + + ), + data: `${description} ${connectionType.data?.category?.join(' ')}`, + isSelected, + }; +}; + const getConnectionTypeSelectOptions = ( isPreview: boolean, - selectedConnectionType?: ConnectionTypeConfigMapObj, connectionTypes?: ConnectionTypeConfigMapObj[], + selectedConnectionType?: ConnectionTypeConfigMapObj, + selectedConnectionTypeName?: string, ): TypeaheadSelectOption[] => { - if (isPreview && selectedConnectionType?.metadata.annotations?.['openshift.io/display-name']) { - return [ - { - value: '', - content: selectedConnectionType.metadata.annotations['openshift.io/display-name'], - isSelected: true, - }, - ]; - } - if (!isPreview && connectionTypes) { - return connectionTypes.map((t) => ({ - value: getResourceNameFromK8sResource(t), - content: getDisplayNameFromK8sResource(t), - description: ( - - {getDescriptionFromK8sResource(t) && ( - - - - )} - {t.data?.category?.length && ( - - - - )} - - ), - data: `${getDescriptionFromK8sResource(t)} ${t.data?.category?.join(' ')}`, - isSelected: - !!selectedConnectionType && - getResourceNameFromK8sResource(t) === - getResourceNameFromK8sResource(selectedConnectionType), - })); + if (isPreview) { + const displayName = selectedConnectionType?.metadata.annotations?.['openshift.io/display-name']; + if (displayName) { + return [ + { + value: '', + content: displayName, + isSelected: true, + }, + ]; + } + } else { + if (!connectionTypes || connectionTypes.length === 0) { + if (selectedConnectionType) { + return [createSelectOption(selectedConnectionType, true)]; + } + if (selectedConnectionTypeName) { + return [ + { + value: selectedConnectionTypeName, + content: selectedConnectionTypeName, + isSelected: true, + }, + ]; + } + } + if (connectionTypes) { + return connectionTypes.map((t) => + createSelectOption( + t, + !!selectedConnectionType && + getResourceNameFromK8sResource(t) === + (selectedConnectionTypeName || + getResourceNameFromK8sResource(selectedConnectionType)), + ), + ); + } } return []; }; @@ -73,33 +105,36 @@ type Props = Pick< React.ComponentProps, 'onChange' | 'onValidate' > & { - connectionType?: ConnectionTypeConfigMapObj; - setConnectionType?: (obj?: ConnectionTypeConfigMapObj) => void; - connectionTypes?: ConnectionTypeConfigMapObj[]; + connectionType?: ConnectionTypeConfigMapObj | string; + setConnectionType?: (name: string) => void; + options?: ConnectionTypeConfigMapObj[]; isPreview?: boolean; connectionNameDesc?: K8sNameDescriptionFieldData; setConnectionNameDesc?: K8sNameDescriptionFieldUpdateFunction; connectionValues?: { [key: string]: ConnectionTypeValueType; }; - disableTypeSelection?: boolean; }; const ConnectionTypeForm: React.FC = ({ - connectionType, + connectionType: connectionTypeUnion, setConnectionType, - connectionTypes, + options, isPreview = false, connectionNameDesc, setConnectionNameDesc, connectionValues, onChange, onValidate, - disableTypeSelection, }) => { - const options: TypeaheadSelectOption[] = React.useMemo( - () => getConnectionTypeSelectOptions(isPreview, connectionType, connectionTypes), - [isPreview, connectionType, connectionTypes], + const [connectionTypeName, connectionType] = + typeof connectionTypeUnion === 'string' + ? [connectionTypeUnion] + : [connectionTypeUnion?.metadata.name, connectionTypeUnion]; + + const selectOptions: TypeaheadSelectOption[] = React.useMemo( + () => getConnectionTypeSelectOptions(isPreview, options, connectionType, connectionTypeName), + [isPreview, options, connectionType, connectionTypeName], ); return ( @@ -108,11 +143,13 @@ const ConnectionTypeForm: React.FC = ({ - setConnectionType?.(connectionTypes?.find((c) => c.metadata.name === selection)) - } - isDisabled={isPreview || disableTypeSelection} + selectOptions={selectOptions} + onSelect={(_, selection) => { + if (typeof selection === 'string') { + setConnectionType?.(selection); + } + }} + isDisabled={isPreview || !options || options.length <= 1} placeholder={ isPreview && !connectionType?.metadata.annotations?.['openshift.io/display-name'] ? 'Unspecified' @@ -137,7 +174,7 @@ const ConnectionTypeForm: React.FC = ({ )} - {(isPreview || connectionType?.metadata.name) && ( + {(isPreview || connectionTypeName) && ( > = ({ 'data-testid': dataTestId, }) => { const isPreview = mode === 'preview'; - - // ensure the value is not undefined - React.useEffect(() => { - if (value == null) { - onChange?.(field.properties.defaultValue ?? false); - } - // do not run when callback changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); - return ( ( + connectionTypes: T[], +): T[] => + connectionTypes.filter((t) => t.metadata.annotations?.['opendatahub.io/enabled'] === 'true'); diff --git a/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx b/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx index 3ea827c639..286f4f98e9 100644 --- a/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/ConnectionsList.tsx @@ -11,6 +11,7 @@ import { ProjectObjectType, typedEmptyImage } from '~/concepts/design/utils'; import { Connection } from '~/concepts/connectionTypes/types'; import { useWatchConnectionTypes } from '~/utilities/useWatchConnectionTypes'; import { createSecret, replaceSecret } from '~/api'; +import { filterEnabledConnectionTypes } from '~/concepts/connectionTypes/utils'; import ConnectionsTable from './ConnectionsTable'; import { ManageConnectionModal } from './ManageConnectionsModal'; @@ -23,6 +24,10 @@ const ConnectionsList: React.FC = () => { currentProject, } = React.useContext(ProjectDetailsContext); const [connectionTypes, connectionTypesLoaded, connectionTypesError] = useWatchConnectionTypes(); + const enabledConnectionTypes = React.useMemo( + () => filterEnabledConnectionTypes(connectionTypes), + [connectionTypes], + ); const [manageConnectionModal, setManageConnectionModal] = React.useState<{ connection?: Connection; @@ -47,6 +52,7 @@ const ConnectionsList: React.FC = () => { onClick={() => { setManageConnectionModal({}); }} + isDisabled={enabledConnectionTypes.length === 0} > Add connection , @@ -65,6 +71,10 @@ const ConnectionsList: React.FC = () => { key={`action-${ProjectSectionID.CONNECTIONS}`} variant="primary" data-testid="create-connection-button" + onClick={() => { + setManageConnectionModal({}); + }} + isDisabled={enabledConnectionTypes.length === 0} > Create connection diff --git a/frontend/src/pages/projects/screens/detail/connections/ManageConnectionsModal.tsx b/frontend/src/pages/projects/screens/detail/connections/ManageConnectionsModal.tsx index 6888a7096c..32a6f97bf1 100644 --- a/frontend/src/pages/projects/screens/detail/connections/ManageConnectionsModal.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/ManageConnectionsModal.tsx @@ -13,6 +13,7 @@ import { ProjectKind, SecretKind } from '~/k8sTypes'; import { useK8sNameDescriptionFieldData } from '~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField'; import { assembleConnectionSecret, + filterEnabledConnectionTypes, getDefaultValues, parseConnectionSecretValues, } from '~/concepts/connectionTypes/utils'; @@ -40,36 +41,38 @@ export const ManageConnectionModal: React.FC = ({ const [isModified, setIsModified] = React.useState(false); const enabledConnectionTypes = React.useMemo( - () => - connectionTypes.filter((t) => t.metadata.annotations?.['opendatahub.io/enabled'] === 'true'), + () => filterEnabledConnectionTypes(connectionTypes), [connectionTypes], ); + const connectionTypeRef = connection?.metadata.annotations['opendatahub.io/connection-type']; + const [selectedConnectionType, setSelectedConnectionType] = React.useState< ConnectionTypeConfigMapObj | undefined >(() => { - if (isEdit && connection) { - return connectionTypes.find( - (t) => - t.metadata.name === connection.metadata.annotations['opendatahub.io/connection-type'], - ); + if (isEdit) { + return connectionTypes.find((t) => t.metadata.name === connectionTypeRef); } if (enabledConnectionTypes.length === 1) { return enabledConnectionTypes[0]; } return undefined; }); + + const connectionTypeName = selectedConnectionType?.metadata.name || connectionTypeRef; + const { data: nameDescData, onDataChange: setNameDescData } = useK8sNameDescriptionFieldData({ initialData: connection, }); const [connectionValues, setConnectionValues] = React.useState<{ [key: string]: ConnectionTypeValueType; }>(() => { - if (connection?.data) { - return parseConnectionSecretValues(connection, selectedConnectionType); - } - if (enabledConnectionTypes.length === 1) { - return getDefaultValues(enabledConnectionTypes[0]); + if (isEdit) { + if (connection) { + return parseConnectionSecretValues(connection, selectedConnectionType); + } + } else if (selectedConnectionType) { + return getDefaultValues(selectedConnectionType); } return {}; }); @@ -79,9 +82,9 @@ export const ManageConnectionModal: React.FC = ({ }>({}); const isFormValid = React.useMemo( () => - !!selectedConnectionType && + !!connectionTypeName && !!nameDescData.name && - !selectedConnectionType.data?.fields?.find( + !selectedConnectionType?.data?.fields?.find( (field) => isConnectionTypeDataField(field) && field.required && @@ -89,7 +92,7 @@ export const ManageConnectionModal: React.FC = ({ field.type !== ConnectionTypeFieldType.Boolean, ) && !Object.values(validations).includes(false), - [selectedConnectionType, nameDescData, connectionValues, validations], + [connectionTypeName, selectedConnectionType, nameDescData, connectionValues, validations], ); // if user changes connection types, don't discard previous entries in case of accident @@ -137,19 +140,14 @@ export const ManageConnectionModal: React.FC = ({ setError(undefined); // this shouldn't ever happen, but type safety - if (!selectedConnectionType) { + if (!connectionTypeName) { setError(new Error('No connection type selected')); setIsSaving(false); return; } onSubmit( - assembleConnectionSecret( - project, - selectedConnectionType, - nameDescData, - connectionValues, - ), + assembleConnectionSecret(project, connectionTypeName, nameDescData, connectionValues), ) .then(() => { onClose(true); @@ -160,7 +158,7 @@ export const ManageConnectionModal: React.FC = ({ }); }} error={error} - isSubmitDisabled={!isFormValid || !isModified} + isSubmitDisabled={!isFormValid || !isModified || isSaving} isSubmitLoading={isSaving} alertTitle="" /> @@ -168,7 +166,7 @@ export const ManageConnectionModal: React.FC = ({ > {isEdit && ( = ({ )} { + options={!isEdit ? enabledConnectionTypes : undefined} + connectionType={selectedConnectionType || (isEdit ? connectionTypeRef : undefined)} + setConnectionType={(name: string) => { + const obj = connectionTypes.find((c) => c.metadata.name === name); if (!isModified) { setIsModified(true); } @@ -203,7 +202,6 @@ export const ManageConnectionModal: React.FC = ({ onValidate={(field, isValid) => setValidations((prev) => ({ ...prev, [field.envVar]: isValid })) } - disableTypeSelection={isEdit || enabledConnectionTypes.length === 1} /> ); diff --git a/frontend/src/pages/projects/screens/detail/connections/__tests__/ManageConnectionsModal.spec.tsx b/frontend/src/pages/projects/screens/detail/connections/__tests__/ManageConnectionsModal.spec.tsx index 3c02ac2e4f..3c8162994f 100644 --- a/frontend/src/pages/projects/screens/detail/connections/__tests__/ManageConnectionsModal.spec.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/__tests__/ManageConnectionsModal.spec.tsx @@ -496,7 +496,7 @@ describe('Edit connection modal', () => { connectionType: 's3', data: { UNMATCHED_1: window.btoa('unmatched1!'), - env1: window.btoa('saved data'), + env1: window.btoa('true'), UNMATCHED_2: window.btoa('unmatched2!'), }, })} @@ -505,8 +505,8 @@ describe('Edit connection modal', () => { name: 's3', fields: [ { - type: 'short-text', - name: 'Short text', + type: 'boolean', + name: 'Checkbox field', envVar: 'env1', properties: {}, }, @@ -517,8 +517,35 @@ describe('Edit connection modal', () => { ); expect(screen.getByRole('combobox')).toHaveValue('s3'); - expect(screen.getByRole('textbox', { name: 'Short text' })).toHaveValue('saved data'); + expect(screen.getByRole('checkbox', { name: 'Checkbox field' })).toBeChecked(); + expect(screen.getByRole('textbox', { name: 'UNMATCHED_1' })).toHaveValue('unmatched1!'); + expect(screen.getByRole('textbox', { name: 'UNMATCHED_2' })).toHaveValue('unmatched2!'); + }); + + it('should list non matching values as short text with missing connection type', async () => { + render( + , + ); + + expect(screen.getByRole('combobox')).toHaveValue('s3'); expect(screen.getByRole('textbox', { name: 'UNMATCHED_1' })).toHaveValue('unmatched1!'); + expect(screen.getByRole('textbox', { name: 'env1' })).toHaveValue('true'); expect(screen.getByRole('textbox', { name: 'UNMATCHED_2' })).toHaveValue('unmatched2!'); }); });