Skip to content

Commit

Permalink
Duplicate connection page UI
Browse files Browse the repository at this point in the history
  • Loading branch information
emilys314 authored and jeff-phillips-18 committed Aug 13, 2024
1 parent e42fdcd commit 78d616b
Show file tree
Hide file tree
Showing 17 changed files with 670 additions and 4 deletions.
73 changes: 73 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { TableRow } from './components/table';

class CreateConnectionTypeTableRow extends TableRow {
findSectionHeading() {
return this.find().findByTestId('section-heading');
}

findName() {
return this.find().findByTestId('field-name');
}

findType() {
return this.find().findByTestId('field-type');
}

findDefault() {
return this.find().findByTestId('field-default');
}

findEnvVar() {
return this.find().findByTestId('field-env');
}

findRequired() {
return this.find().findByTestId('field-required');
}
}

class CreateConnectionTypePage {
visitCreatePage() {
cy.visitWithLogin('/connectionTypes/create');
cy.findAllByText('Create connection type').should('exist');
}

visitDuplicatePage(name = 'existing') {
cy.visitWithLogin(`/connectionTypes/duplicate/${name}`);
cy.findAllByText('Create connection type').should('exist');
}

findConnectionTypeName() {
return cy.findByTestId('connection-type-name');
}

findConnectionTypeDesc() {
return cy.findByTestId('connection-type-description');
}

findConnectionTypeEnable() {
return cy.findByTestId('connection-type-enable');
}

findConnectionTypePreviewToggle() {
return cy.findByTestId('preview-drawer-toggle-button');
}

findFieldsTable() {
return cy.findByTestId('connection-type-fields-table');
}

findAllFieldsTableRows() {
return this.findFieldsTable().findAllByTestId('row');
}

getFieldsTableRow(index: number) {
return new CreateConnectionTypeTableRow(() => this.findAllFieldsTableRows().eq(index));
}

findSubmitButton() {
return cy.findByTestId('submit-button');
}
}

export const createConnectionTypePage = new CreateConnectionTypePage();
19 changes: 19 additions & 0 deletions frontend/src/__tests__/cypress/cypress/support/commands/odh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import type {
} from '~/concepts/pipelines/kfTypes';
import type { GrpcResponse } from '~/__mocks__/mlmd/utils';
import type { BuildMockPipelinveVersionsType } from '~/__mocks__';
import type { ConnectionTypeConfigMap } from '~/concepts/connectionTypes/types';

type SuccessErrorResponse = {
success: boolean;
Expand Down Expand Up @@ -581,6 +582,24 @@ declare global {
path: { namespace: string };
},
response: OdhResponse<number>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/connection-types',
response: ConnectionTypeConfigMap[],
) => Cypress.Chainable<null>) &
((
type: 'PATCH /api/connection-types/:name',
options: {
path: { name: string };
},
response: { success: boolean; error: string },
) => Cypress.Chainable<null>) &
((
type: 'GET /api/connection-types/:name',
options: {
path: { name: string };
},
response: ConnectionTypeConfigMap,
) => Cypress.Chainable<null>);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
mockConnectionTypeConfigMap,
mockConnectionTypeConfigMapObj,
} from '~/__mocks__/mockConnectionType';
import { createConnectionTypePage } from '~/__tests__/cypress/cypress/pages/connectionTypes';
import { asClusterAdminUser } from '~/__tests__/cypress/cypress/utils/mockUsers';

