diff --git a/frontend/src/__mocks__/mockConnectionType.ts b/frontend/src/__mocks__/mockConnectionType.ts index 988f9ae23e..e713e10fc7 100644 --- a/frontend/src/__mocks__/mockConnectionType.ts +++ b/frontend/src/__mocks__/mockConnectionType.ts @@ -14,6 +14,7 @@ type MockConnectionTypeConfigMap = { username?: string; preInstalled?: boolean; fields?: ConnectionTypeField[]; + category?: string[]; }; export const mockConnectionTypeConfigMap = ( @@ -60,6 +61,7 @@ export const mockConnectionTypeConfigMapObj = ({ : undefined), }, data: { + category: 'category' in rest ? rest.category : ['Database', 'Testing'], fields: 'fields' in rest ? rest.fields : mockFields, }, }); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts index 1a1bef53c2..b4c2495048 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts @@ -89,14 +89,14 @@ describe('duplicate', () => { // 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.findType().should('have.text', 'Text - Short'); 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.findType().should('have.text', 'Text - Short'); row2.findDefault().should('have.text', 'This is the default value'); row2.findRequired().should('be.checked'); }); diff --git a/frontend/src/concepts/connectionTypes/CategoryLabel.tsx b/frontend/src/concepts/connectionTypes/CategoryLabel.tsx new file mode 100644 index 0000000000..1ec197c37d --- /dev/null +++ b/frontend/src/concepts/connectionTypes/CategoryLabel.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { Label } from '@patternfly/react-core'; + +type Props = { + category: string; +}; + +const CategoryLabel: React.FC = ({ category }) => ; + +export default CategoryLabel; diff --git a/frontend/src/concepts/connectionTypes/ConnectionTypePreview.tsx b/frontend/src/concepts/connectionTypes/ConnectionTypePreview.tsx index c189395872..dd65e03a20 100644 --- a/frontend/src/concepts/connectionTypes/ConnectionTypePreview.tsx +++ b/frontend/src/concepts/connectionTypes/ConnectionTypePreview.tsx @@ -1,44 +1,113 @@ import * as React from 'react'; -import { Divider, Form, FormGroup, FormSection, Title } from '@patternfly/react-core'; -import FormGroupText from '~/components/FormGroupText'; +import { + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Form, + FormGroup, + FormSection, + HelperText, + HelperTextItem, + LabelGroup, + MenuToggleStatus, + Popover, + Title, +} from '@patternfly/react-core'; import ConnectionTypeFormFields from '~/concepts/connectionTypes/fields/ConnectionTypeFormFields'; import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; import NameDescriptionField from '~/concepts/k8s/NameDescriptionField'; -import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +import { getDescriptionFromK8sResource } from '~/concepts/k8s/utils'; import UnspecifiedValue from '~/concepts/connectionTypes/fields/UnspecifiedValue'; +import SimpleSelect from '~/components/SimpleSelect'; +import CategoryLabel from '~/concepts/connectionTypes/CategoryLabel'; +import TruncatedText from '~/components/TruncatedText'; type Props = { obj?: ConnectionTypeConfigMapObj; }; -const ConnectionTypePreview: React.FC = ({ obj }) => ( -
- Add connection - - - - {(obj && getDisplayNameFromK8sResource(obj)) || } - +// TODO consider refactoring this form for reuse when creating connection type instances +const ConnectionTypePreview: React.FC = ({ obj }) => { + const connectionTypeName = obj && obj.metadata.annotations?.['openshift.io/display-name']; + const connectionTypeDescription = (obj && getDescriptionFromK8sResource(obj)) ?? undefined; + return ( + + Add connection + + undefined} + /> + + {connectionTypeDescription ? ( + + + + ) : undefined} + + + + Connection type name + + {connectionTypeName || } + + + {connectionTypeDescription ? ( + + Connection type description + + {connectionTypeDescription || } + + + ) : undefined} + + Category + + {obj?.data?.category?.length ? ( + + {obj.data.category.map((category) => ( + + ))} + + ) : ( + + )} + + + + } + > + + + + - - - {(obj && getDescriptionFromK8sResource(obj)) || } - - - - - - - - - -); + + + + + + ); +}; export default ConnectionTypePreview; diff --git a/frontend/src/concepts/connectionTypes/ConnectionTypePreviewDrawer.tsx b/frontend/src/concepts/connectionTypes/ConnectionTypePreviewDrawer.tsx index 38d00e074b..b2f990ce7e 100644 --- a/frontend/src/concepts/connectionTypes/ConnectionTypePreviewDrawer.tsx +++ b/frontend/src/concepts/connectionTypes/ConnectionTypePreviewDrawer.tsx @@ -7,10 +7,10 @@ import { DrawerCloseButton, DrawerContent, Title, - DrawerPanelBody, Card, CardBody, - Text, + Divider, + DrawerContentBody, } from '@patternfly/react-core'; import ConnectionTypePreview from '~/concepts/connectionTypes/ConnectionTypePreview'; import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; @@ -24,44 +24,39 @@ type Props = { const ConnectionTypePreviewDrawer: React.FC = ({ children, isExpanded, onClose, obj }) => { const panelContent = ( - - - - Preview connection type - - - onClose()} /> - - - - + + + + + Preview connection type + + + onClose()} /> + +
- - This preview shows the user view of the connection type form, and is for reference only. - Updates in the developer view are automatically in the user view. - - - - - - + This preview shows the user view of the connection type form, and is for reference only. + Updates in the developer view are automatically in the user view.
-
+ + + + + + + + +
); diff --git a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts index 5ff9f247dd..7be6062f76 100644 --- a/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts +++ b/frontend/src/concepts/connectionTypes/__tests__/utils.spec.ts @@ -1,5 +1,10 @@ import { mockConnectionTypeConfigMapObj } from '~/__mocks__/mockConnectionType'; -import { DropdownField, HiddenField, TextField, UriField } from '~/concepts/connectionTypes/types'; +import { + ConnectionTypeFieldType, + DropdownField, + HiddenField, + TextField, +} from '~/concepts/connectionTypes/types'; import { defaultValueToString, fieldNameToEnvVar, @@ -12,13 +17,15 @@ describe('toConnectionTypeConfigMap / toConnectionTypeConfigMapObj', () => { it('should serialize / deserialize connection type fields', () => { const ct = mockConnectionTypeConfigMapObj({}); const configMap = toConnectionTypeConfigMap(ct); + expect(typeof configMap.data?.category).toBe('string'); expect(typeof configMap.data?.fields).toBe('string'); expect(ct).toEqual(toConnectionTypeConfigMapObj(toConnectionTypeConfigMap(ct))); }); it('should serialize / deserialize connection type with missing fields', () => { - const ct = mockConnectionTypeConfigMapObj({ fields: undefined }); + const ct = mockConnectionTypeConfigMapObj({ fields: undefined, category: undefined }); const configMap = toConnectionTypeConfigMap(ct); + expect(configMap.data?.category).toBeUndefined(); expect(configMap.data?.fields).toBeUndefined(); expect(ct).toEqual(toConnectionTypeConfigMapObj(configMap)); }); @@ -83,7 +90,7 @@ describe('defaultValueToString', () => { defaultValue: 'test value', }, } satisfies HiddenField), - ).toBe('••••••••••'); + ).toBe('test value'); }); it('should return single variant dropdown value as string', () => { @@ -133,7 +140,7 @@ describe('defaultValueToString', () => { defaultValue: ['2'], }, } satisfies DropdownField), - ).toBe('Two'); + ).toBe('Two (Value: 2)'); expect( defaultValueToString({ type: 'dropdown', @@ -150,7 +157,7 @@ describe('defaultValueToString', () => { defaultValue: ['2', '3'], }, } satisfies DropdownField), - ).toBe('Two'); + ).toBe('Two (Value: 2)'); }); it('should return multi variant dropdown value as string', () => { @@ -200,30 +207,22 @@ describe('defaultValueToString', () => { defaultValue: ['2', '3'], }, } satisfies DropdownField), - ).toBe('Two, Three'); + ).toBe('Two (Value: 2), Three (Value: 3)'); }); }); 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'); + expect(fieldTypeToString('text')).toBe('Text - Long'); + expect(fieldTypeToString(ConnectionTypeFieldType.Text)).toBe('Text - Long'); + expect(fieldTypeToString('short-text')).toBe('Text - Short'); + expect(fieldTypeToString(ConnectionTypeFieldType.ShortText)).toBe('Text - Short'); + expect(fieldTypeToString('hidden')).toBe('Text - Hidden'); + expect(fieldTypeToString(ConnectionTypeFieldType.Hidden)).toBe('Text - Hidden'); + expect(fieldTypeToString('uri')).toBe('URI'); + expect(fieldTypeToString(ConnectionTypeFieldType.URI)).toBe('URI'); + expect(fieldTypeToString('numeric')).toBe('Numeric'); + expect(fieldTypeToString(ConnectionTypeFieldType.Numeric)).toBe('Numeric'); }); }); diff --git a/frontend/src/concepts/connectionTypes/createConnectionTypeUtils.ts b/frontend/src/concepts/connectionTypes/createConnectionTypeUtils.ts index 06fd1320ed..5c55875c98 100644 --- a/frontend/src/concepts/connectionTypes/createConnectionTypeUtils.ts +++ b/frontend/src/concepts/connectionTypes/createConnectionTypeUtils.ts @@ -9,6 +9,7 @@ export const extractConnectionTypeFromMap = ( enabled: boolean; username: string; fields: ConnectionTypeField[]; + category: string[]; } => ({ k8sName: configMap?.metadata.name ?? '', name: configMap?.metadata.annotations?.['openshift.io/display-name'] ?? '', @@ -16,6 +17,7 @@ export const extractConnectionTypeFromMap = ( enabled: configMap?.metadata.annotations?.['opendatahub.io/enabled'] === 'true', username: configMap?.metadata.annotations?.['opendatahub.io/username'] ?? '', fields: configMap?.data?.fields ?? [], + category: configMap?.data?.category ?? [], }); export const createConnectionTypeObj = ( @@ -25,6 +27,7 @@ export const createConnectionTypeObj = ( enabled: boolean, username: string, fields: ConnectionTypeField[], + category: string[], ): ConnectionTypeConfigMapObj => ({ kind: 'ConfigMap', apiVersion: 'v1', @@ -39,6 +42,7 @@ export const createConnectionTypeObj = ( labels: { 'opendatahub.io/dashboard': 'true', 'opendatahub.io/connection-type': 'true' }, }, data: { + category, fields, }, }); diff --git a/frontend/src/concepts/connectionTypes/fields/BooleanFormField.tsx b/frontend/src/concepts/connectionTypes/fields/BooleanFormField.tsx index 28875c0ace..c2b7a1210a 100644 --- a/frontend/src/concepts/connectionTypes/fields/BooleanFormField.tsx +++ b/frontend/src/concepts/connectionTypes/fields/BooleanFormField.tsx @@ -12,13 +12,23 @@ const BooleanFormField: React.FC> = ({ '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 ( = ({ fields, isPreview, onChange {renderDataFields(fieldGroup.fields)} ) : ( - {renderDataFields(fieldGroup.fields)} + {renderDataFields(fieldGroup.fields)} ), )} ); }; -export default ConnectionTypeFormFields; +export default React.memo(ConnectionTypeFormFields); diff --git a/frontend/src/concepts/connectionTypes/fields/DataFormFieldGroup.tsx b/frontend/src/concepts/connectionTypes/fields/DataFormFieldGroup.tsx index 7ab2dd984e..022efaba0f 100644 --- a/frontend/src/concepts/connectionTypes/fields/DataFormFieldGroup.tsx +++ b/frontend/src/concepts/connectionTypes/fields/DataFormFieldGroup.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; -import { FormGroup, GenerateId } from '@patternfly/react-core'; +import { FormGroup, GenerateId, Popover } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { ConnectionTypeDataField } from '~/concepts/connectionTypes/types'; +import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; type Props = { field: ConnectionTypeDataField; @@ -16,6 +18,16 @@ const DataFormFieldGroup: React.FC = ({ field, children }): React.ReactNo data-testid={`field ${field.type} ${field.envVar}`} // do not mark read only fields as required isRequired={field.required && !field.properties.defaultReadOnly} + labelIcon={ + field.description ? ( + + } + aria-label="More info" + /> + + ) : undefined + } > {children(id)} diff --git a/frontend/src/concepts/connectionTypes/fields/DefaultValueTextRenderer.tsx b/frontend/src/concepts/connectionTypes/fields/DefaultValueTextRenderer.tsx index 7ffdc17d25..0800ef6e4e 100644 --- a/frontend/src/concepts/connectionTypes/fields/DefaultValueTextRenderer.tsx +++ b/frontend/src/concepts/connectionTypes/fields/DefaultValueTextRenderer.tsx @@ -10,11 +10,12 @@ type Props = { field: ConnectionTypeDataField; mode?: FieldMode; children: React.ReactNode; + component?: 'div' | 'pre'; }; -const DefaultValueTextRenderer: React.FC = ({ id, field, mode, children }) => +const DefaultValueTextRenderer: React.FC = ({ id, field, mode, children, component }) => mode !== 'default' && field.properties.defaultReadOnly ? ( - + {defaultValueToString(field) ?? (mode === 'preview' ? : '-')} ) : ( diff --git a/frontend/src/concepts/connectionTypes/fields/DropdownFormField.tsx b/frontend/src/concepts/connectionTypes/fields/DropdownFormField.tsx index c9e1de730f..f720dc985b 100644 --- a/frontend/src/concepts/connectionTypes/fields/DropdownFormField.tsx +++ b/frontend/src/concepts/connectionTypes/fields/DropdownFormField.tsx @@ -52,7 +52,7 @@ const DropdownFormField: React.FC> = ({ > {isMulti ? ( <> - Count{' '} + Select {field.name}{' '} {(isPreview ? field.properties.defaultValue?.length : value?.length) ?? 0}{' '} selected @@ -64,7 +64,7 @@ const DropdownFormField: React.FC> = ({ (i) => i.value === field.properties.defaultValue?.[0], )?.label : field.properties.items?.find((i) => value?.includes(i.value))?.label) || - 'Select a value' + `Select ${field.name}` )} )} @@ -76,6 +76,7 @@ const DropdownFormField: React.FC> = ({ key={i.value} hasCheckbox={isMulti} selected={selected?.includes(i.value)} + description={`Value: ${i.value}`} > {i.label} diff --git a/frontend/src/concepts/connectionTypes/fields/HiddenFormField.tsx b/frontend/src/concepts/connectionTypes/fields/HiddenFormField.tsx index 8e1ea1ce87..4b56519eb9 100644 --- a/frontend/src/concepts/connectionTypes/fields/HiddenFormField.tsx +++ b/frontend/src/concepts/connectionTypes/fields/HiddenFormField.tsx @@ -1,8 +1,11 @@ import * as React from 'react'; +import { EyeIcon } from '@patternfly/react-icons'; +import { Button, Flex, FlexItem, Popover } from '@patternfly/react-core'; import { HiddenField } from '~/concepts/connectionTypes/types'; import PasswordInput from '~/components/PasswordInput'; import { FieldProps } from '~/concepts/connectionTypes/fields/types'; -import DefaultValueTextRenderer from '~/concepts/connectionTypes/fields/DefaultValueTextRenderer'; +import FormGroupText from '~/components/FormGroupText'; +import UnspecifiedValue from '~/concepts/connectionTypes/fields/UnspecifiedValue'; const HiddenFormField: React.FC> = ({ id, @@ -13,21 +16,47 @@ const HiddenFormField: React.FC> = ({ 'data-testid': dataTestId, }) => { const isPreview = mode === 'preview'; - return ( - - onChange(v)} - /> - + return mode !== 'default' && field.properties.defaultReadOnly ? ( + + {field.properties.defaultValue ? ( + + + {field.properties.defaultValue.replace(/./g, '•')} + + + + + + + + ) : isPreview ? ( + + ) : ( + '-' + )} + + ) : ( + onChange(v)} + /> ); }; diff --git a/frontend/src/concepts/connectionTypes/fields/TextFormField.tsx b/frontend/src/concepts/connectionTypes/fields/TextFormField.tsx index d2d21921fe..8837edbd5e 100644 --- a/frontend/src/concepts/connectionTypes/fields/TextFormField.tsx +++ b/frontend/src/concepts/connectionTypes/fields/TextFormField.tsx @@ -7,7 +7,7 @@ import DefaultValueTextRenderer from '~/concepts/connectionTypes/fields/DefaultV const TextFormField: React.FC> = ({ id, field, mode, onChange, value }) => { const isPreview = mode === 'preview'; return ( - +