Repository: opendatahub-io/odh-dashboard
Pull request #3953: Improve validation for customized hardware profile size
Author: login: DaoDaoNoCode
Labels: name: lgtm
name: approved
Created: 2025-03-19T15:39:42Z | Merged: 2025-04-01T20:45:49Z

<!--- If this is a non-code change, this template is not required; reference any issues or top-level descriptions as needed -->
<!--- All code change PRs should relate to an issue, reference it here; see example below -->
<!--- https://issues.redhat.com/browse/RHOAIENG-123456 -->
JIRA: https://issues.redhat.com/browse/RHOAIENG-21738

## Description
<!--- Describe your changes in detail; the what, the why, any findings, etc -->
<!--- Include any screenshots of changed UI; Include any gifs if it was a flow / UX change -->
Improve the hardware profile validation error message.

1. When it's not within the range, show the unit
<img width="343" alt="Screenshot 2025-03-19 at 11 30 58 AM" src="https://github.com/user-attachments/assets/294254f4-618e-447a-bec0-db8ab2929f90" />

2. Allow users to input empty value, and show a different error message
<img width="343" alt="Screenshot 2025-03-19 at 11 38 05 AM" src="https://github.com/user-attachments/assets/2e14055e-bbee-4c3e-a16a-a6440fa3edb1" />

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
1. Go to anywhere you can customize a hardware profile size (like workbench spawner page or ilab fine-tune form)
2. Test the CPU and Memory fields, make sure it can show the error message correctly

## Test Impact
<!--- What tests have you done to cover the implemented functionality -->
<!--- If tests are not applicable, explain why here -->
Added some tests to verify the changes

## Request review criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->

