Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
import { ListWorkspacesType, isWorkspaceReady } from '../../../lib/api/types/crate/listWorkspaces.ts';
import { useMemo, useState } from 'react';
import { MembersAvatarView } from './MembersAvatarView.tsx';
import { DeleteWorkspaceResource, DeleteWorkspaceType } from '../../../lib/api/types/crate/deleteWorkspace.ts';
import { useApiResourceMutation, useApiResource } from '../../../lib/api/useApiResource.ts';
import { useApiResource } from '../../../lib/api/useApiResource.ts';
import { DISPLAY_NAME_ANNOTATION } from '../../../lib/api/types/shared/keyNames.ts';
import { DeleteConfirmationDialog } from '../../Dialogs/DeleteConfirmationDialog.tsx';
import { KubectlDeleteWorkspace } from '../../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteWorkspace.tsx';
import { useToast } from '../../../context/ToastContext.tsx';
import { ListControlPlanes } from '../../../lib/api/types/crate/controlPlanes.ts';
import IllustratedError from '../../Shared/IllustratedError.tsx';
import { APIError } from '../../../lib/api/error.ts';
Expand All @@ -24,13 +22,19 @@ import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/Illustr
import styles from './WorkspacesList.module.css';
import { ControlPlanesListMenu } from '../ControlPlanesListMenu.tsx';
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';
import { useDeleteWorkspace as _useDeleteWorkspace } from '../../../hooks/useDeleteWorkspace.ts';

interface Props {
projectName: string;
workspace: ListWorkspacesType;
useDeleteWorkspace?: typeof _useDeleteWorkspace;
}

