From 4ca7bac891488cec5a84fc4a6428533a7343ac07 Mon Sep 17 00:00:00 2001 From: Hubert Date: Wed, 5 Nov 2025 23:03:22 +0100 Subject: [PATCH 01/10] Tests for resources --- src/components/ControlPlane/ActionsMenu.tsx | 5 +- .../ControlPlane/ManagedResources.cy.tsx | 328 ++++++++++++++++++ .../ControlPlane/ManagedResources.tsx | 12 +- 3 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 src/components/ControlPlane/ManagedResources.cy.tsx diff --git a/src/components/ControlPlane/ActionsMenu.tsx b/src/components/ControlPlane/ActionsMenu.tsx index 7a911df0..13ad9da6 100644 --- a/src/components/ControlPlane/ActionsMenu.tsx +++ b/src/components/ControlPlane/ActionsMenu.tsx @@ -17,7 +17,7 @@ export type ActionsMenuProps = { buttonIcon?: string; }; -export function ActionsMenu({ item, actions, buttonIcon = 'overflow' }: ActionsMenuProps) { +export function ActionsMenu({ item, actions }: ActionsMenuProps) { const popoverRef = useRef(null); const [open, setOpen] = useState(false); @@ -30,10 +30,11 @@ export function ActionsMenu({ item, actions, buttonIcon = 'overflow' }: Actio return ( <> - @@ -141,10 +142,10 @@ export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSideP

{t('yaml.diffConfirmMessage')}

- - diff --git a/src/hooks/useHandleResourcePatch.spec.ts b/src/hooks/useHandleResourcePatch.spec.ts new file mode 100644 index 00000000..ae37062c --- /dev/null +++ b/src/hooks/useHandleResourcePatch.spec.ts @@ -0,0 +1,140 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import { useHandleResourcePatch } from './useHandleResourcePatch.ts'; +import { fetchApiServerJson } from '../lib/api/fetch.ts'; +import { assertNonNullish } from '../utils/test/vitest-utils.ts'; + +vi.mock('../lib/api/fetch.ts'); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock('../context/ToastContext', () => ({ + useToast: () => ({ + show: vi.fn(), + }), +})); + +vi.mock('../hooks/useResourcePluralNames', () => ({ + useResourcePluralNames: () => ({ + getPluralKind: (kind: string) => `${kind.toLowerCase()}s`, + }), +})); + +describe('useHandleResourcePatch', () => { + let fetchMock: Mock; + const mockErrorDialogRef = { current: null }; + + beforeEach(() => { + fetchMock = vi.mocked(fetchApiServerJson); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should successfully patch a namespaced resource', async () => { + // ARRANGE + fetchMock.mockResolvedValue(undefined); + + const item = { + kind: 'Subaccount', + apiVersion: 'account.btp.sap.crossplane.io/v1alpha1', + metadata: { + name: 'test-subaccount', + namespace: 'test-namespace', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = false; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const call = fetchMock.mock.calls[0]; + assertNonNullish(call); + const [url, _config, _excludeMcpConfig, method, body] = call; + + expect(url).toBe( + '/apis/account.btp.sap.crossplane.io/v1alpha1/namespaces/test-namespace/subaccounts/test-subaccount', + ); + expect(method).toBe('PATCH'); + expect(body).toBe(JSON.stringify(parsed)); + }); + + it('should successfully patch a cluster-scoped resource', async () => { + // ARRANGE + fetchMock.mockResolvedValue(undefined); + + const item = { + kind: 'ClusterRole', + apiVersion: 'rbac.authorization.k8s.io/v1', + metadata: { + name: 'test-role', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = false; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const call = fetchMock.mock.calls[0]; + assertNonNullish(call); + const [url] = call; + + expect(url).toBe('/apis/rbac.authorization.k8s.io/v1/clusterroles/test-role'); + }); + + it('should handle patch failure', async () => { + // ARRANGE + fetchMock.mockRejectedValue(new Error('Network error')); + + const item = { + kind: 'Subaccount', + apiVersion: 'account.btp.sap.crossplane.io/v1alpha1', + metadata: { + name: 'test-subaccount', + namespace: 'test-namespace', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = true; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(false); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/lib/api/types/crossplane/useHandleResourcePatch.ts b/src/hooks/useHandleResourcePatch.ts similarity index 80% rename from src/lib/api/types/crossplane/useHandleResourcePatch.ts rename to src/hooks/useHandleResourcePatch.ts index 856f8c9e..45250839 100644 --- a/src/lib/api/types/crossplane/useHandleResourcePatch.ts +++ b/src/hooks/useHandleResourcePatch.ts @@ -1,12 +1,12 @@ import { useContext } from 'react'; import type { RefObject } from 'react'; import { useTranslation } from 'react-i18next'; -import { ApiConfigContext } from '../../../../components/Shared/k8s'; -import { useToast } from '../../../../context/ToastContext.tsx'; -import { useResourcePluralNames } from '../../../../hooks/useResourcePluralNames'; -import { ErrorDialogHandle } from '../../../../components/Shared/ErrorMessageBox.tsx'; -import { fetchApiServerJson } from '../../fetch.ts'; -import { APIError } from '../../error.ts'; +import { ApiConfigContext } from '../components/Shared/k8s/index.ts'; +import { useToast } from '../context/ToastContext.tsx'; +import { useResourcePluralNames } from './useResourcePluralNames.ts'; +import { ErrorDialogHandle } from '../components/Shared/ErrorMessageBox.tsx'; +import { fetchApiServerJson } from '../lib/api/fetch.ts'; +import { APIError } from '../lib/api/error.ts'; export type PatchableResourceRef = { kind: string; From 4f1b670c35ff6ce45f6d617365e94f1312f644f9 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 11:36:39 +0100 Subject: [PATCH 03/10] adding logs - tests are ok locally --- .../ControlPlane/ManagedResources.cy.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index cc9511c6..14af9ea5 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -212,6 +212,7 @@ describe('ManagedResources - Edit Resource', () => { const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => { patchHandlerCreated = true; return async (item: any) => { + cy.log('Patch handler called!'); patchCalled = true; patchedItem = item; return true; @@ -316,15 +317,27 @@ describe('ManagedResources - Edit Resource', () => { cy.contains('test-subaccount').should('be.visible'); // Verify patch not called yet - cy.then(() => cy.wrap(patchCalled).should('equal', false)); + cy.then(() => { + cy.log(`patchCalled before Apply: ${patchCalled}`); + cy.wrap(patchCalled).should('equal', false); + }); // Click Apply button cy.contains('Apply changes').click(); - // Confirm in dialog + // Wait for dialog and confirm + cy.get('ui5-dialog', { timeout: 10000 }).should('exist'); cy.contains('Yes').click({ force: true }); + // Give it time to process + cy.wait(2000); + // Verify patch was called + cy.then(() => { + cy.log(`patchCalled after Apply: ${patchCalled}`); + cy.log(`patchedItem: ${JSON.stringify(patchedItem)}`); + }); + cy.then(() => cy.wrap(patchCalled).should('equal', true)); cy.then(() => cy.wrap(patchedItem).should('not.be.null')); }); From c3a31779faa5fcd407d4df79d17b73c035cbbcf0 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 11:57:54 +0100 Subject: [PATCH 04/10] PR changes --- .../ControlPlane/ManagedResources.cy.tsx | 33 ++++++++----------- src/components/Yaml/YamlSidePanel.tsx | 4 +-- src/components/YamlEditor/YamlEditor.tsx | 2 +- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 14af9ea5..52549d59 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -212,7 +212,6 @@ describe('ManagedResources - Edit Resource', () => { const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => { patchHandlerCreated = true; return async (item: any) => { - cy.log('Patch handler called!'); patchCalled = true; patchedItem = item; return true; @@ -271,11 +270,15 @@ describe('ManagedResources - Edit Resource', () => { ]; before(() => { - // Ignore Monaco Editor disposal errors cy.on('uncaught:exception', (err) => { + // Ignore Monaco Editor errors if (err.message.includes('TextModel got disposed')) { return false; } + // Ignore DiffEditorWidget errors + if (err.message.includes('DiffEditorWidget')) { + return false; + } return true; }); }); @@ -314,31 +317,23 @@ describe('ManagedResources - Edit Resource', () => { // Verify YAML panel opened cy.contains('YAML').should('be.visible'); - cy.contains('test-subaccount').should('be.visible'); // Verify patch not called yet - cy.then(() => { - cy.log(`patchCalled before Apply: ${patchCalled}`); - cy.wrap(patchCalled).should('equal', false); - }); + cy.then(() => cy.wrap(patchCalled).should('equal', false)); // Click Apply button - cy.contains('Apply changes').click(); + cy.get('[data-testid="yaml-apply-button"]').should('be.visible').click(); - // Wait for dialog and confirm - cy.get('ui5-dialog', { timeout: 10000 }).should('exist'); - cy.contains('Yes').click({ force: true }); + // Confirm in dialog + cy.get('[data-testid="yaml-confirm-button"]', { timeout: 10000 }).should('be.visible').click({ force: true }); - // Give it time to process - cy.wait(2000); + // Wait for success message + cy.contains('Update submitted', { timeout: 10000 }).should('be.visible'); // Verify patch was called cy.then(() => { - cy.log(`patchCalled after Apply: ${patchCalled}`); - cy.log(`patchedItem: ${JSON.stringify(patchedItem)}`); + expect(patchCalled).to.equal(true); + expect(patchedItem).to.not.be.null; }); - - cy.then(() => cy.wrap(patchCalled).should('equal', true)); - cy.then(() => cy.wrap(patchedItem).should('not.be.null')); }); -}); +}); \ No newline at end of file diff --git a/src/components/Yaml/YamlSidePanel.tsx b/src/components/Yaml/YamlSidePanel.tsx index 17b96ba8..44187f71 100644 --- a/src/components/Yaml/YamlSidePanel.tsx +++ b/src/components/Yaml/YamlSidePanel.tsx @@ -142,10 +142,10 @@ export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSideP

{t('yaml.diffConfirmMessage')}

- - diff --git a/src/components/YamlEditor/YamlEditor.tsx b/src/components/YamlEditor/YamlEditor.tsx index c55e4c66..f602d7a0 100644 --- a/src/components/YamlEditor/YamlEditor.tsx +++ b/src/components/YamlEditor/YamlEditor.tsx @@ -85,7 +85,7 @@ export const YamlEditor = (props: YamlEditorProps) => { {t('yaml.editorTitle')} - From e796c05bdb77b5cc541a3caaf2b1b32ab3c7f800 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 12:05:18 +0100 Subject: [PATCH 05/10] lint fix --- src/components/ControlPlane/ManagedResources.cy.tsx | 6 +++--- src/components/Yaml/YamlSidePanel.tsx | 4 ++-- src/components/YamlEditor/YamlEditor.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 52549d59..7adc91c2 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -332,8 +332,8 @@ describe('ManagedResources - Edit Resource', () => { // Verify patch was called cy.then(() => { - expect(patchCalled).to.equal(true); - expect(patchedItem).to.not.be.null; + cy.wrap(patchCalled).should('equal', true); + cy.wrap(patchedItem).should('not.be.null'); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/Yaml/YamlSidePanel.tsx b/src/components/Yaml/YamlSidePanel.tsx index 44187f71..17b96ba8 100644 --- a/src/components/Yaml/YamlSidePanel.tsx +++ b/src/components/Yaml/YamlSidePanel.tsx @@ -142,10 +142,10 @@ export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSideP

{t('yaml.diffConfirmMessage')}

- - diff --git a/src/components/YamlEditor/YamlEditor.tsx b/src/components/YamlEditor/YamlEditor.tsx index f602d7a0..1c8a2716 100644 --- a/src/components/YamlEditor/YamlEditor.tsx +++ b/src/components/YamlEditor/YamlEditor.tsx @@ -85,7 +85,7 @@ export const YamlEditor = (props: YamlEditorProps) => { {t('yaml.editorTitle')} - From 8c5d5053e7f98e7fa776766aefcfa410e406c748 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 12:11:44 +0100 Subject: [PATCH 06/10] tests working locally - testing on devops --- .../ControlPlane/ManagedResources.cy.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 7adc91c2..13284f79 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -205,15 +205,17 @@ describe('ManagedResources - Delete Resource', () => { }); describe('ManagedResources - Edit Resource', () => { - let patchHandlerCreated = false; - let patchCalled = false; - let patchedItem: any = null; + const state = { + patchHandlerCreated: false, + patchCalled: false, + patchedItem: null as any, + }; const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => { - patchHandlerCreated = true; + state.patchHandlerCreated = true; return async (item: any) => { - patchCalled = true; - patchedItem = item; + state.patchCalled = true; + state.patchedItem = item; return true; }; }; @@ -271,11 +273,9 @@ describe('ManagedResources - Edit Resource', () => { before(() => { cy.on('uncaught:exception', (err) => { - // Ignore Monaco Editor errors if (err.message.includes('TextModel got disposed')) { return false; } - // Ignore DiffEditorWidget errors if (err.message.includes('DiffEditorWidget')) { return false; } @@ -284,9 +284,9 @@ describe('ManagedResources - Edit Resource', () => { }); beforeEach(() => { - patchHandlerCreated = false; - patchCalled = false; - patchedItem = null; + state.patchHandlerCreated = false; + state.patchCalled = false; + state.patchedItem = null; }); it('opens edit panel and can apply changes', () => { @@ -305,7 +305,7 @@ describe('ManagedResources - Edit Resource', () => { ); // Verify patch handler was initialized - cy.then(() => cy.wrap(patchHandlerCreated).should('equal', true)); + cy.then(() => cy.wrap(state.patchHandlerCreated).should('equal', true)); // Expand resource group cy.get('button[aria-label*="xpand"]').first().click({ force: true }); @@ -319,7 +319,7 @@ describe('ManagedResources - Edit Resource', () => { cy.contains('YAML').should('be.visible'); // Verify patch not called yet - cy.then(() => cy.wrap(patchCalled).should('equal', false)); + cy.then(() => cy.wrap(state.patchCalled).should('equal', false)); // Click Apply button cy.get('[data-testid="yaml-apply-button"]').should('be.visible').click(); @@ -330,10 +330,8 @@ describe('ManagedResources - Edit Resource', () => { // 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.wrap(patchedItem).should('not.be.null'); - }); + // Verify patch was called - use should callback to access current value + cy.wrap(state).its('patchCalled').should('equal', true); + cy.wrap(state).its('patchedItem').should('not.be.null'); }); -}); +}); \ No newline at end of file From 5ee4bbc3b3c48eb96d0b3e34b4e1028512112213 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 12:16:48 +0100 Subject: [PATCH 07/10] lint fix --- src/components/ControlPlane/ManagedResources.cy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 13284f79..52eb80eb 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -334,4 +334,4 @@ describe('ManagedResources - Edit Resource', () => { cy.wrap(state).its('patchCalled').should('equal', true); cy.wrap(state).its('patchedItem').should('not.be.null'); }); -}); \ No newline at end of file +}); From 937c6e1d8c1df7d3f96b5a29b0ced2039f2d54b0 Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 12:26:14 +0100 Subject: [PATCH 08/10] tests fix (working locally) --- .../ControlPlane/ManagedResources.cy.tsx | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 52eb80eb..433f56f8 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -205,17 +205,15 @@ describe('ManagedResources - Delete Resource', () => { }); describe('ManagedResources - Edit Resource', () => { - const state = { - patchHandlerCreated: false, - patchCalled: false, - patchedItem: null as any, - }; + let patchHandlerCreated = false; + let patchCalled = false; + let patchedItem: any = null; const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => { - state.patchHandlerCreated = true; + patchHandlerCreated = true; return async (item: any) => { - state.patchCalled = true; - state.patchedItem = item; + patchCalled = true; + patchedItem = item; return true; }; }; @@ -284,9 +282,9 @@ describe('ManagedResources - Edit Resource', () => { }); beforeEach(() => { - state.patchHandlerCreated = false; - state.patchCalled = false; - state.patchedItem = null; + patchHandlerCreated = false; + patchCalled = false; + patchedItem = null; }); it('opens edit panel and can apply changes', () => { @@ -305,7 +303,7 @@ describe('ManagedResources - Edit Resource', () => { ); // Verify patch handler was initialized - cy.then(() => cy.wrap(state.patchHandlerCreated).should('equal', true)); + cy.then(() => cy.wrap(patchHandlerCreated).should('equal', true)); // Expand resource group cy.get('button[aria-label*="xpand"]').first().click({ force: true }); @@ -318,9 +316,6 @@ describe('ManagedResources - Edit Resource', () => { // Verify YAML panel opened cy.contains('YAML').should('be.visible'); - // Verify patch not called yet - cy.then(() => cy.wrap(state.patchCalled).should('equal', false)); - // Click Apply button cy.get('[data-testid="yaml-apply-button"]').should('be.visible').click(); @@ -330,8 +325,8 @@ describe('ManagedResources - Edit Resource', () => { // Wait for success message cy.contains('Update submitted', { timeout: 10000 }).should('be.visible'); - // Verify patch was called - use should callback to access current value - cy.wrap(state).its('patchCalled').should('equal', true); - cy.wrap(state).its('patchedItem').should('not.be.null'); + // Verify patch was called + cy.then(() => cy.wrap(patchCalled).should('equal', true)); + cy.then(() => cy.wrap(patchedItem).should('not.be.null')); }); }); From 7c9e9ef58c71285dea36ef7479bf57241c95460c Mon Sep 17 00:00:00 2001 From: Hubert Date: Mon, 10 Nov 2025 13:07:45 +0100 Subject: [PATCH 09/10] fix --- src/components/ControlPlane/ManagedResources.cy.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/ControlPlane/ManagedResources.cy.tsx b/src/components/ControlPlane/ManagedResources.cy.tsx index 433f56f8..875cba62 100644 --- a/src/components/ControlPlane/ManagedResources.cy.tsx +++ b/src/components/ControlPlane/ManagedResources.cy.tsx @@ -205,12 +205,10 @@ describe('ManagedResources - Delete Resource', () => { }); describe('ManagedResources - Edit Resource', () => { - let patchHandlerCreated = false; let patchCalled = false; let patchedItem: any = null; const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => { - patchHandlerCreated = true; return async (item: any) => { patchCalled = true; patchedItem = item; @@ -282,7 +280,6 @@ describe('ManagedResources - Edit Resource', () => { }); beforeEach(() => { - patchHandlerCreated = false; patchCalled = false; patchedItem = null; }); @@ -302,9 +299,6 @@ describe('ManagedResources - Edit Resource', () => { , ); - // Verify patch handler was initialized - cy.then(() => cy.wrap(patchHandlerCreated).should('equal', true)); - // Expand resource group cy.get('button[aria-label*="xpand"]').first().click({ force: true }); cy.contains('test-subaccount').should('be.visible'); From 3251058d148a1d55e9cfce56e9dca5455ee0d6dd Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Thu, 13 Nov 2025 16:10:18 +0100 Subject: [PATCH 10/10] Update src/components/ControlPlane/ActionsMenu.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/ControlPlane/ActionsMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ControlPlane/ActionsMenu.tsx b/src/components/ControlPlane/ActionsMenu.tsx index f3280f1b..5939dc47 100644 --- a/src/components/ControlPlane/ActionsMenu.tsx +++ b/src/components/ControlPlane/ActionsMenu.tsx @@ -14,7 +14,6 @@ export type ActionItem = { export type ActionsMenuProps = { item: T; actions: ActionItem[]; - buttonIcon?: string; }; export function ActionsMenu({ item, actions }: ActionsMenuProps) {