diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx index 204df2e4f929..11933401a5c7 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx @@ -29,6 +29,7 @@ import { useTrackAction } from '@/hooks/useTracking'; import { TRACKING } from '@/configuration/tracking.constants'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; import RouteModal from '@/components/route-modal/RouteModal'; +import { useRenameServiceForm } from './useRenameServiceForm'; interface RenameServiceProps { service: database.Service; @@ -42,6 +43,7 @@ const RenameService = ({ service, onError, onSuccess }: RenameServiceProps) => { const track = useTrackAction(); const { t } = useTranslation('pci-databases-analytics/services/service'); const toast = useToast(); + const form = useRenameServiceForm(service); const { editService, isPending } = useEditService({ onError: (err) => { toast.toast({ @@ -65,29 +67,7 @@ const RenameService = ({ service, onError, onSuccess }: RenameServiceProps) => { } }, }); - // define the schema for the form - const schema = z.object({ - description: z - .string() - .min(3, { - message: t('renameServiceErrorMinLength', { min: 3 }), - }) - .max(30, { - message: t('renameServiceErrorMaxLength', { max: 30 }), - }), - }); - // generate a form controller - const form = useForm>({ - resolver: zodResolver(schema), - defaultValues: { - description: '', - }, - }); - // fill form with service values - useEffect(() => { - if (!service) return; - form.setValue('description', service.description); - }, [service, form]); + const onSubmit = form.handleSubmit((formValues) => { track(TRACKING.renameService.confirm(service.engine)); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx index 996f36edc853..6b954c398bb1 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx @@ -5,6 +5,7 @@ import ServiceStatusBadge from '../../_components/ServiceStatusBadge.component'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { humanizeEngine } from '@/lib/engineNameHelper'; +import ServiceNameWithUpdate from './ServiceNameWithUpdate.component'; export const ServiceHeader = ({ service }: { service: database.Service }) => { const { t } = useTranslation('regions'); @@ -17,7 +18,7 @@ export const ServiceHeader = ({ service }: { service: database.Service }) => {
-

{service.description ?? 'Dashboard'}

+
diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.component.tsx new file mode 100644 index 000000000000..ea983f021e71 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.component.tsx @@ -0,0 +1,126 @@ +import { useParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { Check, Pen, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Skeleton } from '@/components/ui/skeleton'; +import * as database from '@/types/cloud/project/database'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Button } from '@/components/ui/button'; +import { useRenameServiceForm } from './useRenameServiceForm'; +import { Input } from '@/components/ui/input'; +import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; +import { useToast } from '@/components/ui/use-toast'; +import { getCdbApiErrorMessage } from '@/lib/apiHelper'; + +const ServiceNameWithUpdate = ({ service }: { service: database.Service }) => { + const { t } = useTranslation('pci-databases-analytics/services/service'); + const [isEditing, setIsEditing] = useState(false); + const { projectId } = useParams(); + const form = useRenameServiceForm(service); + const toast = useToast(); + const { editService } = useEditService({ + onError: (err) => { + toast.toast({ + title: t('renameServiceToastErrorTitle'), + variant: 'destructive', + description: getCdbApiErrorMessage(err), + }); + }, + onEditSuccess: (renamedService) => { + toast.toast({ + title: t('renameServiceToastSuccessTitle'), + description: t('renameServiceToastSuccessDescription', { + newName: renamedService.description, + }), + }); + setIsEditing(false); + }, + }); + const onSubmit = form.handleSubmit((formValues) => { + editService({ + serviceId: service.id, + projectId, + engine: service.engine, + data: { + description: formValues.description, + }, + }); + }); + + useEffect(() => { + form.reset(); + form.setValue('description', service.description); + }, [isEditing]); + + if (!service) { + return ; + } + if (isEditing) { + return ( +
+ + ( + + + + + + + )} + /> +
+ + +
+ + + ); + } + return ( +
+

{service.description}

+ +
+ ); +}; + +export default ServiceNameWithUpdate; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.spec.tsx new file mode 100644 index 000000000000..0da576cb9ac0 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceNameWithUpdate.spec.tsx @@ -0,0 +1,160 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import ServiceNameWithUpdate from './ServiceNameWithUpdate.component'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as serviceApi from '@/data/api/database/service.api'; + +describe('ServiceNameWithUpdate', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + }; + }); + + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + + vi.mock('@/data/api/database/service.api', () => ({ + editService: vi.fn((s) => s), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should display the service name', () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + expect(screen.getByText(mockedService.description)).toBeInTheDocument(); + }); + + it('should switch to edit mode on clicking the edit button', () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('edit-button')); + }); + + expect(screen.getByTestId('rename-service-input')).toBeInTheDocument(); + }); + + it('should submit the new name and show success toast', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('edit-button')); + }); + + act(() => { + fireEvent.change(screen.getByTestId('rename-service-input'), { + target: { value: 'NewServiceName' }, + }); + }); + + act(() => { + fireEvent.click(screen.getByTestId('validate-button')); + }); + + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalledWith({ + serviceId: mockedService.id, + projectId: 'projectId', + engine: mockedService.engine, + data: { + description: 'NewServiceName', + }, + }); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'renameServiceToastSuccessTitle', + description: 'renameServiceToastSuccessDescription', + }); + }); + }); + + it('should show error toast on API failure', async () => { + vi.mocked(serviceApi.editService).mockImplementation(() => { + throw apiErrorMock; + }); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('edit-button')); + }); + + act(() => { + fireEvent.change(screen.getByTestId('rename-service-input'), { + target: { value: 'NewServiceName' }, + }); + }); + + act(() => { + fireEvent.click(screen.getByTestId('validate-button')); + }); + + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'renameServiceToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); + + it('should exit edit mode on cancel button click', () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('edit-button')); + }); + + expect(screen.getByTestId('rename-service-input')).toBeInTheDocument(); + + act(() => { + fireEvent.click(screen.getByTestId('cancel-button')); + }); + + expect( + screen.queryByTestId('rename-service-input'), + ).not.toBeInTheDocument(); + expect(screen.getByText(mockedService.description)).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/useRenameServiceForm.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/useRenameServiceForm.tsx new file mode 100644 index 000000000000..bf38fa374611 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/useRenameServiceForm.tsx @@ -0,0 +1,33 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import * as database from '@/types/cloud/project/database'; + +export function useRenameServiceForm(service: database.Service) { + const { t } = useTranslation('pci-databases-analytics/services/service'); + const schema = z.object({ + description: z + .string() + .min(3, { + message: t('renameServiceErrorMinLength', { min: 3 }), + }) + .max(30, { + message: t('renameServiceErrorMaxLength', { max: 30 }), + }), + }); + // generate a form controller + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: { + description: '', + }, + }); + // fill form with service values + useEffect(() => { + if (!service) return; + form.setValue('description', service.description); + }, [service, form]); + return form; +}