Skip to content

Commit 3c8a177

Browse files
authored
[ACM-16080] Dynamic VM actions in search results and details page (stolostron#4145)
* [ACM-16080] Dynamic VM actions in search results and details page Signed-off-by: zlayne <zlayne@redhat.com> * add test for detailspage Signed-off-by: zlayne <zlayne@redhat.com> --------- Signed-off-by: zlayne <zlayne@redhat.com>
1 parent 61389c9 commit 3c8a177

File tree

4 files changed

+227
-57
lines changed

4 files changed

+227
-57
lines changed

frontend/src/routes/Search/Details/DetailsPage.test.tsx

+120-29
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,13 @@
55
import { render, screen, waitFor } from '@testing-library/react'
66
import { MemoryRouter, Route, Routes } from 'react-router-dom-v5-compat'
77
import { RecoilRoot } from 'recoil'
8+
import { settingsState } from '../../../atoms'
89
import { nockGet, nockIgnoreApiPaths, nockIgnoreRBAC, nockPostRequest } from '../../../lib/nock-util'
910
import { waitForNocks } from '../../../lib/test-util'
1011
import { NavigationPath } from '../../../NavigationPath'
1112
import Search from '../Search'
1213
import { getResourceParams } from './DetailsPage'
1314

14-
jest.mock('react-router-dom-v5-compat', () => {
15-
const originalModule = jest.requireActual('react-router-dom-v5-compat')
16-
return {
17-
__esModule: true,
18-
...originalModule,
19-
useLocation: () => ({
20-
pathname: '/multicloud/search/resources',
21-
search:
22-
'?cluster=local-cluster&kind=Pod&apiversion=v1&namespace=testNamespace&name=testLocalPod&_hubClusterResource=true',
23-
state: {
24-
from: '/multicloud/search',
25-
fromSearch: '?filters={%22textsearch%22:%22kind%3APod%22}',
26-
},
27-
}),
28-
useNavigate: () => jest.fn(),
29-
}
30-
})
31-
Object.defineProperty(window, 'location', {
32-
value: {
33-
pathname: '/multicloud/search/resources',
34-
search:
35-
'?cluster=local-cluster&kind=Pod&apiversion=v1&namespace=testNamespace&name=testLocalPod&_hubClusterResource=true',
36-
state: {
37-
from: '/multicloud/search',
38-
fromSearch: '?filters={%22textsearch%22:%22kind%3APod%2',
39-
},
40-
},
41-
})
42-
4315
const mockLocalClusterPod = {
4416
kind: 'Pod',
4517
apiVersion: 'v1',
@@ -91,12 +63,47 @@ const mockLocalClusterPod = {
9163

9264
describe('DetailsPage', () => {
9365
beforeEach(async () => {
66+
// jest.resetAllMocks()
9467
nockIgnoreRBAC()
9568
nockIgnoreApiPaths()
9669
})
70+
afterEach(() => {
71+
jest.resetAllMocks()
72+
// Object.defineProperty(window, 'location', {
73+
// value: {},
74+
// })
75+
})
9776
const metricNock = nockPostRequest('/metrics?search-details', {})
9877

9978
it('should render local-cluster resource details correctly', async () => {
79+
jest.mock('react-router-dom-v5-compat', () => {
80+
const originalModule = jest.requireActual('react-router-dom-v5-compat')
81+
return {
82+
__esModule: true,
83+
...originalModule,
84+
useLocation: () => ({
85+
pathname: '/multicloud/search/resources',
86+
search:
87+
'?cluster=local-cluster&kind=Pod&apiversion=v1&namespace=testNamespace&name=testLocalPod&_hubClusterResource=true',
88+
state: {
89+
from: '/multicloud/search',
90+
fromSearch: '?filters={%22textsearch%22:%22kind%3APod%22}',
91+
},
92+
}),
93+
useNavigate: () => jest.fn(),
94+
}
95+
})
96+
Object.defineProperty(window, 'location', {
97+
value: {
98+
pathname: '/multicloud/search/resources',
99+
search:
100+
'?cluster=local-cluster&kind=Pod&apiversion=v1&namespace=testNamespace&name=testLocalPod&_hubClusterResource=true',
101+
state: {
102+
from: '/multicloud/search',
103+
fromSearch: '?filters={%22textsearch%22:%22kind%3APod%2',
104+
},
105+
},
106+
})
100107
render(
101108
<RecoilRoot>
102109
<MemoryRouter initialEntries={[NavigationPath.resources]}>
@@ -148,4 +155,88 @@ describe('DetailsPage', () => {
148155
const res = getResourceParams()
149156
expect(res).toMatchSnapshot()
150157
})
158+
159+
it('should render VirtualMachine resource details correctly', async () => {
160+
jest.mock('react-router-dom-v5-compat', () => {
161+
const originalModule = jest.requireActual('react-router-dom-v5-compat')
162+
return {
163+
__esModule: true,
164+
...originalModule,
165+
useLocation: () => ({
166+
pathname: '/multicloud/search/resources',
167+
search:
168+
'?cluster=local-cluster&kind=VirtualMachine&apiversion=kubevirt.io/v1&namespace=openshift-cnv&name=test-vm&_hubClusterResource=true',
169+
state: {
170+
from: '/multicloud/search',
171+
fromSearch: '?filters={%22textsearch%22:%22kind%3AVirtualMachine%22}',
172+
},
173+
}),
174+
useNavigate: () => jest.fn(),
175+
}
176+
})
177+
Object.defineProperty(window, 'location', {
178+
value: {
179+
pathname: '/multicloud/search/resources',
180+
search:
181+
'?cluster=local-cluster&kind=VirtualMachine&apiversion=kubevirt.io/v1&namespace=openshift-cnv&name=test-vm&_hubClusterResource=true',
182+
state: {
183+
from: '/multicloud/search',
184+
fromSearch: '?filters={%22textsearch%22:%22kind%3AVirtualMachine%2',
185+
},
186+
},
187+
})
188+
render(
189+
<RecoilRoot
190+
initializeState={(snapshot) => {
191+
snapshot.set(settingsState, { VIRTUAL_MACHINE_ACTIONS: 'enabled' })
192+
}}
193+
>
194+
<MemoryRouter initialEntries={[NavigationPath.resources]}>
195+
<Routes>
196+
<Route path={`${NavigationPath.search}/*`} element={<Search />} />
197+
</Routes>
198+
</MemoryRouter>
199+
</RecoilRoot>
200+
)
201+
202+
// Wait for delete resource requests to finish
203+
await waitForNocks([
204+
metricNock,
205+
nockGet({
206+
apiVersion: 'kubevirt.io/v1',
207+
kind: 'VirtualMachine',
208+
metadata: {
209+
creationTimestamp: '2024-10-02T20:02:14Z',
210+
name: 'test-vm',
211+
namespace: 'openshift-cnv',
212+
},
213+
spec: {
214+
running: true,
215+
template: {
216+
metadata: {
217+
creationTimestamp: null,
218+
},
219+
spec: {},
220+
},
221+
status: {
222+
created: true,
223+
desiredGeneration: 9,
224+
observedGeneration: 9,
225+
printableStatus: 'Running',
226+
ready: true,
227+
runStrategy: 'Always',
228+
},
229+
},
230+
}),
231+
])
232+
233+
// Test that the component has rendered correctly with data
234+
await waitFor(() =>
235+
expect(
236+
screen.getByRole('heading', {
237+
name: /test-vm/i,
238+
})
239+
).toBeTruthy()
240+
)
241+
})
151242
})

frontend/src/routes/Search/Details/DetailsPage.tsx

+40-21
Original file line numberDiff line numberDiff line change
@@ -167,33 +167,51 @@ export default function DetailsPage() {
167167
</DropdownItem>,
168168
]
169169
if (vmActionsEnabled && kind.toLowerCase() === 'virtualmachine') {
170+
const printableStatus = resource?.status?.printableStatus ?? ''
170171
actions.unshift(
171172
...[
172-
{
173-
action: 'Start',
174-
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachines/${name}/start`,
175-
managedPath: '/virtualmachines/start',
176-
},
177-
{
178-
action: 'Stop',
179-
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachines/${name}/stop`,
180-
managedPath: '/virtualmachines/stop',
181-
},
173+
printableStatus === 'Stopped'
174+
? {
175+
action: 'Start',
176+
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachines/${name}/start`,
177+
managedPath: '/virtualmachines/start',
178+
isDisabled: [
179+
'Migrating',
180+
'Provisioning',
181+
'Running',
182+
'Starting',
183+
'Stopping',
184+
'Terminating',
185+
'Unknown',
186+
].includes(printableStatus),
187+
}
188+
: {
189+
action: 'Stop',
190+
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachines/${name}/stop`,
191+
managedPath: '/virtualmachines/stop',
192+
isDisabled: ['Provisioning', 'Stopped', 'Stopping', 'Terminating', 'Unknown'].includes(printableStatus),
193+
},
182194
{
183195
action: 'Restart',
184196
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachines/${name}/restart`,
185197
managedPath: '/virtualmachines/restart',
198+
isDisabled: ['Migrating', 'Provisioning', 'Stopped', 'Stopping', 'Terminating', 'Unknown'].includes(
199+
printableStatus
200+
),
186201
},
187-
{
188-
action: 'Pause',
189-
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachineinstances/${name}/pause`,
190-
managedPath: '/virtualmachineinstances/pause',
191-
},
192-
{
193-
action: 'Unpause',
194-
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachineinstances/${name}/unpause`,
195-
managedPath: '/virtualmachineinstances/unpause',
196-
},
202+
printableStatus === 'Paused'
203+
? {
204+
action: 'Unpause',
205+
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachineinstances/${name}/unpause`,
206+
managedPath: '/virtualmachineinstances/unpause',
207+
isDisabled: printableStatus !== 'Paused',
208+
}
209+
: {
210+
action: 'Pause',
211+
hubPath: `/apis/subresources.kubevirt.io/v1/namespaces/${namespace}/virtualmachineinstances/${name}/pause`,
212+
managedPath: '/virtualmachineinstances/pause',
213+
isDisabled: printableStatus !== 'Running',
214+
},
197215
].map((action) => (
198216
<DropdownItem
199217
key={`${action.action}-vm-resource`}
@@ -208,6 +226,7 @@ export default function DetailsPage() {
208226
t
209227
)
210228
}
229+
isDisabled={action.isDisabled}
211230
>
212231
{t(`{{action}} {{resourceKind}}`, { action: action.action, resourceKind: kind })}
213232
</DropdownItem>
@@ -216,7 +235,7 @@ export default function DetailsPage() {
216235
)
217236
}
218237
return actions
219-
}, [cluster, kind, name, namespace, isHubClusterResource, vmActionsEnabled, navigate, toast, t])
238+
}, [resource, cluster, kind, name, namespace, isHubClusterResource, vmActionsEnabled, navigate, toast, t])
220239

221240
return (
222241
<AcmPage

frontend/src/routes/Search/SearchResults/RelatedResults.tsx

+27-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import {
1010
StackItem,
1111
} from '@patternfly/react-core'
1212
import _ from 'lodash'
13-
import { useMemo } from 'react'
13+
import { useContext, useMemo } from 'react'
14+
import { useNavigate } from 'react-router-dom-v5-compat'
1415
import { useTranslation } from '../../../lib/acm-i18next'
1516
import { useRecoilValue, useSharedAtoms } from '../../../shared-recoil'
16-
import { AcmLoadingPage, AcmTable, compareStrings } from '../../../ui-components'
17+
import { AcmLoadingPage, AcmTable, AcmToastContext, compareStrings } from '../../../ui-components'
18+
import { useAllClusters } from '../../Infrastructure/Clusters/ManagedClusters/components/useAllClusters'
19+
import { getVirtualMachineRowActions } from '../../Infrastructure/VirtualMachines/utils'
1720
import { IDeleteExternalResourceModalProps } from '../components/Modals/DeleteExternalResourceModal'
1821
import { IDeleteModalProps } from '../components/Modals/DeleteResourceModal'
1922
import { convertStringToQuery, federatedErrorText } from '../search-helper'
@@ -33,6 +36,11 @@ export function RenderItemContent(
3336
) {
3437
const { currentQuery, relatedKind, setDeleteResource, setDeleteExternalResource, hasFederatedError } = props
3538
const { t } = useTranslation()
39+
const navigate = useNavigate()
40+
const toast = useContext(AcmToastContext)
41+
const allClusters = useAllClusters(true)
42+
const { settingsState } = useSharedAtoms()
43+
const vmActionsEnabled = useRecoilValue(settingsState)?.VIRTUAL_MACHINE_ACTIONS === 'enabled'
3644
const { useSearchResultLimit } = useSharedAtoms()
3745
const searchResultLimit = useSearchResultLimit()
3846
const rowActions = useGetRowActions(relatedKind, currentQuery, false, setDeleteResource, setDeleteExternalResource)
@@ -71,7 +79,23 @@ export function RenderItemContent(
7179
emptyState={undefined} // table only shown for kinds with related resources
7280
columns={colDefs}
7381
keyFn={(item: any) => item?._uid.toString() ?? `${item.name}-${item.namespace}-${item.cluster}`}
74-
rowActions={rowActions}
82+
rowActions={relatedKind.toLowerCase() !== 'virtualmachine' ? rowActions : undefined}
83+
rowActionResolver={
84+
// use the row action resolvers so we can dynamically display/enabled certain actions based on the resource status.
85+
relatedKind.toLowerCase() === 'virtualmachine'
86+
? (item: any) =>
87+
getVirtualMachineRowActions(
88+
item,
89+
allClusters,
90+
setDeleteResource,
91+
setDeleteExternalResource,
92+
vmActionsEnabled,
93+
toast,
94+
navigate,
95+
t
96+
)
97+
: undefined
98+
}
7599
/>
76100
)
77101
}

0 commit comments

Comments
 (0)