export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Props) {
export function ControlPlaneListWorkspaceGridTile({
projectName,
workspace,
useDeleteWorkspace = _useDeleteWorkspace,
}: Props) {
const [isCreateManagedControlPlaneWizardOpen, setIsCreateManagedControlPlaneWizardOpen] = useState(false);
const [initialTemplateName, setInitialTemplateName] = useState<string | undefined>(undefined);
const workspaceName = workspace.metadata.name;
Expand All @@ -40,13 +44,10 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr

const { t } = useTranslation();

const toast = useToast();
const [dialogDeleteWsIsOpen, setDialogDeleteWsIsOpen] = useState(false);

const { data: controlplanes, error: cpsError } = useApiResource(ListControlPlanes(projectName, workspaceName));
const { trigger } = useApiResourceMutation<DeleteWorkspaceType>(
DeleteWorkspaceResource(projectNamespace, workspaceName),
);
const { deleteWorkspace } = useDeleteWorkspace(projectName, projectNamespace, workspaceName);

const { mcpCreationGuide } = useLink();
const errorView = createErrorView(cpsError);
Expand Down Expand Up @@ -181,10 +182,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
kubectl={<KubectlDeleteWorkspace projectName={projectName} resourceName={workspaceName} />}
isOpen={dialogDeleteWsIsOpen}
setIsOpen={setDialogDeleteWsIsOpen}
onDeletionConfirmed={async () => {
await trigger();
toast.show(t('ControlPlaneListWorkspaceGridTile.deleteConfirmationDialog'));
}}
onDeletionConfirmed={deleteWorkspace}
/>
{isCreateManagedControlPlaneWizardOpen ? (
<CreateManagedControlPlaneWizardContainer
Expand Down
162 changes: 162 additions & 0 deletions src/components/Dialogs/CreateWorkspaceDialogContainer.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { CreateWorkspaceDialogContainer } from './CreateWorkspaceDialogContainer';
import { useCreateWorkspace, CreateWorkspaceParams } from '../../hooks/useCreateWorkspace';
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding';

describe('CreateWorkspaceDialogContainer', () => {
let createWorkspacePayload: CreateWorkspaceParams | null = null;

const fakeUseCreateWorkspace: typeof useCreateWorkspace = () => ({
createWorkspace: async (data: CreateWorkspaceParams): Promise<void> => {
createWorkspacePayload = data;
},
isLoading: false,
});

const fakeUseAuthOnboarding = (() => ({
user: {
email: '[email protected]',
},
})) as typeof useAuthOnboarding;

beforeEach(() => {
createWorkspacePayload = null;
});

it('creates a workspace with valid data', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateWorkspaceDialogContainer
useCreateWorkspace={fakeUseCreateWorkspace}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
project="test-project"
/>,
);

const expectedPayload = {
name: 'test-workspace',
displayName: 'Test Workspace Display Name',
chargingTarget: '12345678-1234-1234-1234-123456789abc',
chargingTargetType: 'btp',
members: [
{
name: '[email protected]',
roles: ['admin'],
kind: 'User',
},
],
};

// Fill in the form (using Shadow DOM selectors)
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
cy.get('#displayName').find('input[id*="inner"]').type('Test Workspace Display Name');

// Select charging target type
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();

// Fill charging target
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Verify the hook was called with correct data
cy.then(() => cy.wrap(createWorkspacePayload).deepEqualJson(expectedPayload));

// Dialog should close on success
cy.wrap(setIsOpen).should('have.been.calledWith', false);
});

it('validates required fields', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateWorkspaceDialogContainer
useCreateWorkspace={fakeUseCreateWorkspace}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
project="test-project"
/>,
);

// Try to submit without filling required fields
cy.get('ui5-button').contains('Create').click();

// Should show validation errors - check for value-state="Negative" attribute
cy.get('#name').should('have.attr', 'value-state', 'Negative');

// Or check if error message exists in DOM (even if hidden by CSS)
cy.contains('This field is required').should('exist');

// Dialog should not close
cy.wrap(setIsOpen).should('not.have.been.called');
});

it('validates charging target format for BTP', () => {
const setIsOpen = cy.stub();

cy.mount(
<CreateWorkspaceDialogContainer
useCreateWorkspace={fakeUseCreateWorkspace}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
project="test-project"
/>,
);

cy.get('#name').find('input[id*="inner"]').type('test-workspace');
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();

// Invalid format
cy.get('#chargingTarget').find('input[id*="inner"]').type('invalid-format');
cy.get('ui5-button').contains('Create').click();

// Should show validation error - check for value-state="Negative" attribute
cy.get('#chargingTarget').should('have.attr', 'value-state', 'Negative');

// Dialog should not close
cy.wrap(setIsOpen).should('not.have.been.called');
});

it('should not close dialog when creation fails', () => {
const failingUseCreateWorkspace: typeof useCreateWorkspace = () => ({
createWorkspace: async (): Promise<void> => {
throw new Error('Creation failed'); // Simulate failure by throwing error
},
isLoading: false,
});

const setIsOpen = cy.stub();

cy.mount(
<CreateWorkspaceDialogContainer
useCreateWorkspace={failingUseCreateWorkspace}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={setIsOpen}
project="test-project"
/>,
);

// Fill in the form
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
cy.get('#chargingTargetType').click();
cy.contains('BTP').click();
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');

// Submit the form
cy.get('ui5-button').contains('Create').click();

// Dialog should NOT close on failure
cy.wrap(setIsOpen).should('not.have.been.called');

// Dialog should still be visible
cy.contains('Create').should('be.visible');
});
});
47 changes: 21 additions & 26 deletions src/components/Dialogs/CreateWorkspaceDialogContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useApiResourceMutation, useRevalidateApiResource } from '../../lib/api/useApiResource';
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import { APIError } from '../../lib/api/error';
import { CreateProjectWorkspaceDialog, OnCreatePayload } from './CreateProjectWorkspaceDialog.tsx';
import {
CreateWorkspace,
CreateWorkspaceResource,
CreateWorkspaceType,
} from '../../lib/api/types/crate/createWorkspace';
import { projectnameToNamespace } from '../../utils';
import { ListWorkspaces } from '../../lib/api/types/crate/listWorkspaces';
import { useToast } from '../../context/ToastContext.tsx';
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { useAuthOnboarding as _useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { Member, MemberRoles } from '../../lib/api/types/shared/members.ts';
import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts';
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
import { useCreateWorkspace as _useCreateWorkspace } from '../../hooks/useCreateWorkspace.ts';
import { APIError } from '../../lib/api/error.ts';
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';

export type CreateDialogProps = {
name: string;
Expand All @@ -32,10 +25,14 @@ export function CreateWorkspaceDialogContainer({
isOpen,
setIsOpen,
project = '',
useCreateWorkspace = _useCreateWorkspace,
useAuthOnboarding = _useAuthOnboarding,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
project?: string;
useCreateWorkspace?: typeof _useCreateWorkspace;
useAuthOnboarding?: typeof _useAuthOnboarding;
}) {
const { t } = useTranslation();
const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]);
Expand All @@ -59,6 +56,11 @@ export function CreateWorkspaceDialogContainer({
const { user } = useAuthOnboarding();

const username = user?.email;
const namespace = projectnameToNamespace(project);

const { createWorkspace } = useCreateWorkspace(project, namespace);
const errorDialogRef = useRef<ErrorDialogHandle>(null);

const clearForm = useCallback(() => {
resetField('name');
resetField('chargingTarget');
Expand All @@ -74,30 +76,23 @@ export function CreateWorkspaceDialogContainer({
clearForm();
}
}, [resetField, setValue, username, isOpen, clearForm]);
const namespace = projectnameToNamespace(project);
const toast = useToast();

const { trigger } = useApiResourceMutation<CreateWorkspaceType>(CreateWorkspaceResource(namespace));
const revalidate = useRevalidateApiResource(ListWorkspaces(project));
const errorDialogRef = useRef<ErrorDialogHandle>(null);

const handleWorkspaceCreate = async ({
name,
displayName,
chargingTarget,
chargingTargetType,
members,
}: OnCreatePayload): Promise<boolean> => {
try {
await trigger(
CreateWorkspace(name, namespace, {
displayName: displayName,
chargingTarget: chargingTarget,
members: members,
}),
);
await revalidate();
await createWorkspace({
name,
displayName,
chargingTarget,
chargingTargetType,
members,
});
setIsOpen(false);
toast.show(t('CreateWorkspaceDialog.toastMessage'));
return true;
} catch (e) {
console.error(e);
Expand Down
Loading
Loading