Skip to content

Commit

Permalink
feat(RegisterModel): 9914 autofill connection (#3084)
Browse files Browse the repository at this point in the history
* feat(RegisterModel): 9914 autofill connection

Signed-off-by: gitdallas <[email protected]>

squash me

Signed-off-by: gitdallas <[email protected]>

squashme

Signed-off-by: gitdallas <[email protected]>

* pull radio body out of radiobody prop

Signed-off-by: gitdallas <[email protected]>

* fixed lint

Signed-off-by: gitdallas <[email protected]>

---------

Signed-off-by: gitdallas <[email protected]>
  • Loading branch information
gitdallas authored Aug 15, 2024
1 parent b16f222 commit e454189
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ class RegisterModelPage {
return cy.get(selector);
}

findObjectStorageAutofillButton() {
return cy.findByTestId('object-storage-autofill-button');
}

findConnectionAutofillModal() {
return cy.findByTestId('connection-autofill-modal');
}

findProjectSelector() {
return this.findConnectionAutofillModal().findByTestId('project-selector-dropdown');
}

findConnectionSelector() {
return this.findConnectionAutofillModal().findByTestId('select-data-connection');
}

findAutofillButton() {
return cy.findByTestId('autofill-modal-button');
}

findSubmitButton() {
return cy.findByTestId('create-button');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { mockDashboardConfig, mockDscStatus, mockK8sResourceList } from '~/__mocks__';
import {
mockDashboardConfig,
mockDscStatus,
mockK8sResourceList,
mockProjectK8sResource,
mockSecretK8sResource,
} from '~/__mocks__';
import { mockDsciStatus } from '~/__mocks__/mockDsciStatus';
import { StackCapability, StackComponent } from '~/concepts/areas/types';
import { ServiceModel } from '~/__tests__/cypress/cypress/utils/models';
import { ProjectModel, SecretModel, ServiceModel } from '~/__tests__/cypress/cypress/utils/models';
import {
FormFieldSelector,
registerModelPage,
Expand Down Expand Up @@ -42,7 +48,13 @@ const initIntercepts = () => {
requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ],
}),
);

cy.interceptK8sList(
ProjectModel,
mockK8sResourceList([
mockProjectK8sResource({ k8sName: 'test-project', displayName: 'Test Project' }),
mockProjectK8sResource({ k8sName: 'test-project-2', displayName: 'Test Project 2' }),
]),
);
cy.interceptK8sList(
ServiceModel,
mockK8sResourceList([
Expand Down Expand Up @@ -93,6 +105,67 @@ describe('Register model page', () => {
registerModelPage.visit();
});

it('Has Object storage autofill button if Object storage is selected', () => {
registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click();
registerModelPage
.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE)
.should('be.checked');
registerModelPage.findObjectStorageAutofillButton().should('be.visible');
});

it('Does not have Object storage autofill button if Object storage is not selected', () => {
registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_URI).click();
registerModelPage
.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE)
.should('not.be.checked');
registerModelPage.findObjectStorageAutofillButton().should('not.exist');
});

it('Can open Object storage autofill modal', () => {
registerModelPage.findConnectionAutofillModal().should('not.exist');
registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click();
registerModelPage.findObjectStorageAutofillButton().click();
registerModelPage.findConnectionAutofillModal().should('exist');
});

it('Project selection with no connections displays message stating no connections available', () => {
registerModelPage.findConnectionAutofillModal().should('not.exist');
registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click();
registerModelPage.findObjectStorageAutofillButton().click();
registerModelPage
.findConnectionSelector()
.contains('Select a project to view its available data connections');
registerModelPage.findProjectSelector().findDropdownItem('Test Project').click();
registerModelPage.findConnectionSelector().contains('No available data connections');
});

it('Project selection with connections displays connections and fills form', () => {
cy.interceptK8sList(
SecretModel,
mockK8sResourceList([mockSecretK8sResource({ s3Bucket: 'cmhvZHMtcHVibGlj' })]),
);
registerModelPage.findConnectionAutofillModal().should('not.exist');
registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click();
registerModelPage.findObjectStorageAutofillButton().click();
registerModelPage
.findConnectionSelector()
.contains('Select a project to view its available data connections');
registerModelPage.findProjectSelector().findDropdownItem('Test Project').click();
registerModelPage.findConnectionSelector().contains('Select data connection');
registerModelPage.findConnectionSelector().findDropdownItem('Test Secret').click();
registerModelPage.findAutofillButton().click();
registerModelPage.findConnectionAutofillModal().should('not.exist');
registerModelPage
.findFormField(FormFieldSelector.LOCATION_ENDPOINT)
.should('have.value', 'https://s3.amazonaws.com/');
registerModelPage
.findFormField(FormFieldSelector.LOCATION_BUCKET)
.should('have.value', 'rhods-public');
registerModelPage
.findFormField(FormFieldSelector.LOCATION_REGION)
.should('have.value', 'us-east-1');
});