describe('create', () => {
it('Display base page', () => {
asClusterAdminUser();
createConnectionTypePage.visitCreatePage();

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

it('Allows create button with valid name', () => {
asClusterAdminUser();
createConnectionTypePage.visitCreatePage();

createConnectionTypePage.findConnectionTypeName().should('have.value', '');
createConnectionTypePage.findSubmitButton().should('be.disabled');

createConnectionTypePage.findConnectionTypeName().type('hello');
createConnectionTypePage.findSubmitButton().should('be.enabled');
});
});

describe('duplicate', () => {
const existing = mockConnectionTypeConfigMapObj({ name: 'existing' });

beforeEach(() => {
asClusterAdminUser();
cy.interceptOdh(
'GET /api/connection-types/:name',
{ path: { name: 'existing' } },
mockConnectionTypeConfigMap({ name: 'existing' }),
);
});

it('Prefill details from existing connection', () => {
createConnectionTypePage.visitDuplicatePage('existing');

createConnectionTypePage
.findConnectionTypeName()
.should(
'have.value',
`Duplicate of ${existing.metadata.annotations['openshift.io/display-name']}`,
);
createConnectionTypePage
.findConnectionTypeDesc()
.should('have.value', existing.metadata.annotations['openshift.io/description']);
createConnectionTypePage.findConnectionTypeEnable().should('be.checked');
});

it('Prefill fields table from existing connection', () => {
createConnectionTypePage.visitDuplicatePage('existing');

createConnectionTypePage
.findAllFieldsTableRows()
.should('have.length', existing.data?.fields?.length);

// Row 0 - Section
const row0 = createConnectionTypePage.getFieldsTableRow(0);
row0.findName().should('contain.text', 'Short text');
row0.findSectionHeading().should('exist');

// Row 1 - Short text field
const row1 = createConnectionTypePage.getFieldsTableRow(1);
row1.findName().should('contain.text', 'Short text 1');
row1.findType().should('have.text', 'Short text');
row1.findDefault().should('have.text', '-');
row1.findRequired().not('be.checked');

// Row 2 - Short text field
const row2 = createConnectionTypePage.getFieldsTableRow(2);
row2.findName().should('contain.text', 'Short text 2');
row2.findType().should('have.text', 'Short text');
row2.findDefault().should('have.text', 'This is the default value');
row2.findRequired().should('be.checked');
});
});
2 changes: 2 additions & 0 deletions frontend/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils';
import { SupportedArea } from '~/concepts/areas';
import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable';
import ModelRegistrySettingsRoutes from '~/pages/modelRegistrySettings/ModelRegistrySettingsRoutes';
import ConnectionTypeRoutes from '~/pages/connectionTypes/ConnectionTypeRoutes';

const HomePage = React.lazy(() => import('../pages/home/Home'));

Expand Down Expand Up @@ -125,6 +126,7 @@ const AppRoutes: React.FC = () => {
<Route path="/servingRuntimes/*" element={<CustomServingRuntimeRoutes />} />
<Route path="/modelRegistrySettings/*" element={<ModelRegistrySettingsRoutes />} />
<Route path="/groupSettings" element={<GroupSettingsPage />} />
<Route path="/connectionTypes/*" element={<ConnectionTypeRoutes />} />
</>
)}

Expand Down
26 changes: 25 additions & 1 deletion frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { mockConnectionTypeConfigMapObj } from '~/__mocks__/mockConnectionType';
import { DropdownField, HiddenField, TextField } from '~/concepts/connectionTypes/types';
import { DropdownField, HiddenField, TextField, UriField } from '~/concepts/connectionTypes/types';
import {
defaultValueToString,
fieldTypeToString,
toConnectionTypeConfigMap,
toConnectionTypeConfigMapObj,
} from '~/concepts/connectionTypes/utils';
Expand Down Expand Up @@ -201,3 +202,26 @@ describe('defaultValueToString', () => {
).toBe('Two, Three');
});
});

describe('fieldTypeToString', () => {
it('should return default value as string', () => {
expect(
fieldTypeToString({
type: 'text',
name: 'test',
envVar: 'test',
properties: {},
} satisfies TextField),
).toBe('Text');
expect(
fieldTypeToString({
type: 'uri',
name: 'test',
envVar: 'test',
properties: {
defaultValue: '',
},
} satisfies UriField),
).toBe('URI');
});
});
27 changes: 27 additions & 0 deletions frontend/src/concepts/connectionTypes/useConnectionType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types';
import { fetchConnectionType } from '~/services/connectionTypesService';

