Skip to content
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

Merged
merged 29 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
01837c7
filter out cromwell app types; update test
kpierre13 Jan 16, 2024
f21115f
cleanup
kpierre13 Jan 16, 2024
f94f52e
Commenting out azure tests to be used again in the future
kpierre13 Jan 16, 2024
cc9aa48
refactor
kpierre13 Jan 16, 2024
4268574
excluding cromwell app types
kpierre13 Jan 19, 2024
d553483
Merge branch 'dev' into wm-2427_remove_cromwell_app_instances_kp
kpierre13 Jan 22, 2024
b50e128
using environmentPermissions logic
kpierre13 Jan 23, 2024
8dabb33
add a test; revert other tests
kpierre13 Jan 23, 2024
dcdc6a4
change type of resource
kpierre13 Jan 23, 2024
fcb1fbc
cast to access appType
kpierre13 Jan 23, 2024
5e09b16
update tests
kpierre13 Jan 23, 2024
f870837
wip
kpierre13 Jan 24, 2024
6d16f23
new permission and state provider
kpierre13 Jan 29, 2024
5d35d62
cleanup
kpierre13 Jan 29, 2024
58937b1
Merge branch 'dev' into wm-2427_remove_cromwell_app_instances_kp
kpierre13 Jan 29, 2024
268eae7
test cleanup
kpierre13 Jan 29, 2024
fcf557a
test cleanup
kpierre13 Jan 30, 2024
9814dee
test cleanup
kpierre13 Jan 30, 2024
27fead3
cleanup
kpierre13 Jan 30, 2024
8d14b12
tests
kpierre13 Jan 30, 2024
0b70825
tests
kpierre13 Jan 30, 2024
967a632
renaming PR comment
kpierre13 Feb 1, 2024
2466b5a
stronger type safety in test
kpierre13 Feb 5, 2024
30e520a
Merge branch 'dev' into wm-2427_remove_cromwell_app_instances_kp
kpierre13 Feb 5, 2024
c6b70e1
Merge branch 'dev' into wm-2427_remove_cromwell_app_instances_kp
kpierre13 Feb 6, 2024
b68ef1b
cleanup
kpierre13 Feb 6, 2024
60641fc
PR comments
kpierre13 Feb 7, 2024
9125f5b
CI failures
kpierre13 Feb 7, 2024
b3988d1
Merge branch 'dev' into wm-2427_remove_cromwell_app_instances_kp
kpierre13 Feb 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/analysis/Environments/DeleteButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ 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 { cromwellAppToolLabels } from 'src/analysis/utils/tool-utils';
import { Link } from 'src/components/common';
import { makeMenuIcon } from 'src/components/PopupTrigger';
import { isApp } from 'src/libs/ajax/leonardo/models/app-models';
import { App, 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 { makeCromwellAppsNotDeletable } from 'src/pages/EnvironmentsPage/environmentsPermissions';

import { DecoratedComputeResource } from './Environments.models';

Expand All @@ -18,19 +21,28 @@ export interface DeleteButtonProps {
export const DeleteButton = (props: DeleteButtonProps): ReactNode => {
const { resource, onClick } = props;
const resourceType = isApp(resource) ? 'app' : 'runtime';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be deleted

const isDeletable = isResourceDeletable(resourceType, resource);
// Casting resource to type App to access the appType property
const castToApp = resource as App;
Copy link
Contributor

@jdcanas jdcanas Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to cast.

It sounds like we want to function canBeDeleted to take resource: DecoratedComputeResource and check isResourceDeletable and the logic you have in !Object.keys(cromwellAppToolLabels).includes(resource.appType);. It will need to do something like the following

if (isApp(resource)) { // access appType here } 
else { //unimplemented exception,  other resources, aka runtimes, not implemented yet }

Bonus points if you can consolidate it with the canDeleteDisk function but you'd need to re-wire the type here from DecoratedComputeResource to Resource.

Sorry if this is a bit confusing, definitely down to pair and help you get this through. You've got all the pieces in place here though, a great second iteration! tl;dr, we should not need to cast in the DeleteButton, the permissions provider should handle the guts of isDeletable and can take the resource directly and figure it out from there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you're saying. Let me take a stab at this implementation, and I'll reach out if I get stuck. Thank you for all of the feedback!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just a quick clarification, are you advocating for calling isResourceDeletable from within canBeDeleted?

Copy link
Contributor

@jdcanas jdcanas Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left that intentionally vague 😅 . I think the permissionsProvider ultimately was designed to do one thing only, tell you if you have permissions to delete something (which relates to IAM logic).

I think we are encountering the need for another provider, one that can tell you whether a resource is in a state (business logic or otherwise) that allows a specific action.

In the scope of your PR, I could see you doing any of the following, as long as we don't straddle between multiple approaches:

  • extending the permissionsProvider with this isResourceDeletable functionality (seems the least desirable)
  • extending the utility function isResourceDeletable to include your cromwell-specific logic
  • creating a new provider that the delete button consumes that wraps the permissionsProvider and includes the utility function (isResourceDeletable) logic + your new cromwell app logic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I'm a bit scattered here, but trying to move towards an ideal architecture with old code through incremental improvements is a bit tricky 😅 . I'd vote for the third above, but I understand if you would rather go with something along the lines of #2 to not expand the scope of your PR too much

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries! I don't think it'll be too much of a lift to create a new wrapper provider, so I'll do my best to try and implement option #3 here. I'll reach out in Slack if things start to get a little wonky in my understanding.

const appsToDelete = makeCromwellAppsNotDeletable();
const isDeletable = appsToDelete.canBeDeleted(castToApp) && isResourceDeletable(resourceType, 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'],
[Object.keys(cromwellAppToolLabels).includes(castToApp.appType), () => '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']
Copy link
Contributor

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?

Copy link
Contributor Author

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 the undefined to make this function happy.

[makeMenuIcon('trash', undefined), 'Delete']
);
};
4 changes: 4 additions & 0 deletions src/analysis/Environments/Environments.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@ export interface LeoResourcePermissionsProvider {
canPauseResource: (resource: App | ListRuntimeItem) => boolean;
}

export interface CanDeleteProvider {
canBeDeleted: (resource: App) => boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can live in the LeoResourcePermissionProvider and shouldn't need its own interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also call it canDeleteApp to be consistent with other interface method names

}

export type DeleteRuntimeProvider = Pick<LeoRuntimeProvider, 'delete'>;
export type DeleteDiskProvider = Pick<LeoDiskProvider, 'delete'>;
47 changes: 46 additions & 1 deletion src/analysis/Environments/Environments.test.ts
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';
Expand Down Expand Up @@ -524,6 +524,51 @@ describe('Environments', () => {
expect(getTextContentForColumn(fourthAppRow, 8)).toBe(makeCompleteDate(azureApp2.auditInfo.dateAccessed));
});

it('Renders Cromwell apps with disabled delete', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding a detailed test!

Copy link
Contributor

Choose a reason for hiding this comment

The 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],
});

// Act
await act(async () => {
render(h(Environments, props));
});

// Assert
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)
.getAllByRole('button')
.filter((button) => button.textContent.includes('Delete'))[0];
expect(actionColumnButton1).not.toHaveAttribute('disabled');

const secondAppRow: HTMLElement = tableRows[1];
const actionColumnButton2 = within(secondAppRow)
.getAllByRole('button')
.filter((button) => button.textContent.includes('Delete'))[0];
expect(actionColumnButton2).toHaveAttribute('disabled');

const thirdAppRow: HTMLElement = tableRows[2];
const actionColumnButton3 = within(thirdAppRow)
.getAllByRole('button')
.filter((button) => button.textContent.includes('Delete'))[0];
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 },
Expand Down
7 changes: 7 additions & 0 deletions src/analysis/utils/tool-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CloudProvider, cloudProviderTypes } from 'src/libs/workspace-utils';