Self checklist (all need to be checked):
- [x] The developer has manually tested the changes and verified that the changes work
- [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [x] The developer has added tests or explained why testing cannot be added (unit or cypress tests for related changes)

If you have UI changes: 
<!--- You can ignore these if you are doing manifest, backend, internal logic, etc changes; aka non-UI / visual changes -->
- [x] Included any necessary screenshots or gifs if it was a UI change.
- [ ] Included tags to the UX team if it was a UI/UX change.

After the PR is posted & before it merges:
- [ ] The developer has tested their solution on a cluster by using the image produced by the PR to `main`



Diff:
diff --git a/frontend/src/__tests__/cypress/cypress/pages/components/HardwareProfileSection.ts b/frontend/src/__tests__/cypress/cypress/pages/components/HardwareProfileSection.ts
index 8f48dc90e0..fc5b507658 100644
--- a/frontend/src/__tests__/cypress/cypress/pages/components/HardwareProfileSection.ts
+++ b/frontend/src/__tests__/cypress/cypress/pages/components/HardwareProfileSection.ts
@@ -47,12 +47,10 @@ export class HardwareProfileSection {
 
   verifyResourceValidation(label: string, value: string, errorMessage?: string): void {
     this.findCustomizeForm().within(() => {
-      cy.findByTestId(`${label}-input`).clear();
+      cy.findByTestId(`${label}-input`).findByLabelText('Input').clear();
       cy.findByTestId(`${label}-input`).type(`{moveToEnd}${value}`, { delay: 100 });
       if (errorMessage) {
         cy.findByText(errorMessage).should('exist');
-      } else {
-        cy.findByText('Value must be').should('not.exist');
       }
     });
   }
diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/manageHardwareProfiles.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/manageHardwareProfiles.cy.ts
index 25256600c8..111f54775d 100644
--- a/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/manageHardwareProfiles.cy.ts
+++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/manageHardwareProfiles.cy.ts
@@ -153,7 +153,7 @@ describe('Manage Hardware Profile', () => {
     editNodeResourceModal.findNodeResourceMinInput().type('3');
     editNodeResourceModal.findNodeResourceMinErrorMessage().should('exist');
     editNodeResourceModal.findNodeResourceMinInput().clear();
-    editNodeResourceModal.findNodeResourceMinErrorMessage().should('not.exist');
+    editNodeResourceModal.findNodeResourceMinErrorMessage().should('exist');
     editNodeResourceModal.findCancelButton().click();
 
     createHardwareProfile.getNodeResourceTableRow('memory').findEditAction().click();
@@ -167,7 +167,7 @@ describe('Manage Hardware Profile', () => {
     editNodeResourceModal.findNodeResourceMinInput().type('3');
     editNodeResourceModal.findNodeResourceMinErrorMessage().should('exist');
     editNodeResourceModal.findNodeResourceMinInput().clear();
-    editNodeResourceModal.findNodeResourceMinErrorMessage().should('not.exist');
+    editNodeResourceModal.findNodeResourceMinErrorMessage().should('exist');
     editNodeResourceModal.findCancelButton().click();
 
     createHardwareProfile.getNodeResourceTableRow('test-gpu').findEditAction().click();
diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/workbenchHardwareProfiles.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/workbenchHardwareProfiles.cy.ts
index 9d220b82ef..7002296502 100644
--- a/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/workbenchHardwareProfiles.cy.ts
+++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/hardwareProfiles/workbenchHardwareProfiles.cy.ts
@@ -189,13 +189,36 @@ describe('Workbench Hardware Profiles', () => {
     hardwareProfileSection.findCustomizeButton().click();
 
     // Test CPU validation
-    hardwareProfileSection.verifyResourceValidation('cpu-requests', '3', 'Must be at least 4');
-    hardwareProfileSection.verifyResourceValidation('cpu-requests', '1');
+    hardwareProfileSection.verifyResourceValidation(
+      'cpu-requests',
+      '3',
+      'Must be at least 4 Cores',
+    );
+    hardwareProfileSection.verifyResourceValidation('cpu-requests', '', 'CPU must be provided');
+    hardwareProfileSection.verifyResourceValidation('cpu-requests', '9', 'Must not exceed 8 Cores');
+    hardwareProfileSection.verifyResourceValidation('cpu-requests', '6');
+    hardwareProfileSection.verifyResourceValidation(
+      'cpu-limits',
+      '5',
+      'Limit must be greater than or equal to request',
+    );
 
     // Test Memory validation
-    hardwareProfileSection.verifyResourceValidation('memory-requests', '1', 'Must be at least 8Gi');
-    hardwareProfileSection.verifyResourceValidation('memory-requests', '5');
-    hardwareProfileSection.verifyResourceValidation('memory-requests', '3');
+    hardwareProfileSection.verifyResourceValidation(
+      'memory-requests',
+      '1',
+      'Must be at least 8 GiB',
+    );
+    hardwareProfileSection.verifyResourceValidation(
+      'memory-requests',
+      '',
+      'Memory must be provided',
+    );
+    hardwareProfileSection.verifyResourceValidation(
+      'memory-requests',
+      '17',
+      'Must not exceed 16 GiB',
+    );
   });
 
   it('should not display hardware profile selection when feature flag is disabled', () => {
diff --git a/frontend/src/components/ValueUnitField.tsx b/frontend/src/components/ValueUnitField.tsx
index 272bc4b698..b3324f02c6 100644
--- a/frontend/src/components/ValueUnitField.tsx
+++ b/frontend/src/components/ValueUnitField.tsx
@@ -57,7 +57,7 @@ const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
         <NumberInputWrapper
           min={minAsNumber}
           max={maxAsNumber}
-          value={currentValue}
+          value={Number(currentValue)}
           validated={validated}
           onBlur={
             onBlur &&
@@ -66,7 +66,7 @@ const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
             })
           }
           onChange={(value) => {
-            onChange(`${value ?? minAsNumber ?? ''}${currentUnitOption.unit}`);
+            onChange(`${value ?? ''}${currentUnitOption.unit}`);
           }}
           isDisabled={isDisabled}
           data-testid={dataTestId ? `${dataTestId}-input` : undefined}
@@ -99,7 +99,7 @@ const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
               <DropdownItem
                 key={option.unit}
                 onClick={() => {
-                  onChange(`${currentValue}${option.unit}`);
+                  onChange(`${currentValue ?? ''}${option.unit}`);
                   setOpen(false);
                 }}
               >
diff --git a/frontend/src/concepts/hardwareProfiles/HardwareProfileSelect.tsx b/frontend/src/concepts/hardwareProfiles/HardwareProfileSelect.tsx
index 36f20d7c1a..8de57080f8 100644
--- a/frontend/src/concepts/hardwareProfiles/HardwareProfileSelect.tsx
+++ b/frontend/src/concepts/hardwareProfiles/HardwareProfileSelect.tsx
@@ -73,11 +73,11 @@ const HardwareProfileSelect: React.FC<HardwareProfileSelectProps> = ({
             if (identifier.resourceType === IdentifierResourceType.CPU) {
               // Convert CPU to smallest unit for comparison
               const [value, unit] = splitValueUnit(maxValue.toString(), CPU_UNITS);
-              score += value * unit.weight;
+              score += (value ?? 0) * unit.weight;
             } else if (identifier.resourceType === IdentifierResourceType.MEMORY) {
               // Convert memory to smallest unit for comparison
               const [value, unit] = splitValueUnit(maxValue.toString(), MEMORY_UNITS_FOR_PARSING);
-              score += value * unit.weight;
+              score += (value ?? 0) * unit.weight;
             } else {
               score += Number(maxValue);
             }
diff --git a/frontend/src/concepts/hardwareProfiles/utils.ts b/frontend/src/concepts/hardwareProfiles/utils.ts
index 518054b4e9..ce66cebe5f 100644
--- a/frontend/src/concepts/hardwareProfiles/utils.ts
+++ b/frontend/src/concepts/hardwareProfiles/utils.ts
@@ -126,11 +126,11 @@ export const formatResourceValue = (
   switch (resourceType) {
     case IdentifierResourceType.CPU: {
       const [cpuValue, cpuUnit] = splitValueUnit(valueStr, CPU_UNITS);
-      return `${cpuValue} ${cpuUnit.name}`;
+      return `${cpuValue ?? ''} ${cpuUnit.name}`;
     }
     case IdentifierResourceType.MEMORY: {
       const [memoryValue, memoryUnit] = splitValueUnit(valueStr, MEMORY_UNITS_FOR_PARSING);
-      return `${memoryValue} ${memoryUnit.name}`;
+      return `${memoryValue ?? ''} ${memoryUnit.name}`;
     }
     default:
       return v;
diff --git a/frontend/src/concepts/hardwareProfiles/validationUtils.ts b/frontend/src/concepts/hardwareProfiles/validationUtils.ts
index 4df6a51cfa..ae93be64e8 100644
--- a/frontend/src/concepts/hardwareProfiles/validationUtils.ts
+++ b/frontend/src/concepts/hardwareProfiles/validationUtils.ts
@@ -6,6 +6,8 @@ import {
   ValueUnitString,
   determineUnit,
   splitValueUnit,
+  CPU_UNITS,
+  MEMORY_UNITS_FOR_PARSING,
 } from '~/utilities/valueUnits';
 import { hasCPUandMemory } from '~/pages/hardwareProfiles/manage/ManageNodeResourceSection';
 import { createIdentifierWarningMessage } from '~/pages/hardwareProfiles/utils';
@@ -29,16 +31,23 @@ export type ResourceSchema = z.ZodEffects<
 export const createCpuSchema = (minCount: ValueUnitCPU, maxCount?: ValueUnitCPU): ResourceSchema =>
   z.union([z.string(), z.number()]).superRefine((val, ctx) => {
     const stringVal = String(val);
+    const [value] = splitValueUnit(stringVal, CPU_UNITS);
+    if (value === undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: `CPU must be provided`,
+      });
+    }
     if (isCpuLimitLarger(stringVal, minCount)) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must be at least ${formatResourceValue(minCount)}`,
+        message: `Must be at least ${formatResourceValue(minCount, IdentifierResourceType.CPU)}`,
       });
     }
     if (maxCount && isCpuLimitLarger(maxCount, stringVal)) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must not exceed ${formatResourceValue(maxCount)}`,
+        message: `Must not exceed ${formatResourceValue(maxCount, IdentifierResourceType.CPU)}`,
       });
     }
   });
@@ -49,17 +58,23 @@ export const createMemorySchema = (
 ): ResourceSchema =>
   z.union([z.string(), z.number()]).superRefine((val, ctx) => {
     const stringVal = String(val);
-
+    const [value] = splitValueUnit(stringVal, MEMORY_UNITS_FOR_PARSING);
+    if (value === undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: `Memory must be provided`,
+      });
+    }
     if (isMemoryLimitLarger(stringVal, minCount)) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must be at least ${formatResourceValue(minCount)}`,
+        message: `Must be at least ${formatResourceValue(minCount, IdentifierResourceType.MEMORY)}`,
       });
     }
     if (isMemoryLimitLarger(maxCount, stringVal)) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must not exceed ${formatResourceValue(maxCount)}`,
+        message: `Must not exceed ${formatResourceValue(maxCount, IdentifierResourceType.MEMORY)}`,
       });
     }
   });
