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 12, 2024
1 parent a1accaf commit 60d87b6
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import K8sNameDescriptionField, {
useK8sNameDescriptionFieldData,
} from '~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField';
import { isK8sNameDescriptionDataValid } from '~/concepts/k8s/K8sNameDescriptionField/utils';
import { Identifier } from '~/types';
import { ManageNodeResourceSection } from '~/pages/hardwareProfiles/ManageNodeResourceSection';
import { ManageAcceleratorProfileFooter } from './ManageAcceleratorProfileFooter';
import { ManageAcceleratorProfileTolerationsSection } from './ManageAcceleratorProfileTolerationsSection';
import { AcceleratorProfileFormData, ManageAcceleratorProfileSectionID } from './types';
Expand Down Expand Up @@ -62,6 +64,16 @@ const ManageAcceleratorProfile: React.FC<ManageAcceleratorProfileProps> = ({

const validFormData = isK8sNameDescriptionDataValid(profileNameDesc) && !!state.identifier;

const [identifiers, setIdentifiers] = React.useState<Identifier[]>([
{
displayName: 'test',
identifier: 'test',
minCount: '1Gi',
maxCount: '10Gi',
defaultCount: '2Gi',
},
]);

return (
<ApplicationsPage
title={
Expand Down Expand Up @@ -106,6 +118,7 @@ const ManageAcceleratorProfile: React.FC<ManageAcceleratorProfileProps> = ({
/>
<ManageAcceleratorProfileDetailsSection state={state} setState={setState} />
</FormSection>
<ManageNodeResourceSection identifiers={identifiers} setIdentifiers={setIdentifiers} />
<ManageAcceleratorProfileTolerationsSection
tolerations={state.tolerations ?? []}
setTolerations={(tolerations) => setState('tolerations', tolerations)}
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/pages/hardwareProfiles/ManageNodeResourceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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])}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core';
import ValueUnitField from '~/components/ValueUnitField';
import { MEMORY_UNITS_FOR_SELECTION } from '~/utilities/valueUnits';

type CountFormFielddProps = {
label: string;
fieldId: string;
size: string;
setSize: (val: string) => void;
errorMessage?: string;
isValid?: boolean;
};

const CountFormField: React.FC<CountFormFielddProps> = ({
label,
fieldId,
size,
setSize,
errorMessage,
isValid = true,
}) => (
<FormGroup label={label} fieldId={fieldId} data-testid={fieldId}>
<ValueUnitField
onBlur={(value) => setSize(value)}
onChange={(value) => setSize(value)}
options={MEMORY_UNITS_FOR_SELECTION}
value={size}
/>
{!isValid && (
<FormHelperText>
<HelperText>
<HelperTextItem data-testid="persistent-storage-warning-can-not-edit" variant="error">
{errorMessage}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
);

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

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

const ManageNodeResourceModal: React.FC<ManageNodeResourceModalProps> = ({
onClose,
existingIdentifier,
onSave,
}) => {
const [identifier, setIdentifier] = useState<Identifier>(EMPTY_IDENTIFIER);
const isButtonDisabled =
!identifier.displayName ||
!identifier.identifier ||
!identifier.maxCount ||
!validateDefaultCount(identifier) ||
!validateMinCount(identifier);
useEffect(() => {
if (existingIdentifier) {
setIdentifier(existingIdentifier);
}
}, [existingIdentifier]);

const handleUpdate = (updatedIdentifier: Identifier) => {
setIdentifier(updatedIdentifier);
};

const onBeforeClose = () => {
setIdentifier(EMPTY_IDENTIFIER);
onClose();
};

return (
<Modal
title={existingIdentifier ? 'Edit resource' : 'Add resource'}
variant="medium"
isOpen
onClose={() => {
onBeforeClose();
}}
footer={
<DashboardModalFooter
submitLabel={existingIdentifier ? 'Update' : 'Add'}
onSubmit={() => {
onSave(identifier);
onBeforeClose();
}}
onCancel={() => onBeforeClose()}
isSubmitDisabled={isButtonDisabled}
alertTitle="Error saving resource"
/>
}
>
<NodeResourceForm identifier={identifier} onUpdate={handleUpdate} />
</Modal>
);
};

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

type NodeResourceFormProps = {
identifier: Identifier;
onUpdate: (data: Identifier) => void;
};

const NodeResourceForm: React.FC<NodeResourceFormProps> = ({ identifier, onUpdate }) => {
const handleFieldUpdate = (field: keyof Identifier, value: unknown) => {
onUpdate({ ...identifier, [field]: value });
};

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

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

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

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

<CountFormField
size={String(identifier.maxCount) || ''}
setSize={(value: string) => handleFieldUpdate('maxCount', value)}
label="Maximum allowed"
fieldId="maximum-allowed"
/>
</Form>
);
};

export default NodeResourceForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { EmptyState, EmptyStateBody, Title } from '@patternfly/react-core';
import React from 'react';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { Table } from '~/components/table';
import { Identifier } from '~/types';
import { nodeResourceColumns } from './const';
import NodeResourceTableRow from './NodeResourceTableRow';
import ManageNodeResourceModal from './ManageNodeResourceModal';

type NodeResourceTableProps = {
identifiers: Identifier[];
onUpdate: (identifiers: Identifier[]) => void;
viewOnly?: boolean;
};

const NodeResourceTable: React.FC<NodeResourceTableProps> = ({
identifiers,
onUpdate,
viewOnly,
}) => {
const [editIdentifier, setEditIdentifier] = React.useState<Identifier | undefined>();
const [currentIndex, setCurrentIndex] = React.useState<number | undefined>();

if (identifiers.length === 0) {
return (
<EmptyState
titleText={
<Title headingLevel="h2" size="lg">
No node resource
</Title>
}
icon={PlusCircleIcon}
variant="xs"
data-testid="node-resource-empty-state"
>
<EmptyStateBody>No node resource body</EmptyStateBody>
</EmptyState>
);
}

return (
<>
<Table
data={identifiers}
columns={
viewOnly
? nodeResourceColumns.filter((column) => column.field !== 'kebab')
: nodeResourceColumns
}
data-testid="node-resource-table"
rowRenderer={(identifier, rowIndex) => (
<NodeResourceTableRow
key={identifier.identifier + rowIndex}
identifier={identifier}
onEdit={(newIdentifier) => {
setEditIdentifier(newIdentifier);
setCurrentIndex(rowIndex);
}}
onDelete={() => {
const updatedIdentifiers = [...identifiers];
updatedIdentifiers.splice(rowIndex, 1);
onUpdate(updatedIdentifiers);
}}
showKebab={!!viewOnly}
/>
)}
/>
{editIdentifier ? (
<ManageNodeResourceModal
existingIdentifier={editIdentifier}
onClose={() => {
setEditIdentifier(undefined);
setCurrentIndex(undefined);
}}
onSave={(identifier) => {
if (currentIndex !== undefined) {
const updatedIdentifiers = [...identifiers];
updatedIdentifiers[currentIndex] = identifier;
onUpdate(updatedIdentifiers);
}
}}
/>
) : null}
</>
);
};

export default NodeResourceTable;
Loading

0 comments on commit 60d87b6

Please sign in to comment.