Skip to content

Commit 4274c57

Browse files
committed
Render step name in Fault settings and make it editable
- If the Chaos Fault's step name differs from its template name, both are now displayed - An edit icon appears next to the title, allowing users to edit it Signed-off-by: Luke Zhan <[email protected]>
1 parent 7186709 commit 4274c57

File tree

6 files changed

+225
-10
lines changed

6 files changed

+225
-10
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.displayMode {
2+
display: flex;
3+
align-items: center;
4+
gap: var(--spacing-small);
5+
width: 100%;
6+
min-height: 45px;
7+
8+
.stepNameTextContainer {
9+
display: flex;
10+
flex-direction: column;
11+
gap: var(--spacing-tiny);
12+
flex: 1;
13+
}
14+
}
15+
16+
.editMode {
17+
display: flex;
18+
align-items: center;
19+
gap: var(--spacing-small);
20+
width: 100%;
21+
min-height: 45px;
22+
animation: fadeIn 0.2s ease-out;
23+
24+
.stepNameTextInput {
25+
flex: 1;
26+
min-width: 200px;
27+
margin-bottom: 0;
28+
}
29+
}
30+
31+
@keyframes fadeIn {
32+
from {
33+
opacity: 0;
34+
transform: translateY(-2px);
35+
}
36+
37+
to {
38+
opacity: 1;
39+
transform: translateY(0);
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
declare namespace EditableStepNameModuleScssNamespace {
2+
export interface IEditableStepNameModuleScss {
3+
displayMode: string;
4+
editMode: string;
5+
fadeIn: string;
6+
stepNameTextContainer: string;
7+
stepNameTextInput: string;
8+
}
9+
}
10+
11+
declare const EditableStepNameModuleScssModule: EditableStepNameModuleScssNamespace.IEditableStepNameModuleScss;
12+
13+
export = EditableStepNameModuleScssModule;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React from 'react';
2+
import { Color, FontVariation } from '@harnessio/design-system';
3+
import { Button, ButtonSize, ButtonVariation, Text, TextInput, useToaster } from '@harnessio/uicore';
4+
import css from './EditableStepName.module.scss';
5+
6+
export interface EditableStepNameProps {
7+
stepName?: string;
8+
faultName: string;
9+
onSave: (newStepName: string) => Promise<void>;
10+
fontSize?: FontVariation;
11+
showSubtitle?: boolean;
12+
disabled?: boolean;
13+
}
14+
15+
export function EditableStepName({
16+
stepName,
17+
faultName,
18+
onSave,
19+
fontSize = FontVariation.H5,
20+
showSubtitle = true,
21+
disabled = false
22+
}: EditableStepNameProps): React.ReactElement {
23+
const { showError } = useToaster();
24+
const [isEditing, setIsEditing] = React.useState<boolean>(false);
25+
const [editedValue, setEditedValue] = React.useState<string>('');
26+
const [isSaving, setIsSaving] = React.useState<boolean>(false);
27+
28+
const displayName = stepName || faultName;
29+
const shouldShowSubtitle = showSubtitle && stepName && stepName !== faultName;
30+
31+
const handleEditStart = (): void => {
32+
setEditedValue(displayName);
33+
setIsEditing(true);
34+
};
35+
36+
const handleSave = async (): Promise<void> => {
37+
if (editedValue.trim() && editedValue !== stepName) {
38+
setIsSaving(true);
39+
try {
40+
await onSave(editedValue.trim());
41+
setIsEditing(false);
42+
} catch (error) {
43+
showError('Failed to update step name');
44+
} finally {
45+
setIsSaving(false);
46+
}
47+
} else {
48+
setIsEditing(false);
49+
}
50+
setEditedValue('');
51+
};
52+
53+
const handleCancel = (): void => {
54+
setIsEditing(false);
55+
setEditedValue('');
56+
};
57+
58+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
59+
if (e.key === 'Enter') {
60+
e.stopPropagation();
61+
e.preventDefault();
62+
handleSave();
63+
} else if (e.key === 'Escape') {
64+
e.stopPropagation();
65+
e.preventDefault();
66+
handleCancel();
67+
}
68+
};
69+
70+
if (isEditing) {
71+
return (
72+
<div className={css.editMode}>
73+
<TextInput
74+
wrapperClassName={css.stepNameTextInput}
75+
value={editedValue}
76+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEditedValue(e.target.value)}
77+
onKeyDown={handleKeyDown}
78+
autoFocus
79+
disabled={isSaving}
80+
/>
81+
<Button
82+
variation={ButtonVariation.ICON}
83+
icon="tick"
84+
size={ButtonSize.SMALL}
85+
onClick={handleSave}
86+
disabled={isSaving}
87+
loading={isSaving}
88+
/>
89+
<Button
90+
variation={ButtonVariation.ICON}
91+
icon="cross"
92+
size={ButtonSize.SMALL}
93+
onClick={handleCancel}
94+
disabled={isSaving}
95+
/>
96+
</div>
97+
);
98+
}
99+
100+
return (
101+
<div className={css.displayMode}>
102+
<div className={css.stepNameTextContainer}>
103+
<Text font={{ variation: fontSize }}>{displayName}</Text>
104+
{shouldShowSubtitle && (
105+
<Text color={Color.GREY_600} font={{ variation: FontVariation.SMALL, italic: true }}>
106+
{faultName}
107+
</Text>
108+
)}
109+
</div>
110+
{!disabled && (
111+
<Button
112+
variation={ButtonVariation.ICON}
113+
icon="Edit"
114+
size={ButtonSize.SMALL}
115+
onClick={handleEditStart}
116+
minimal
117+
/>
118+
)}
119+
</div>
120+
);
121+
}
122+
123+
export default EditableStepName;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import EditableStepName from './EditableStepName';
2+
3+
export default EditableStepName;

