Skip to content

Commit dd66369

Browse files
feat: [M3-10324] - Add type-to-confirm to Images (linode#12740)
* support type to confirm on Images * fix up cypress test and loading states * remove question mark from dialog title * add unit test for image row fix * fix type-safety issue * Added changeset: Type-to-confirm to Image deletion dialog --------- Co-authored-by: Banks Nussman <[email protected]>
1 parent ea8c9da commit dd66369

File tree

8 files changed

+123
-126
lines changed

8 files changed

+123
-126
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
Type-to-confirm to Image deletion dialog ([#12740](https://github.com/linode/manager/pull/12740))

packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { apiMatcher } from 'support/util/intercepts';
1616
import { randomLabel, randomPhrase } from 'support/util/random';
1717
import { chooseRegion } from 'support/util/regions';
1818

19+
import { DISALLOWED_IMAGE_REGIONS } from 'src/constants';
20+
1921
import type { EventStatus } from '@linode/api-v4';
2022
import type { RecPartial } from 'factory.ts';
2123

@@ -121,7 +123,7 @@ const uploadImage = (label: string) => {
121123
// See also BAC-862.
122124
const region = chooseRegion({
123125
capabilities: ['Object Storage'],
124-
exclude: ['au-mel', 'gb-lon', 'sg-sin-2'],
126+
exclude: DISALLOWED_IMAGE_REGIONS,
125127
});
126128
const upload = 'machine-images/test-image.gz';
127129
cy.visitWithLogin('/images/create/upload');
@@ -238,8 +240,13 @@ describe('machine image', () => {
238240
.findByTitle(`Delete Image ${updatedLabel}`)
239241
.should('be.visible')
240242
.within(() => {
243+
cy.findByLabelText('Image Label')
244+
.should('be.visible')
245+
.should('be.enabled')
246+
.type(updatedLabel);
247+
241248
ui.buttonGroup
242-
.findButtonByTitle('Delete Image')
249+
.findButtonByTitle('Delete')
243250
.should('be.visible')
244251
.should('be.enabled')
245252
.click();

packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface EntityInfo {
2828
| 'Bucket'
2929
| 'Database'
3030
| 'Domain'
31+
| 'Image'
3132
| 'Kubernetes'
3233
| 'Linode'
3334
| 'Load Balancer'
@@ -87,7 +88,7 @@ interface TypeToConfirmDialogProps {
8788
*/
8889
reversePrimaryButtonPosition?: boolean;
8990
/** Props for the secondary button */
90-
secondaryButtonProps?: Omit<ActionButtonsProps, 'label'>;
91+
secondaryButtonProps?: ActionButtonsProps;
9192
}
9293

9394
type CombinedProps = TypeToConfirmDialogProps &
@@ -176,10 +177,10 @@ export const TypeToConfirmDialog = (props: CombinedProps) => {
176177
};
177178

178179
const cancelProps: ActionButtonsProps = {
179-
...secondaryButtonProps,
180180
'data-testid': 'cancel',
181181
label: 'Cancel',
182182
onClick: () => onClose?.({}, 'escapeKeyDown'),
183+
...secondaryButtonProps,
183184
};
184185

185186
return {
@@ -207,7 +208,7 @@ export const TypeToConfirmDialog = (props: CombinedProps) => {
207208
}
208209

209210
const typeInstructions =
210-
entity.action === 'cancellation'
211+
entity.action === 'cancellation' && entity.type === 'AccountSetting'
211212
? 'type your Username '
212213
: `type the name of the ${entity.type} ${entity.subType || ''} `;
213214

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useDeleteImageMutation, useImageQuery } from '@linode/queries';
2+
import { useSnackbar } from 'notistack';
3+
import React from 'react';
4+
5+
import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog';
6+
7+
interface Props {
8+
imageId: string | undefined;
9+
onClose: () => void;
10+
open: boolean;
11+
}
12+
13+
export const DeleteImageDialog = (props: Props) => {
14+
const { imageId, open, onClose } = props;
15+
const { enqueueSnackbar } = useSnackbar();
16+
17+
const {
18+
data: image,
19+
isLoading,
20+
error,
21+
} = useImageQuery(imageId ?? '', Boolean(imageId));
22+
23+
const { mutate: deleteImage, isPending } = useDeleteImageMutation({
24+
onSuccess() {
25+
enqueueSnackbar('Image has been scheduled for deletion.', {
26+
variant: 'info',
27+
});
28+
onClose();
29+
},
30+
});
31+
32+
const isPendingUpload = image?.status === 'pending_upload';
33+
34+
return (
35+
<TypeToConfirmDialog
36+
entity={{
37+
type: 'Image',
38+
primaryBtnText: isPendingUpload ? 'Cancel Upload' : 'Delete',
39+
action: isPendingUpload ? 'cancellation' : 'deletion',
40+
name: image?.label,
41+
}}
42+
errors={error}
43+
isFetching={isLoading}
44+
label="Image Label"
45+
loading={isPending}
46+
onClick={() => deleteImage({ imageId: imageId ?? '' })}
47+
onClose={onClose}
48+
open={open}
49+
secondaryButtonProps={{
50+
label: isPendingUpload ? 'Keep Image' : 'Cancel',
51+
}}
52+
title={
53+
isPendingUpload
54+
? 'Cancel Upload'
55+
: `Delete Image ${image?.label ?? imageId}`
56+
}
57+
/>
58+
);
59+
};

packages/manager/src/features/Images/ImagesLanding/ImageRow.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,27 @@ describe('Image Table Row', () => {
109109
).toBeNull();
110110
});
111111

112+
it('should not show an unencrypted icon when an Image is still "pending_upload"', () => {
113+
// The API does not populate the "distributed-sites" capability until the image is done creating.
114+
// We must account for this because the image would show as "Unencrypted" while it is creating,
115+
// then suddenly show as encrypted once it was done creating. We don't want that.
116+
// Therefore, we decided we won't show the unencrypted icon until the image is done uploading to
117+
// prevent confusion.
118+
const image = imageFactory.build({
119+
capabilities: ['cloud-init'],
120+
status: 'pending_upload',
121+
type: 'manual',
122+
});
123+
124+
const { queryByLabelText } = renderWithTheme(
125+
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
126+
);
127+
128+
expect(
129+
queryByLabelText('This image is not encrypted.', { exact: false })
130+
).toBeNull();
131+
});
132+
112133
it('should show N/A if Image does not have any regions', () => {
113134
const image = imageFactory.build({ regions: [] });
114135

packages/manager/src/features/Images/ImagesLanding/ImageRow.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const ImageRow = (props: Props) => {
7878
<Stack alignItems="center" direction="row" gap={1}>
7979
{type === 'manual' &&
8080
status !== 'creating' &&
81+
status !== 'pending_upload' &&
8182
!image.capabilities.includes('distributed-sites') && (
8283
<TooltipIcon
8384
icon={<UnlockIcon height="18px" width="18px" />}

0 commit comments

Comments
 (0)