diff --git a/frontend/src/__mocks__/mockConnectionType.ts b/frontend/src/__mocks__/mockConnectionType.ts index d6b543b4f0..66a8ddc62a 100644 --- a/frontend/src/__mocks__/mockConnectionType.ts +++ b/frontend/src/__mocks__/mockConnectionType.ts @@ -542,3 +542,38 @@ const mockFields: ConnectionTypeField[] = [ }, }, ]; + +export const mockModelServingFields: ConnectionTypeField[] = [ + { + type: 'short-text', + name: 'Access key', + description: '', + envVar: 'AWS_ACCESS_KEY_ID', + required: true, + properties: {}, + }, + { + type: 'hidden', + name: 'Secret key', + description: '', + envVar: 'AWS_SECRET_ACCESS_KEY', + required: true, + properties: {}, + }, + { + type: 'short-text', + name: 'Endpoint', + description: '', + envVar: 'AWS_S3_ENDPOINT', + required: true, + properties: {}, + }, + { + type: 'short-text', + name: 'Bucket', + description: '', + envVar: 'AWS_S3_BUCKET', + required: true, + properties: {}, + }, +]; diff --git a/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts b/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts index 720df36f54..e7412c6007 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts @@ -96,6 +96,7 @@ class CreateConnectionTypePage { rowNames.map((name, index) => this.getFieldsTableRow(index).findName().should('contain.text', name), ); + return this; } getCategorySection() { @@ -136,7 +137,8 @@ class ConnectionTypeRow extends TableRow { } shouldHaveName(name: string) { - return this.findConnectionTypeName().should('have.text', name); + this.findConnectionTypeName().should('have.text', name); + return this; } findConnectionTypeDescription() { @@ -147,16 +149,28 @@ class ConnectionTypeRow extends TableRow { return this.find().findByTestId('connection-type-creator'); } + findConnectionTypeCompatibility() { + return this.find().findByTestId('connection-type-compatibility'); + } + shouldHaveDescription(description: string) { - return this.findConnectionTypeDescription().should('contain.text', description); + this.findConnectionTypeDescription().should('contain.text', description); + return this; + } + + shouldHaveModelServingCompatibility() { + this.findConnectionTypeCompatibility().should('have.text', 'Model serving'); + return this; } shouldHaveCreator(creator: string) { - return this.findConnectionTypeCreator().should('have.text', creator); + this.findConnectionTypeCreator().should('have.text', creator); + return this; } shouldShowPreInstalledLabel() { - return this.find().findByTestId('connection-type-user-label').should('exist'); + this.find().findByTestId('connection-type-user-label').should('exist'); + return this; } findEnabled() { @@ -173,10 +187,12 @@ class ConnectionTypeRow extends TableRow { shouldBeEnabled() { this.findEnabled().should('be.checked'); + return this; } shouldBeDisabled() { this.findEnabled().should('not.be.checked'); + return this; } findEnableStatus() { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/connectionTypes.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/connectionTypes.cy.ts index ca65bb8111..1d8862553e 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/connectionTypes.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/connectionTypes.cy.ts @@ -5,7 +5,10 @@ import { } from '~/__tests__/cypress/cypress/utils/mockUsers'; import { connectionTypesPage } from '~/__tests__/cypress/cypress/pages/connectionTypes'; import { mockDashboardConfig } from '~/__mocks__'; -import { mockConnectionTypeConfigMap } from '~/__mocks__/mockConnectionType'; +import { + mockConnectionTypeConfigMap, + mockModelServingFields, +} from '~/__mocks__/mockConnectionType'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; it('Connection types should not be available for non product admins', () => { @@ -54,6 +57,7 @@ describe('Connection types', () => { name: 'test-2', displayName: 'Test display name', description: 'Test description', + fields: mockModelServingFields, }), ]); }); @@ -76,6 +80,7 @@ describe('Connection types', () => { row2.shouldHaveDescription('description 2'); row2.shouldShowPreInstalledLabel(); row2.shouldBeDisabled(); + row2.shouldHaveModelServingCompatibility(); }); it('should delete connection type', () => { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts index 0c6b0cd9f8..71fa8edb6f 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/connections.cy.ts @@ -54,10 +54,14 @@ describe('Connections', () => { initIntercepts(); projectDetails.visitSection('test-project', 'connections'); projectDetails.shouldBeEmptyState('Connections', 'connections', false); - connectionsPage.findTable().findByText('test1').should('exist'); - connectionsPage.findTable().findByText('s3').should('exist'); - connectionsPage.findTable().findByText('test2').should('exist'); - connectionsPage.findTable().findByText('postgres').should('exist'); + const row1 = connectionsPage.getConnectionRow('test1'); + row1.find().findByText('test1').should('exist'); + row1.find().findByText('s3').should('exist'); + row1.find().findByText('Model serving').should('exist'); + const row2 = connectionsPage.getConnectionRow('test2'); + row2.find().findByText('test2').should('exist'); + row2.find().findByText('postgres').should('exist'); + row1.find().findByText('Model serving').should('exist'); }); it('Delete a connection', () => { diff --git a/frontend/src/concepts/connectionTypes/CompatibilityLabel.tsx b/frontend/src/concepts/connectionTypes/CompatibilityLabel.tsx new file mode 100644 index 0000000000..0bb95d018b --- /dev/null +++ b/frontend/src/concepts/connectionTypes/CompatibilityLabel.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Label } from '@patternfly/react-core'; +import { CompatibleTypes } from '~/concepts/connectionTypes/utils'; + +type Props = { + type: CompatibleTypes; +}; + +const CompatibilityLabel: React.FC = ({ type }) => ; + +export default CompatibilityLabel; diff --git a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts index e0c1bb0383..824f7ef701 100644 --- a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts +++ b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts @@ -6,9 +6,12 @@ import { TextField, } from '~/concepts/connectionTypes/types'; import { + CompatibleTypes, defaultValueToString, fieldNameToEnvVar, fieldTypeToString, + getCompatibleTypes, + isModelServingCompatible, isValidEnvVar, toConnectionTypeConfigMap, toConnectionTypeConfigMapObj, @@ -262,3 +265,50 @@ describe('isValidEnvVar', () => { expect(isValidEnvVar('has space')).toBe(false); }); }); + +describe('isModelServingCompatible', () => { + it('should identify model serving compatible env vars', () => { + expect(isModelServingCompatible([])).toBe(false); + expect( + isModelServingCompatible([ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_S3_ENDPOINT', + 'AWS_S3_BUCKET', + ]), + ).toBe(true); + expect(isModelServingCompatible(['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'])).toBe(false); + expect( + isModelServingCompatible([ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_S3_ENDPOINT', + 'AWS_S3_BUCKET', + 'URI', + ]), + ).toBe(true); + }); +}); + +describe('getCompatibleTypes', () => { + it('should return compatible types', () => { + expect(getCompatibleTypes(['AWS_ACCESS_KEY_ID'])).toEqual([]); + expect( + getCompatibleTypes([ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_S3_ENDPOINT', + 'AWS_S3_BUCKET', + ]), + ).toEqual([CompatibleTypes.ModelServing]); + expect( + getCompatibleTypes([ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_S3_ENDPOINT', + 'AWS_S3_BUCKET', + 'URI', + ]), + ).toEqual([CompatibleTypes.ModelServing]); + }); +}); diff --git a/frontend/src/concepts/connectionTypes/utils.ts b/frontend/src/concepts/connectionTypes/utils.ts index 0f0896ed44..68c821c0d3 100644 --- a/frontend/src/concepts/connectionTypes/utils.ts +++ b/frontend/src/concepts/connectionTypes/utils.ts @@ -5,6 +5,7 @@ import { ConnectionTypeFieldType, ConnectionTypeFieldTypeUnion, } from '~/concepts/connectionTypes/types'; +import { enumIterator } from '~/utilities/utils'; export const toConnectionTypeConfigMapObj = ( configMap: ConnectionTypeConfigMap, @@ -85,3 +86,24 @@ export const fieldNameToEnvVar = (name: string): string => { const ENV_VAR_NAME_REGEX = new RegExp('^[_a-zA-Z][_a-zA-Z0-9]*$'); export const isValidEnvVar = (name: string): boolean => ENV_VAR_NAME_REGEX.test(name); + +export const isModelServingCompatible = (envVars: string[]): boolean => + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_S3_ENDPOINT', 'AWS_S3_BUCKET'].every( + (envVar) => envVars.includes(envVar), + ); + +export enum CompatibleTypes { + ModelServing = 'Model serving', +} + +const compatibilities: Record boolean> = { + [CompatibleTypes.ModelServing]: isModelServingCompatible, +}; + +export const getCompatibleTypes = (envVars: string[]): CompatibleTypes[] => + enumIterator(CompatibleTypes).reduce((acc, [, value]) => { + if (compatibilities[value](envVars)) { + acc.push(value); + } + return acc; + }, []); diff --git a/frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx b/frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx index a5e93d6081..fcafc629c2 100644 --- a/frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx +++ b/frontend/src/pages/connectionTypes/ConnectionTypesTableRow.tsx @@ -9,7 +9,10 @@ import { TimestampTooltipVariant, Truncate, } from '@patternfly/react-core'; -import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; +import { + ConnectionTypeConfigMapObj, + isConnectionTypeDataField, +} from '~/concepts/connectionTypes/types'; import { relativeTime } from '~/utilities/time'; import { updateConnectionTypeEnabled } from '~/services/connectionTypesService'; import useNotification from '~/utilities/useNotification'; @@ -22,6 +25,8 @@ import { } from '~/concepts/k8s/utils'; import { connectionTypeColumns } from '~/pages/connectionTypes/columns'; import CategoryLabel from '~/concepts/connectionTypes/CategoryLabel'; +import { getCompatibleTypes } from '~/concepts/connectionTypes/utils'; +import CompatibilityLabel from '~/concepts/connectionTypes/CompatibilityLabel'; type ConnectionTypesTableRowProps = { obj: ConnectionTypeConfigMapObj; @@ -78,6 +83,12 @@ const ConnectionTypesTableRow: React.FC = ({ setIsEnabled(obj.metadata.annotations?.['opendatahub.io/enabled'] === 'true'); }, [obj.metadata.annotations]); + const compatibleTypes = getCompatibleTypes( + obj.data?.fields + ?.filter(isConnectionTypeDataField) + .filter((field) => field.required) + .map((field) => field.envVar) ?? [], + ); return ( @@ -99,7 +110,18 @@ const ConnectionTypesTableRow: React.FC = ({ '-' )} - + + {compatibleTypes.length ? ( + + {compatibleTypes.map((compatibleType) => ( + + ))} + + ) : ( + '-' + )} + + {ownedByDSC(obj) ? ( ) : ( @@ -107,7 +129,7 @@ const ConnectionTypesTableRow: React.FC = ({ )} @@ -115,7 +137,7 @@ const ConnectionTypesTableRow: React.FC = ({ {createdDate ? relativeTime(Date.now(), createdDate.getTime()) : 'Unknown'} - + [] = label: 'Name', field: 'name', sortable: sorter, - width: 30, + width: 20, }, { label: 'Category', field: 'category', sortable: false, - width: 25, + width: 20, + }, + { + label: 'Compatibility', + field: 'compatibility', + sortable: false, + width: 15, }, { label: 'Creator', diff --git a/frontend/src/pages/projects/screens/detail/connections/ConnectionsTableRow.tsx b/frontend/src/pages/projects/screens/detail/connections/ConnectionsTableRow.tsx index 4677a65037..54adbdb135 100644 --- a/frontend/src/pages/projects/screens/detail/connections/ConnectionsTableRow.tsx +++ b/frontend/src/pages/projects/screens/detail/connections/ConnectionsTableRow.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; -import { Truncate } from '@patternfly/react-core'; +import { LabelGroup, Truncate } from '@patternfly/react-core'; import { Connection, ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; import { TableRowTitleDescription } from '~/components/table'; import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +import { getCompatibleTypes } from '~/concepts/connectionTypes/utils'; +import CompatibilityLabel from '~/concepts/connectionTypes/CompatibilityLabel'; type ConnectionsTableRowProps = { obj: Connection; @@ -28,6 +30,8 @@ const ConnectionsTableRow: React.FC = ({ ); }, [obj, connectionTypes]); + const compatibleTypes = getCompatibleTypes(Object.keys(obj.data)); + return ( @@ -41,6 +45,17 @@ const ConnectionsTableRow: React.FC = ({ /> {connectionTypeDisplayName} + + {compatibleTypes.length ? ( + + {compatibleTypes.map((compatibleType) => ( + + ))} + + ) : ( + '-' + )} + - [] = [ { field: 'name', label: 'Name', - width: 35, + width: 30, sortable: (a, b) => (a.metadata.annotations['openshift.io/display-name'] ?? '').localeCompare( b.metadata.annotations['openshift.io/display-name'] ?? '', @@ -14,16 +14,22 @@ export const columns: SortableData[] = [ { field: 'type', label: 'Type', - width: 25, + width: 20, sortable: (a, b) => a.metadata.annotations['opendatahub.io/connection-type'].localeCompare( b.metadata.annotations['opendatahub.io/connection-type'], ), }, + { + field: 'compatibility', + label: 'Compatibility', + width: 20, + sortable: false, + }, { field: 'connections', label: 'Connected resources', - width: 35, + width: 25, sortable: false, }, {