Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RHOAIENG-10318] Add connection type field modal #3082

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions frontend/src/concepts/connectionTypes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ type Field<T extends ConnectionTypeFieldType | string> = {
description?: string;
};

export type ConnectionTypeCommonProperties<V = string> = {
defaultValue?: V;
defaultReadOnly?: boolean;
};

// P default to an empty set of properties
// eslint-disable-next-line @typescript-eslint/ban-types
type DataField<T extends ConnectionTypeFieldType | string, V = string, P = {}> = Field<T> & {
export type DataField<T extends ConnectionTypeFieldType | string, V = string, P = {}> = Field<T> & {
envVar: string;
required?: boolean;
properties: P & {
defaultValue?: V;
defaultReadOnly?: boolean;
};
properties: P & ConnectionTypeCommonProperties<V>;
};

export type SectionField = Field<ConnectionTypeFieldType.Section | 'section'>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import * as React from 'react';
import {
Checkbox,
Form,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
MenuToggle,
Modal,
Popover,
Select,
SelectList,
SelectOption,
TextArea,
TextInput,
ValidatedOptions,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter';
import {
ConnectionTypeCommonProperties,
ConnectionTypeDataField,
ConnectionTypeFieldType,
} from '~/concepts/connectionTypes/types';
import { TextForm } from '~/pages/connectionTypes/fields/TextForm';
import { isEnumMember } from '~/utilities/utils';
import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton';

const ENV_VAR_NAME_REGEX = new RegExp('^[-._a-zA-Z][-._a-zA-Z0-9]*$');

const isConnectionTypeFieldType = (
fieldType: string | number | undefined,
): fieldType is ConnectionTypeFieldType =>
isEnumMember(fieldType?.toString(), ConnectionTypeFieldType);

interface ConnectionTypeFieldModalProps {
field?: ConnectionTypeDataField;
isOpen?: boolean;
onClose: () => void;
onSubmit: (field: ConnectionTypeDataField) => void;
isEdit?: boolean;
}

const fieldTypeLabels: { [key: string]: string } = {
[ConnectionTypeFieldType.Boolean]: 'Boolean',
[ConnectionTypeFieldType.Dropdown]: 'Short text',
[ConnectionTypeFieldType.File]: 'File',
[ConnectionTypeFieldType.Hidden]: 'Hidden',
[ConnectionTypeFieldType.Numeric]: 'Numeric',
[ConnectionTypeFieldType.ShortText]: 'Short text',
[ConnectionTypeFieldType.Text]: 'Text',
[ConnectionTypeFieldType.URI]: 'URI',
};

const validateForType = (value: string, fieldType: ConnectionTypeFieldType): ValidatedOptions => {
switch (fieldType) {
default:
return ValidatedOptions.default;
}
};

export const ConnectionTypeDataFieldModal: React.FC<ConnectionTypeFieldModalProps> = ({
field,
isOpen,
onClose,
onSubmit,
isEdit,
}) => {
const [name, setName] = React.useState<string>(field?.name || '');
const [description, setDescription] = React.useState<string | undefined>(field?.description);
const [envVar, setEnvVar] = React.useState<string>(field?.envVar || '');
const [fieldType, setFieldType] = React.useState<ConnectionTypeFieldType>(
ConnectionTypeFieldType.ShortText,
);
const [required, setRequired] = React.useState<boolean | undefined>(field?.required);
const [isTypeSelectOpen, setIsTypeSelectOpen] = React.useState<boolean>(false);
const [textProperties, setTextProperties] = React.useState<ConnectionTypeCommonProperties>(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-explicit-any
(field?.properties as any) || {},
);

const envVarValidation =
!envVar || ENV_VAR_NAME_REGEX.test(envVar) ? ValidatedOptions.default : ValidatedOptions.error;
const valid = React.useMemo(
() => !!name && !!envVar && envVarValidation === ValidatedOptions.default,
[envVar, envVarValidation, name],
);

const handleSubmit = () => {
switch (fieldType) {
case ConnectionTypeFieldType.Hidden:
case ConnectionTypeFieldType.File:
case ConnectionTypeFieldType.ShortText:
case ConnectionTypeFieldType.Text:
case ConnectionTypeFieldType.URI:
onSubmit({
name,
description,
envVar,
type: fieldType,
properties: {
defaultValue: textProperties.defaultValue,
defaultReadOnly: textProperties.defaultValue
? textProperties.defaultReadOnly
: undefined,
},
required,
});
}
onClose();
};

const fieldTypeForm = React.useMemo(() => {
switch (fieldType) {
case ConnectionTypeFieldType.Hidden:
case ConnectionTypeFieldType.File:
case ConnectionTypeFieldType.ShortText:
case ConnectionTypeFieldType.Text:
return (
<TextForm
properties={textProperties}
onChange={(updatedProperties) => setTextProperties(updatedProperties)}
validate={(value) => validateForType(value, fieldType)}
/>
);
}
return null;
}, [fieldType, textProperties]);

return (
<Modal
isOpen
variant="medium"
title={isEdit ? 'Edit field' : 'Add field'}
onClose={onClose}
footer={
<DashboardModalFooter
onCancel={onClose}
onSubmit={handleSubmit}
submitLabel={isEdit ? 'Edit' : 'Add'}
isSubmitDisabled={!valid}
alertTitle="Error"
/>
}
data-testid="archive-model-version-modal"
>
<Form>
<FormGroup fieldId="name" label="Field name" isRequired>
<TextInput
id="name"
value={name}
onChange={(_ev, value) => setName(value)}
data-testid="field-name-input"
/>
</FormGroup>
<FormGroup
fieldId="description"
label="Field description"
labelIcon={
<Popover
aria-label="field description help"
headerContent="Field description"
bodyContent="Use the field description to provide users in your organization with additional information about a field, or instructions for completing the field. Your input will appear in a popover, like this one."
>
<DashboardPopupIconButton
icon={<OutlinedQuestionCircleIcon />}
aria-label="More info for section heading"
/>
</Popover>
}
>
<TextArea
id="description"
data-testid="field-description-input"
value={description}
onChange={(_ev, value) => setDescription(value)}
/>
</FormGroup>
<FormGroup
fieldId="envVar"
label="Environment variable"
labelIcon={
<Popover
aria-label="environment variable help"
headerContent="Environment variable"
bodyContent="Environment variables grant you access to the value provided when attaching the connection to your workbench."
>
<DashboardPopupIconButton
icon={<OutlinedQuestionCircleIcon />}
aria-label="More info for section heading"
/>
</Popover>
}
isRequired
>
<TextInput
id="envVar"
value={envVar}
onChange={(_ev, value) => setEnvVar(value)}
data-testid="field-env-var-input"
validated={envVarValidation}
/>
{envVarValidation === ValidatedOptions.error ? (
<FormHelperText>
<HelperText>
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
{`Invalid variable name. The name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit.`}
</HelperTextItem>
</HelperText>
</FormHelperText>
) : null}
</FormGroup>
<FormGroup
fieldId="fieldType"
label="Field type"
isRequired
data-testid="field-type-select"
>
<Select
id="fieldType"
isOpen={isTypeSelectOpen}
shouldFocusToggleOnSelect
selected={fieldType}
onSelect={(_e, selection) => {
if (isConnectionTypeFieldType(selection)) {
setFieldType(selection);
setIsTypeSelectOpen(false);
}
}}
onOpenChange={(open) => setIsTypeSelectOpen(open)}
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
id="type-select"
isFullWidth
onClick={() => {
setIsTypeSelectOpen((open) => !open);
}}
isExpanded={isOpen}
>
{fieldTypeLabels[fieldType]}
</MenuToggle>
)}
>
<SelectList>
<SelectOption
value={ConnectionTypeFieldType.ShortText}
data-testid="field-short-text-select"
>
{fieldTypeLabels[ConnectionTypeFieldType.ShortText]}
</SelectOption>
<SelectOption
value={ConnectionTypeFieldType.Hidden}
data-testid="field-hidden-select"
>
{fieldTypeLabels[ConnectionTypeFieldType.Hidden]}
</SelectOption>
</SelectList>
</Select>
</FormGroup>
{fieldTypeForm}
<FormGroup fieldId="isRequired">
<Checkbox
id="isRequired"
data-testid="field-required-checkbox"
label="Field is required"
isChecked={required || false}
onChange={(_ev, checked) => {
setRequired(checked);
}}
/>
</FormGroup>
</Form>
</Modal>
);
};
37 changes: 37 additions & 0 deletions frontend/src/pages/connectionTypes/fields/TextForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import { Checkbox, FormGroup, TextInput, ValidatedOptions } from '@patternfly/react-core';
import { ConnectionTypeCommonProperties } from '~/concepts/connectionTypes/types';

type TextFormProps = {
properties: ConnectionTypeCommonProperties;
onChange: (newProperties: ConnectionTypeCommonProperties) => void;
validate: (value: string) => ValidatedOptions;
};

export const TextForm: React.FC<TextFormProps> = ({ properties, onChange, validate }) => (
<>
<FormGroup fieldId="defaultValue" label="Default value">
<TextInput
id="defaultValue"
value={properties.defaultValue || ''}
onChange={(_ev, value) =>
onChange({ defaultValue: value, defaultReadOnly: properties.defaultReadOnly })
}
validated={
properties.defaultValue ? validate(properties.defaultValue) : ValidatedOptions.default
}
data-testid="field-default-value-input"
/>
<Checkbox
id="defaultReadOnly"
label="Default value is read-only"
isDisabled={!properties.defaultValue}
isChecked={(properties.defaultValue && properties.defaultReadOnly) || false}
onChange={(_ev, checked) =>
onChange({ defaultValue: properties.defaultValue, defaultReadOnly: checked })
}
data-testid="field-default-value-readonly-checkbox"
/>
</FormGroup>
</>
);
Loading
Loading