Skip to content

Commit

Permalink
CNV-36170: Multi NetworkPolicies
Browse files Browse the repository at this point in the history
  • Loading branch information
upalatucci committed Apr 23, 2024
1 parent 8ef3b38 commit 6e1f224
Show file tree
Hide file tree
Showing 27 changed files with 914 additions and 32 deletions.
9 changes: 9 additions & 0 deletions frontend/packages/console-app/console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,15 @@
}
}
},

{
"type": "console.page/route",
"properties": {
"exact": true,
"path": ["/k8s/all-namespaces/networkpolicies/~enable-multi", "/k8s/ns/:ns/networkpolicies/~enable-multi"],
"component": { "$codeRef": "networkPolicyListPage.NetworkPolicyListPage" }
}
},
{
"type": "console.navigation/resource-cluster",
"properties": {
Expand Down
11 changes: 10 additions & 1 deletion frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@
"Create by completing the form.": "Create by completing the form.",
"Create by manually entering YAML or JSON definitions, or by dragging and dropping a file into the editor.": "Create by manually entering YAML or JSON definitions, or by dragging and dropping a file into the editor.",
"Not all YAML property values are supported in the form editor. Some data would be lost.": "Not all YAML property values are supported in the form editor. Some data would be lost.",
"Create NetworkPolicy": "Create NetworkPolicy",
"Create {{kind}}": "Create {{kind}}",
"Policy for": "Policy for",
"Select one or more NetworkAttachmentDefinitions": "Select one or more NetworkAttachmentDefinitions",
"Allow pods from the same namespace": "Allow pods from the same namespace",
"Allow pods from inside the cluster": "Allow pods from inside the cluster",
"Allow peers by IP block": "Allow peers by IP block",
Expand Down Expand Up @@ -322,6 +324,10 @@
"Add egress rules to be applied to your selected pods. Traffic is allowed to pods if it matches at least one rule.": "Add egress rules to be applied to your selected pods. Traffic is allowed to pods if it matches at least one rule.",
"Add egress rule": "Add egress rule",
"Cancel": "Cancel",
"Enable {{kind}}": "Enable {{kind}}",
"{{kind}} disabled": "{{kind}} disabled",
"Cluster administrator permissions are required to enable this feature.": "Cluster administrator permissions are required to enable this feature.",
"MultiNetworkPolicies": "MultiNetworkPolicies",
"{{path}} is missing.": "{{path}} is missing.",
"{{path}} should be an Array.": "{{path}} should be an Array.",
"{{path}} should not be empty.": "{{path}} should not be empty.",
Expand Down Expand Up @@ -359,6 +365,9 @@
"Add allowed source": "Add allowed source",
"Add allowed destination": "Add allowed destination",
"Remove peer": "Remove peer",
"Current selections": "Current selections",
"Clear input value": "Clear input value",
"No results found for \"{{inputValue}}\"": "No results found for \"{{inputValue}}\"",
"Mark as schedulable": "Mark as schedulable",
"Mark as unschedulable": "Mark as unschedulable",
"This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.": "This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.",
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/console-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"storageProvisionerDocs": "src/components/storage/Documentation",
"nodeStatus": "src/components/nodes/status",
"nodeActions": "src/components/nodes/menu-actions.tsx",
"oauthConfigDetailsPage": "src/components/oauth-config/OAuthConfigDetailsPage.tsx"
"oauthConfigDetailsPage": "src/components/oauth-config/OAuthConfigDetailsPage.tsx",
"networkPolicyListPage": "src/components/network-policies/network-policy-list/NetworkPolicyListPage.tsx"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ jest.mock('@console/shared/src/hooks/useUserSettingsCompatibility', () => ({
useUserSettingsCompatibility: () => ['', () => {}],
}));

jest.mock('react-router-dom-v5-compat', () => ({
...require.requireActual('react-router-dom-v5-compat'),
useParams: jest.fn(() => ({ ns: 'default' })),
useLocation: jest.fn(() => ({
pathname: '/k8s/ns/default/networking.k8s.io~v1~NetworkPolicy/~new/form',
})),
}));

const emptyPolicy: NetworkPolicyKind = {
metadata: {
name: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ jest.mock('@console/shared/src/hooks/useUserSettingsCompatibility', () => ({
useUserSettingsCompatibility: () => ['', () => {}],
}));

jest.mock('react-router-dom-v5-compat', () => ({
...require.requireActual('react-router-dom-v5-compat'),
useParams: jest.fn(() => ({ ns: 'default' })),
useLocation: jest.fn(() => ({
pathname: '/k8s/ns/default/networking.k8s.io~v1~NetworkPolicy/~new/form',
})),
}));

const emptyPolicy: NetworkPolicyKind = {
metadata: {
name: '',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react';
import { Alert, AlertVariant, FormGroup } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import {
getGroupVersionKindForModel,
useK8sWatchResource,
} from '@console/dynamic-plugin-sdk/src/lib-core';
import { Loading } from '@console/internal/components/utils';
import { NetworkAttachmentDefinitionModel } from '@console/network-attachment-definition-plugin/src';
import { NetworkAttachmentDefinitionKind } from '@console/network-attachment-definition-plugin/src/types';
import { getName, getNamespace } from '@console/shared/src';
import { NetworkPolicy } from './network-policy-model';
import SelectMultiTypeahead from './SelectMultiTypeahead/SelectMultiTypeahead';

type NADsSelectorProps = {
namespace: string;
networkPolicy: NetworkPolicy;
onPolicyChange: (policy: NetworkPolicy) => void;
};

const NetworkAttachmentDefinitionModelGroupVersionKind = getGroupVersionKindForModel(
NetworkAttachmentDefinitionModel,
);

const NADsSelector: React.FC<NADsSelectorProps> = ({
namespace,
networkPolicy,
onPolicyChange,
}) => {
const { t } = useTranslation();

const [nads, loaded, loadError] = useK8sWatchResource<NetworkAttachmentDefinitionKind[]>({
groupVersionKind: NetworkAttachmentDefinitionModelGroupVersionKind,
isList: true,
namespace,
});

const [nadsDefault, loadedDefaultNads, loadErrorDefaultNads] = useK8sWatchResource<
NetworkAttachmentDefinitionKind[]
>(
namespace !== 'default'
? {
groupVersionKind: NetworkAttachmentDefinitionModelGroupVersionKind,
isList: true,
namespace: 'default',
}
: null,
);

const nadsOptions = React.useMemo(() => {
const allNads = [...(nads || []), ...(nadsDefault || [])];

return allNads.map((nad) => ({
value: `${getNamespace(nad)}/${getName(nad)}`,
}));
}, [nads, nadsDefault]);

const onChange = (newNADs: string[]) => {
onPolicyChange({ ...networkPolicy, policyFor: newNADs });
};

if (!loaded || !loadedDefaultNads) return <Loading />;

if (loadError || loadErrorDefaultNads)
return (
<Alert title={t('Error')} variant={AlertVariant.danger}>
{loadError}
</Alert>
);

return (
<FormGroup
fieldId="multi-networkpolicy-policyfor"
isRequired
label={t('console-app~Policy for')}
>
<SelectMultiTypeahead
options={nadsOptions}
placeholder={t('console-app~Select one or more NetworkAttachmentDefinitions')}
selected={networkPolicy.policyFor || []}
setSelected={onChange}
/>
</FormGroup>
);
};

export default NADsSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,19 @@ reviewers:
- jotak
- mariomac
- OlivierCazade
- vojtechszocs
- pcbailey
- yaacov
- metalice
- avivtur
- hstastna
- upalatucci
approvers:
- jotak
- yaacov
- pcbailey
- metalice
- avivtur
- hstastna
- upalatucci
- vojtechszocs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';
import {
MenuToggleElement,
Select,
SelectList,
SelectOption,
SelectOptionProps,
} from '@patternfly/react-core';
import { NO_RESULTS_VALUE } from './constants';
import Toggle from './Toggle';
import { filterOptions } from './utils';

type SelectMultiTypeaheadProps = {
options: SelectOptionProps[];
placeholder: string;
selected: string[];
setSelected: (newSelection: string[]) => void;
};

const SelectMultiTypeahead: React.FC<SelectMultiTypeaheadProps> = ({
options,
placeholder,
selected,
setSelected,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState<string>('');
const [focusedItemIndex, setFocusedItemIndex] = React.useState<null | number>(null);
const textInputRef = React.useRef<HTMLInputElement>();

const selectOptions = inputValue ? filterOptions(options, inputValue) : options;

const onSelect = (value: string) => {
if (value && value !== NO_RESULTS_VALUE) {
setSelected(
selected.includes(value)
? selected.filter((selection) => selection !== value)
: [...selected, value],
);

setIsOpen(true);
}

textInputRef.current?.focus();
};

return (
<Select
id="multi-typeahead-select"
isOpen={isOpen}
onOpenChange={() => setIsOpen(false)}
onSelect={(event?: React.MouseEvent<Element, MouseEvent>, selection?: string | number) =>
onSelect(selection as string)
}
selected={selected}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<Toggle
focusedItemIndex={focusedItemIndex}
inputValue={inputValue}
isOpen={isOpen}
onSelect={onSelect}
placeholder={placeholder}
selected={selected}
selectOptions={selectOptions}
setFocusedItemIndex={setFocusedItemIndex}
setInputValue={setInputValue}
setIsOpen={setIsOpen}
setSelected={setSelected}
textInputRef={textInputRef}
toggleRef={toggleRef}
/>
)}
>
<SelectList id="select-multi-typeahead-listbox" isAriaMultiselectable>
{selectOptions.map((option, index) => (
<SelectOption
className={option.className}
id={`select-multi-typeahead-${option.value.replace(' ', '-')}`}
isFocused={focusedItemIndex === index}
key={option.value || option.children}
{...option}
>
{option.children || option.value}
</SelectOption>
))}
</SelectList>
</Select>
);
};

export default SelectMultiTypeahead;

0 comments on commit 6e1f224

Please sign in to comment.