Skip to content

Commit

Permalink
Node resource table and modal
Browse files Browse the repository at this point in the history
  • Loading branch information
dpanshug committed Dec 18, 2024
1 parent 065da14 commit c2f6c05
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 27 deletions.
6 changes: 3 additions & 3 deletions frontend/src/__mocks__/mockHardwareProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export const mockHardwareProfile = ({
{
displayName: 'Memory',
identifier: 'memory',
minCount: 5,
maxCount: 2,
defaultCount: 2,
minCount: '5Gi',
maxCount: '2Gi',
defaultCount: '2Gi',
},
],
description = '',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/api/k8s/__tests__/hardwareProfiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ const data: HardwareProfileKind['spec'] = {
{
displayName: 'Memory',
identifier: 'memory',
minCount: 5,
maxCount: 2,
defaultCount: 2,
minCount: '5Gi',
maxCount: '2Gi',
defaultCount: '2Gi',
},
],
description: 'test description',
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/pages/hardwareProfiles/ManageNodeResourceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { FormSection, Flex, FlexItem, Button } from '@patternfly/react-core';
import { Identifier } from '~/types';
import NodeResourceTable from './nodeResource/NodeResourceTable';
import ManageNodeResourceModal from './nodeResource/ManageNodeResourceModal';

type ManageNodeResourceSectionProps = {
identifiers: Identifier[];
setIdentifiers: (identifiers: Identifier[]) => void;
};

export const ManageNodeResourceSection: React.FC<ManageNodeResourceSectionProps> = ({
identifiers,
setIdentifiers,
}) => {
const [isNodeResourceModalOpen, setIsNodeResourceModalOpen] = React.useState<boolean>(false);
return (
<>
<FormSection
title={
<Flex>
<FlexItem>Node resources</FlexItem>
<FlexItem>
<Button
variant="secondary"
onClick={() => setIsNodeResourceModalOpen(true)}
data-testid="add-node-resource-button"
>
Add resource
</Button>
</FlexItem>
</Flex>
}
>
<NodeResourceTable
identifiers={identifiers}
onUpdate={(newIdentifiers) => setIdentifiers(newIdentifiers)}
/>
</FormSection>
{isNodeResourceModalOpen ? (
<ManageNodeResourceModal
onClose={() => setIsNodeResourceModalOpen(false)}
onSave={(identifier) => setIdentifiers([...identifiers, identifier])}
identifiers={identifiers}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core';
import MemoryField from '~/components/MemoryField';
import CPUField from '~/components/CPUField';
import NumberInputWrapper from '~/components/NumberInputWrapper';

type CountFormFieldProps = {
label: string;
fieldId: string;
size: number | string;
setSize: (value: number | string) => void;
identifier: string;
errorMessage?: string;
isValid?: boolean;
};

const CountFormField: React.FC<CountFormFieldProps> = ({
label,
fieldId,
size,
setSize,
identifier,
errorMessage,
isValid = true,
}) => {
const renderInputField = () => {
switch (identifier) {
case 'cpu':
return <CPUField onChange={(value) => setSize(value)} value={size} />;
case 'memory':
return <MemoryField onChange={(value) => setSize(value)} value={String(size)} />;
default:
return (
<NumberInputWrapper
min={0}
value={Number(size)}
onChange={(value) => {
if (value) {
setSize(value);
}
}}
/>
);
}
};

return (
<FormGroup label={label} fieldId={fieldId} data-testid={fieldId}>
{renderInputField()}
{!isValid && errorMessage && (
<FormHelperText>
<HelperText>
<HelperTextItem data-testid={`${fieldId}-error`} variant="error">
{errorMessage}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
);
};

export default CountFormField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { Modal } from '@patternfly/react-core/deprecated';
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter';
import { Identifier } from '~/types';
import useGenericObjectState from '~/utilities/useGenericObjectState';
import { CPU_UNITS, MEMORY_UNITS_FOR_SELECTION, UnitOption } from '~/utilities/valueUnits';
import { EMPTY_IDENTIFIER } from './const';
import NodeResourceForm from './NodeResourceForm';
import { validateDefaultCount, validateMinCount } from './utils';

type ManageNodeResourceModalProps = {
onClose: () => void;
existingIdentifier?: Identifier;
onSave: (identifier: Identifier) => void;
identifiers: Identifier[];
};

const ManageNodeResourceModal: React.FC<ManageNodeResourceModalProps> = ({
onClose,
existingIdentifier,
onSave,
identifiers,
}) => {
const [identifier, setIdentifier] = useGenericObjectState<Identifier>(
existingIdentifier || EMPTY_IDENTIFIER,
);

const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>();

const isUniqueIdentifier = React.useMemo(() => {
if (existingIdentifier) {
return true;
}
return !identifiers.some((i) => i.identifier === identifier.identifier);
}, [existingIdentifier, identifier.identifier, identifiers]);

React.useEffect(() => {
switch (identifier.identifier) {
case 'cpu':
setUnitOptions(CPU_UNITS);
break;
case 'memory':
setUnitOptions(MEMORY_UNITS_FOR_SELECTION);
break;
default:
setUnitOptions(undefined);
}
}, [identifier]);

const isButtonDisabled = React.useMemo(() => {
const isValidCounts = unitOptions
? validateDefaultCount(identifier, unitOptions) && validateMinCount(identifier, unitOptions)
: true;

return (
!identifier.displayName || !identifier.identifier || !isUniqueIdentifier || !isValidCounts
);
}, [identifier, unitOptions, isUniqueIdentifier]);

const handleSubmit = () => {
onSave(identifier);
onClose();
};

return (
<Modal
title={existingIdentifier ? 'Edit resource' : 'Add resource'}
variant="medium"
isOpen
onClose={onClose}
footer={
<DashboardModalFooter
submitLabel={existingIdentifier ? 'Update' : 'Add'}
onSubmit={handleSubmit}
onCancel={onClose}
isSubmitDisabled={isButtonDisabled}
/>
}
>
<NodeResourceForm
identifier={identifier}
setIdentifier={setIdentifier}
unitOptions={unitOptions}
existingIdentifier={!!existingIdentifier}
isUniqueIdentifier={isUniqueIdentifier}
/>
</Modal>
);
};

export default ManageNodeResourceModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { TextInput, FormGroup, Form } from '@patternfly/react-core';
import { Identifier } from '~/types';
import { UpdateObjectAtPropAndValue } from '~/pages/projects/types';
import { UnitOption } from '~/utilities/valueUnits';
import { validateDefaultCount, validateMinCount } from './utils';
import CountFormField from './CountFormField';

type NodeResourceFormProps = {
identifier: Identifier;
setIdentifier: UpdateObjectAtPropAndValue<Identifier>;
unitOptions?: UnitOption[];
existingIdentifier?: boolean;
isUniqueIdentifier?: boolean;
};

const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
identifier,
setIdentifier,
unitOptions,
existingIdentifier,
isUniqueIdentifier,
}) => {
const [validated, setValidated] = React.useState<'default' | 'error' | 'success'>('default');

React.useEffect(() => {
setValidated(isUniqueIdentifier ? 'default' : 'error');
}, [isUniqueIdentifier]);

return (
<Form>
<FormGroup label="Resource label" fieldId="resource-label">
<TextInput
value={identifier.displayName || ''}
onChange={(_, value) => setIdentifier('displayName', value)}
/>
</FormGroup>

<FormGroup label="Resource identifier" fieldId="resource-identifier">
<TextInput
value={identifier.identifier || ''}
onChange={(_, value) => setIdentifier('identifier', value)}
isDisabled={
existingIdentifier &&
(identifier.identifier === 'cpu' || identifier.identifier === 'memory')
}
validated={validated}
/>
</FormGroup>

<CountFormField
label="Default"
fieldId="default"
identifier={identifier.identifier}
size={identifier.defaultCount}
setSize={(value) => setIdentifier('defaultCount', value)}
isValid={unitOptions ? validateDefaultCount(identifier, unitOptions) : true}
errorMessage="Default must be equal to or between the minimum and maximum allowed limits."
/>

<CountFormField
label="Minimum allowed"
fieldId="minimum-allowed"
identifier={identifier.identifier}
size={identifier.minCount}
setSize={(value) => setIdentifier('minCount', value)}
isValid={unitOptions ? validateMinCount(identifier, unitOptions) : true}
errorMessage="Minimum allowed value cannot exceed the maximum allowed value."
/>

<CountFormField
label="Default"
fieldId="default"
identifier={identifier.identifier}
size={identifier.maxCount}
setSize={(value) => setIdentifier('maxCount', value)}
/>
</Form>
);
};
export default NodeResourceForm;
Loading

0 comments on commit c2f6c05

Please sign in to comment.