Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 'name@domain.com',
},
})) 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: 'name@domain.com',
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