Skip to content

Commit

Permalink
surface conflicting env var names as warnings and errors
Browse files Browse the repository at this point in the history
  • Loading branch information
christianvogt committed Aug 26, 2024
1 parent a7ad26d commit e13871f
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 35 deletions.
4 changes: 4 additions & 0 deletions frontend/src/concepts/connectionTypes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export type ConnectionTypeField =

export type ConnectionTypeDataField = Exclude<ConnectionTypeField, SectionField>;

export const isConnectionTypeDataField = (
field: ConnectionTypeField,
): field is ConnectionTypeDataField => field.type !== ConnectionTypeFieldType.Section;

export type ConnectionTypeConfigMap = K8sResourceCommon & {
metadata: {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ import {
SelectOption,
TextArea,
TextInput,
ValidatedOptions,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import {
ExclamationCircleIcon,
OutlinedQuestionCircleIcon,
WarningTriangleIcon,
} from '@patternfly/react-icons';
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter';
import {
ConnectionTypeDataField,
connectionTypeDataFields,
ConnectionTypeField,
ConnectionTypeFieldType,
isConnectionTypeDataField,
} from '~/concepts/connectionTypes/types';
import { fieldNameToEnvVar } from '~/concepts/connectionTypes/utils';
import { fieldNameToEnvVar, fieldTypeToString } from '~/concepts/connectionTypes/utils';
import { isEnumMember } from '~/utilities/utils';
import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton';
import DataFieldPropertiesForm from '~/pages/connectionTypes/manage/DataFieldPropertiesForm';
import { fieldTypeToString } from '~/concepts/connectionTypes/utils';

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

Expand All @@ -42,6 +46,7 @@ type Props = {
onClose: () => void;
onSubmit: (field: ConnectionTypeDataField) => void;
isEdit?: boolean;
fields?: ConnectionTypeField[];
};

export const ConnectionTypeDataFieldModal: React.FC<Props> = ({
Expand All @@ -50,6 +55,7 @@ export const ConnectionTypeDataFieldModal: React.FC<Props> = ({
onClose,
onSubmit,
isEdit,
fields,

Check warning on line 58 in frontend/src/pages/connectionTypes/manage/ConnectionTypeDataFieldModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/connectionTypes/manage/ConnectionTypeDataFieldModal.tsx#L58

Added line #L58 was not covered by tests
}) => {
const [name, setName] = React.useState<string>(field?.name || '');
const [description, setDescription] = React.useState<string | undefined>(field?.description);
Expand All @@ -65,16 +71,16 @@ export const ConnectionTypeDataFieldModal: React.FC<Props> = ({
const [isTypeSelectOpen, setIsTypeSelectOpen] = React.useState<boolean>(false);
const [properties, setProperties] = React.useState<unknown>(field?.properties || {});
const [isPropertiesValid, setPropertiesValid] = React.useState(true);

const [autoGenerateEnvVar, setAutoGenerateEnvVar] = React.useState<boolean>(!envVar);
const envVarValidation =
!envVar || ENV_VAR_NAME_REGEX.test(envVar) ? ValidatedOptions.default : ValidatedOptions.error;
const isValid =
!!fieldType &&
isPropertiesValid &&
!!name &&
!!envVar &&
envVarValidation === ValidatedOptions.default;

const isEnvVarConflict = React.useMemo(
() => !!fields?.find((f) => f !== field && isConnectionTypeDataField(f) && f.envVar === envVar),
[fields, field, envVar],
);

const isEnvVarValid = !envVar || ENV_VAR_NAME_REGEX.test(envVar);

const isValid = !!fieldType && isPropertiesValid && !!name && !!envVar && isEnvVarValid;

const newField = fieldType
? // Cast from specific type to generic type
Expand Down Expand Up @@ -179,17 +185,24 @@ export const ConnectionTypeDataFieldModal: React.FC<Props> = ({
setEnvVar(value);
}}
data-testid="field-env-var-input"
validated={envVarValidation}
validated={!isEnvVarValid ? 'error' : isEnvVarConflict ? 'warning' : 'default'}
/>
{envVarValidation === ValidatedOptions.error ? (
<FormHelperText>
<FormHelperText>
{!isEnvVarValid ? (
<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}
) : undefined}
{isEnvVarConflict ? (
<HelperText data-testid="envvar-conflict-warning">
<HelperTextItem icon={<WarningTriangleIcon />} variant="warning">
This environment variable name is already being used for an existing field.
</HelperTextItem>
</HelperText>
) : undefined}
</FormHelperText>
</FormGroup>
<FormGroup
fieldId="fieldType"
Expand All @@ -204,6 +217,7 @@ export const ConnectionTypeDataFieldModal: React.FC<Props> = ({
selected={fieldType}
onSelect={(_e, selection) => {
if (isConnectionTypeFieldType(selection)) {
setPropertiesValid(true);
setProperties({});
setFieldType(selection);
setIsTypeSelectOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Props = {
onClose: () => void;
onSubmit: (field: ConnectionTypeField) => void;
isEdit?: boolean;
fields?: ConnectionTypeField[];
};

const ConnectionTypeFieldModal: React.FC<Props> = (props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
EmptyStateVariant,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { Table, Thead, Tbody, Tr, Th, ThProps } from '@patternfly/react-table';
import { Table, Thead, Tbody, Tr, Th } from '@patternfly/react-table';
import { ConnectionTypeField, ConnectionTypeFieldType } from '~/concepts/connectionTypes/types';
import useDraggableTableControlled from '~/utilities/useDraggableTableControlled';
import { columns } from '~/pages/connectionTypes/manage/fieldTableColumns';
import ConnectionTypeFieldModal from './ConnectionTypeFieldModal';
import ManageConnectionTypeFieldsTableRow from './ManageConnectionTypeFieldsTableRow';

Expand Down Expand Up @@ -51,14 +52,6 @@ type Props = {
onFieldsChange: (fields: ConnectionTypeField[]) => void;
};

const columns: ThProps[] = [
{ label: 'Section heading/field name', width: 30 },
{ label: 'Type', width: 10 },
{ label: 'Default value', width: 30 },
{ label: 'Environment variable', width: 20 },
{ label: 'Required', width: 10 },
];

const ManageConnectionTypeFieldsTable: React.FC<Props> = ({ fields, onFieldsChange }) => {
const [modalField, setModalField] = React.useState<
{ field?: ConnectionTypeField; index?: number; isEdit?: boolean } | undefined
Expand Down Expand Up @@ -88,9 +81,9 @@ const ManageConnectionTypeFieldsTable: React.FC<Props> = ({ fields, onFieldsChan
<Tbody {...tableProps.tbodyProps}>
{rowsToRender.map(({ data: row, rowProps }, index) => (
<ManageConnectionTypeFieldsTableRow
fields={fields}
key={index}
row={row}
columns={columns}
onEdit={() => {
setModalField({
field: row,
Expand Down Expand Up @@ -154,6 +147,7 @@ const ManageConnectionTypeFieldsTable: React.FC<Props> = ({ fields, onFieldsChan
)}
{modalField ? (
<ConnectionTypeFieldModal
fields={fields}
field={modalField.field}
isEdit={modalField.isEdit}
onClose={() => setModalField(undefined)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import * as React from 'react';
import { ActionsColumn, Td, ThProps, Tr } from '@patternfly/react-table';
import { Button, Label, Switch } from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { ActionsColumn, Td, Tr } from '@patternfly/react-table';
import { Button, Icon, Label, Switch } from '@patternfly/react-core';
import {
ConnectionTypeField,
ConnectionTypeFieldType,
SectionField,
isConnectionTypeDataField,
} from '~/concepts/connectionTypes/types';
import { defaultValueToString, fieldTypeToString } from '~/concepts/connectionTypes/utils';
import type { RowProps } from '~/utilities/useDraggableTableControlled';
import TruncatedText from '~/components/TruncatedText';
import { columns } from '~/pages/connectionTypes/manage/fieldTableColumns';

type Props = {
fields: ConnectionTypeField[];
row: ConnectionTypeField;
columns: ThProps[];
onEdit: () => void;
onDelete: () => void;
onDuplicate: (field: ConnectionTypeField) => void;
Expand All @@ -21,15 +24,25 @@ type Props = {
} & RowProps;

const ManageConnectionTypeFieldsTableRow: React.FC<Props> = ({
fields,
row,
columns,
onEdit,
onDelete,
onDuplicate,
onAddField,
onChange,
...props
}) => {
const isEnvVarConflict = React.useMemo(
() =>
row.type === ConnectionTypeFieldType.Section
? false
: !!fields.find(
(f) => f !== row && isConnectionTypeDataField(f) && f.envVar === row.envVar,
),
[row, fields],
);

if (row.type === ConnectionTypeFieldType.Section) {
return (
<Tr draggable isStriped data-testid="row" {...props}>
Expand Down Expand Up @@ -98,6 +111,14 @@ const ManageConnectionTypeFieldsTableRow: React.FC<Props> = ({
</Td>
<Td dataLabel={columns[3].label} data-testid="field-env">
{row.envVar || '-'}
{isEnvVarConflict ? (
<>
<Icon status="danger" size="sm" className="pf-v5-u-ml-xs">
<ExclamationCircleIcon />
</Icon>
<span className="pf-v5-u-screen-reader">This environment variable is in conflict.</span>
</>
) : undefined}
</Td>
<Td dataLabel={columns[4].label}>
<Switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import {
} from '@patternfly/react-core';
import { useNavigate } from 'react-router';
import { OpenDrawerRightIcon } from '@patternfly/react-icons';
import { uniq } from 'lodash-es';
import { useUser } from '~/redux/selectors';
import NameDescriptionField from '~/concepts/k8s/NameDescriptionField';
import { ConnectionTypeConfigMapObj, ConnectionTypeField } from '~/concepts/connectionTypes/types';
import {
ConnectionTypeConfigMapObj,
ConnectionTypeField,
isConnectionTypeDataField,
} from '~/concepts/connectionTypes/types';
import ConnectionTypePreviewDrawer from '~/concepts/connectionTypes/ConnectionTypePreviewDrawer';
import {
createConnectionTypeObj,
Expand Down Expand Up @@ -73,10 +78,15 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
[connectionNameDesc, connectionEnabled, connectionFields, username, category],
);

const isEnvVarConflict = React.useMemo(() => {
const envVars = connectionFields.filter(isConnectionTypeDataField).map((f) => f.envVar);
return uniq(envVars).length !== envVars.length;
}, [connectionFields]);

const isValid = React.useMemo(() => {
const trimmedName = connectionNameDesc.name.trim();
return Boolean(trimmedName);
}, [connectionNameDesc.name]);
return Boolean(trimmedName) && !isEnvVarConflict;
}, [connectionNameDesc.name, isEnvVarConflict]);

const onCancel = () => {
navigate('/connectionTypes');
Expand Down Expand Up @@ -144,6 +154,12 @@ const ManageConnectionTypePage: React.FC<Props> = ({ prefill, isEdit, onSave })
Add fields to prompt users to input information, and optionally assign default values
to those fields.
<FormGroup>
{isEnvVarConflict ? (
<Alert isInline variant="danger" title="Environment variables conflict">

Check warning on line 158 in frontend/src/pages/connectionTypes/manage/ManageConnectionTypePage.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/connectionTypes/manage/ManageConnectionTypePage.tsx#L158

Added line #L158 was not covered by tests
Environment variables for one or more fields are conflicting. Change them to
resolve and proceed.
</Alert>
) : null}
<ManageConnectionTypeFieldsTable
fields={connectionFields}
onFieldsChange={setConnectionFields}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { ConnectionTypeDataFieldModal } from '~/pages/connectionTypes/manage/ConnectionTypeDataFieldModal';
import { ShortTextField, TextField } from '~/concepts/connectionTypes/types';

describe('ConnectionTypeDataFieldModal', () => {
it('should render the modal', () => {
Expand Down Expand Up @@ -127,4 +128,38 @@ describe('ConnectionTypeDataFieldModal', () => {
type: 'short-text',
});
});

it('should display env var conflict warning', () => {
const onClose = jest.fn();
const onSubmit = jest.fn();
const field: ShortTextField = {
type: 'short-text',
name: 'test',
envVar: 'test-envvar',
properties: {},
};
const field2: TextField = {
type: 'text',
name: 'test-2',
envVar: 'test-envvar',
properties: {},
};
render(
<ConnectionTypeDataFieldModal
onClose={onClose}
onSubmit={onSubmit}
field={field}
fields={[field, field2]}
/>,
);
const fieldEnvVarInput = screen.getByTestId('field-env-var-input');
screen.getByTestId('envvar-conflict-warning');
expect(screen.getByTestId('modal-submit-button')).not.toBeDisabled();

act(() => {
fireEvent.change(fieldEnvVarInput, { target: { value: 'new-env-value' } });
});

expect(screen.queryByTestId('envvar-conflict-warning')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import ManageConnectionTypeFieldsTableRow from '~/pages/connectionTypes/manage/ManageConnectionTypeFieldsTableRow';
import { ShortTextField, TextField } from '~/concepts/connectionTypes/types';

const renderRow = (
props: Pick<React.ComponentProps<typeof ManageConnectionTypeFieldsTableRow>, 'row'> &
Partial<React.ComponentProps<typeof ManageConnectionTypeFieldsTableRow>>,
) => {
const fn = jest.fn();
return (
<table>
<tbody>
<ManageConnectionTypeFieldsTableRow
fields={[]}
onAddField={fn}
onChange={fn}
onDelete={fn}
onDragEnd={fn}
onDragStart={fn}
onDrop={fn}
onDuplicate={fn}
onEdit={fn}
id="test"
{...props}
/>
</tbody>
</table>
);
};

describe('ManageConnectionTypeFieldsTableRow', () => {
it('should display env variable conflict icon', () => {
const field: ShortTextField = {
type: 'short-text',
name: 'test',
envVar: 'test-envvar',
properties: {},
};
const field2: TextField = {
type: 'text',
name: 'test-2',
envVar: 'test-envvar',
properties: {},
};

const result = render(renderRow({ row: field, fields: [field] }));
expect(screen.getByTestId('field-env')).not.toHaveTextContent('conflict');

result.rerender(renderRow({ row: field, fields: [field, field2] }));
expect(screen.getByTestId('field-env')).toHaveTextContent('conflict');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ThProps } from '@patternfly/react-table';

export const columns: ThProps[] = [
{ label: 'Section heading/field name', width: 30 },
{ label: 'Type', width: 10 },
{ label: 'Default value', width: 30 },
{ label: 'Environment variable', width: 20 },
{ label: 'Required', width: 10 },
];

0 comments on commit e13871f

Please sign in to comment.