Skip to content

Commit 8c24ca5

Browse files
feat: delete project dialog (#190)
Co-authored-by: Andreas Kienle <andreas.kienle@sap.com>
1 parent 929dc93 commit 8c24ca5

File tree

11 files changed

+301
-170
lines changed

11 files changed

+301
-170
lines changed

public/locales/en.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@
119119
"DeleteConfirmationDialog": {
120120
"deleteButton": "Delete",
121121
"cancelButton": "Cancel",
122-
"deleteMessage": "You are about to delete the resource",
123-
"deleteMessageType": "Please type",
124-
"deleteMessageConfirm": "to confirm deletion!"
122+
"header": "Delete {{resourceName}}",
123+
"deleteMessage": "You are about to delete the resource <b>{{resourceName}}</b>.",
124+
"deleteConfirmation": "To confirm, type “{{resourceName}}” in the box below"
125125
},
126126
"Loading": {
127127
"title": "Getting Ready",
@@ -157,7 +157,9 @@
157157
},
158158
"ProjectsListView": {
159159
"pageTitle": "Let's get started",
160-
"title": "Projects"
160+
"title": "Projects",
161+
"deleteProject": "Delete project",
162+
"deleteConfirmationDialog": "Project deleted"
161163
},
162164
"ControlPlaneView": {
163165
"accessError": "Managed Control Plane does not have access information yet",
@@ -190,6 +192,12 @@
190192
"mainCommandDescription": "Run this command to delete the workspace:",
191193
"verificationCommandDescription": "To verify the workspace has been deleted, run:"
192194
},
195+
"DeleteProjectDialog": {
196+
"title": "Delete a Project",
197+
"introSection1": "The below instructions will help you delete the project named \"{{projectName}}\" using kubectl.",
198+
"introSection2": "Remember that this action is <bold1>irreversible</bold1> and all resources within the project will be <bold2>permanently deleted</bold2>.",
199+
"mainCommandDescription": "Run this command to delete the project:"
200+
},
193201
"KubectlDeleteMcpDialog": {
194202
"title": "Delete a Managed Control Plane",
195203
"introSection1": "The below will help you delete the Managed Control Plane \"{{mcpName}}\" from workspace \"{{workspaceNamespace}}\" using kubectl.",

src/components/Dialogs/DeleteConfirmationDialog.cy.tsx

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ describe('DeleteConfirmationDialog', () => {
2222

2323
cy.get('ui5-dialog').should('be.visible').should('have.attr', 'open');
2424

25-
cy.contains('Confirm deletion').should('be.visible');
25+
cy.contains('Delete test-resource').should('be.visible');
2626

27-
cy.contains('You are about to delete the resource test-resource').should(
28-
'be.visible',
29-
);
27+
cy.contains('You are about to delete the resource test-resource').should('be.visible');
3028
});
3129

3230
it('should not be visible when isOpen is false', () => {
@@ -44,19 +42,15 @@ describe('DeleteConfirmationDialog', () => {
4442
it('should enable Delete button when correct resource name is typed', () => {
4543
mountDialog();
4644

47-
cy.get('ui5-input[id*="mcp-name-input"]')
48-
.find(' input[id*="inner"]')
49-
.type('test-resource', { force: true });
45+
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
5046

5147
cy.get('ui5-button').contains('Delete').should('not.have.attr', 'disabled');
5248
});
5349

5450
it('should keep Delete button disabled when incorrect name is typed', () => {
5551
mountDialog();
5652

57-
cy.get('ui5-input[id*="mcp-name-input"]')
58-
.find(' input[id*="inner"]')
59-
.type('wrong-name', { force: true });
53+
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('wrong-name', { force: true });
6054

6155
cy.get('ui5-button').contains('Delete').should('have.attr', 'disabled');
6256
});
@@ -74,9 +68,7 @@ describe('DeleteConfirmationDialog', () => {
7468
it('should call onDeletionConfirmed and setIsOpen when Delete is confirmed', () => {
7569
mountDialog();
7670

77-
cy.get('ui5-input[id*="mcp-name-input"]')
78-
.find(' input[id*="inner"]')
79-
.type('test-resource');
71+
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource');
8072

8173
cy.get('ui5-button').contains('Delete').click();
8274

@@ -90,30 +82,22 @@ describe('DeleteConfirmationDialog', () => {
9082
mountDialog();
9183

9284
// Type something
93-
cy.get('ui5-input[id*="mcp-name-input"]')
94-
.find(' input[id*="inner"]')
95-
.type('test-resource', { force: true });
85+
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
9686

9787
// Close dialog
9888
cy.get('ui5-button').contains('Cancel').click();
9989

10090
// Reopen dialog
10191
mountDialog();
10292

103-
cy.get('ui5-input[id*="mcp-name-input"]')
104-
.find(' input[id*="inner"]')
105-
.should('have.value', '');
93+
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').should('have.value', '');
10694
});
10795

10896
it('should display correct resource name in all labels', () => {
10997
mountDialog({ resourceName: 'custom-resource' });
11098

111-
cy.contains('You are about to delete the resource custom-resource.').should(
112-
'be.visible',
113-
);
99+
cy.contains('You are about to delete the resource custom-resource.').should('be.visible');
114100

115-
cy.contains('Please type custom-resource to confirm deletion!').should(
116-
'be.visible',
117-
);
101+
cy.contains('To confirm, type “custom-resource” in the box below').should('be.visible');
118102
});
119103
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.dialogContent {
2+
display: flex;
3+
flex-direction: column;
4+
margin-block-end: 1rem;
5+
align-items: flex-start;
6+
}
7+
8+
.message {
9+
color: var(--sapContent_LabelColor);
10+
}
11+
12+
.confirmLabel {
13+
margin-block-start: 1rem;
14+
font-weight: bold;
15+
}
16+
17+
.confirmationInput {
18+
width: 100%;
19+
}
20+
Lines changed: 65 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import { ReactNode, useEffect, useRef, useState } from 'react';
2-
import {
3-
Bar,
4-
Button,
5-
Dialog,
6-
Form,
7-
FormGroup,
8-
FormItem,
9-
Input,
10-
InputDomRef,
11-
Label,
12-
} from '@ui5/webcomponents-react';
1+
import { ReactNode, useState } from 'react';
2+
import { Bar, Button, Dialog, Input, InputDomRef, Label } from '@ui5/webcomponents-react';
133
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
14-
import { useTranslation } from 'react-i18next';
4+
import { Trans, useTranslation } from 'react-i18next';
5+
6+
import styles from './DeleteConfirmationDialog.module.css';
7+
import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base';
158

169
interface DeleteConfirmationDialogProps {
1710
isOpen: boolean;
@@ -30,96 +23,70 @@ export function DeleteConfirmationDialog({
3023
onCanceled,
3124
kubectl,
3225
}: DeleteConfirmationDialogProps) {
33-
const [confirmed, setConfirmed] = useState(false);
34-
const confirmationInput = useRef<InputDomRef>(null);
26+
const [confirmationText, setConfirmationText] = useState('');
3527
const { t } = useTranslation();
3628

37-
useEffect(() => {
38-
return () => {
39-
setConfirmed(false);
40-
if (confirmationInput.current) {
41-
confirmationInput.current.value = '';
42-
}
43-
};
44-
}, [isOpen]);
45-
46-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47-
const onConfirmationInputChange = (event: any) => {
48-
if (event.target.value === resourceName) {
49-
setConfirmed(true);
50-
} else {
51-
setConfirmed(false);
52-
}
29+
const onConfirmationInputChange = (event: Ui5CustomEvent<InputDomRef>) => {
30+
setConfirmationText(event.target.value);
5331
};
5432

33+
const isConfirmed = confirmationText === resourceName;
34+
5535
return (
56-
<>
57-
<Dialog
58-
stretch={false}
59-
headerText="Confirm deletion"
60-
open={isOpen}
61-
footer={
62-
<Bar
63-
design="Footer"
64-
endContent={
65-
<>
66-
<Button
67-
design={ButtonDesign.Transparent}
68-
onClick={() => {
69-
setIsOpen(false);
70-
onCanceled && onCanceled();
71-
}}
72-
>
73-
{t('DeleteConfirmationDialog.cancelButton')}
74-
</Button>
75-
<Button
76-
design={ButtonDesign.Negative}
77-
disabled={confirmed === false}
78-
onClick={() => {
79-
setIsOpen(false);
80-
onDeletionConfirmed && onDeletionConfirmed();
81-
}}
82-
>
83-
{t('DeleteConfirmationDialog.deleteButton')}
84-
</Button>
85-
</>
86-
}
36+
<Dialog
37+
stretch={false}
38+
headerText={t('DeleteConfirmationDialog.header', { resourceName })}
39+
open={isOpen}
40+
footer={
41+
<Bar
42+
design="Footer"
43+
endContent={
44+
<>
45+
<Button
46+
design={ButtonDesign.Transparent}
47+
onClick={() => {
48+
setIsOpen(false);
49+
onCanceled && onCanceled();
50+
}}
51+
>
52+
{t('DeleteConfirmationDialog.cancelButton')}
53+
</Button>
54+
<Button
55+
design={ButtonDesign.Negative}
56+
disabled={!isConfirmed}
57+
onClick={() => {
58+
setIsOpen(false);
59+
onDeletionConfirmed && onDeletionConfirmed();
60+
}}
61+
>
62+
{t('DeleteConfirmationDialog.deleteButton')}
63+
</Button>
64+
{kubectl}
65+
</>
66+
}
67+
/>
68+
}
69+
>
70+
<div className={styles.dialogContent}>
71+
<span className={styles.message}>
72+
<Trans
73+
i18nKey="DeleteConfirmationDialog.deleteMessage"
74+
values={{ resourceName }}
75+
components={{
76+
b: <b />,
77+
}}
8778
/>
88-
}
89-
>
90-
<Form layout="S1 M1 L1 XL1">
91-
<FormGroup>
92-
<Label>
93-
{t('DeleteConfirmationDialog.deleteMessage')} {resourceName}.
94-
</Label>
95-
<Label>
96-
{' '}
97-
{t('DeleteConfirmationDialog.deleteMessageType')}{' '}
98-
<b>{resourceName}</b>{' '}
99-
{t('DeleteConfirmationDialog.deleteMessageConfirm')}
100-
</Label>
101-
</FormGroup>
102-
<FormGroup>
103-
<FormItem
104-
labelContent={
105-
<Label>
106-
{t('DeleteConfirmationDialog.deleteMessageType')}{' '}
107-
{resourceName}{' '}
108-
{t('DeleteConfirmationDialog.deleteMessageConfirm')}
109-
</Label>
110-
}
111-
>
112-
<Input
113-
ref={confirmationInput}
114-
id="mcp-name-input"
115-
placeholder=""
116-
onInput={onConfirmationInputChange}
117-
/>
118-
</FormItem>
119-
<FormItem>{kubectl}</FormItem>
120-
</FormGroup>
121-
</Form>
122-
</Dialog>
123-
</>
79+
</span>
80+
<Label className={styles.confirmLabel} for="mcp-name-input">
81+
{t('DeleteConfirmationDialog.deleteConfirmation', { resourceName })}
82+
</Label>
83+
<Input
84+
id="mcp-name-input"
85+
value={confirmationText}
86+
className={styles.confirmationInput}
87+
onInput={onConfirmationInputChange}
88+
/>
89+
</div>
90+
</Dialog>
12491
);
12592
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useState } from 'react';
2+
3+
import { KubectlInfoButton } from '../KubectlInfoButton';
4+
import { KubectlDeleteProjectDialog } from '../KubectlDeleteProjectDialog.tsx';
5+
6+
interface KubectlDeleteProjectProps {
7+
projectName?: string;
8+
}
9+
10+
export const KubectlDeleteProject = ({ projectName }: KubectlDeleteProjectProps) => {
11+
const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
12+
13+
const openInfoDialog = () => setIsInfoDialogOpen(true);
14+
const closeInfoDialog = () => setIsInfoDialogOpen(false);
15+
16+
return (
17+
<>
18+
<KubectlInfoButton onClick={openInfoDialog} />
19+
<KubectlDeleteProjectDialog projectName={projectName} isOpen={isInfoDialogOpen} onClose={closeInfoDialog} />
20+
</>
21+
);
22+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { KubectlBaseDialog, CustomCommand } from './KubectlBaseDialog';
2+
import { Text } from '@ui5/webcomponents-react';
3+
import { useTranslation, Trans } from 'react-i18next';
4+
import { Fragment } from 'react/jsx-runtime';
5+
6+
interface KubectlDeleteProjectDialogProps {
7+
onClose: () => void;
8+
resourceName?: string;
9+
projectName?: string;
10+
isOpen: boolean;
11+
}
12+
13+
export const KubectlDeleteProjectDialog = ({ onClose, projectName, isOpen }: KubectlDeleteProjectDialogProps) => {
14+
const { t } = useTranslation();
15+
16+
const projectNamespace = projectName ?? '<project-names>"';
17+
18+
const customCommands: CustomCommand[] = [
19+
{
20+
command: `kubectl delete project ${projectNamespace}`,
21+
description: t('DeleteProjectDialog.mainCommandDescription'),
22+
isMainCommand: true,
23+
},
24+
];
25+
26+
const introSection = [
27+
<Fragment key="intro-1">
28+
<Text>
29+
{t('DeleteProjectDialog.introSection1', {
30+
projectName,
31+
})}
32+
</Text>
33+
<Text>
34+
<Trans
35+
i18nKey="DeleteProjectDialog.introSection2"
36+
components={{
37+
bold1: <b />,
38+
bold2: <b />,
39+
}}
40+
/>
41+
</Text>
42+
</Fragment>,
43+
];
44+
45+
return (
46+
<KubectlBaseDialog
47+
title={t('DeleteProjectDialog.title')}
48+
introSection={introSection}
49+
customCommands={customCommands}
50+
open={isOpen}
51+
onClose={onClose}
52+
/>
53+
);
54+
};

0 commit comments

Comments
 (0)