export type RuntimeToolLabel = 'Jupyter' | 'RStudio' | 'JupyterLab';
export type AppToolLabel = 'GALAXY' | 'CROMWELL' | 'HAIL_BATCH' | 'WDS' | 'WORKFLOWS_APP' | 'CROMWELL_RUNNER_APP';
export type CromwellAppToolLabel = 'CROMWELL' | 'WORKFLOWS_APP' | 'CROMWELL_RUNNER_APP';
export type AppAccessScope = 'USER_PRIVATE' | 'WORKSPACE_SHARED';
export type LaunchableToolLabel = 'spark' | 'terminal' | 'RStudio' | 'JupyterLab';
export type ToolLabel = RuntimeToolLabel | AppToolLabel;
Expand Down Expand Up @@ -48,6 +49,12 @@ export const appToolLabels: Record<AppToolLabel, AppToolLabel> = {
WDS: 'WDS',
};

export const cromwellAppToolLabels: Record<CromwellAppToolLabel, CromwellAppToolLabel> = {
CROMWELL: 'CROMWELL',
WORKFLOWS_APP: 'WORKFLOWS_APP',
CROMWELL_RUNNER_APP: 'CROMWELL_RUNNER_APP',
};

export const appAccessScopes: Record<AppAccessScope, AppAccessScope> = {
USER_PRIVATE: 'USER_PRIVATE',
WORKSPACE_SHARED: 'WORKSPACE_SHARED',
Expand Down
11 changes: 10 additions & 1 deletion src/pages/EnvironmentsPage/environmentsPermissions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LeoResourcePermissionsProvider } from 'src/analysis/Environments/Environments.models';
import { CanDeleteProvider, LeoResourcePermissionsProvider } from 'src/analysis/Environments/Environments.models';
import { getCreatorForCompute } from 'src/analysis/utils/resource-utils';
import { cromwellAppToolLabels } from 'src/analysis/utils/tool-utils';
import { App } from 'src/libs/ajax/leonardo/models/app-models';
import { PersistentDisk } from 'src/libs/ajax/leonardo/models/disk-models';
import { ListRuntimeItem } from 'src/libs/ajax/leonardo/models/runtime-models';
Expand All @@ -21,6 +22,14 @@ const makePermissionsProvider = (userEmailGetter: () => string): LeoResourcePerm
return permissionsProvider;
};

export const makeCromwellAppsNotDeletable = (): CanDeleteProvider => {
return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should move to the above makePermissionsProvider once you consolidate CanDeleteProvider into LeoResourcePermissionsProvider

canBeDeleted: (resource: App) => {
return !Object.keys(cromwellAppToolLabels).includes(resource.appType);
},
};
};

const currentUserEmailGetter = (): string => {
return getTerraUser().email!;
};
Expand Down
Loading