@@ -77,13 +92,19 @@ export const createNumericSchema = (minCount: number, maxCount: number): Resourc
     if (value < minCount) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must be at least ${formatResourceValue(minCount)}`,
+        message: `Must be at least ${formatResourceValue(
+          minCount,
+          IdentifierResourceType.ACCELERATOR,
+        )}`,
       });
     }
     if (value > maxCount) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
-        message: `Must not exceed ${formatResourceValue(maxCount)}`,
+        message: `Must not exceed ${formatResourceValue(
+          maxCount,
+          IdentifierResourceType.ACCELERATOR,
+        )}`,
       });
     }
   });
@@ -245,20 +266,34 @@ export const hardwareProfileWarningSchema = z
           //Exit to prevent errors from splitValueUnit
           continue;
         }
-        const [minCount] = splitValueUnit(
+        const minCount = splitValueUnit(
           identifier.minCount.toString(),
           determineUnit(identifier),
           true,
-        );
+        )[0];
         const [maxCount] = identifier.maxCount
           ? splitValueUnit(identifier.maxCount.toString(), determineUnit(identifier), true)
           : [undefined];
-        const [defaultCount] = splitValueUnit(
+        const defaultCount = splitValueUnit(
           identifier.defaultCount.toString(),
           determineUnit(identifier),
           true,
-        );
-        if (!Number.isInteger(minCount)) {
+        )[0];
+        if (minCount === undefined || defaultCount === undefined) {
+          ctx.addIssue({
+            code: z.ZodIssueCode.custom,
+            message: createIdentifierWarningMessage(
+              `The resource count for ${
+                identifier.resourceType ?? identifier.displayName
+              } has the unit only but doesn't have a value.`,
+              data.isDefault,
+            ),
+            params: {
+              type: HardwareProfileWarningType.OTHER,
+            },
+          });
+        }
+        if (minCount !== undefined && !Number.isInteger(minCount)) {
           ctx.addIssue({
             code: z.ZodIssueCode.custom,
             message: createIdentifierWarningMessage(
@@ -300,7 +335,7 @@ export const hardwareProfileWarningSchema = z
             },
           });
         }
