From 929c5402750f145c1c0bea0999112ba558c758e2 Mon Sep 17 00:00:00 2001 From: Shahazadi-Shaguftha-Syed Date: Tue, 23 Jun 2026 10:27:18 +0530 Subject: [PATCH] frontend: CreateNamespaceButton: Fix react-hooks/set-state-in-effect by deriving validation state --- .../CreateNamespaceButton.stories.tsx | 37 +++++++++++++++++ .../namespace/CreateNamespaceButton.tsx | 41 +++++++++---------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/namespace/CreateNamespaceButton.stories.tsx b/frontend/src/components/namespace/CreateNamespaceButton.stories.tsx index 857f6d9211e..b8e6b84f554 100644 --- a/frontend/src/components/namespace/CreateNamespaceButton.stories.tsx +++ b/frontend/src/components/namespace/CreateNamespaceButton.stories.tsx @@ -16,6 +16,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { screen } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; import React from 'react'; import { expect, userEvent, waitFor } from 'storybook/test'; import { TestContext } from '../../test'; @@ -112,3 +113,39 @@ export const NotValidNameLong: StoryObj = { expect(button).not.toBeEnabled(); }, }; +export const NamespaceAlreadyExists: StoryObj = { + parameters: { + msw: { + handlers: { + story: [ + http.post('http://localhost:4466/api/v1/namespaces', () => + HttpResponse.json({ message: 'namespace already exists' }, { status: 409 }) + ), + ], + }, + }, + }, + play: async () => { + await userEvent.click(screen.getByLabelText('Create')); + + await waitFor(() => expect(screen.getByLabelText('Dialog')).toBeVisible()); + + await waitFor(() => userEvent.type(screen.getByRole('textbox'), 'existing-namespace'), { + timeout: 5000, + }); + + const createButton = await screen.findByRole('button', { name: 'Create' }); + await userEvent.click(createButton); + + const errorMessage = await screen.findByText('A namespace with this name already exists.'); + expect(errorMessage).toBeVisible(); + + await waitFor(() => userEvent.type(screen.getByRole('textbox'), '-edited'), { + timeout: 5000, + }); + + expect( + screen.queryByText('A namespace with this name already exists.') + ).not.toBeInTheDocument(); + }, +}; diff --git a/frontend/src/components/namespace/CreateNamespaceButton.tsx b/frontend/src/components/namespace/CreateNamespaceButton.tsx index 2647c2256b2..607e67640a4 100644 --- a/frontend/src/components/namespace/CreateNamespaceButton.tsx +++ b/frontend/src/components/namespace/CreateNamespaceButton.tsx @@ -21,7 +21,7 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import TextField from '@mui/material/TextField'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { getCluster } from '../../lib/cluster'; @@ -35,9 +35,8 @@ import { AuthVisible } from '../common/Resource'; export default function CreateNamespaceButton() { const { t } = useTranslation(['glossary', 'translation']); const [namespaceName, setNamespaceName] = useState(''); - const [isValidNamespaceName, setIsValidNamespaceName] = useState(false); - const [nameHelperMessage, setNameHelperMessage] = useState(''); const [namespaceDialogOpen, setNamespaceDialogOpen] = useState(false); + const [namespaceExistsError, setNamespaceExistsError] = useState(false); const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); const dispatch: AppDispatch = useDispatch(); @@ -63,8 +62,7 @@ export default function CreateNamespaceButton() { console.error('Error creating namespace:', error); if (statusCode === 409) { setNamespaceDialogOpen(true); - setIsValidNamespaceName(false); - setNameHelperMessage(t('translation|A namespace with this name already exists.')); + setNamespaceExistsError(true); } throw error; } @@ -94,23 +92,19 @@ export default function CreateNamespaceButton() { ); } - useEffect(() => { - const isValidNamespaceFormat = Namespace.isValidNamespaceFormat(namespaceName); - setIsValidNamespaceName(isValidNamespaceFormat); + const isValidNamespaceFormat = Namespace.isValidNamespaceFormat(namespaceName); + const formatHelperMessage = !isValidNamespaceFormat + ? namespaceName.length > 63 + ? t('translation|Namespaces must be under 64 characters.') + : t( + "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." + ) + : ''; - if (!isValidNamespaceFormat) { - if (namespaceName.length > 63) { - setNameHelperMessage(t('translation|Namespaces must be under 64 characters.')); - } else { - setNameHelperMessage( - t( - "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." - ) - ); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [namespaceName]); + const isValidNamespaceName = isValidNamespaceFormat && !namespaceExistsError; + const nameHelperMessage = namespaceExistsError + ? t('translation|A namespace with this name already exists.') + : formatHelperMessage; return ( @@ -144,7 +138,10 @@ export default function CreateNamespaceButton() { } fullWidth value={namespaceName} - onChange={event => setNamespaceName(event.target.value.toLowerCase())} + onChange={event => { + setNamespaceName(event.target.value.toLowerCase()); + setNamespaceExistsError(false); + }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault();