Repository: linode/manager
Pull request #12207: fix: [M3-9668, M3-9538] -  Fixed Formatting of the `volume status` and broken spacing in `MultipleIPInput` component
Author: login: hasyed-akamai
Labels: name: Bug
description: Fixes for regressions or bugs
name: Approved
description: Multiple approvals and ready to merge!
Created: 2025-05-13T07:20:29Z | Merged: 2025-05-16T05:47:22Z

## Description 📝
This PR fixes the following bugs:

- Incorrect formatting of the volume status for the `Key Formatting` status
- Broken spacing in the `MultipleIPInput` component

## Changes 🔄
- Fixed status formatting logic for volume status related to `Key Formatting`
- Corrected layout and spacing issues in the `MultipleIPInput` component

## Target Release Date 🗓️
N/A

## Preview  
| Before | After |
|--------|-------|
| ![Before](https://github.com/user-attachments/assets/a2a40b91-3508-42b3-9f44-19df9ae3d235) | ![After](https://github.com/user-attachments/assets/edab2115-b99f-440f-9b91-4c5ee8a81ec1) |
| ![Before](https://github.com/user-attachments/assets/21432c8a-ef57-41a7-bc0f-46f7eeadbaa7) | ![After](https://github.com/user-attachments/assets/9c2fb273-5cb7-4078-8632-781aae6fa156) |

## How to Test
- For Volume Status 
  - The GET request for /volumes in serverHandlers.ts can be updated to include "key_rotating" in the statuses array. Then visit the Volumes landing page with the MSW on.
- For MultipleIPInput 
  - go to `/kubernetes/create` and enable `Control Plane ACL` and verify broken spacing in `MultipleIPInput`.

## Verification steps
- Verify volume status displays the correct label for the `Key Formatting` state in the Volumes section
- Test the `MultipleIPInput` component and confirm consistent spacing and layout
- Confirm all tests pass (CI should be green)
- Confirm no new visual or functional regressions in related components

<details>
<summary> Author Checklist </summary>

## As an Author, to speed up the review process, I considered 🤔

👀 Doing a self-review  
❔ Our [contribution guidelines](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md)  
🤏 Splitting features into small PRs  
➕ Adding a [changeset](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md#writing-a-changeset)  
🧪 Providing/improving test coverage  
🔐 Removing all sensitive information from the code and PR description  
🚩 Using a feature flag to protect the release  
👣 Providing comprehensive reproduction steps  
📑 Providing or updating our documentation  
🕛 Scheduling a pair reviewing session  
📱 Providing mobile support  
♿ Providing accessibility support  

</details>

- [x] I have read and considered all applicable items listed above.

## As an Author, before moving this PR from Draft to Open, I confirmed ✅

- [x] All unit tests are passing  
- [x] TypeScript compilation succeeded without errors  
- [x] Code passes all linting rules  



Diff:
diff --git a/packages/manager/.changeset/pr-12207-fixed-1747120917933.md b/packages/manager/.changeset/pr-12207-fixed-1747120917933.md
new file mode 100644
index 00000000000..498774f077b
--- /dev/null
+++ b/packages/manager/.changeset/pr-12207-fixed-1747120917933.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Fixed
+---
+
+Formatting of the volume status and broken spacing in MultipleIPInput component ([#12207](https://github.com/linode/manager/pull/12207))
diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts
index 8288734266a..37f33e292f3 100644
--- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts
+++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts
@@ -49,7 +49,7 @@ describe('volume clone flow', () => {
           .should('be.visible')
           .closest('tr')
           .within(() => {
-            cy.findByText('active').should('be.visible');
+            cy.findByText('Active').should('be.visible');
             cy.findByLabelText(
               `Action menu for Volume ${volume.label}`
             ).click();
diff --git a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts
index a34ca3cc7ec..a197a9f376b 100644
--- a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts
+++ b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts
@@ -68,7 +68,7 @@ describe('volume resize flow', () => {
           .should('be.visible')
           .closest('tr')
           .within(() => {
-            cy.findByText('active').should('be.visible');
+            cy.findByText('Active').should('be.visible');
             cy.findByText(`${oldSize} GB`).should('be.visible');
             cy.findByLabelText(
               `Action menu for Volume ${volume.label}`
diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts
index 9b1b021897c..78ffb0dbdc7 100644
--- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts
+++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts
@@ -45,7 +45,7 @@ describe('volume update flow', () => {
           .should('be.visible')
           .closest('tr')
           .within(() => {
-            cy.findByText('active').should('be.visible');
+            cy.findByText('Active').should('be.visible');
           });
         ui.actionMenu
           .findByTitle(`Action menu for Volume ${volume.label}`)
@@ -106,7 +106,7 @@ describe('volume update flow', () => {
           .should('be.visible')
           .closest('tr')
           .within(() => {
-            cy.findByText('active').should('be.visible');
+            cy.findByText('Active').should('be.visible');
           });
 
         ui.actionMenu
diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts
index f6ff9e71a2d..5e3cc477dd7 100644
--- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts
+++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts
@@ -78,7 +78,7 @@ describe('volume upgrade/migration', () => {
 
       cy.wait('@getEvents');
 
-      cy.findByText(`migrating (${percentage}%)`).should('be.visible');
+      cy.findByText(`Migrating (${percentage}%)`).should('be.visible');
     }
 
     const mockFinishedMigrationEvent = eventFactory.build({
@@ -94,7 +94,7 @@ describe('volume upgrade/migration', () => {
 
     mockGetEvents([]);
 
-    cy.findByText('active').should('be.visible');
+    cy.findByText('Active').should('be.visible');
 
     ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
   });
@@ -173,7 +173,7 @@ describe('volume upgrade/migration', () => {
 
       cy.wait('@getEvents');
 
-      cy.findByText(`migrating (${percentage}%)`).should('be.visible');
+      cy.findByText(`Migrating (${percentage}%)`).should('be.visible');
     }
 
     const mockFinishedMigrationEvent = eventFactory.build({
@@ -189,7 +189,7 @@ describe('volume upgrade/migration', () => {
 
     mockGetEvents([]);
 
-    cy.findByText('active').should('be.visible');
+    cy.findByText('Active').should('be.visible');
 
     ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
   });
@@ -264,7 +264,7 @@ describe('volume upgrade/migration', () => {
 
       cy.wait('@getEvents');
 
-      cy.findByText(`migrating (${percentage}%)`).should('be.visible');
+      cy.findByText(`Migrating (${percentage}%)`).should('be.visible');
     }
 
     const mockFinishedMigrationEvent = eventFactory.build({
@@ -280,7 +280,7 @@ describe('volume upgrade/migration', () => {
 
     mockGetEvents([]);
 
-    cy.findByText('active').should('be.visible');
+    cy.findByText('Active').should('be.visible');
 
     ui.toast.assertMessage(`Volume ${volume.label} has been migrated to NVMe.`);
   });
diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx
index 08a5b00bf1b..c6a66c6a3aa 100644
--- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx
+++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx
@@ -1,8 +1,10 @@
 import {
   Button,
   CloseIcon,
+  IconButton,
   InputLabel,
   Notice,
+  Stack,
   TextField,
   TooltipIcon,
   Typography,
@@ -245,54 +247,58 @@ export const MultipleIPInput = React.memo((props: MultipeIPInputProps) => {
         <Typography className={classes.helperText}>{helperText}</Typography>
       )}
       {error && <Notice spacingTop={8} text={error} variant="error" />}
-      {ips.map((thisIP, idx) => (
-        <Grid
-          container
-          data-testid="domain-transfer-input"
-          direction="row"
-          key={`domain-transfer-ip-${idx}`}
-          spacing={2}
-          sx={{
-            justifyContent: 'center',
-            maxWidth: forVPCIPv4Ranges ? '415px' : undefined,
-          }}
-        >
-          <Grid size={11}>
-            <TextField
-              className={classes.input}
-              errorText={thisIP.error}
-              hideLabel
-              InputProps={{
-                'aria-label': `${title} ip-address-${idx}`,
-                disabled,
-                ...props.inputProps,
-              }}
-              // Prevent unique ID errors, since TextField sets the input element's ID to the label
-              label={`domain-transfer-ip-${idx}`}
-              onBlur={(e) => handleBlur(e, idx)}
-              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
-                handleChange(e, idx)
-              }
-              placeholder={placeholder}
-              value={thisIP.address}
-            />
-          </Grid>
-          {/** Don't show the button for the first input since it won't do anything, unless this component is
-           * used in DBaaS or for Linode VPC interfaces
-           */}
-          <Grid size={1}>
-            {(idx > 0 || forDatabaseAccessControls || forVPCIPv4Ranges) && (
-              <Button
-                className={classes.button}
-                disabled={disabled}
-                onClick={() => removeInput(idx)}
-              >
-                <CloseIcon data-testid={`delete-ip-${idx}`} />
-              </Button>
-            )}
+      <Stack spacing={1}>
+        {ips.map((thisIP, idx) => (
+          <Grid
+            container
+            data-testid="domain-transfer-input"
+            direction="row"
+            key={`domain-transfer-ip-${idx}`}
+            spacing={2}
+            sx={{
+              justifyContent: 'center',
+              maxWidth: forVPCIPv4Ranges ? '415px' : undefined,
+            }}
+          >
+            <Grid size={11}>
+              <TextField
+                className={classes.input}
+                errorText={thisIP.error}
+                hideLabel
+                InputProps={{
+                  'aria-label': `${title} ip-address-${idx}`,
+                  disabled,
+                  ...props.inputProps,
+                }}
+                // Prevent unique ID errors, since TextField sets the input element's ID to the label
+                label={`domain-transfer-ip-${idx}`}
+                onBlur={(e) => handleBlur(e, idx)}
+                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
+                  handleChange(e, idx)
+                }
+                placeholder={placeholder}
+                value={thisIP.address}
+              />
+            </Grid>
+            {/** Don't show the button for the first input since it won't do anything, unless this component is
+             * used in DBaaS or for Linode VPC interfaces
+             */}
+            <Grid size={1}>
+              {(idx > 0 || forDatabaseAccessControls || forVPCIPv4Ranges) && (
+                <IconButton
+                  aria-disabled={disabled}
+                  className={classes.button}
+                  data-testid="button"
+                  disabled={disabled}
+                  onClick={() => removeInput(idx)}
+                >
+                  <CloseIcon data-testid={`delete-ip-${idx}`} />
+                </IconButton>
+              )}
+            </Grid>
           </Grid>
-        </Grid>
-      ))}
+        ))}
+      </Stack>
       {addIPButton}
     </div>
   );
diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx
index f3ae90e98fa..86d7b7c05b6 100644
--- a/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx
+++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTableRow.tsx
@@ -1,7 +1,7 @@
 import { useProfile } from '@linode/queries';
 import { Tooltip } from '@linode/ui';
 import { Hidden } from '@linode/ui';
-import { capitalize, truncate } from '@linode/utilities';
+import { capitalize, getFormattedStatus, truncate } from '@linode/utilities';
 import * as React from 'react';
 
 import { Link } from 'src/components/Link';
@@ -70,7 +70,7 @@ export const MaintenanceTableRow = (props: AccountMaintenance) => {
         </TableCell>
       </Hidden>
       <Hidden smDown>
-        <TableCell noWrap>{capitalize(type.replace('_', ' '))}</TableCell>
+        <TableCell noWrap>{getFormattedStatus(type)}</TableCell>
       </Hidden>
       <TableCell statusCell>
         <StatusIcon status={statusIconMap[status] ?? 'other'} />
diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx
index 17870d4ce05..a3abe9d1581 100644
--- a/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx
+++ b/packages/manager/src/features/Images/ImagesLanding/ImageStatus.tsx
@@ -1,5 +1,5 @@
 import { Stack, TooltipIcon } from '@linode/ui';
-import { capitalizeAllWords } from '@linode/utilities';
+import { capitalizeAllWords, getFormattedStatus } from '@linode/utilities';
 import React from 'react';
 
 import { StatusIcon } from 'src/components/StatusIcon/StatusIcon';
@@ -65,7 +65,7 @@ export const ImageStatus = (props: Props) => {
   return (
     <Stack direction="row">
       <StatusIcon status={imageStatusIconMap[image.status]} />
-      {capitalizeAllWords(image.status.replace('_', ' '))}
+      {getFormattedStatus(image.status)}
       {showEventProgress && ` (${event.percent_complete}%)`}
     </Stack>
   );
diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx
index 81a847bcfe7..c2e3fbb6b99 100644
--- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.tsx
@@ -1,6 +1,6 @@
 import { useImageQuery, useRegionsQuery } from '@linode/queries';
 import { FormControlLabel, Radio } from '@linode/ui';
-import { capitalize, formatStorageUnits } from '@linode/utilities';
+import { formatStorageUnits, getFormattedStatus } from '@linode/utilities';
 import React from 'react';
 
 import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
@@ -46,7 +46,7 @@ export const LinodeSelectTableRow = (props: Props) => {
       </TableCell>
       <TableCell statusCell>
         <StatusIcon status={getLinodeIconStatus(linode.status)} />
-        {capitalize(linode.status.replace('_', ' '))}
+        {getFormattedStatus(linode.status)}
       </TableCell>
       <TableCell>{image?.label ?? linode.image}</TableCell>
       <TableCell>
diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx
index ec7f6d8bf5f..cadf4187b20 100644
--- a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.tsx
@@ -1,8 +1,8 @@
 import { useImageQuery, useRegionsQuery } from '@linode/queries';
 import { Button, Stack } from '@linode/ui';
 import {
-  capitalizeAllWords,
   formatStorageUnits,
+  getFormattedStatus,
   isNotNullOrUndefined,
 } from '@linode/utilities';
 import Grid from '@mui/material/Grid';
@@ -75,7 +75,7 @@ export const SelectLinodeCard = ({
             aria-label={`Linode status ${linode?.status ?? iconStatus}`}
             status={iconStatus}
           />
-          {capitalizeAllWords(linode.status.replace('_', ' '))}
+          {getFormattedStatus(linode.status)}
         </Stack>
         {shouldShowPowerButton && (
           <Button buttonType="outlined" onClick={handlePowerOff}>
diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx
index c12fd08ee68..457d4e32226 100644
--- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx
@@ -1,6 +1,6 @@
 import { Tooltip, TooltipIcon, Typography } from '@linode/ui';
 import { Hidden } from '@linode/ui';
-import { capitalizeAllWords, formatStorageUnits } from '@linode/utilities';
+import { formatStorageUnits, getFormattedStatus } from '@linode/utilities';
 import * as React from 'react';
 
 import Flag from 'src/assets/icons/flag.svg';
@@ -122,7 +122,7 @@ export const LinodeRow = (props: Props) => {
           ) : (
             <>
               <StatusIcon status={iconStatus} />
-              {capitalizeAllWords(status.replace('_', ' '))}
+              {getFormattedStatus(status)}
             </>
           )
         ) : (
diff --git a/packages/manager/src/features/Linodes/transitions.ts b/packages/manager/src/features/Linodes/transitions.ts
index 266611aa992..6d3352b7ead 100644
--- a/packages/manager/src/features/Linodes/transitions.ts
+++ b/packages/manager/src/features/Linodes/transitions.ts
@@ -1,4 +1,4 @@
-import { capitalizeAllWords } from '@linode/utilities';
+import { getFormattedStatus } from '@linode/utilities';
 
 import {
   isEventRelevantToLinode,
@@ -73,7 +73,7 @@ export const transitionText = (
     return transitionActionMap[recentEvent.action];
   }
 
-  return capitalizeAllWords(status.replace('_', ' '));
+  return getFormattedStatus(status);
 };
 
 // Given a list of Events, returns a set of all Linode IDs that are involved in an in-progress event.
diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx
index 8c8d5994799..f3d8c4ae605 100644
--- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx
+++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx
@@ -1,5 +1,5 @@
 import { usePlacementGroupQuery } from '@linode/queries';
-import { capitalizeAllWords } from '@linode/utilities';
+import { getFormattedStatus } from '@linode/utilities';
 import { useParams } from '@tanstack/react-router';
 import * as React from 'react';
 
@@ -97,7 +97,7 @@ export const PlacementGroupsLinodesTableRow = React.memo((props: Props) => {
         ) : (
           <>
             <StatusIcon status={iconStatus} />
-            {capitalizeAllWords(status.replace('_', ' '))}
+            {getFormattedStatus(status)}
           </>
         )}
       </TableCell>
diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx
index c5d21f92d68..4ba7f209e6e 100644
--- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx
+++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx
@@ -1,7 +1,7 @@
 import { useLinodeQuery } from '@linode/queries';
 import { Box, CircleProgress, TooltipIcon, Typography } from '@linode/ui';
 import { Hidden } from '@linode/ui';
-import { capitalizeAllWords } from '@linode/utilities';
+import { getFormattedStatus } from '@linode/utilities';
 import ErrorOutline from '@mui/icons-material/ErrorOutline';
 import * as React from 'react';
 
@@ -204,7 +204,7 @@ export const SubnetLinodeRow = (props: Props) => {
             />
           </>
         ) : (
-          capitalizeAllWords(linode.status.replace('_', ' '))
+          getFormattedStatus(linode.status)
         )}
       </TableCell>
       <Hidden smDown>
diff --git a/packages/manager/src/features/Volumes/VolumeTableRow.tsx b/packages/manager/src/features/Volumes/VolumeTableRow.tsx
index 4bc248bfeda..06bae2921d2 100644
--- a/packages/manager/src/features/Volumes/VolumeTableRow.tsx
+++ b/packages/manager/src/features/Volumes/VolumeTableRow.tsx
@@ -1,6 +1,7 @@
 import { useNotificationsQuery, useRegionsQuery } from '@linode/queries';
 import { Box, Chip, Typography } from '@linode/ui';
 import { Hidden } from '@linode/ui';
+import { getFormattedStatus } from '@linode/utilities';
 import * as React from 'react';
 // eslint-disable-next-line no-restricted-imports
 import { useHistory } from 'react-router-dom';
@@ -148,7 +149,8 @@ export const VolumeTableRow = React.memo((props: Props) => {
       </TableCell>
       <TableCell statusCell>
         <StatusIcon status={volumeStatusIconMap[volumeStatus]} />
-        {volumeStatus} {getEventProgress(mostRecentVolumeEvent)}
+        {getFormattedStatus(volumeStatus)}{' '}
+        {getEventProgress(mostRecentVolumeEvent)}
       </TableCell>
       {isVolumesLanding && (
         <TableCell data-qa-volume-region data-testid="region" noWrap>
diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts
index 63a893ccf14..41a715dedfc 100644
--- a/packages/manager/src/mocks/serverHandlers.ts
+++ b/packages/manager/src/mocks/serverHandlers.ts
@@ -1401,6 +1401,7 @@ export const handlers = [
       'active',
       'creating',
       'migrating',
+      'key_rotating',
       'offline',
       'resizing',
     ];
diff --git a/packages/utilities/src/helpers/formatStatus.test.ts b/packages/utilities/src/helpers/formatStatus.test.ts
new file mode 100644
index 00000000000..3705c957351
--- /dev/null
+++ b/packages/utilities/src/helpers/formatStatus.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from 'vitest';
+
+import { getFormattedStatus } from './formatStatus';
+
+describe('getFormattedStatus', () => {
+  it('should capitalize a single lowercase word', () => {
+    expect(getFormattedStatus('active')).toBe('Active');
+  });
+
+  it('should replace underscore with space and capitalize each word', () => {
+    expect(getFormattedStatus('in_progress')).toBe('In Progress');
+  });
+
+  it('should handle multiple underscores correctly', () => {
+    expect(getFormattedStatus('waiting_for_user_action')).toBe(
+      'Waiting For User Action',
+    );
+  });
+
+  it('should handle mixed case inputs', () => {
+    expect(getFormattedStatus('In_Progress')).toBe('In Progress');
+  });
+
+  it('should handle empty string', () => {
+    expect(getFormattedStatus('')).toBe('');
+  });
+});
diff --git a/packages/utilities/src/helpers/formatStatus.ts b/packages/utilities/src/helpers/formatStatus.ts
new file mode 100644
index 00000000000..03f9dc17240
--- /dev/null
+++ b/packages/utilities/src/helpers/formatStatus.ts
@@ -0,0 +1,5 @@
+import { capitalizeAllWords } from './capitalize';
+
+export const getFormattedStatus = (status: string): string => {
+  return capitalizeAllWords(status.replace(/_/g, ' '));
+};
diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts
index 104575d7396..732c3b9ac29 100644
--- a/packages/utilities/src/helpers/index.ts
+++ b/packages/utilities/src/helpers/index.ts
@@ -15,6 +15,7 @@ export * from './env';
 export * from './escapeRegExp';
 export * from './evenizeNumber';
 export * from './formatDuration';
+export * from './formatStatus';
 export * from './formatStorageUnits';
 export * from './formatUptime';
 export * from './getAll';


Reviews:
user: login: bnussman-akamai
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-13T14:27:34Z
user: login: mjac0bs
body: Approving pending a few bits of clean up and all tests passing.

Thank you for fixing these and addressing feedback! ++ for the unit test coverage. 🙏🏼 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:06:20Z
user: login: bnussman-akamai
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T01:36:42Z
user: login: hasyed-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T08:26:13Z
user: login: hasyed-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T08:52:55Z
user: login: mjac0bs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:50:49Z
user: login: bnussman-akamai
body: Looks good pending e2e failures are unrelated
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T16:39:15Z
user: login: jdamore-linode
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:30:51Z
user: login: coliu-akamai
body: ✅ key rotating
✅ multiple IP input
✅ didn't see any regressions

thank you!


state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:52:58Z

Review comments:
path: packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx
user: login: bnussman-akamai
body: Rather than using margin to accomplish the desired spacing, I think we should refactor this to use a parent `Stack` with `spacing` if possible. If Stack isn't a good fit, we could use a parent Grid with `spacing`, but I feel like `Stack` should be enough for this simple layout.

While we're here, I think we should also update the "Remove IP Button" to use a proper `IconButton` component rather than the incorrect `Button` component
created_at: 2025-05-13T13:18:52Z
author_association: MEMBER
path: packages/manager/src/features/Volumes/VolumeTableRow.tsx
user: login: bnussman-akamai
body: `getVolumeStatus` seems a bit rigid here. We may want to just find and replace any `_` with a ` ` instead. 
.
Optionally, we could make a shared util function called `getFormattedStatus` or something like it and use it here.

```typescript
function getFormattedStatus(status: string): string {
  return capitalizeAllWords(status.replace('_', ' ')
}
```
With a nice shared util function, we could update tons of other instances in the codebase to use the shared util

![Screenshot 2025-05-13 at 10 04 32 AM](https://github.com/user-attachments/assets/b2e89c71-9bbe-4e0e-9e8e-12c437163b86)

created_at: 2025-05-13T14:08:10Z
author_association: MEMBER
diff_hunk: @@ -100,7 +101,10 @@ export const NodeBalancerDetail = () => {
             editableTextTitle: nodeBalancerLabel,
             errorText: labelError,
             onCancel: cancelUpdate,
-            onEdit: (label) => updateNodeBalancer({ label }),
+            onEdit: async (label) => {
+              await updateNodeBalancer({ label });
+              refetch();
+            },
path: packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx
user: login: bnussman-akamai
body: This is not our recommend way of handing cache updates.

```suggestion
            onEdit: async (label) => {
              await updateNodeBalancer({ label });
              refetch();
            },
```

We need to update `packages/queries/src/nodebalancers/nodebalancers.ts` somewhat heavily to account for the conditional call for v4 and v4beta

We need to invalidate the nodebalancer query differently. Let me know if you need more details on what this looks like

It looks like there are now redundant "getNodebalancer" query keys that need to be consolidated
created_at: 2025-05-13T14:27:29Z
author_association: MEMBER
path: packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx
user: login: mjac0bs
body: It looks like we separated this bug fix out into #12217. Can we revert the change in this file?
created_at: 2025-05-14T17:40:55Z
author_association: CONTRIBUTOR
path: packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx
user: login: mjac0bs
body: Same here! (#12217)
created_at: 2025-05-14T17:41:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+import { capitalizeAllWords } from './capitalize';
+
+export const getFormattedStatus = (status: string): string => {
+  return capitalizeAllWords(status.replace(/_/g, ' '));
path: packages/utilities/src/helpers/formatStatus.ts
user: login: mjac0bs
body: I think capitalizeAllWords was recently modified to handle delimiters (e.g. `s.split(delimiter).map(capitalize).join(' ')`) and we should be able to do the following here without the `replace`:

```suggestion
  return capitalizeAllWords(status, '_'));
```

This utility is fine, because it's clear what it's doing and avoids us having to pass in the second '_' prop everywhere, so I don't mind it existing rather than update the use of capitalizeAllWords everywhere directly.
created_at: 2025-05-14T18:02:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+"@linode/manager": Fixed
+---
+
+Formatting of the volume status and broken spacing in MultipleIPInput component and Nodebalancer regression ([#12207](https://github.com/linode/manager/pull/12207))
path: packages/manager/.changeset/pr-12207-fixed-1747120917933.md
user: login: mjac0bs
body: ```suggestion
Formatting of the volume status and broken spacing in MultipleIPInput component ([#12207](https://github.com/linode/manager/pull/12207))
```
created_at: 2025-05-14T18:04:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+import { capitalizeAllWords } from './capitalize';
+
+export const getFormattedStatus = (status: string): string => {
+  return capitalizeAllWords(status.replace(/_/g, ' '));
path: packages/utilities/src/helpers/formatStatus.ts
user: login: bnussman-akamai
body: There are some places in the codebase where status status don't have an `_`

For example:
https://github.com/linode/manager/blob/f3639f96a21dd0f18d7559a236b5308578a73156/packages/api-v4/src/images/types.ts#L11-L12

Makes me wonder if we should keep the `replace` for robustness. Or maybe just use `capitalizeAllWords` globally? What do you guys think?
created_at: 2025-05-15T01:36:41Z
author_association: MEMBER
path: packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx
user: login: hasyed-akamai
body: Yes, it has been addressed in the https://github.com/linode/manager/pull/12217. sure i will revert back the changes in the files you mentioned
created_at: 2025-05-15T08:26:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+import { capitalizeAllWords } from './capitalize';
+
+export const getFormattedStatus = (status: string): string => {
+  return capitalizeAllWords(status.replace(/_/g, ' '));
path: packages/utilities/src/helpers/formatStatus.ts
user: login: hasyed-akamai
body: Keeping the `replace` adds a bit of extra robustness, especially for cases without underscores, so the current approach seems like a reasonable and flexible choice—what are your thoughts?
created_at: 2025-05-15T08:52:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+import { capitalizeAllWords } from './capitalize';
+
+export const getFormattedStatus = (status: string): string => {
+  return capitalizeAllWords(status.replace(/_/g, ' '));
path: packages/utilities/src/helpers/formatStatus.ts
user: login: mjac0bs
body: > There are some places in the codebase where status status don't have an _

Oh, good point! Then current approach is fine, imo. When handling statuses, devs might remember a util that has status in the name more easily than capitalizeAllWords. 
created_at: 2025-05-15T14:50:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: linode-gh-bot
created_at: 2025-05-16T05:40:11Z
body: <!-- PULL REQUEST COMMENT: cloud-manager-ci-comment -->
<!-- BEGIN COMMENT SECTION: cloud-manager-ui-tests -->
## Cloud Manager UI test results
### :small_red_triangle: 2 failing tests on [test run #15 ↗︎](https://baker.linode.com/job/manager/job/manager-test/job/PR-12207/15/display/redirect)

<table><thead><tr><td><strong>:x: Failing</strong></td><td><strong>:white_check_mark: Passing</strong></td><td><strong>:arrow_right_hook: Skipped</strong></td><td><strong>:clock1: Duration</strong></td></tr></thead><tbody><tr><td><code>2 Failing</code></td><td><code>591 Passing</code></td><td><code>4 Skipped</code></td><td><code>118m 51s</code></td></tr></tbody></table>


#### Details
<table><thead><tr><th colspan="3">Failing Tests</th></tr><tr><th></th><th>Spec</th><th>Test</th></tr></thead><tbody>
<tr><td>:x:</td><td><code>lke-create.spec.ts</code></td><td><em>Cloud Manager Cypress Tests→LKE Cluster Creation » LKE Cluster Creation</em></td></tr>
<tr><td>:x:</td><td><code>lke-create.spec.ts</code></td><td><em>Cloud Manager Cypress Tests→LKE Cluster Creation with APL enabled » LKE Cluster Creation with APL enabled</em></td></tr>
</tbody></table>

#### Troubleshooting
Use this command to re-run the failing tests:

```bash
pnpm cy:run -s "cypress/e2e/core/kubernetes/lke-create.spec.ts"
```
<!-- END COMMENT SECTION: cloud-manager-ui-tests -->
author_association: NONE