-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WM-2427] Remove ability for Cromwell appTypes to be deleted #4592
Changes from 27 commits
01837c7
f21115f
f94f52e
cc9aa48
4268574
d553483
b50e128
8dabb33
dcdc6a4
fcb1fbc
5e09b16
f870837
6d16f23
5d35d62
58937b1
268eae7
fcf557a
9814dee
27fead3
8d14b12
0b70825
967a632
2466b5a
30e520a
c6b70e1
b68ef1b
60641fc
9125f5b
b3988d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,44 @@ | ||
import _ from 'lodash/fp'; | ||
import { ReactNode } from 'react'; | ||
import { h } from 'react-hyperscript-helpers'; | ||
import { isResourceDeletable } from 'src/analysis/utils/resource-utils'; | ||
import { getDisplayRuntimeStatus } from 'src/analysis/utils/runtime-utils'; | ||
import { Link } from 'src/components/common'; | ||
import { makeMenuIcon } from 'src/components/PopupTrigger'; | ||
import { isApp } from 'src/libs/ajax/leonardo/models/app-models'; | ||
import { LeoRuntimeStatus } from 'src/libs/ajax/leonardo/models/runtime-models'; | ||
import * as Utils from 'src/libs/utils'; | ||
|
||
import { DecoratedComputeResource } from './Environments.models'; | ||
import { DecoratedComputeResource, LeoResourcePermissionsProvider } from './Environments.models'; | ||
|
||
type DeletePermissionsProvider = Pick<LeoResourcePermissionsProvider, 'canDeleteApp' | 'canDeleteResource'>; | ||
|
||
export interface DeleteButtonProps { | ||
resource: DecoratedComputeResource; | ||
permissions: DeletePermissionsProvider; | ||
onClick: (resource: DecoratedComputeResource) => void; | ||
} | ||
|
||
export const DeleteButton = (props: DeleteButtonProps): ReactNode => { | ||
const { resource, onClick } = props; | ||
const resourceType = isApp(resource) ? 'app' : 'runtime'; | ||
const isDeletable = isResourceDeletable(resourceType, resource); | ||
const { resource, permissions, onClick } = props; | ||
const isDeletable = isApp(resource) ? permissions.canDeleteApp(resource) : permissions.canDeleteResource(resource); | ||
|
||
return h( | ||
Link, | ||
{ | ||
disabled: !isDeletable, | ||
tooltip: isDeletable | ||
? 'Delete cloud environment' | ||
: `Cannot delete a cloud environment while in status ${_.upperCase( | ||
getDisplayRuntimeStatus(resource.status as LeoRuntimeStatus) | ||
)}.`, | ||
tooltip: Utils.cond( | ||
[isDeletable, () => 'Delete cloud environment'], | ||
[!permissions.canDeleteApp(resource), () => 'Deleting not yet supported'], | ||
[ | ||
Utils.DEFAULT, | ||
() => | ||
`Cannot delete a cloud environment while in status ${_.upperCase( | ||
getDisplayRuntimeStatus(resource.status as LeoRuntimeStatus) | ||
)}.`, | ||
] | ||
), | ||
onClick: () => onClick(resource), | ||
}, | ||
[makeMenuIcon('trash'), 'Delete'] | ||
[makeMenuIcon('trash', undefined), 'Delete'] | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { NavLinkProvider } from '@terra-ui-packages/core-utils'; | ||
import { act, fireEvent, getAllByRole, screen } from '@testing-library/react'; | ||
import { act, fireEvent, getAllByRole, screen, within } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import _ from 'lodash/fp'; | ||
import { h } from 'react-hyperscript-helpers'; | ||
|
@@ -21,6 +21,7 @@ import { LeoAppProvider } from 'src/libs/ajax/leonardo/providers/LeoAppProvider' | |
import { LeoDiskProvider } from 'src/libs/ajax/leonardo/providers/LeoDiskProvider'; | ||
import { LeoRuntimeProvider } from 'src/libs/ajax/leonardo/providers/LeoRuntimeProvider'; | ||
import { makeCompleteDate } from 'src/libs/utils'; | ||
import { leoResourcePermissions } from 'src/pages/EnvironmentsPage/environmentsPermissions'; | ||
import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils'; | ||
import { defaultAzureWorkspace, defaultGoogleWorkspace } from 'src/testing/workspace-fixtures'; | ||
import { WorkspaceWrapper } from 'src/workspaces/utils'; | ||
|
@@ -36,10 +37,6 @@ const mockNav: NavLinkProvider<EnvironmentNavActions> = { | |
getUrl: jest.fn().mockReturnValue('/'), | ||
navTo: jest.fn(), | ||
}; | ||
const mockPermissions: LeoResourcePermissionsProvider = { | ||
canDeleteDisk: jest.fn(), | ||
canPauseResource: jest.fn(), | ||
}; | ||
|
||
const defaultUseWorkspacesProps = { | ||
workspaces: [defaultGoogleWorkspace] as WorkspaceWrapper[], | ||
|
@@ -81,8 +78,16 @@ const getMockLeoDiskProvider = (overrides?: Partial<LeoDiskProvider>): LeoDiskPr | |
}; | ||
|
||
const getEnvironmentsProps = (propsOverrides?: Partial<EnvironmentsProps>): EnvironmentsProps => { | ||
const mockPermissions: LeoResourcePermissionsProvider = { | ||
canDeleteDisk: jest.fn(), | ||
canPauseResource: jest.fn(), | ||
canDeleteApp: jest.fn(), | ||
canDeleteResource: jest.fn(), | ||
}; | ||
asMockedFn(mockPermissions.canDeleteDisk).mockReturnValue(true); | ||
asMockedFn(mockPermissions.canPauseResource).mockReturnValue(true); | ||
asMockedFn(mockPermissions.canDeleteApp).mockReturnValue(true); | ||
asMockedFn(mockPermissions.canDeleteResource).mockReturnValue(true); | ||
|
||
const defaultProps: EnvironmentsProps = { | ||
nav: mockNav, | ||
|
@@ -237,6 +242,7 @@ describe('Environments', () => { | |
...defaultUseWorkspacesProps, | ||
workspaces: [defaultGoogleWorkspace, defaultAzureWorkspace], | ||
}); | ||
props.permissions.canDeleteResource = leoResourcePermissions.canDeleteResource; | ||
|
||
// Act | ||
await act(async () => { | ||
|
@@ -524,6 +530,48 @@ describe('Environments', () => { | |
expect(getTextContentForColumn(fourthAppRow, 8)).toBe(makeCompleteDate(azureApp2.auditInfo.dateAccessed)); | ||
}); | ||
|
||
it('Renders Cromwell apps with disabled delete', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for adding a detailed test! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed! this is a great looking test. |
||
// Arrange | ||
const props = getEnvironmentsProps(); | ||
|
||
const googleApp1 = generateTestAppWithGoogleWorkspace({}, defaultGoogleWorkspace); | ||
const azureApp1 = generateTestAppWithAzureWorkspace({ appType: appToolLabels.CROMWELL }, defaultAzureWorkspace); | ||
const azureWorkspace2 = generateAzureWorkspace(); | ||
const azureApp2 = generateTestAppWithAzureWorkspace({ appType: appToolLabels.WORKFLOWS_APP }, azureWorkspace2); | ||
asMockedFn(props.leoAppData.listWithoutProject).mockResolvedValue([googleApp1, azureApp1, azureApp2]); | ||
asMockedFn(props.useWorkspaces).mockReturnValue({ | ||
...defaultUseWorkspacesProps, | ||
workspaces: [defaultGoogleWorkspace, defaultAzureWorkspace, azureWorkspace2], | ||
}); | ||
props.permissions = leoResourcePermissions; | ||
|
||
// Act | ||
await act(async () => { | ||
render(h(Environments, props)); | ||
}); | ||
|
||
// Assert | ||
expect(screen.getAllByRole('row')).toHaveLength(6); | ||
|
||
const tableRows: HTMLElement[] = screen.getAllByRole('row').slice(1); // skip header row | ||
kpierre13 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const firstAppRow: HTMLElement = tableRows[0]; | ||
const actionColumnButton1 = within(firstAppRow).getByRole('button', { name: 'Delete' }); | ||
expect(actionColumnButton1).not.toHaveAttribute('disabled'); | ||
|
||
const secondAppRow: HTMLElement = tableRows[1]; | ||
const actionColumnButton2 = within(secondAppRow).getByRole('button', { name: 'Delete' }); | ||
expect(actionColumnButton2).toHaveAttribute('disabled'); | ||
|
||
const thirdAppRow: HTMLElement = tableRows[2]; | ||
const actionColumnButton3 = within(thirdAppRow).getByRole('button', { name: 'Delete' }); | ||
expect(actionColumnButton3).toHaveAttribute('disabled'); | ||
|
||
// Check for tooltip, and that it's only present for azure apps | ||
const notSupportedTooltip = screen.getAllByText('Deleting not yet supported'); | ||
expect(notSupportedTooltip).toBeInTheDocument; | ||
expect(notSupportedTooltip.length).toBe(2); | ||
}); | ||
|
||
it.each([ | ||
{ app: generateTestAppWithGoogleWorkspace({}, defaultGoogleWorkspace), workspace: defaultGoogleWorkspace }, | ||
{ app: generateTestAppWithAzureWorkspace({}, defaultAzureWorkspace), workspace: defaultAzureWorkspace }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ import _ from 'lodash/fp'; | |
import { getAppStatusForDisplay } from 'src/analysis/utils/app-utils'; | ||
import { getDisplayRuntimeStatus } from 'src/analysis/utils/runtime-utils'; | ||
import { App, isApp } from 'src/libs/ajax/leonardo/models/app-models'; | ||
import { PersistentDisk } from 'src/libs/ajax/leonardo/models/disk-models'; | ||
import { isPersistentDisk, PersistentDisk } from 'src/libs/ajax/leonardo/models/disk-models'; | ||
import { isRuntime, Runtime, runtimeStatuses } from 'src/libs/ajax/leonardo/models/runtime-models'; | ||
import * as Utils from 'src/libs/utils'; | ||
|
||
|
@@ -13,20 +13,17 @@ import * as Utils from 'src/libs/utils'; | |
* https://github.com/DataBiosphere/leonardo/blob/e60c71a9e78b53196c2848cd22a752e22a2cf6f5/core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/diskModels.scala | ||
*/ | ||
// TODO: stop using resourceType here when all types are defined.... | ||
export const isResourceDeletable = (resourceType, resource: App | PersistentDisk | Runtime) => | ||
export const isResourceDeletable = (resource: App | PersistentDisk | Runtime) => | ||
_.includes( | ||
_.lowerCase(resource?.status), | ||
Utils.switchCase( | ||
resourceType, | ||
['runtime', () => ['unknown', 'running', 'updating', 'error', 'stopping', 'stopped', 'starting']], | ||
['app', () => ['unspecified', 'running', 'error']], | ||
['disk', () => ['failed', 'ready']], | ||
Utils.cond( | ||
[isApp(resource), () => ['unspecified', 'running', 'error']], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks a lot for cleaning this up! You can remove the TODO on line 15 now! |
||
[isRuntime(resource), () => ['unknown', 'running', 'updating', 'error', 'stopping', 'stopped', 'starting']], | ||
[isPersistentDisk(resource), () => ['failed', 'ready']], | ||
[ | ||
Utils.DEFAULT, | ||
() => { | ||
console.error( | ||
`Cannot determine deletability; resource type ${resourceType} must be one of runtime, app or disk.` | ||
); | ||
console.error('Cannot determine deletability; resource type must be one of runtime, app or disk.'); | ||
return undefined; | ||
}, | ||
] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this
undefined
do?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment, nothing. TS was unhappy here because it expected a
props
arg, but there wasn't one. I added theundefined
to make this function happy.