-        if (maxCount !== undefined && minCount > maxCount) {
+        if (maxCount !== undefined && minCount !== undefined && minCount > maxCount) {
           ctx.addIssue({
             code: z.ZodIssueCode.custom,
             message: createIdentifierWarningMessage(
@@ -316,7 +351,10 @@ export const hardwareProfileWarningSchema = z
             },
           });
         }
-        if (defaultCount < minCount || (maxCount !== undefined && defaultCount > maxCount)) {
+        if (
+          (defaultCount !== undefined && minCount !== undefined && defaultCount < minCount) ||
+          (maxCount !== undefined && defaultCount !== undefined && defaultCount > maxCount)
+        ) {
           ctx.addIssue({
             code: z.ZodIssueCode.custom,
             message: createIdentifierWarningMessage(
diff --git a/frontend/src/pages/hardwareProfiles/__tests__/hardwareProfileWarning.spec.ts b/frontend/src/pages/hardwareProfiles/__tests__/hardwareProfileWarning.spec.ts
index 382bae10f5..e9d77862cc 100644
--- a/frontend/src/pages/hardwareProfiles/__tests__/hardwareProfileWarning.spec.ts
+++ b/frontend/src/pages/hardwareProfiles/__tests__/hardwareProfileWarning.spec.ts
@@ -121,7 +121,7 @@ describe('validateProfileWarning', () => {
           displayName: 'Memory',
           identifier: 'memory',
           resourceType: IdentifierResourceType.MEMORY,
-          minCount: 'Gi',
+          minCount: 'Invalid',
           maxCount: '5Gi',
           defaultCount: '2Gi',
         },
@@ -144,6 +144,38 @@ describe('validateProfileWarning', () => {
     ]);
   });
 
+  it('should generate warnings for missing identifier counts', () => {
+    const hardwareProfileMock = mockHardwareProfile({
+      uid: 'test-4',
+      enabled: false,
+      identifiers: [
+        {
+          displayName: 'Memory',
+          identifier: 'memory',
+          resourceType: IdentifierResourceType.MEMORY,
+          minCount: 'Gi',
+          maxCount: '5Gi',
+          defaultCount: '2Gi',
+        },
+        {
+          displayName: 'CPU',
+          identifier: 'cpu',
+          resourceType: IdentifierResourceType.CPU,
+          minCount: '1',
+          maxCount: '2',
+          defaultCount: '1',
+        },
+      ],
+    });
+    const hardwareProfilesResult = validateProfileWarning(hardwareProfileMock);
+    expect(hardwareProfilesResult).toEqual([
+      {
+        type: HardwareProfileWarningType.OTHER,
+        message: `The resource count for ${IdentifierResourceType.MEMORY} has the unit only but doesn't have a value. Edit the profile to make the profile valid.`,
+      },
+    ]);
+  });
+
   it('should generate warnings for decimal minimum and maximum count', () => {
     const hardwareProfileMock = mockHardwareProfile({
       uid: 'test-7',
diff --git a/frontend/src/pages/hardwareProfiles/nodeResource/CountFormField.tsx b/frontend/src/pages/hardwareProfiles/nodeResource/CountFormField.tsx
index f55c264cd4..c36684da9a 100644
--- a/frontend/src/pages/hardwareProfiles/nodeResource/CountFormField.tsx
+++ b/frontend/src/pages/hardwareProfiles/nodeResource/CountFormField.tsx
@@ -15,6 +15,7 @@ type CountFormFieldProps = {
   errorMessage?: string;
   isValid?: boolean;
   tooltip?: string;
+  isRequired?: boolean;
 };
 
 const CountFormField: React.FC<CountFormFieldProps> = ({
@@ -26,23 +27,38 @@ const CountFormField: React.FC<CountFormFieldProps> = ({
   errorMessage,
   isValid = true,
   tooltip,
+  isRequired,
 }) => {
   const renderInputField = () => {
+    const validated = isValid ? 'default' : 'error';
     switch (type) {
       case IdentifierResourceType.CPU:
-        return <CPUField onChange={(value) => setSize(value)} value={size} />;
+        return (
+          <CPUField
+            validated={validated}
+            onChange={(value) => setSize(value)}
+            value={size}
+            min={0}
+          />
+        );
       case IdentifierResourceType.MEMORY:
-        return <MemoryField onChange={(value) => setSize(value)} value={String(size)} />;
+        return (
+          <MemoryField
+            validated={validated}
+            onChange={(value) => setSize(value)}
+            value={size}
+            min={0}
+          />
+        );
       default:
         return (
           <NumberInputWrapper
+            validated={validated}
             min={0}
             value={Number(size)}
-            onChange={(value) => {
-              if (value) {
-                setSize(value);
-              }
-            }}
+            // If value is undefined, we cannot set it to empty string
+            // Because Number('') will be 0, then the field cannot be cleared
+            onChange={(value) => setSize(value ?? Number.NaN.toString())}
           />
         );
     }
@@ -54,6 +70,7 @@ const CountFormField: React.FC<CountFormFieldProps> = ({
       fieldId={fieldId}
       data-testid={`node-resource-size-${fieldId}`}
       labelHelp={tooltip ? <DashboardHelpTooltip content={tooltip} /> : undefined}
+      isRequired={isRequired}
     >
       {renderInputField()}
       {!isValid && errorMessage && (
diff --git a/frontend/src/pages/hardwareProfiles/nodeResource/ManageNodeResourceModal.tsx b/frontend/src/pages/hardwareProfiles/nodeResource/ManageNodeResourceModal.tsx
index 2d296fb59c..4604c360f2 100644
--- a/frontend/src/pages/hardwareProfiles/nodeResource/ManageNodeResourceModal.tsx
+++ b/frontend/src/pages/hardwareProfiles/nodeResource/ManageNodeResourceModal.tsx
@@ -6,7 +6,7 @@ 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';
+import { validateDefaultCount, validateMaxCount, validateMinCount } from './utils';
 
 type ManageNodeResourceModalProps = {
   onClose: () => void;
@@ -45,7 +45,9 @@ const ManageNodeResourceModal: React.FC<ManageNodeResourceModalProps> = ({
   }, [identifier]);
 
   const isValidCounts =
-    validateDefaultCount(identifier, unitOptions) && validateMinCount(identifier, unitOptions);
+    validateDefaultCount(identifier, unitOptions).isValid &&
+    validateMinCount(identifier, unitOptions).isValid &&
+    validateMaxCount(identifier, unitOptions).isValid;
 
   const isButtonDisabled =
     !identifier.displayName || !identifier.identifier || !isUniqueIdentifier || !isValidCounts;
diff --git a/frontend/src/pages/hardwareProfiles/nodeResource/NodeResourceForm.tsx b/frontend/src/pages/hardwareProfiles/nodeResource/NodeResourceForm.tsx
index 4a63b25f7f..b549aa7ccd 100644
--- a/frontend/src/pages/hardwareProfiles/nodeResource/NodeResourceForm.tsx
+++ b/frontend/src/pages/hardwareProfiles/nodeResource/NodeResourceForm.tsx
@@ -25,7 +25,12 @@ import {
   HARDWARE_PROFILE_COLUMN_HELP_TOOLTIP,
 } from '~/pages/hardwareProfiles/nodeResource/const';
 import DashboardHelpTooltip from '~/concepts/dashboard/DashboardHelpTooltip';
-import { validateDefaultCount, validateMinCount } from './utils';
+import {
+  getValidationMessage,
+  validateDefaultCount,
+  validateMaxCount,
+  validateMinCount,
+} from './utils';
 import CountFormField from './CountFormField';
 
 type NodeResourceFormProps = {
@@ -42,6 +47,10 @@ const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
   isUniqueIdentifier,
 }) => {
   const validated = isUniqueIdentifier ? 'default' : 'error';
+  const defaultCountValidation = validateDefaultCount(identifier, unitOptions);
+  const minCountValidation = validateMinCount(identifier, unitOptions);
+  const maxCountValidation = validateMaxCount(identifier, unitOptions);
+  const [isUnlimitedMax, setUnlimitedMax] = React.useState(false);
 
   return (
     <Form>
@@ -123,9 +132,10 @@ const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
         type={identifier.resourceType}
         size={identifier.defaultCount}
         setSize={(value) => setIdentifier('defaultCount', value)}
-        isValid={validateDefaultCount(identifier, unitOptions)}
-        errorMessage="Default must be equal to or between the minimum and maximum allowed limits."
+        isValid={defaultCountValidation.isValid}
+        errorMessage={getValidationMessage(defaultCountValidation.issues)}
         tooltip={HARDWARE_PROFILE_COLUMN_HELP_TOOLTIP.defaultCount}
+        isRequired
       />
 
       <CountFormField
@@ -134,9 +144,10 @@ const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
         type={identifier.resourceType}
         size={identifier.minCount}
         setSize={(value) => setIdentifier('minCount', value)}
-        isValid={validateMinCount(identifier, unitOptions)}
-        errorMessage="Minimum allowed value cannot exceed the maximum allowed value (if specified)."
+        isValid={minCountValidation.isValid}
+        errorMessage={getValidationMessage(minCountValidation.issues)}
         tooltip={HARDWARE_PROFILE_COLUMN_HELP_TOOLTIP.minCount}
+        isRequired
       />
 
       <FormGroup
@@ -152,8 +163,11 @@ const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
                   id="limited-radio"
                   name="max-limit"
                   label="Set maximum limit"
-                  isChecked={identifier.maxCount !== undefined}
-                  onChange={() => setIdentifier('maxCount', identifier.minCount || 1)}
+                  isChecked={!isUnlimitedMax}
+                  onChange={() => {
+                    setUnlimitedMax(false);
+                    setIdentifier('maxCount', identifier.minCount || 1);
+                  }}
                   data-testid="node-resource-max-limited"
                 />
               </SplitItem>
@@ -162,20 +176,25 @@ const NodeResourceForm: React.FC<NodeResourceFormProps> = ({
                   id="unlimited-radio"
                   name="max-limit"
                   label="No maximum limit"
-                  isChecked={!identifier.maxCount}
-                  onChange={() => setIdentifier('maxCount', undefined)}
+                  isChecked={isUnlimitedMax}
+                  onChange={() => {
+                    setUnlimitedMax(true);
+                    setIdentifier('maxCount', undefined);
+                  }}
                   data-testid="node-resource-max-unlimited"
                 />
               </SplitItem>
             </Split>
           </StackItem>
           <StackItem>
-            {identifier.maxCount !== undefined && (
+            {!isUnlimitedMax && (
               <CountFormField
                 fieldId="maximum-allowed"
                 type={identifier.resourceType}
-                size={identifier.maxCount}
+                size={identifier.maxCount ?? 1}
                 setSize={(value) => setIdentifier('maxCount', value)}
+                isValid={maxCountValidation.isValid}
+                errorMessage={getValidationMessage(maxCountValidation.issues)}
               />
             )}
           </StackItem>
diff --git a/frontend/src/pages/hardwareProfiles/nodeResource/__tests__/utils.spec.ts b/frontend/src/pages/hardwareProfiles/nodeResource/__tests__/utils.spec.ts
index dcb4fd2925..1326ac8258 100644
--- a/frontend/src/pages/hardwareProfiles/nodeResource/__tests__/utils.spec.ts
+++ b/frontend/src/pages/hardwareProfiles/nodeResource/__tests__/utils.spec.ts
@@ -1,11 +1,13 @@
 import { Identifier } from '~/types';
 import {
+  getValidationMessage,
   validateDefaultCount,
+  validateMaxCount,
   validateMinCount,
 } from '~/pages/hardwareProfiles/nodeResource/utils';
-import { MEMORY_UNITS_FOR_SELECTION } from '~/utilities/valueUnits';
+import { CPU_UNITS, MEMORY_UNITS_FOR_SELECTION } from '~/utilities/valueUnits';
 
-const identifier: Identifier = {
+const memoryIdentifier: Identifier = {
   displayName: 'test',
   identifier: 'test',
   defaultCount: '2Gi',
@@ -13,53 +15,543 @@ const identifier: Identifier = {
   maxCount: '4Gi',
 };
 
-describe('validateDefaultCount', () => {
-  it('should return true if defaultCount is between minCount and maxCount', () => {
-    const result = validateDefaultCount(identifier, MEMORY_UNITS_FOR_SELECTION);
-    expect(result).toBe(true);
+const cpuIdentifier: Identifier = {
+  displayName: 'test',
+  identifier: 'test',
+  defaultCount: '2',
+  minCount: '500m',
+  maxCount: '4',
+};
+
+const otherIdentifier: Identifier = {
+  displayName: 'test',
+  identifier: 'test',
+  defaultCount: '4',
+  minCount: '2',
+  maxCount: '8',
+};
+
+describe('CPU', () => {
+  describe('validateDefaultCount', () => {
+    it('should return true if defaultCount is between minCount and maxCount', () => {
+      const result = validateDefaultCount(cpuIdentifier, CPU_UNITS);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if defaultCount is less than minCount', () => {
+      const result = validateDefaultCount(
+        {
+          ...cpuIdentifier,
+          defaultCount: '200m',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return false if defaultCount is greater than maxCount', () => {
+      const result = validateDefaultCount(
+        {
+          ...cpuIdentifier,
+          defaultCount: '5',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateDefaultCount(
+        {
+          ...cpuIdentifier,
+          defaultCount: '5',
+          maxCount: undefined,
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if the field is empty', () => {
+      const result = validateDefaultCount(
+        {
+          ...cpuIdentifier,
+          defaultCount: 'm',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Default value must be provided.');
+    });
+
+    it('should return false if the field is zero', () => {
+      const result = validateDefaultCount(
+        {
+          ...cpuIdentifier,
+          defaultCount: '0',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Default value must be a positive number.');
+    });
+  });
+
+  describe('validateMinCount', () => {
+    it('should return true if minCount is less than maxCount', () => {
+      const result = validateMinCount(cpuIdentifier, CPU_UNITS);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is greater than maxCount', () => {
+      const result = validateMinCount(
+        {
+          ...cpuIdentifier,
+          minCount: '8',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMinCount(
+        {
+          ...cpuIdentifier,
+          minCount: '8',
+          maxCount: undefined,
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is empty', () => {
+      const result = validateMinCount(
+        {
+          ...cpuIdentifier,
+          minCount: 'm',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Minimum allowed value must be provided.');
+    });
+
+    it('should return false if minCount is zero', () => {
+      const result = validateMinCount(
+        {
+          ...cpuIdentifier,
+          minCount: '0m',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Minimum allowed value must be a positive number.',
+      );
+    });
+  });
+
+  describe('validateMaxCount', () => {
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMaxCount(
+        {
+          ...cpuIdentifier,
+          maxCount: undefined,
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if maxCount is empty', () => {
+      const result = validateMaxCount(
+        {
+          ...cpuIdentifier,
+          maxCount: 'm',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Maximum allowed value must be provided.');
+    });
+
+    it('should return false if maxCount is zero', () => {
+      const result = validateMaxCount(
+        {
+          ...cpuIdentifier,
+          maxCount: '0m',
+        },
+        CPU_UNITS,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Maximum allowed value must be a positive number.',
+      );
+    });
+  });
+});
+
+describe('Memory', () => {
+  describe('validateDefaultCount', () => {
+    it('should return true if defaultCount is between minCount and maxCount', () => {
+      const result = validateDefaultCount(memoryIdentifier, MEMORY_UNITS_FOR_SELECTION);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if defaultCount is less than minCount', () => {
+      const result = validateDefaultCount(
+        {
+          ...memoryIdentifier,
+          defaultCount: '512Mi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return false if defaultCount is greater than maxCount', () => {
+      const result = validateDefaultCount(
+        {
+          ...memoryIdentifier,
+          defaultCount: '8Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateDefaultCount(
+        {
+          ...memoryIdentifier,
+          defaultCount: '8Gi',
+          maxCount: undefined,
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if the field is empty', () => {
+      const result = validateDefaultCount(
+        {
+          ...memoryIdentifier,
+          defaultCount: 'Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Default value must be provided.');
+    });
+
+    it('should return false if the field is zero', () => {
+      const result = validateDefaultCount(
+        {
+          ...memoryIdentifier,
+          defaultCount: '0Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Default value must be a positive number.');
+    });
   });
 
-  it('should return false if defaultCount is less than minCount', () => {
-    const result = validateDefaultCount(
-      {
-        ...identifier,
-        defaultCount: '512Mi',
-      },
-      MEMORY_UNITS_FOR_SELECTION,
-    );
+  describe('validateMinCount', () => {
+    it('should return true if minCount is less than maxCount', () => {
+      const result = validateMinCount(memoryIdentifier, MEMORY_UNITS_FOR_SELECTION);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is greater than maxCount', () => {
+      const result = validateMinCount(
+        {
+          ...memoryIdentifier,
+          minCount: '8Gi',
+          maxCount: '4Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMinCount(
+        {
+          ...memoryIdentifier,
+          minCount: '8Gi',
+          maxCount: undefined,
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is empty', () => {
+      const result = validateMinCount(
+        {
+          ...memoryIdentifier,
+          minCount: 'Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
 
-    expect(result).toBe(false);
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Minimum allowed value must be provided.');
+    });
+
+    it('should return false if minCount is zero', () => {
+      const result = validateMinCount(
+        {
+          ...memoryIdentifier,
+          minCount: '0Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Minimum allowed value must be a positive number.',
+      );
+    });
   });
 
-  it('should return false if defaultCount is greater than maxCount', () => {
-    const result = validateDefaultCount(
-      {
-        ...identifier,
-        defaultCount: '8Gi',
-      },
-      MEMORY_UNITS_FOR_SELECTION,
-    );
+  describe('validateMaxCount', () => {
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMaxCount(
+        {
+          ...memoryIdentifier,
+          maxCount: undefined,
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if maxCount is empty', () => {
+      const result = validateMaxCount(
+        {
+          ...memoryIdentifier,
+          maxCount: 'Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Maximum allowed value must be provided.');
+    });
 
-    expect(result).toBe(false);
+    it('should return false if maxCount is zero', () => {
+      const result = validateMaxCount(
+        {
+          ...memoryIdentifier,
+          maxCount: '0Gi',
+        },
+        MEMORY_UNITS_FOR_SELECTION,
+      );
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Maximum allowed value must be a positive number.',
+      );
+    });
   });
 });
 
-describe('validateMinCount', () => {
-  it('should return true if minCount is less than maxCount', () => {
-    const result = validateMinCount(identifier, MEMORY_UNITS_FOR_SELECTION);
-    expect(result).toBe(true);
+describe('Other', () => {
+  describe('validateDefaultCount', () => {
+    it('should return true if defaultCount is between minCount and maxCount', () => {
+      const result = validateDefaultCount(otherIdentifier);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if defaultCount is less than minCount', () => {
+      const result = validateDefaultCount({
+        ...otherIdentifier,
+        defaultCount: '1',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return false if defaultCount is greater than maxCount', () => {
+      const result = validateDefaultCount({
+        ...otherIdentifier,
+        defaultCount: '9',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Default value must be equal to or between the minimum and maximum allowed limits.',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateDefaultCount({
+        ...otherIdentifier,
+        defaultCount: '9',
+        maxCount: undefined,
+      });
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if the field is empty', () => {
+      const result = validateDefaultCount({
+        ...otherIdentifier,
+        defaultCount: 'undefined',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a number.');
+    });
+
+    it('should return false if the field is zero', () => {
+      const result = validateDefaultCount({
+        ...otherIdentifier,
+        defaultCount: '0',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a positive number.');
+    });
   });
 
-  it('should return false if minCount is greater than maxCount', () => {
-    const result = validateMinCount(
-      {
-        ...identifier,
-        minCount: '8Gi',
-        maxCount: '4Gi',
-      },
-      MEMORY_UNITS_FOR_SELECTION,
-    );
-
-    expect(result).toBe(false);
+  describe('validateMinCount', () => {
+    it('should return true if minCount is less than maxCount', () => {
+      const result = validateMinCount(otherIdentifier);
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is greater than maxCount', () => {
+      const result = validateMinCount({
+        ...otherIdentifier,
+        minCount: '9',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe(
+        'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
+      );
+    });
+
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMinCount({
+        ...otherIdentifier,
+        minCount: '9',
+        maxCount: undefined,
+      });
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if minCount is empty', () => {
+      const result = validateMinCount({
+        ...otherIdentifier,
+        minCount: 'undefined',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a number.');
+    });
+
+    it('should return false if minCount is zero', () => {
+      const result = validateMinCount({
+        ...otherIdentifier,
+        minCount: '0',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a positive number.');
+    });
+  });
+
+  describe('validateMaxCount', () => {
+    it('should return true if maxCount is not defined', () => {
+      const result = validateMaxCount({
+        ...otherIdentifier,
+        maxCount: undefined,
+      });
+
+      expect(result.isValid).toBe(true);
+      expect(getValidationMessage(result.issues)).toBeUndefined();
+    });
+
+    it('should return false if maxCount is empty', () => {
+      const result = validateMaxCount({
+        ...otherIdentifier,
+        maxCount: 'undefined',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a number.');
+    });
+
+    it('should return false if maxCount is zero', () => {
+      const result = validateMaxCount({
+        ...otherIdentifier,
+        maxCount: '0',
+      });
+
+      expect(result.isValid).toBe(false);
+      expect(getValidationMessage(result.issues)).toBe('Value must be a positive number.');
+    });
   });
 });
diff --git a/frontend/src/pages/hardwareProfiles/nodeResource/utils.ts b/frontend/src/pages/hardwareProfiles/nodeResource/utils.ts
index b04e923694..0eacb8efca 100644
--- a/frontend/src/pages/hardwareProfiles/nodeResource/utils.ts
+++ b/frontend/src/pages/hardwareProfiles/nodeResource/utils.ts
@@ -1,63 +1,194 @@
-import { z } from 'zod';
+import { z, ZodIssue } from 'zod';
 import { Identifier } from '~/types';
-import { isLarger, UnitOption } from '~/utilities/valueUnits';
+import { isLarger, splitValueUnit, UnitOption } from '~/utilities/valueUnits';
 
 const defaultCountSchema = (identifier: Identifier, unitOptions?: UnitOption[]) =>
-  z.union([z.string(), z.number()]).refine(
-    (defaultCount) => {
-      if (unitOptions) {
-        const isDefaultAboveMin = isLarger(
-          String(defaultCount),
-          String(identifier.minCount),
-          unitOptions,
-          true,
-        );
-        const isDefaultBelowMax =
-          identifier.maxCount === undefined ||
-          isLarger(String(identifier.maxCount), String(defaultCount), unitOptions, true);
-        return isDefaultAboveMin && isDefaultBelowMax;
-      }
-
+  z.union([z.string(), z.number()]).superRefine((defaultCount, ctx) => {
+    if (!unitOptions) {
       const defaultVal = Number(defaultCount);
+      if (Number.isNaN(defaultVal)) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a number.',
+        });
+        return;
+      }
+      if (defaultVal === 0) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a positive number.',
+        });
+        return;
+      }
       const minCount = Number(identifier.minCount);
       const maxCount = identifier.maxCount !== undefined ? Number(identifier.maxCount) : undefined;
 
-      return defaultVal >= minCount && (maxCount === undefined || defaultVal <= maxCount);
-    },
-    {
-      message: 'Default must be equal to or between the minimum and maximum allowed limits.',
-    },
-  );
-
-const minCountSchema = (identifier: Identifier, unitOptions?: UnitOption[]) =>
-  z.union([z.string(), z.number()]).refine(
-    (minCount) => {
-      if (unitOptions) {
-        return (
-          identifier.maxCount === undefined ||
-          isLarger(String(identifier.maxCount), String(minCount), unitOptions, true)
-        );
+      if (defaultVal < minCount || (maxCount !== undefined && defaultVal > maxCount)) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message:
+            'Default value must be equal to or between the minimum and maximum allowed limits.',
+        });
       }
+      return;
+    }
+    const [defaultVal] = splitValueUnit(String(defaultCount), unitOptions);
+    const [minVal] = splitValueUnit(String(identifier.minCount), unitOptions);
+    const maxVal =
+      identifier.maxCount !== undefined
+        ? splitValueUnit(String(identifier.maxCount), unitOptions)[0]
+        : undefined;
+    if (defaultVal === undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Default value must be provided.',
+      });
+      return;
+    }
+    if (defaultVal === 0) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Default value must be a positive number.',
+      });
+      return;
+    }
+    if (
+      (minVal !== undefined &&
+        isLarger(String(identifier.minCount), String(defaultCount), unitOptions)) ||
+      (maxVal !== undefined &&
+        isLarger(String(defaultCount), String(identifier.maxCount), unitOptions))
+    ) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message:
+          'Default value must be equal to or between the minimum and maximum allowed limits.',
+      });
+    }
+  });
 
+const minCountSchema = (identifier: Identifier, unitOptions?: UnitOption[]) =>
+  z.union([z.string(), z.number()]).superRefine((minCount, ctx) => {
+    if (!unitOptions) {
       const minVal = Number(minCount);
+      if (Number.isNaN(minVal)) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a number.',
+        });
+        return;
+      }
+      if (minVal === 0) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a positive number.',
+        });
+        return;
+      }
       const maxCount = identifier.maxCount !== undefined ? Number(identifier.maxCount) : undefined;
+      if (maxCount !== undefined && minVal > maxCount) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
+        });
+      }
+      return;
+    }
+    const [minVal] = splitValueUnit(String(minCount), unitOptions);
+    const maxVal =
+      identifier.maxCount !== undefined
+        ? splitValueUnit(String(identifier.maxCount), unitOptions)[0]
+        : undefined;
+    if (minVal === undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Minimum allowed value must be provided.',
+      });
+      return;
+    }
+    if (minVal === 0) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Minimum allowed value must be a positive number.',
+      });
+      return;
+    }
+    if (
+      maxVal !== undefined &&
+      isLarger(String(minCount), String(identifier.maxCount), unitOptions)
+    ) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
+      });
+    }
+  });
 
-      return maxCount === undefined || minVal <= maxCount;
-    },
-    {
-      message: 'Minimum allowed value cannot exceed the maximum allowed value (if specified).',
-    },
-  );
+const maxCountSchema = (unitOptions?: UnitOption[]) =>
+  z.union([z.string(), z.number()]).superRefine((maxCount, ctx) => {
+    if (!unitOptions) {
+      const maxVal = Number(maxCount);
+      if (Number.isNaN(maxVal)) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a number.',
+        });
+        return;
+      }
+      if (maxVal === 0) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: 'Value must be a positive number.',
+        });
+        return;
+      }
+      return;
+    }
+    const [maxVal] = splitValueUnit(String(maxCount), unitOptions);
+    if (maxVal === undefined) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Maximum allowed value must be provided.',
+      });
+      return;
+    }
+    if (maxVal === 0) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        message: 'Maximum allowed value must be a positive number.',
+      });
+    }
+  });
 
 export const validateDefaultCount = (
   identifier: Identifier,
   unitOptions?: UnitOption[],
-): boolean => {
+): { isValid: boolean; issues: ZodIssue[] | undefined } => {
   const result = defaultCountSchema(identifier, unitOptions).safeParse(identifier.defaultCount);
-  return result.success;
+  return { isValid: result.success, issues: result.error?.issues };
 };
 
-export const validateMinCount = (identifier: Identifier, unitOptions?: UnitOption[]): boolean => {
+export const validateMinCount = (
+  identifier: Identifier,
+  unitOptions?: UnitOption[],
+): { isValid: boolean; issues: ZodIssue[] | undefined } => {
   const result = minCountSchema(identifier, unitOptions).safeParse(identifier.minCount);
-  return result.success;
+  return { isValid: result.success, issues: result.error?.issues };
 };
+
+export const validateMaxCount = (
+  identifier: Identifier,
+  unitOptions?: UnitOption[],
+): { isValid: boolean; issues: ZodIssue[] | undefined } => {
+  if (identifier.maxCount === undefined) {
+    return { isValid: true, issues: undefined };
+  }
+  const result = maxCountSchema(unitOptions).safeParse(identifier.maxCount);
+  return {
+    isValid: result.success,
+    issues: result.error?.issues,
+  };
+};
+
+export const getValidationMessage = (
+  validationIssues: ZodIssue[] | undefined,
+): string | undefined => validationIssues?.map((issue) => issue.message).join(', ');
diff --git a/frontend/src/pages/hardwareProfiles/utils.ts b/frontend/src/pages/hardwareProfiles/utils.ts
index c4aa27723c..6146292424 100644
--- a/frontend/src/pages/hardwareProfiles/utils.ts
+++ b/frontend/src/pages/hardwareProfiles/utils.ts
@@ -98,26 +98,26 @@ export const isHardwareProfileIdentifierValid = (identifier: Identifier): boolea
     ) {
       return false;
     }
-    const [minCount] = splitValueUnit(
+    const minCount = splitValueUnit(
       identifier.minCount.toString(),
       determineUnit(identifier),
       true,
-    );
+    )[0];
     const [maxCount] = identifier.maxCount
       ? splitValueUnit(identifier.maxCount.toString(), determineUnit(identifier), true)
       : [undefined];
-    const [defaultCount] = splitValueUnit(
+    const defaultCount = splitValueUnit(
       identifier.defaultCount.toString(),
       determineUnit(identifier),
       true,
-    );
+    )[0];
     if (
       !Number.isInteger(minCount) ||
       (maxCount !== undefined && !Number.isInteger(maxCount)) ||
       !Number.isInteger(defaultCount) ||
-      (maxCount && minCount > maxCount) ||
-      defaultCount < minCount ||
-      (maxCount && defaultCount > maxCount)
+      (maxCount && minCount !== undefined && minCount > maxCount) ||
+      (defaultCount !== undefined && minCount !== undefined && defaultCount < minCount) ||
+      (maxCount && defaultCount !== undefined && defaultCount > maxCount)
     ) {
       return false;
     }
diff --git a/frontend/src/utilities/__tests__/valueUnits.spec.ts b/frontend/src/utilities/__tests__/valueUnits.spec.ts
index 67f3a1abbf..3abbff0776 100644
--- a/frontend/src/utilities/__tests__/valueUnits.spec.ts
+++ b/frontend/src/utilities/__tests__/valueUnits.spec.ts
@@ -27,6 +27,9 @@ describe('splitValueUnit', () => {
   it('should default back to base if unable to match a legit value', () => {
     expect(splitValueUnit('', options)).toEqual([1, options[0]]);
   });
+  it('should return undefined as the value and unit if matches unit only', () => {
+    expect(splitValueUnit('unit', options)).toEqual([undefined, options[0]]);
+  });
   it('should return the value and unit', () => {
     expect(splitValueUnit('1unit', options)).toEqual([1, options[0]]);
     expect(splitValueUnit('1name', options)).toEqual([1, options[0]]);
diff --git a/frontend/src/utilities/valueUnits.ts b/frontend/src/utilities/valueUnits.ts
index 9d85d52d55..3be2ed75cf 100644
--- a/frontend/src/utilities/valueUnits.ts
+++ b/frontend/src/utilities/valueUnits.ts
@@ -68,7 +68,12 @@ export const splitValueUnit = (
   value: ValueUnitString,
   options: UnitOption[],
   strict = false,
-): [value: number, unit: UnitOption] => {
+): [value: number | undefined, unit: UnitOption] => {
+  // If value is empty string
+  const unitOnly = options.find((o) => o.unit === value);
+  if (unitOnly) {
+    return [undefined, unitOnly];
+  }
   const match = value.match(/^(\d*\.?\d*)(.*)$/);
   if (!(match && match[1])) {
     if (strict) {
@@ -96,9 +101,9 @@ export const convertToUnit = (
   const targetUnit = options.find(({ unit }) => unit === targetUnitStr);
   const lowestUnit = options.find(({ weight }) => weight === 1);
   if (!targetUnit || !lowestUnit) {
-    return [parsedValue, parsedUnit];
+    return [parsedValue ?? 0, parsedUnit];
   }
-  const valueInLowestUnit = parsedValue * parsedUnit.weight;
+  const valueInLowestUnit = (parsedValue ?? 0) * parsedUnit.weight;
   const valueInTargetUnit = valueInLowestUnit / targetUnit.weight;
   return [valueInTargetUnit, targetUnit];
 };
@@ -110,7 +115,7 @@ const calculateDelta = (
 ): number => {
   const [val1, unit1] = splitValueUnit(value1, units);
   const [val2, unit2] = splitValueUnit(value2, units);
-  return val1 * unit1.weight - val2 * unit2.weight;
+  return (val1 ?? 0) * unit1.weight - (val2 ?? 0) * unit2.weight;
 };
 
 export const isEqual = (


Reviews:
user: login: rsun19
body: @DaoDaoNoCode Should we also apply these changes to the `Edit node resource` form under hardware profiles? It is still forcing a minimum value there. 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:16:33Z
user: login: rsun19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T17:58:35Z
user: login: DaoDaoNoCode
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T18:01:06Z
user: login: Gkrumbach07
body: ![Screenshot 2025-04-01 at 3 23 41 PM](https://github.com/user-attachments/assets/71138a44-9b8a-4296-a268-b5b8cc2cf2b1)

/lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-01T20:24:02Z

Review comments:
diff_hunk: @@ -66,7 +66,7 @@ const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
             })
path: frontend/src/components/ValueUnitField.tsx
user: login: rsun19
body: Unrelated, but should we change the `Math.max()` to `Number()` on line 65? It seems weird to use the `max` method just for the purpose of number conversion.
created_at: 2025-04-01T17:46:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,7 +66,7 @@ const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
             })
path: frontend/src/components/ValueUnitField.tsx
user: login: DaoDaoNoCode
body: Interesting, I didn't even notice this, let me double check
created_at: 2025-04-01T18:01:06Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T15:49:49Z
body: ## [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.87%. Comparing base [(`8274796`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/8274796f5e79bf689b54f21910d9e02ba4cd1cea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) to head [(`a32a357`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/a32a3572bd35c83e186bebe6ed5ef8a0e964d988?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> Report is 30 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953/graphs/tree.svg?width=650&height=150&src=pr&token=UQM7NLQUXJ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)

```diff
@@            Coverage Diff             @@
##             main    #3953      +/-   ##
==========================================
+ Coverage   84.85%   84.87%   +0.01%     
==========================================
  Files        1568     1568              
  Lines       37436    37531      +95     
  Branches    10577    10621      +44     
==========================================
+ Hits        31766    31854      +88     
- Misses       5670     5677       +7     
```

[see 17 files with indirect coverage changes](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Last update [8274796...a32a357](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/3953?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: DaoDaoNoCode
created_at: 2025-03-20T14:49:07Z
body: @rsun19 I will take a look into this. The edit node resource modal is not using zod validation, I might need to take a closer look at how to make the changes.
author_association: MEMBER
user: login: DaoDaoNoCode
created_at: 2025-03-21T14:25:16Z
body: Updated Manage node resources modal:

Field empty:
<img width="1512" alt="Screenshot 2025-03-21 at 10 23 24 AM" src="https://github.com/user-attachments/assets/7022c678-1295-472b-be57-9532fc527c06" />

Field is zero:
<img width="1512" alt="Screenshot 2025-03-21 at 10 24 09 AM" src="https://github.com/user-attachments/assets/0269d07a-5bf1-4146-97dc-689c626a5614" />

Field exceeds limit
<img width="1512" alt="Screenshot 2025-03-21 at 10 24 19 AM" src="https://github.com/user-attachments/assets/6478792e-cb86-4087-9437-b439915128f8" />

No limit:
<img width="1512" alt="Screenshot 2025-03-21 at 10 24 57 AM" src="https://github.com/user-attachments/assets/a446ce0c-203b-4048-8628-ebc1abdf60ec" />


author_association: MEMBER
user: login: rsun19
created_at: 2025-04-01T19:17:24Z
body: Everything else lgtm
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-04-01T20:24:08Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/opendatahub-io/odh-dashboard/pull/3953#pullrequestreview-2733965642" title="LGTM">Gkrumbach07</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=opendatahub-io%2Fodh-dashboard).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[frontend/OWNERS](https://github.com/opendatahub-io/odh-dashboard/blob/main/frontend/OWNERS)~~ [Gkrumbach07]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR