Skip to content
Merged
6 changes: 3 additions & 3 deletions src/components/ControlPlane/ActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ export type ActionItem<T> = {
export type ActionsMenuProps<T> = {
item: T;
actions: ActionItem<T>[];
buttonIcon?: string;
};

export function ActionsMenu<T>({ item, actions, buttonIcon = 'overflow' }: ActionsMenuProps<T>) {
export function ActionsMenu<T>({ item, actions }: ActionsMenuProps<T>) {
const popoverRef = useRef<MenuDomRef>(null);
const [open, setOpen] = useState(false);

Expand All @@ -30,10 +29,11 @@ export function ActionsMenu<T>({ item, actions, buttonIcon = 'overflow' }: Actio

return (
<>
<Button icon={buttonIcon} design="Transparent" onClick={handleOpenerClick} />
<Button icon="overflow" design="Transparent" data-testid="ActionsMenu-opener" onClick={handleOpenerClick} />
<Menu
ref={popoverRef}
open={open}
data-testid="ActionsMenu"
onItemClick={(event) => {
const element = event.detail.item as HTMLElement & { disabled?: boolean };
const actionKey = element.dataset.actionKey;
Expand Down
2 changes: 1 addition & 1 deletion src/components/ControlPlane/GitRepositories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
import { useSplitter } from '../Splitter/SplitterContext.tsx';
import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx';
import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
import { useHandleResourcePatch } from '../../hooks/useHandleResourcePatch.ts';
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import type { GitReposResponse } from '../../lib/api/types/flux/listGitRepo';
import { ActionsMenu, type ActionItem } from './ActionsMenu';
Expand Down
2 changes: 1 addition & 1 deletion src/components/ControlPlane/Kustomizations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
import { useSplitter } from '../Splitter/SplitterContext.tsx';
import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx';
import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
import { useHandleResourcePatch } from '../../hooks/useHandleResourcePatch.ts';
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import type { KustomizationsResponse } from '../../lib/api/types/flux/listKustomization';
import { ActionsMenu, type ActionItem } from './ActionsMenu';
Expand Down
326 changes: 326 additions & 0 deletions src/components/ControlPlane/ManagedResources.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ManagedResources } from './ManagedResources.tsx';
import { SplitterProvider } from '../Splitter/SplitterContext.tsx';
import { ManagedResourceGroup } from '../../lib/shared/types.ts';
import { MemoryRouter } from 'react-router-dom';
import { useApiResourceMutation, useApiResource } from '../../lib/api/useApiResource.ts';
import '@ui5/webcomponents-cypress-commands';
import { useHandleResourcePatch } from '../../hooks/useHandleResourcePatch.ts';
import { SplitterLayout } from '../Splitter/SplitterLayout.tsx';
import { useResourcePluralNames } from '../../hooks/useResourcePluralNames';

describe('ManagedResources - Delete Resource', () => {
let deleteCalled = false;
let patchCalled = false;
let triggerCallCount = 0;

const fakeUseApiResourceMutation: typeof useApiResourceMutation = (): any => {
return {
data: undefined,
error: undefined,
reset: () => {},
trigger: async (): Promise<any> => {
const currentTriggerCall = triggerCallCount++;
if (currentTriggerCall === 0) {
deleteCalled = true;
} else {
patchCalled = true;
}
return undefined;
},
isMutating: false,
};
};

const fakeUseApiResource: typeof useApiResource = (): any => {
return {
data: mockManagedResources,
error: undefined,
isLoading: false,
isValidating: false,
mutate: async () => undefined,
};
};

const fakeUseResourcePluralNames: typeof useResourcePluralNames = (): any => {
return {
getPluralKind: (kind: string) => `${kind.toLowerCase()}s`,
isLoading: false,
error: undefined,
};
};

const mockManagedResources: ManagedResourceGroup[] = [
{
items: [
{
apiVersion: 'account.btp.sap.crossplane.io/v1alpha1',
kind: 'Subaccount',
metadata: {
name: 'test-subaccount',
namespace: 'test-namespace',
creationTimestamp: '2024-01-01T00:00:00Z',
resourceVersion: '1',
labels: {},
},
spec: {},
status: {
conditions: [
{
type: 'Ready',
status: 'True',
lastTransitionTime: '2024-01-01T00:00:00Z',
},
{
type: 'Synced',
status: 'True',
lastTransitionTime: '2024-01-01T00:00:00Z',
},
],
},
} as any,
],
},
];

beforeEach(() => {
deleteCalled = false;
patchCalled = false;
triggerCallCount = 0;
});

it('deletes a managed resource', () => {
cy.mount(
<MemoryRouter>
<SplitterProvider>
<ManagedResources
useApiResourceMutation={fakeUseApiResourceMutation}
useApiResource={fakeUseApiResource}
useResourcePluralNames={fakeUseResourcePluralNames}
/>
</SplitterProvider>
</MemoryRouter>,
);

// Expand resource group
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
cy.contains('test-subaccount').should('be.visible');

// Open actions menu and click Delete
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
cy.contains('Delete').click({ force: true });

// Type confirmation text
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');

// Verify delete not called yet
cy.then(() => cy.wrap(deleteCalled).should('equal', false));

// Click delete button
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();

// Verify delete was called
cy.then(() => cy.wrap(deleteCalled).should('equal', true));
});

it('force deletes a managed resource', () => {
cy.mount(
<MemoryRouter>
<SplitterProvider>
<ManagedResources
useApiResourceMutation={fakeUseApiResourceMutation}
useApiResource={fakeUseApiResource}
useResourcePluralNames={fakeUseResourcePluralNames}
/>
</SplitterProvider>
</MemoryRouter>,
);

// Expand resource group
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
cy.contains('test-subaccount').should('be.visible');

// Open actions menu and click Delete
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
cy.contains('Delete').click({ force: true });

// Expand Advanced section
cy.contains('Advanced').click();

// Enable force deletion checkbox
cy.contains('Force deletion').click({ force: true });

// Type confirmation text
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');

// Verify neither delete nor patch called yet
cy.then(() => cy.wrap(deleteCalled).should('equal', false));
cy.then(() => cy.wrap(patchCalled).should('equal', false));

// Click delete button
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();

// Verify both delete and patch were called
cy.then(() => cy.wrap(deleteCalled).should('equal', true));
cy.then(() => cy.wrap(patchCalled).should('equal', true));
});

it('keeps delete button disabled until confirmation text is entered', () => {
cy.mount(
<MemoryRouter>
<SplitterProvider>
<ManagedResources
useApiResourceMutation={fakeUseApiResourceMutation}
useApiResource={fakeUseApiResource}
useResourcePluralNames={fakeUseResourcePluralNames}
/>
</SplitterProvider>
</MemoryRouter>,
);

// Expand resource group
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
cy.contains('test-subaccount').should('be.visible');

// Open actions menu and click Delete
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
cy.contains('Delete').click({ force: true });

// Delete button should be disabled initially
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('have.attr', 'disabled');

// Type wrong text
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('wrong-text');

// Delete button should still be disabled
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('have.attr', 'disabled');

// Clear input and type correct text
cy.get('ui5-dialog[open]').find('ui5-input').find('input[id*="inner"]').clear({ force: true });
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');

// Delete button should now be enabled
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('not.have.attr', 'disabled');
});
});

describe('ManagedResources - Edit Resource', () => {
let patchCalled = false;
let patchedItem: any = null;

const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => {
return async (item: any) => {
patchCalled = true;
patchedItem = item;
return true;
};
};

const fakeUseApiResource: typeof useApiResource = (): any => {
return {
data: mockManagedResources,
error: undefined,
isLoading: false,
isValidating: false,
mutate: async () => undefined,
};
};

const fakeUseResourcePluralNames: typeof useResourcePluralNames = (): any => {
return {
getPluralKind: (kind: string) => `${kind.toLowerCase()}s`,
isLoading: false,
error: undefined,
};
};

const mockManagedResources: ManagedResourceGroup[] = [
{
items: [
{
apiVersion: 'account.btp.sap.crossplane.io/v1alpha1',
kind: 'Subaccount',
metadata: {
name: 'test-subaccount',
namespace: 'test-namespace',
creationTimestamp: '2024-01-01T00:00:00Z',
resourceVersion: '1',
labels: {},
},
spec: {},
status: {
conditions: [
{
type: 'Ready',
status: 'True',
lastTransitionTime: '2024-01-01T00:00:00Z',
},
{
type: 'Synced',
status: 'True',
lastTransitionTime: '2024-01-01T00:00:00Z',
},
],
},
} as any,
],
},
];

before(() => {
cy.on('uncaught:exception', (err) => {
if (err.message.includes('TextModel got disposed')) {
return false;
}
if (err.message.includes('DiffEditorWidget')) {
return false;
}
return true;
});
});

beforeEach(() => {
patchCalled = false;
patchedItem = null;
});

it('opens edit panel and can apply changes', () => {
cy.mount(
<MemoryRouter>
<SplitterProvider>
<SplitterLayout>
<ManagedResources
useHandleResourcePatch={fakeUseHandleResourcePatch}
useApiResource={fakeUseApiResource}
useResourcePluralNames={fakeUseResourcePluralNames}
/>
</SplitterLayout>
</SplitterProvider>
</MemoryRouter>,
);

// Expand resource group
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
cy.contains('test-subaccount').should('be.visible');

// Open actions menu and click Edit
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
cy.contains('Edit').click({ force: true });

// Verify YAML panel opened
cy.contains('YAML').should('be.visible');

// Click Apply button
cy.get('[data-testid="yaml-apply-button"]').should('be.visible').click();

// Confirm in dialog
cy.get('[data-testid="yaml-confirm-button"]', { timeout: 10000 }).should('be.visible').click({ force: true });

// Wait for success message
cy.contains('Update submitted', { timeout: 10000 }).should('be.visible');

// Verify patch was called
cy.then(() => cy.wrap(patchCalled).should('equal', true));
cy.then(() => cy.wrap(patchedItem).should('not.be.null'));
});
});
Loading
Loading