chaoscenter/web/src/views/ExperimentCreationFaultConfiguration/ExperimentCreationFaultConfiguration.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { FormikProps } from 'formik';
1919
import { cloneDeep, isEmpty, isEqual } from 'lodash-es';
2020
import { DrawerTypes } from '@components/Drawer/Drawer';
2121
import Drawer from '@components/Drawer';
22+
import EditableStepName from '@components/EditableStepName';
2223
import { useStrings } from '@strings';
2324
import TargetApplicationTabController from '@controllers/TargetApplicationTab';
2425
import type { FaultData } from '@models';
@@ -43,6 +44,7 @@ interface ExperimentCreationTuneFaultProps {
4344
environmentID: string | undefined;
4445
faultTuneOperation: GetFaultTunablesOperation;
4546
initialServiceIdentifiers: ServiceIdentifiers | undefined;
47+
onStepNameUpdate?: () => void;
4648
}
4749

4850
enum TuneFaultTab {
@@ -58,9 +60,10 @@ export default function ExperimentCreationTuneFaultView({
5860
initialFaultData,
5961
infraID,
6062
// environmentID,
61-
faultTuneOperation
62-
}: // initialServiceIdentifiers
63-
ExperimentCreationTuneFaultProps): React.ReactElement {
63+
faultTuneOperation,
64+
// initialServiceIdentifiers
65+
onStepNameUpdate
66+
}: ExperimentCreationTuneFaultProps): React.ReactElement {
6467
const { getString } = useStrings();
6568
const searchParams = useSearchParams();
6669
const { showError } = useToaster();
@@ -142,11 +145,37 @@ ExperimentCreationTuneFaultProps): React.ReactElement {
142145
onClose();
143146
};
144147

