Skip to content

Commit 662c560

Browse files
authored
feat(extensions): add actions button to enable/disable plugin (#931)
1 parent 6f1036a commit 662c560

4 files changed

Lines changed: 428 additions & 35 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-marketplace': patch
3+
---
4+
5+
add actions button to enable/disable plugin
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import React from 'react';
17+
import Menu, { MenuProps } from '@mui/material/Menu';
18+
19+
export const ActionsMenu = (props: MenuProps) => (
20+
<Menu
21+
elevation={0}
22+
anchorOrigin={{
23+
vertical: 'bottom',
24+
horizontal: 'left',
25+
}}
26+
transformOrigin={{
27+
vertical: 'top',
28+
horizontal: 'left',
29+
}}
30+
{...props}
31+
/>
32+
);
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { BrowserRouter } from 'react-router-dom';
19+
20+
import { TestApiProvider } from '@backstage/test-utils';
21+
22+
import { fireEvent, render } from '@testing-library/react';
23+
import { MarketplacePluginContent } from './MarketplacePluginContent';
24+
import { marketplaceApiRef } from '../api';
25+
import { usePluginConfigurationPermissions } from '../hooks/usePluginConfigurationPermissions';
26+
import { usePlugin } from '../hooks/usePlugin';
27+
import { usePluginPackages } from '../hooks/usePluginPackages';
28+
import { MarketplacePluginInstallStatus } from '@red-hat-developer-hub/backstage-plugin-marketplace-common';
29+
import { useExtensionsConfiguration } from '../hooks/useExtensionsConfiguration';
30+
import { useNodeEnvironment } from '../hooks/useNodeEnvironment';
31+
32+
jest.mock('@backstage/core-plugin-api', () => {
33+
const actual = jest.requireActual('@backstage/core-plugin-api');
34+
return {
35+
...actual,
36+
attachComponentData: jest.fn(),
37+
useRouteRef: jest.fn().mockImplementation(() => () => '/mock-plugin-route'),
38+
useRouteRefParams: jest
39+
.fn()
40+
.mockReturnValue({ namespace: 'default', name: 'test' }),
41+
};
42+
});
43+
44+
jest.mock('../hooks/usePluginConfigurationPermissions', () => ({
45+
usePluginConfigurationPermissions: jest.fn(),
46+
}));
47+
48+
jest.mock('../hooks/usePlugin', () => ({
49+
usePlugin: jest.fn(),
50+
}));
51+
52+
jest.mock('../hooks/useExtensionsConfiguration', () => ({
53+
useExtensionsConfiguration: jest.fn(),
54+
}));
55+
56+
jest.mock('../hooks/useNodeEnvironment', () => ({
57+
useNodeEnvironment: jest.fn(),
58+
}));
59+
60+
jest.mock('../hooks/usePluginPackages', () => ({
61+
usePluginPackages: jest.fn(),
62+
}));
63+
64+
const usePluginMock = usePlugin as jest.Mock;
65+
const useNodeEnvironmentMock = useNodeEnvironment as jest.Mock;
66+
const usePluginPackagesMock = usePluginPackages as jest.Mock;
67+
const usePluginConfigurationPermissionsMock =
68+
usePluginConfigurationPermissions as jest.Mock;
69+
const useExtensionsConfigurationMock = useExtensionsConfiguration as jest.Mock;
70+
71+
beforeEach(() => {
72+
usePluginConfigurationPermissionsMock.mockReturnValue({
73+
data: {
74+
write: 'ALLOW',
75+
read: 'ALLOW',
76+
},
77+
isLoading: false,
78+
error: null,
79+
refetch: jest.fn(),
80+
});
81+
useExtensionsConfigurationMock.mockReturnValue({
82+
data: {
83+
enabled: true,
84+
},
85+
});
86+
useNodeEnvironmentMock.mockReturnValue({
87+
data: {
88+
nodeEnv: 'test',
89+
},
90+
});
91+
jest.clearAllMocks();
92+
});
93+
94+
const mockMarketplaceApi = {
95+
getPluginPackages: jest.fn(),
96+
};
97+
98+
afterEach(() => {
99+
jest.clearAllMocks();
100+
});
101+
102+
const renderWithProviders = (ui: React.ReactNode) =>
103+
render(
104+
<TestApiProvider apis={[[marketplaceApiRef, mockMarketplaceApi]]}>
105+
<BrowserRouter>{ui}</BrowserRouter>
106+
</TestApiProvider>,
107+
);
108+
109+
describe('MarketplacePluginContent', () => {
110+
const packages = [
111+
{
112+
metadata: {
113+
annotations: {},
114+
name: 'backstage-community-plugin-3scale-backend',
115+
namespace: 'marketplace-plugin-demo',
116+
title: '@backstage-community/plugin-3scale-backend',
117+
},
118+
apiVersion: 'extensions.backstage.io/v1alpha1',
119+
kind: 'Package',
120+
spec: {
121+
packageName: '@backstage-community/plugin-3scale-backend',
122+
dynamicArtifact:
123+
'./dynamic-plugins/dist/backstage-community-plugin-3scale-backend-dynamic',
124+
partOf: ['backstage-community-plugin-3scale-backend'],
125+
},
126+
},
127+
];
128+
129+
const plugin = {
130+
metadata: {
131+
annotations: {},
132+
namespace: 'marketplace-plugin-demo',
133+
name: '3scale',
134+
title: 'APIs with 3scale',
135+
description: 'Synchronize 3scale content into the Backstage catalog.',
136+
},
137+
apiVersion: 'extensions.backstage.io/v1alpha1',
138+
kind: 'Plugin',
139+
spec: {
140+
icon: 'https://janus-idp.io/images/plugins/3scale.svg',
141+
packages: ['backstage-community-plugin-3scale-backend'],
142+
},
143+
};
144+
145+
usePluginPackagesMock.mockReturnValue({
146+
isLoading: false,
147+
data: packages,
148+
});
149+
usePluginMock.mockReturnValue({
150+
isLoading: false,
151+
data: plugin,
152+
});
153+
it('should have the Install button enabled', async () => {
154+
const { getByText } = renderWithProviders(
155+
<MarketplacePluginContent plugin={plugin} enableActionsButtonFeature />,
156+
);
157+
expect(getByText('Install')).toBeInTheDocument();
158+
const installButton = getByText('Install');
159+
expect(installButton).toBeEnabled();
160+
});
161+
162+
it('should have the View button', async () => {
163+
usePluginConfigurationPermissionsMock.mockReturnValue({
164+
data: {
165+
write: 'DENY',
166+
read: 'ALLOW',
167+
},
168+
isLoading: false,
169+
error: null,
170+
refetch: jest.fn(),
171+
});
172+
173+
const { getByText } = renderWithProviders(
174+
<MarketplacePluginContent plugin={plugin} enableActionsButtonFeature />,
175+
);
176+
expect(getByText('View')).toBeInTheDocument();
177+
});
178+
179+
it('should have the View button for production env', async () => {
180+
useNodeEnvironmentMock.mockReturnValue({
181+
data: {
182+
nodeEnv: 'production',
183+
},
184+
});
185+
186+
const { getByText } = renderWithProviders(
187+
<MarketplacePluginContent plugin={plugin} enableActionsButtonFeature />,
188+
);
189+
expect(getByText('View')).toBeInTheDocument();
190+
});
191+
192+
it('should have the Install button disabled', async () => {
193+
usePluginConfigurationPermissionsMock.mockReturnValue({
194+
data: {
195+
write: 'DENY',
196+
read: 'DENY',
197+
},
198+
isLoading: false,
199+
error: null,
200+
refetch: jest.fn(),
201+
});
202+
203+
const { getByText } = renderWithProviders(
204+
<MarketplacePluginContent plugin={plugin} enableActionsButtonFeature />,
205+
);
206+
expect(getByText('Install')).toBeInTheDocument();
207+
const installButton = getByText('Install');
208+
expect(installButton).toBeDisabled();
209+
});
210+
211+
it('should render the Actions button', async () => {
212+
const installedPlugin = {
213+
...plugin,
214+
spec: {
215+
...plugin.spec,
216+
installStatus: MarketplacePluginInstallStatus.Installed,
217+
},
218+
};
219+
220+
const { getByText, getByTestId } = renderWithProviders(
221+
<MarketplacePluginContent
222+
plugin={installedPlugin}
223+
enableActionsButtonFeature
224+
/>,
225+
);
226+
expect(getByText('Actions')).toBeInTheDocument();
227+
const actionsButton = getByTestId('plugin-actions');
228+
fireEvent.click(actionsButton);
229+
230+
expect(getByTestId('actions-button')).toBeInTheDocument();
231+
expect(getByTestId('enable-plugin')).toBeInTheDocument();
232+
});
233+
});

0 commit comments

Comments
 (0)