Skip to content

Commit

Permalink
add model serving compatible select to connection type form (#3396)
Browse files Browse the repository at this point in the history
  • Loading branch information
christianvogt authored Oct 31, 2024
1 parent a4ba326 commit 591f87b
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ class CreateConnectionTypePage {
findDuplicateConnectionTypeButton() {
return cy.findByTestId('duplicate-connection-type');
}

findCompatibleModelServingTypesAlert() {
return cy.findByTestId('compatible-model-serving-types-alert');
}

findModelServingCompatibleTypeDropdown() {
return cy.findByTestId('select-model-serving-compatible-type');
}
}

class CategorySection extends Contextual<HTMLElement> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,31 @@ describe('create', () => {
disableConnectionTypes: false,
}),
);
cy.interceptOdh('GET /api/connection-types', [
mockConnectionTypeConfigMap({
displayName: 'URI - v1',
name: 'uri-v1',
fields: [
{
type: 'uri',
name: 'URI field test',
envVar: 'URI',
required: true,
properties: {},
},
],
}),
]);
});

it('Display base page', () => {
it('Can create connection type', () => {
const categorySection = createConnectionTypePage.getCategorySection();
createConnectionTypePage.visitCreatePage();

createConnectionTypePage.findConnectionTypeName().should('exist');
createConnectionTypePage.findConnectionTypeDesc().should('exist');
createConnectionTypePage.findConnectionTypeEnableCheckbox().should('exist');
createConnectionTypePage.findConnectionTypePreviewToggle().should('exist');
});

it('Allows create button with valid name and category', () => {
const categorySection = createConnectionTypePage.getCategorySection();
createConnectionTypePage.visitCreatePage();

createConnectionTypePage.findConnectionTypeName().should('have.value', '');
createConnectionTypePage.findSubmitButton().should('be.disabled');
Expand All @@ -40,6 +51,28 @@ describe('create', () => {
categorySection.findCategoryTable();
categorySection.findMultiGroupSelectButton('Object-storage');
createConnectionTypePage.findSubmitButton().should('be.enabled');

categorySection.findMultiGroupInput().type('Database');
categorySection.findMultiGroupSelectButton('Database');

categorySection.findMultiGroupInput().type('New category');

categorySection.findMultiGroupSelectButton('Option');
categorySection.findChipItem('New category').should('exist');
categorySection.findMultiGroupInput().type('{esc}');

createConnectionTypePage
.findModelServingCompatibleTypeDropdown()
.findDropdownItem('URI')
.click();
createConnectionTypePage.findCompatibleModelServingTypesAlert().should('exist');
createConnectionTypePage.getFieldsTableRow(0).findSectionHeading().should('exist');
createConnectionTypePage
.getFieldsTableRow(1)
.findName()
.should('contain.text', 'URI field test');

createConnectionTypePage.findSubmitButton().should('be.enabled');
});

it('Shows creation error message when creation fails', () => {
Expand All @@ -60,28 +93,6 @@ describe('create', () => {

createConnectionTypePage.findFooterError().should('contain.text', 'returned error message');
});

it('Selects category or creates new category', () => {
createConnectionTypePage.visitCreatePage();

const categorySection = createConnectionTypePage.getCategorySection();

categorySection.findCategoryTable();
categorySection.findMultiGroupSelectButton('Object-storage');

categorySection.findChipItem('Object storage').should('exist');
categorySection.clearMultiChipItem();

categorySection.findMultiGroupSelectButton('Object-storage');

categorySection.findMultiGroupInput().type('Database');
categorySection.findMultiGroupSelectButton('Database');

categorySection.findMultiGroupInput().type('New category');

categorySection.findMultiGroupSelectButton('Option');
categorySection.findChipItem('New category').should('exist');
});
});

describe('duplicate', () => {
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/components/SimpleMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ const isSpacer = (v: Item | Spacer): v is Spacer => 'isSpacer' in v;
type SimpleDropdownProps = {
dropdownItems: (Item | Spacer)[];
testId?: string;
toggleLabel?: string;
variant?: React.ComponentProps<typeof MenuToggle>['variant'];
} & Omit<
React.ComponentProps<typeof Dropdown>,
'isOpen' | 'isPlain' | 'popperProps' | 'toggle' | 'onChange'
>;

const SimpleMenuActions: React.FC<SimpleDropdownProps> = ({ dropdownItems, testId, ...props }) => {
const SimpleMenuActions: React.FC<SimpleDropdownProps> = ({
dropdownItems,
testId,
toggleLabel,
variant,
...props
}) => {
const [open, setOpen] = React.useState(false);

return (
Expand All @@ -37,17 +45,17 @@ const SimpleMenuActions: React.FC<SimpleDropdownProps> = ({ dropdownItems, testI
onOpenChange={(isOpened) => setOpen(isOpened)}
toggle={(toggleRef) => (
<MenuToggle
aria-label="Actions"
aria-label={toggleLabel ?? 'Actions'}
data-testid={testId}
variant="plain"
variant={variant ?? (toggleLabel ? 'default' : 'plain')}
ref={toggleRef}
onClick={() => setOpen(!open)}
isExpanded={open}
>
<EllipsisVIcon />
{toggleLabel ?? <EllipsisVIcon />}
</MenuToggle>
)}
popperProps={{ position: 'right' }}
popperProps={!toggleLabel ? { position: 'right' } : undefined}
>
<DropdownList>
{dropdownItems.map((itemOrSpacer, i) =>
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
fieldTypeToString,
getCompatibleTypes,
isModelServingCompatible,
isModelServingTypeCompatible,
isValidEnvVar,
ModelServingCompatibleConnectionTypes,
toConnectionTypeConfigMap,
toConnectionTypeConfigMapObj,
} from '~/concepts/connectionTypes/utils';
Expand Down Expand Up @@ -312,3 +314,29 @@ describe('getCompatibleTypes', () => {
).toEqual([CompatibleTypes.ModelServing]);
});
});

describe('isModelServingTypeCompatible', () => {
it('should identify model serving compatible env vars', () => {
expect(
isModelServingTypeCompatible(['invalid'], ModelServingCompatibleConnectionTypes.ModelServing),
).toBe(false);
expect(
isModelServingTypeCompatible(
['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_S3_ENDPOINT', 'AWS_S3_BUCKET'],
ModelServingCompatibleConnectionTypes.ModelServing,
),
).toBe(true);
expect(
isModelServingTypeCompatible(['invalid'], ModelServingCompatibleConnectionTypes.OCI),
).toBe(false);
expect(isModelServingTypeCompatible(['URI'], ModelServingCompatibleConnectionTypes.OCI)).toBe(
true,
);
expect(
isModelServingTypeCompatible(['invalid'], ModelServingCompatibleConnectionTypes.URI),
).toBe(false);
expect(isModelServingTypeCompatible(['URI'], ModelServingCompatibleConnectionTypes.URI)).toBe(
true,
);
});
});
48 changes: 48 additions & 0 deletions frontend/src/concepts/connectionTypes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,38 @@ export const S3ConnectionTypeKeys = [
'AWS_S3_BUCKET',
];

export enum ModelServingCompatibleConnectionTypes {
ModelServing = 's3',
OCI = 'oci-compliant-registry-v1',
URI = 'uri-v1',
}

const modelServinConnectionTypes: Record<
ModelServingCompatibleConnectionTypes,
{
name: string;
envVars: string[];
}
> = {
[ModelServingCompatibleConnectionTypes.ModelServing]: {
name: 'S3 compatible object storage',
envVars: S3ConnectionTypeKeys,
},
[ModelServingCompatibleConnectionTypes.OCI]: {
name: 'OCI compliant registry',
envVars: ['URI'],
},
[ModelServingCompatibleConnectionTypes.URI]: {
name: 'URI',
envVars: ['URI'],
},
};

export const isModelServingTypeCompatible = (
envVars: string[],
type: ModelServingCompatibleConnectionTypes,
): boolean => modelServinConnectionTypes[type].envVars.every((envVar) => envVars.includes(envVar));

export const isModelServingCompatible = (envVars: string[]): boolean =>
S3ConnectionTypeKeys.every((envVar) => envVars.includes(envVar));

Expand Down Expand Up @@ -279,3 +311,19 @@ export const findSectionFields = (

return fields.slice(sectionIndex + 1, nextSectionIndex === -1 ? undefined : nextSectionIndex);
};

export const filterModelServingCompatibleTypes = (
connectionTypes: ConnectionTypeConfigMapObj[],
): {
key: ModelServingCompatibleConnectionTypes;
name: string;
type: ConnectionTypeConfigMapObj;
}[] =>
enumIterator(ModelServingCompatibleConnectionTypes)
.map(([, value]) => {
const connectionType = connectionTypes.find((t) => t.metadata.name === value);
return connectionType
? { key: value, name: modelServinConnectionTypes[value].name, type: connectionType }
: undefined;
})
.filter((t) => t != null);
Loading

0 comments on commit 591f87b

Please sign in to comment.