Skip to content
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
100 changes: 78 additions & 22 deletions ui/apps/platform/src/Containers/Collections/CollectionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,24 @@ import { CaretDownIcon, CubesIcon } from '@patternfly/react-icons';
import { TableComposable, TableVariant, Tbody, Tr, Td } from '@patternfly/react-table';
import { useFormik } from 'formik';
import * as yup from 'yup';
import isEmpty from 'lodash/isEmpty';

import BreadcrumbItemLink from 'Components/BreadcrumbItemLink';
import ConfirmationModal from 'Components/PatternFly/ConfirmationModal';
import useSelectToggle from 'hooks/patternfly/useSelectToggle';
import useToasts from 'hooks/patternfly/useToasts';
import { collectionsBasePath } from 'routePaths';
import { CollectionResponse, deleteCollection } from 'services/CollectionsService';
import {
CollectionResponse,
createCollection,
deleteCollection,
updateCollection,
} from 'services/CollectionsService';
import { CollectionPageAction } from './collections.utils';
import RuleSelector from './RuleSelector';
import CollectionAttacher from './CollectionAttacher';
import CollectionResults from './CollectionResults';
import { Collection, ScopedResourceSelector, SelectorEntityType } from './types';
import { generateRequest } from './converter';

function AttachedCollectionTable({ collections }: { collections: CollectionResponse[] }) {
return collections.length > 0 ? (
Expand Down Expand Up @@ -97,7 +102,7 @@ export type CollectionFormProps = {

function yupResourceSelectorObject() {
return yup.lazy((ruleObject) => {
if (isEmpty(ruleObject)) {
if (ruleObject.type === 'All') {
return yup.object().shape({});
}

Expand Down Expand Up @@ -148,14 +153,24 @@ function CollectionForm({
const [isDeleting, setIsDeleting] = useState(false);
const { toasts, addToast, removeToast } = useToasts();

const { values, errors, handleChange, handleBlur, setFieldValue } = useFormik({
const {
values,
isValid,
errors,
handleChange,
handleBlur,
setFieldValue,
submitForm,
isSubmitting,
setSubmitting,
} = useFormik({
initialValues: initialData,
onSubmit: () => {},
onSubmit: onSaveCollection,
validationSchema: yup.object({
name: yup.string().trim().required(),
description: yup.string(),
embeddedCollections: yup.array(yup.string().trim().required()),
resourceSelectors: yup.object().shape({
embeddedCollectionIds: yup.array(yup.string().trim().required()),
resourceSelector: yup.object().shape({
Deployment: yupResourceSelectorObject(),
Namespace: yupResourceSelectorObject(),
Cluster: yupResourceSelectorObject(),
Expand Down Expand Up @@ -192,11 +207,7 @@ function CollectionForm({
deleteCollection(deleteId)
.request.then(history.goBack)
.catch((err) => {
addToast(
`Could not delete collection ${initialData.name ?? ''}`,
'danger',
err.message
);
addToast(`Could not delete collection ${initialData.name}`, 'danger', err.message);
})
.finally(() => {
setDeleteId(null);
Expand All @@ -208,14 +219,46 @@ function CollectionForm({
setDeleteId(null);
}

function onSaveCollection(collection: Collection) {
if (action.type === 'view') {
// Logically should not happen, but just in case
return;
}

const saveServiceCall =
action.type === 'edit'
? (payload) => updateCollection(action.collectionId, payload)
: (payload) => createCollection(payload);

const requestPayload = generateRequest(collection);
const { request } = saveServiceCall(requestPayload);

request
.then(() => {
history.push({ pathname: `${collectionsBasePath}` });
})
.catch((err) => {
addToast(
`There was an error saving collection '${values.name}'`,
'danger',
err.message
);
setSubmitting(false);
});
}

function onCancelSave() {
history.push({ pathname: `${collectionsBasePath}` });
}

const onResourceSelectorChange = (
entityType: SelectorEntityType,
scopedResourceSelector: ScopedResourceSelector
) => setFieldValue(`resourceSelectors.${entityType}`, scopedResourceSelector);
) => setFieldValue(`resourceSelector.${entityType}`, scopedResourceSelector);

const onEmbeddedCollectionsChange = (newCollections: CollectionResponse[]) =>
setFieldValue(
'embeddedCollections',
'embeddedCollectionIds',
newCollections.map(({ id }) => id)
);

Expand Down Expand Up @@ -389,9 +432,9 @@ function CollectionForm({
)}
<RuleSelector
entityType="Deployment"
scopedResourceSelector={values.resourceSelectors.Deployment}
scopedResourceSelector={values.resourceSelector.Deployment}
handleChange={onResourceSelectorChange}
validationErrors={errors.resourceSelectors?.Deployment}
validationErrors={errors.resourceSelector?.Deployment}
isDisabled={isReadOnly}
/>
<Label
Expand All @@ -403,9 +446,9 @@ function CollectionForm({
</Label>
<RuleSelector
entityType="Namespace"
scopedResourceSelector={values.resourceSelectors.Namespace}
scopedResourceSelector={values.resourceSelector.Namespace}
handleChange={onResourceSelectorChange}
validationErrors={errors.resourceSelectors?.Namespace}
validationErrors={errors.resourceSelector?.Namespace}
isDisabled={isReadOnly}
/>
<Label
Expand All @@ -417,9 +460,9 @@ function CollectionForm({
</Label>
<RuleSelector
entityType="Cluster"
scopedResourceSelector={values.resourceSelectors.Cluster}
scopedResourceSelector={values.resourceSelector.Cluster}
handleChange={onResourceSelectorChange}
validationErrors={errors.resourceSelectors?.Cluster}
validationErrors={errors.resourceSelector?.Cluster}
isDisabled={isReadOnly}
/>
</Flex>
Expand Down Expand Up @@ -451,8 +494,21 @@ function CollectionForm({
</Flex>
{action.type !== 'view' && (
<div className="pf-u-background-color-100 pf-u-p-lg pf-u-py-md">
<Button className="pf-u-mr-md">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button
className="pf-u-mr-md"
onClick={submitForm}
isDisabled={isSubmitting || !isValid}
isLoading={isSubmitting}
>
Save
</Button>
<Button
variant="secondary"
isDisabled={isSubmitting}
onClick={onCancelSave}
>
Cancel
</Button>
</div>
)}
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
} from 'services/CollectionsService';
import { CollectionPageAction } from './collections.utils';
import CollectionForm from './CollectionForm';
import { parseCollection } from './parser';
import { parseCollection } from './converter';
import { Collection } from './types';

export type CollectionsFormPageProps = {
hasWriteAccessForCollections: boolean;
Expand All @@ -23,15 +24,15 @@ const noopRequest = {
cancel: () => {},
};

const defaultCollectionData = {
const defaultCollectionData: Collection = {
name: '',
description: '',
inUse: false,
embeddedCollections: [],
resourceSelectors: {
Deployment: {},
Namespace: {},
Cluster: {},
embeddedCollectionIds: [],
resourceSelector: {
Deployment: { type: 'All' },
Namespace: { type: 'All' },
Cluster: { type: 'All' },
},
};

Expand Down Expand Up @@ -84,6 +85,9 @@ function CollectionsFormPage({
} else if (loading) {
content = <>{/* TODO - Handle UI for loading state */}</>;
} else if (initialData) {
if (pageAction.type === 'clone') {
initialData.name += ' (COPY)';
}
content = (
<CollectionForm
hasWriteAccessForCollections={hasWriteAccessForCollections}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function ByLabelSelector({
handleChange(entityType, newSelector);
} else {
// This was the last value in the last rule, so drop the selector
handleChange(entityType, {});
handleChange(entityType, { type: 'All' });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function ByNameSelector({
handleChange(entityType, newSelector);
} else {
// This was the last value in the rule, so drop the selector
handleChange(entityType, {});
handleChange(entityType, { type: 'All' });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function DeploymentRuleSelector({ defaultSelector, onChange }) {

describe('Collection RuleSelector component', () => {
it('Should render "All entities" option when selector is null', async () => {
let resourceSelector: ScopedResourceSelector = {};
let resourceSelector: ScopedResourceSelector = { type: 'All' };

function onChange(newSelector) {
resourceSelector = newSelector;
Expand All @@ -40,6 +40,7 @@ describe('Collection RuleSelector component', () => {

it('Should allow users to add name selectors', async () => {
let resourceSelector: ByNameResourceSelector = {
type: 'ByName',
field: 'Deployment',
rule: { operator: 'OR', values: [] },
};
Expand All @@ -50,7 +51,7 @@ describe('Collection RuleSelector component', () => {
resourceSelector = newSelector;
}

render(<DeploymentRuleSelector defaultSelector={{}} onChange={onChange} />);
render(<DeploymentRuleSelector defaultSelector={{ type: 'All' }} onChange={onChange} />);

await user.click(screen.getByLabelText('Select deployments by name or label'));
await user.click(screen.getByText('Deployments with names matching'));
Expand Down Expand Up @@ -98,12 +99,13 @@ describe('Collection RuleSelector component', () => {
await user.click(screen.getByLabelText('Delete visa-processor'));
await user.click(screen.getByLabelText('Delete discover-processor'));

expect(resourceSelector).toEqual({});
expect(resourceSelector).toEqual({ type: 'All' });
expect(screen.getByText('All deployments')).toBeInTheDocument();
});

it('Should allow users to add label key/value selectors', async () => {
let resourceSelector: ByLabelResourceSelector = {
type: 'ByLabel',
field: 'Deployment Label',
rules: [{ operator: 'OR', key: '', values: [''] }],
};
Expand All @@ -114,7 +116,7 @@ describe('Collection RuleSelector component', () => {
resourceSelector = newSelector;
}

render(<DeploymentRuleSelector defaultSelector={{}} onChange={onChange} />);
render(<DeploymentRuleSelector defaultSelector={{ type: 'All' }} onChange={onChange} />);

await user.click(screen.getByLabelText('Select deployments by name or label'));
await user.click(screen.getByText('Deployments with labels matching'));
Expand Down Expand Up @@ -181,6 +183,7 @@ describe('Collection RuleSelector component', () => {
);

expect(resourceSelector).toEqual({
type: 'ByLabel',
field: 'Deployment Label',
rules: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import React from 'react';
import { Select, SelectOption } from '@patternfly/react-core';
import pluralize from 'pluralize';
import { FormikErrors } from 'formik';
import isEmpty from 'lodash/isEmpty';

import useSelectToggle from 'hooks/patternfly/useSelectToggle';
import {
isByLabelSelector,
isByNameSelector,
RuleSelectorOption,
ScopedResourceSelector,
SelectorEntityType,
selectorOptions,
} from '../types';
import ByNameSelector from './ByNameSelector';
import ByLabelSelector from './ByLabelSelector';

const selectorOptions = ['All', 'ByName', 'ByLabel'] as const;

type RuleSelectorOption = typeof selectorOptions[number];

function isRuleSelectorOption(value: string): value is RuleSelectorOption {
return selectorOptions.includes(value as RuleSelectorOption);
}
Expand Down Expand Up @@ -49,12 +44,14 @@ function RuleSelector({
}

const selectorMap: Record<RuleSelectorOption, ScopedResourceSelector> = {
All: {},
All: { type: 'All' },
ByName: {
type: 'ByName',
field: entityType,
rule: { operator: 'OR', values: [''] },
},
ByLabel: {
type: 'ByLabel',
field: `${entityType} Label`,
rules: [{ operator: 'OR', key: '', values: [''] }],
},
Expand All @@ -64,15 +61,7 @@ function RuleSelector({
closeSelect();
}

let selection: RuleSelectorOption = 'All';

if (isEmpty(scopedResourceSelector)) {
selection = 'All';
} else if (isByNameSelector(scopedResourceSelector)) {
selection = 'ByName';
} else if (isByLabelSelector(scopedResourceSelector)) {
selection = 'ByLabel';
}
const selection = scopedResourceSelector.type;

return (
<div
Expand All @@ -93,7 +82,7 @@ function RuleSelector({
<SelectOption value="ByLabel">{pluralEntity} with labels matching</SelectOption>
</Select>

{isByNameSelector(scopedResourceSelector) && (
{scopedResourceSelector.type === 'ByName' && (
<ByNameSelector
entityType={entityType}
scopedResourceSelector={scopedResourceSelector}
Expand All @@ -103,7 +92,7 @@ function RuleSelector({
/>
)}

{isByLabelSelector(scopedResourceSelector) && (
{scopedResourceSelector.type === 'ByLabel' && (
<ByLabelSelector
entityType={entityType}
scopedResourceSelector={scopedResourceSelector}
Expand Down
Loading