it('Disables submit until required fields are filled in object storage mode', () => {
registerModelPage.findSubmitButton().should('be.disabled');
registerModelPage.findFormField(FormFieldSelector.MODEL_NAME).type('Test model name');
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/concepts/projects/ProjectSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ProjectSelectorProps = {
filterLabel?: string;
showTitle?: boolean;
selectorLabel?: string;
isFullWidth?: boolean;
};

const ProjectSelector: React.FC<ProjectSelectorProps> = ({
Expand All @@ -33,6 +34,7 @@ const ProjectSelector: React.FC<ProjectSelectorProps> = ({
filterLabel,
showTitle = false,
selectorLabel = 'Project',
isFullWidth = false,
}) => {
const { projects, updatePreferredProject } = React.useContext(ProjectsContext);
const selection = projects.find(byName(namespace));
Expand Down Expand Up @@ -60,6 +62,7 @@ const ProjectSelector: React.FC<ProjectSelectorProps> = ({
variant={primary ? 'primary' : undefined}
onClick={() => setDropdownOpen(!dropdownOpen)}
isExpanded={dropdownOpen}
isFullWidth={isFullWidth}
data-testid="project-selector-dropdown"
>
{toggleLabel}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
Menu,
MenuContent,
MenuItem,
Dropdown,
MenuList,
MenuToggle,
Spinner,
} from '@patternfly/react-core';
import React from 'react';
import { DataConnection } from '~/pages/projects/types';
import { getDataConnectionDisplayName } from '~/pages/projects/screens/detail/data-connections/utils';
// TODO: temporarily importing across pages so not to interfere with ongoing dataconnection work
import useDataConnections from '~/pages/projects/screens/detail/data-connections/useDataConnections';

type ConnectionDropdownProps = {
onSelect: (connectionInfo: DataConnection) => void;
project?: string;
selectedConnection?: DataConnection;
};
export const ConnectionDropdown = ({
onSelect,
project,
selectedConnection,
}: ConnectionDropdownProps): React.JSX.Element => {
const [isOpen, setIsOpen] = React.useState(false);
const [connections, connectionsLoaded, connectionsLoadError] = useDataConnections(project);

const onToggle = () => {
setIsOpen(!isOpen);
};

const filteredConnections = connections.filter((c) => c.data.data?.AWS_S3_BUCKET);

const getToggleContent = () => {
if (!project) {
return 'Select a project to view its available data connections';
}
if (connectionsLoadError) {
return 'Error loading connections';
}
if (!connectionsLoaded) {
return (
<>
<Spinner size="sm" /> Loading Data Connections for the selected project...
</>
);
}
if (!filteredConnections.length) {
return 'No available data connections';
}
if (selectedConnection) {
return getDataConnectionDisplayName(selectedConnection);
}
return 'Select data connection';
};

const onSelectConnection = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
option?: string | number | null,
) => {
setIsOpen(false);
if (typeof option === 'string') {
const value = connections.find((d) => d.data.metadata.name === option);
if (!value) {
return;
}
onSelect(value);
}
};
return (
<Dropdown
onOpenChange={(isOpened) => setIsOpen(isOpened)}
toggle={(toggleRef) => (
<MenuToggle
isDisabled={!filteredConnections.length}
isFullWidth
data-testid="select-data-connection"
ref={toggleRef}
onClick={onToggle}
isExpanded={isOpen}
>
{getToggleContent()}
</MenuToggle>
)}
isOpen={isOpen}
>
<Menu onSelect={onSelectConnection} isScrollable isPlain>
<MenuContent>
<MenuList>
{filteredConnections.map((dataItem) => (
<MenuItem key={dataItem.data.metadata.name} itemId={dataItem.data.metadata.name}>
{getDataConnectionDisplayName(dataItem)}
</MenuItem>
))}
</MenuList>
</MenuContent>
</Menu>
</Dropdown>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { Modal, Button, FormGroup, HelperText, Form, FormHelperText } from '@patternfly/react-core';
import ProjectSelector from '~/concepts/projects/ProjectSelector';
import { DataConnection } from '~/pages/projects/types';
import { ConnectionDropdown } from './ConnectionDropdown';

export const ConnectionModal: React.FC<{
isOpen: boolean;
onClose: () => void;
onSubmit: (connection: DataConnection) => void;
}> = ({ isOpen = false, onClose, onSubmit }) => {
const [project, setProject] = React.useState<string | undefined>(undefined);
const [connection, setConnection] = React.useState<DataConnection | undefined>(undefined);

return (
<Modal
isOpen={isOpen}
data-testid="connection-autofill-modal"
variant="medium"
title="Autofill from data connection"
description="Select a project to list its object storage data connections. Select a data connection to autofill the model location."
onClose={() => {
setProject(undefined);
setConnection(undefined);
onClose();
}}
actions={[
<Button
isDisabled={!connection}
data-testid="autofill-modal-button"
key="confirm"
onClick={() => {
if (connection) {
onSubmit(connection);
}
}}
>
Autofill
</Button>,
<Button key="cancel" variant="link" onClick={onClose}>
Cancel
</Button>,
]}
>
<Form>
<FormGroup label="Project" isRequired fieldId="autofillProject">
<ProjectSelector
isFullWidth
onSelection={(projectName: string) => {
setProject(projectName);
setConnection(undefined);
}}
namespace={project || ''}
invalidDropdownPlaceholder="Select project"
/>
</FormGroup>
<FormGroup label="Data connection name" isRequired fieldId="autofillConnection">
<ConnectionDropdown
onSelect={setConnection}
selectedConnection={connection}
project={project}
/>
<FormHelperText>
<HelperText>
Data connection list includes only object storage types that contain a bucket.
</HelperText>
</FormHelperText>
</FormGroup>
</Form>
</Modal>
);
};
Loading

0 comments on commit e454189

Please sign in to comment.