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

feat(ui): [ENTESB-15353] - support custom labeling of integration #9718

Merged
merged 5 commits into from Sep 7, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,26 @@
import '@testing-library/dom';
import '@testing-library/jest-dom/extend-expect';
import { render } from '@testing-library/react';
import * as React from 'react';
import { IntegrationEditorLabels } from '../../src/Integration';

function renderComponent(props) {
const utils = render(<IntegrationEditorLabels {...props} />);
const labelSelector = utils.getByTestId('integration-label-select');
return { ...utils, labelSelector };
}

export default describe('IntegrationEditorLabels.tsx', () => {
// when there are no pre-existing labels
it('the selector should load without pre-existing labels', () => {
const labels = [];
const onSelectLabels = jest.fn();
const { labelSelector } = renderComponent({
initialLabels: labels,
onSelectLabels,
});

// the component is initialized and loads without pre-existing labels
expect(labelSelector).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion app/ui-react/packages/ui/package.json
Expand Up @@ -32,8 +32,8 @@
"@storybook/addon-viewport": "^5.3.8",
"@storybook/react": "^5.3.8",
"@storybook/theming": "^5.3.8",
"@testing-library/react": "^9.3.2",
"@testing-library/jest-dom": "^4.0.0",
"@testing-library/react": "^9.3.2",
"@types/classnames": "^2.2.6",
"@types/codemirror": "^0.0.85",
"@types/expect": "^24.3.0",
Expand Down
Expand Up @@ -16,85 +16,91 @@ export interface IIntegrationEditorExtensionTableProps {
i18nHeaderName: string;
i18nTableDescription: string;
i18nTableName: string;
onSelect: (extensionIds: string[]) => void;
onSelectExtensions: (extensionIds: string[]) => void;
/**
* These are provided by the API, and determine
* which rows will be selected on page load.
*/
preSelectedExtensionIds: string[];
}

export const IntegrationEditorExtensionTable: React.FunctionComponent<IIntegrationEditorExtensionTableProps> = (
{
export const IntegrationEditorExtensionTable: React.FunctionComponent<IIntegrationEditorExtensionTableProps> =
({
extensionsAvailable,
i18nHeaderDescription,
i18nHeaderLastUpdated,
i18nHeaderName,
i18nTableDescription,
i18nTableName,
onSelect,
preSelectedExtensionIds
onSelectExtensions,
preSelectedExtensionIds,
}) => {
/**
* Table state for array of IDs for selected extensions,
* starting with the preselected list
*/
const [selectedExtensionIds, setSelectedExtensionIds] = React.useState<string[]>(
preSelectedExtensionIds
);

const handleSelectExtension = (extensionId: string) => {
/**
* Make a shallow copy of selectedExtensions array
* Table state for array of IDs for selected extensions,
* starting with the preselected list
*/
const tempArray = selectedExtensionIds.slice();
if (!tempArray.includes(extensionId)) {
tempArray.push(extensionId);
}
const [selectedExtensionIds, setSelectedExtensionIds] = React.useState<
string[]
>(preSelectedExtensionIds);

setSelectedExtensionIds(tempArray);
};
const handleSelectExtension = (extensionId: string) => {
/**
* Make a shallow copy of selectedExtensions array
*/
const tempArray = selectedExtensionIds.slice();
if (!tempArray.includes(extensionId)) {
tempArray.push(extensionId);
}

const handleDeselectExtension = (extensionId: string) => {
/**
* Make a shallow copy of selectedExtensions array,
* then find the index of the selected id
*/
const tempArray = selectedExtensionIds.filter(id => id !== extensionId);
setSelectedExtensionIds(tempArray);
};
setSelectedExtensionIds(tempArray);
};

const onTableChange = React.useCallback((extensionId: string, isSelected: boolean) => {
if (isSelected) {
handleSelectExtension(extensionId);
} else {
handleDeselectExtension(extensionId);
}
}, [handleSelectExtension, handleDeselectExtension]);
const handleDeselectExtension = (extensionId: string) => {
/**
* Make a shallow copy of selectedExtensions array,
* then find the index of the selected id
*/
const tempArray = selectedExtensionIds.filter((id) => id !== extensionId);
setSelectedExtensionIds(tempArray);
};

const onTableChangeAll = React.useCallback((newList: string[]) => {
setSelectedExtensionIds(newList);
}, [setSelectedExtensionIds]);
const onTableChange = React.useCallback(
(extensionId: string, isSelected: boolean) => {
if (isSelected) {
handleSelectExtension(extensionId);
} else {
handleDeselectExtension(extensionId);
}
},
[handleSelectExtension, handleDeselectExtension]
);

const callOnSelect = () => {
onSelect(selectedExtensionIds);
};
const onTableChangeAll = React.useCallback(
(newList: string[]) => {
setSelectedExtensionIds(newList);
},
[setSelectedExtensionIds]
);

useEffect(() => {
callOnSelect();
}, [callOnSelect]);
const callOnSelect = () => {
onSelectExtensions(selectedExtensionIds);
};

return (
<IntegrationEditorExtensionTableRows
extensionsAvailable={extensionsAvailable}
extensionIdsSelected={preSelectedExtensionIds}
i18nHeaderDescription={i18nHeaderDescription}
i18nHeaderLastUpdated={i18nHeaderLastUpdated}
i18nHeaderName={i18nHeaderName}
i18nTableDescription={i18nTableDescription}
i18nTableName={i18nTableName}
onSelect={onTableChange}
onSelectAll={onTableChangeAll}
/>
);
}
useEffect(() => {
callOnSelect();
}, [callOnSelect]);

return (
<IntegrationEditorExtensionTableRows
extensionsAvailable={extensionsAvailable}
extensionIdsSelected={preSelectedExtensionIds}
i18nHeaderDescription={i18nHeaderDescription}
i18nHeaderLastUpdated={i18nHeaderLastUpdated}
i18nHeaderName={i18nHeaderName}
i18nTableDescription={i18nTableDescription}
i18nTableName={i18nTableName}
onSelect={onTableChange}
onSelectAll={onTableChangeAll}
/>
);
};
@@ -0,0 +1,113 @@
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core';
import * as React from 'react';

export interface IIntegrationEditorLabelsProps {
initialLabels: string[];
onSelectLabels: (labels: string[]) => void;
}

/**
* Valid format: alphanumeric=alphanumeric
* e.g. Rachel=pizza, lex=hotdogs123
* @param input
*/
const validateLabel = (input: string): boolean => {
const regexIncludeEqual = /(^\w+)(=)(\w+$)/g;
return regexIncludeEqual.test(input);
};

export const IntegrationEditorLabels: React.FunctionComponent<IIntegrationEditorLabelsProps> =
({ initialLabels, onSelectLabels }) => {
const [labels, setLabels] = React.useState(initialLabels);
const [isOpen, setIsOpen] = React.useState(false);
const labelRef = React.useRef(labels);
const isValid = React.useRef(true);

React.useEffect(() => {
if (labelRef.current === labels) {
return;
} else {
labelRef.current = labels;
onSelectLabels(labelRef.current);
}
}, [labels]);

const onCreateOption = (newValue: string) => {
// don't create if it's invalid
if (!validateLabel(newValue)) {
isValid.current = false;
return;
}
};

const onToggle = (isOpenNew: boolean) => {
setIsOpen(isOpenNew);
};

const onSelect = (
event: React.MouseEvent | React.ChangeEvent,
value: any
) => {
if (labels.includes(value)) {
setLabels(labels.filter((item) => item !== value));
} else if (validateLabel(value)) {
setLabels([...labels, value]);
}

setIsOpen(false);
};

const clearSelection = () => {
setLabels([]);
setIsOpen(false);
};

const titleId = 'integration-editor-select';
const placeholderText = 'Specify a label in this format: key=value';

return (
<FormGroup
fieldId={'integration-label-select'}
label={'Labels'}
data-testid={'integration-label-select'}
>
<div>
<span id={titleId} hidden={true}>
{placeholderText}
</span>
<Select
aria-labelledby={titleId}
isCreatable={true}
isOpen={isOpen}
id={'integration-label-select'}
name={'integration-label-select'}
onClear={clearSelection}
onCreateOption={onCreateOption}
onSelect={onSelect}
onToggle={onToggle}
placeholderText={placeholderText}
selections={labels}
typeAheadAriaLabel={placeholderText}
validated={isValid.current ? 'default' : 'error'}
variant={SelectVariant.typeaheadMulti}
>
{initialLabels &&
initialLabels.map((option, index) => (
<SelectOption
key={index}
value={option}
label={option}
data-testid={'label-option-' + option}
/>
))}
</Select>
{!isValid && <p>Please use the following format: key=value</p>}
</div>
</FormGroup>
);
};
1 change: 1 addition & 0 deletions app/ui-react/packages/ui/src/Integration/Editor/index.ts
Expand Up @@ -8,6 +8,7 @@ export * from './IntegrationEditorChooseAction';
export * from './IntegrationEditorExtensionTable';
export * from './IntegrationEditorExtensionTableRows';
export * from './IntegrationEditorForm';
export * from './IntegrationEditorLabels';
export * from './IntegrationEditorLayout';
export * from './IntegrationEditorNothingToConfigure';
export * from './IntegrationEditorNothingToConfigureAlert';
Expand Down
@@ -0,0 +1,23 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import * as React from 'react';

const stories = storiesOf('Integration/Editor/IntegrationEditorLabels', module);

import { IntegrationEditorLabels } from '../../../src/Integration/Editor/IntegrationEditorLabels';

const labels = ['rachel=pizza', 'lex=hotdogs'];

stories.add('Pre-Existing Labels', () => (
<IntegrationEditorLabels
initialLabels={labels}
onSelectLabels={action('Selected')}
/>
));

stories.add('No Labels', () => (
<IntegrationEditorLabels
initialLabels={[]}
onSelectLabels={action('Selected')}
/>
));