export const useConnectionType = (
name?: string,
): [boolean, Error | undefined, ConnectionTypeConfigMapObj | undefined] => {
const [loaded, setLoaded] = React.useState(false);
const [error, setError] = React.useState<Error>();
const [connectionType, setConnectionType] = React.useState<ConnectionTypeConfigMapObj>();

React.useEffect(() => {
if (name) {
fetchConnectionType(name)
.then((res) => {
setLoaded(true);
setConnectionType(res);
})
.catch((err) => {
setLoaded(true);
setError(err);
});
}
}, [name]);

return [loaded, error, connectionType];
};
11 changes: 11 additions & 0 deletions frontend/src/concepts/connectionTypes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ export const defaultValueToString = <T extends ConnectionTypeDataField>(
}
return defaultValue == null ? defaultValue : `${defaultValue}`;
};

export const fieldTypeToString = <T extends ConnectionTypeDataField>(field: T): string => {
if (field.type === ConnectionTypeFieldType.URI) {
return field.type.toUpperCase();
}

const withSpaces = field.type.replace(/-/g, ' ');
const withCapitalized = withSpaces[0].toUpperCase() + withSpaces.slice(1);

return withCapitalized;
};
12 changes: 9 additions & 3 deletions frontend/src/concepts/k8s/NameDescriptionField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import { isValidK8sName, translateDisplayNameForK8s } from '~/concepts/k8s/utils

type NameDescriptionFieldProps = {
nameFieldId: string;
nameFieldLabel?: string;
descriptionFieldId: string;
descriptionFieldLabel?: string;
data: NameDescType;
setData?: (data: NameDescType) => void;
autoFocusName?: boolean;
K8sLabelName?: string;
showK8sName?: boolean;
disableK8sName?: boolean;
maxLength?: number;
Expand All @@ -29,10 +32,13 @@ type NameDescriptionFieldProps = {

const NameDescriptionField: React.FC<NameDescriptionFieldProps> = ({
nameFieldId,
nameFieldLabel = 'Name',
descriptionFieldId,
descriptionFieldLabel = 'Description',
data,
setData,
autoFocusName,
K8sLabelName = 'Resource name',
showK8sName,
disableK8sName,
maxLength,
Expand All @@ -58,7 +64,7 @@ const NameDescriptionField: React.FC<NameDescriptionFieldProps> = ({
return (
<Stack hasGutter>
<StackItem>
<FormGroup label="Name" isRequired fieldId={nameFieldId}>
<FormGroup label={nameFieldLabel} isRequired fieldId={nameFieldId}>
<TextInput
aria-readonly={!setData}
isRequired
Expand Down Expand Up @@ -90,7 +96,7 @@ const NameDescriptionField: React.FC<NameDescriptionFieldProps> = ({
{showK8sName && (
<StackItem>
<FormGroup
label="Resource name"
label={K8sLabelName}
labelIcon={
<Tooltip
position="right"
Expand Down Expand Up @@ -145,7 +151,7 @@ const NameDescriptionField: React.FC<NameDescriptionFieldProps> = ({
</StackItem>
)}
<StackItem>
<FormGroup label="Description" fieldId={descriptionFieldId}>
<FormGroup label={descriptionFieldLabel} fieldId={descriptionFieldId}>
<TextArea
aria-readonly={!setData}
resizeOrientation="vertical"
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/pages/connectionTypes/ConnectionTypeRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { Navigate, Routes, Route } from 'react-router-dom';
import { CreateConnectionTypePage } from './CreateConnectionType/CreateConnectionTypePage';
import { DuplicateConnectionTypePage } from './CreateConnectionType/DuplicateConnectionTypePage';
import { EditConnectionTypePage } from './CreateConnectionType/EditConnectionTypePage';

const ConnectionTypeRoutes: React.FC = () => (
<Routes>
<Route path="/">
<Route path="create" element={<CreateConnectionTypePage />} />
<Route path="duplicate/:name" element={<DuplicateConnectionTypePage />} />
<Route path="edit/:name" element={<EditConnectionTypePage />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</Routes>
);

export default ConnectionTypeRoutes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core';

export const CreateConnectionTypeBreadcrumbs: React.FunctionComponent = () => (
<Breadcrumb ouiaId="BasicBreadcrumb">
<BreadcrumbItem to="/connectionTypes">Connection types</BreadcrumbItem>
<BreadcrumbItem isActive>Create connection type</BreadcrumbItem>
</Breadcrumb>
);
Loading

0 comments on commit 78d616b

Please sign in to comment.