148+
const handleStepNameSave = async (newStepName: string): Promise<void> => {
149+
if (faultData) {
150+
await experimentHandler?.updateFaultStepName(experimentKey, faultData.faultName, newStepName);
151+
setFaultData(prev => (prev ? { ...prev, stepName: newStepName } : prev));
152+
// Refresh visual builder with new step name
153+
onStepNameUpdate?.();
154+
}
155+
};
156+
157+
const hasUnsavedChanges = (): boolean => {
158+
// Don't compare stepName since it's saved automatically
159+
const initialDataWithoutStepName = initialFaultData ? { ...initialFaultData, stepName: undefined } : undefined;
160+
const currentDataWithoutStepName = faultData ? { ...faultData, stepName: undefined } : undefined;
161+
162+
return (
163+
!isEqual(initialDataWithoutStepName, currentDataWithoutStepName) ||
164+
faultWeight !== faultData?.weight ||
165+
(tuneExperimentRef.current?.dirty ?? false)
166+
);
167+
};
168+
145169
const header = (
146170
<Layout.Horizontal spacing={'small'} flex={{ distribution: 'space-between' }}>
147171
<Layout.Horizontal spacing={'small'} flex={{ alignItems: 'center' }}>
148172
<Icon name="chaos-scenario-builder" size={28} />
149-
<Text font={{ variation: FontVariation.H5 }}>{faultData?.faultName}</Text>
173+
<EditableStepName
174+
stepName={faultData?.stepName}
175+
faultName={faultData?.faultName ?? ''}
176+
onSave={handleStepNameSave}
177+
fontSize={FontVariation.H5}
178+
/>
150179
</Layout.Horizontal>
151180
<Layout.Horizontal spacing={'small'} flex={{ alignItems: 'center' }}>
152181
<Button
@@ -225,11 +254,7 @@ ExperimentCreationTuneFaultProps): React.ReactElement {
225254
</div>
226255
}
227256
handleClose={() => {
228-
if (
229-
!isEqual(initialFaultData, faultData) ||
230-
faultWeight !== faultData?.weight ||
231-
tuneExperimentRef.current?.dirty
232-
) {
257+
if (hasUnsavedChanges()) {
233258
openTuneConfirmDialog();
234259
return;
235260
}

chaoscenter/web/src/views/ExperimentVisualBuilder/ExperimentVisualBuilder.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default function ExperimentVisualBuilderView({
6161
const [prevNodeIdentifier, setPrevNodeIdentifier] = React.useState<string>('');
6262
const [isEditMode, setIsEditMode] = React.useState<boolean>(true);
6363
const [infraDetails, setInfraDetails] = React.useState<InfraDetails | undefined>();
64+
const [refreshTrigger, setRefreshTrigger] = React.useState<number>(0);
6465

6566
const infrastructureType = searchParams.get('infrastructureType') as InfrastructureType | undefined;
6667
const experimentHandler = experimentYamlService.getInfrastructureTypeHandler(infrastructureType);
@@ -95,6 +96,10 @@ export default function ExperimentVisualBuilderView({
9596
if (yamlUploaded) setViewFilter(VisualYamlSelectedView.YAML);
9697
};
9798

99+
const triggerRefresh = (): void => {
100+
setRefreshTrigger(prev => prev + 1);
101+
};
102+
98103
const handleRemoveFault = (faultName: string): void => {
99104
experimentHandler?.removeFaultsFromManifest(experimentKey, faultName).then(experiment => {
100105
const steps = experimentHandler.getFaultsFromExperimentManifest(experiment?.manifest, isEditMode);
@@ -120,8 +125,12 @@ export default function ExperimentVisualBuilderView({
120125
}
121126
const steps = experimentHandler.getFaultsFromExperimentManifest(experiment?.manifest, isEditMode);
122127
setExperimentSteps(steps);
128+
129+
if (refreshTrigger > 0) {
130+
setUnsavedChanges();
131+
}
123132
});
124-
}, [isEditMode, experimentKey, experimentHandler]);
133+
}, [isEditMode, experimentKey, experimentHandler, refreshTrigger]);
125134

126135
// Initiate DiagramFactory
127136
const diagram = new DiagramFactory('graph');
@@ -212,6 +221,7 @@ export default function ExperimentVisualBuilderView({
212221
environmentID={infraDetails?.environmentID}
213222
faultTuneOperation={tuneFaultDrawerOpen.operation}
214223
initialServiceIdentifiers={serviceIdentifiers}
224+
onStepNameUpdate={triggerRefresh}
215225
/>
216226
)}
217227
<ChaosDiagram

0 commit comments

Comments
 (0)