Skip to content

Commit d21c985

Browse files
refactor: update ExampleDialog loading handlers
1 parent d827f1a commit d21c985

File tree

4 files changed

+81
-65
lines changed

4 files changed

+81
-65
lines changed

src/web/src/__tests__/components/WSEditorCommandContent.test.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,22 @@ describe("WSEditorCommandContent", () => {
131131
loadingMessage: "Deleting commands...",
132132
fn: vi.fn().mockResolvedValue(undefined),
133133
};
134-
vi.mocked(commandApi).updateCommand.mockResolvedValue(mockCommand);
135-
vi.mocked(commandApi).updateCommandExamples.mockResolvedValue(mockCommand);
134+
vi.mocked(commandApi).updateCommand = {
135+
loadingMessage: "Updating command...",
136+
fn: vi.fn().mockResolvedValue(mockCommand),
137+
};
138+
vi.mocked(commandApi).renameCommand = {
139+
loadingMessage: "Renaming command...",
140+
fn: vi.fn().mockResolvedValue(mockCommand),
141+
};
142+
vi.mocked(commandApi).updateCommandExamples = {
143+
loadingMessage: "Updating command examples...",
144+
fn: vi.fn().mockResolvedValue(mockCommand),
145+
};
146+
vi.mocked(commandApi).generateSwaggerExamples = {
147+
loadingMessage: "Generating examples from OpenAPI...",
148+
fn: vi.fn().mockResolvedValue([]),
149+
};
136150
vi.mocked(commandApi).updateCommandOutputs.mockResolvedValue(mockCommand);
137151
});
138152

@@ -473,7 +487,7 @@ describe("WSEditorCommandContent", () => {
473487

474488
it("handles example dialog close with changes", async () => {
475489
const updatedCommand = { ...mockCommand, version: "2.0" };
476-
vi.mocked(commandApi).updateCommandExamples.mockResolvedValue(updatedCommand);
490+
(vi.mocked(commandApi).updateCommandExamples.fn as any).mockResolvedValue(updatedCommand);
477491

478492
render(<WSEditorCommandContent {...defaultProps} />);
479493

src/web/src/services/commandApi.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,39 @@ export const commandApi = {
1818
},
1919
},
2020

21-
updateCommand: async (leafUrl: string, data: any): Promise<any> => {
22-
const res = await axios.patch(leafUrl, data);
23-
return res.data;
21+
updateCommand: {
22+
loadingMessage: "Updating command...",
23+
fn: async (leafUrl: string, data: any): Promise<any> => {
24+
const res = await axios.patch(leafUrl, data);
25+
return res.data;
26+
},
2427
},
2528

26-
renameCommand: async (leafUrl: string, newName: string): Promise<any> => {
27-
const res = await axios.post(`${leafUrl}/Rename`, { name: newName });
28-
return res.data;
29+
renameCommand: {
30+
loadingMessage: "Renaming command...",
31+
fn: async (leafUrl: string, newName: string): Promise<any> => {
32+
const res = await axios.post(`${leafUrl}/Rename`, { name: newName });
33+
return res.data;
34+
},
2935
},
3036

31-
updateCommandExamples: async (leafUrl: string, examples: any[]): Promise<any> => {
32-
const res = await axios.patch(leafUrl, { examples });
33-
return res.data;
37+
updateCommandExamples: {
38+
loadingMessage: "Updating command examples...",
39+
fn: async (leafUrl: string, examples: any[]): Promise<any> => {
40+
const res = await axios.patch(leafUrl, { examples });
41+
return res.data;
42+
},
3443
},
3544

36-
generateSwaggerExamples: async (leafUrl: string): Promise<any[]> => {
37-
const res = await axios.post(`${leafUrl}/GenerateExamples`, { source: "swagger" });
38-
return res.data.map((v: any) => ({
39-
name: v.name,
40-
commands: v.commands,
41-
}));
45+
generateSwaggerExamples: {
46+
loadingMessage: "Generating examples from OpenAPI...",
47+
fn: async (leafUrl: string): Promise<any[]> => {
48+
const res = await axios.post(`${leafUrl}/GenerateExamples`, { source: "swagger" });
49+
return res.data.map((v: any) => ({
50+
name: v.name,
51+
commands: v.commands,
52+
}));
53+
},
4254
},
4355

4456
addSubcommands: async (resourceUrl: string, data: any): Promise<void> => {

src/web/src/views/workspace/components/WSEditorCommandContent/CommandDialog.tsx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import {
22
Alert,
3-
Box,
43
Button,
54
Dialog,
65
DialogActions,
76
DialogContent,
87
DialogTitle,
98
FormControlLabel,
109
InputLabel,
11-
LinearProgress,
1210
Radio,
1311
RadioGroup,
1412
TextField,
1513
} from "@mui/material";
1614
import React, { useState, useCallback } from "react";
1715
import { commandApi, errorHandlerApi } from "../../../../services";
16+
import { useAsyncOperation } from "../../../../services/hooks";
17+
import { AsyncOperationBanner } from "../../../../components";
1818
import { DecodeResponseCommand } from "../../utils/decodeResponseCommand";
1919
import type { Command } from "../../interfaces";
2020

@@ -26,13 +26,15 @@ export interface CommandDialogProps {
2626
}
2727

2828
const CommandDialog: React.FC<CommandDialogProps> = ({ workspaceUrl, open, command, onClose }) => {
29+
const updateOperation = useAsyncOperation(commandApi.updateCommand);
30+
const renameOperation = useAsyncOperation(commandApi.renameCommand);
31+
2932
const [name, setName] = useState(command.names.join(" "));
3033
const [shortHelp, setShortHelp] = useState(command.help?.short ?? "");
3134
const [longHelp, setLongHelp] = useState(command.help?.lines?.join("\n") ?? "");
3235
const [stage, setStage] = useState(command.stage);
3336
const [confirmation, setConfirmation] = useState(command.confirmation ?? "");
3437
const [invalidText, setInvalidText] = useState<string | undefined>(undefined);
35-
const [updating, setUpdating] = useState(false);
3638

3739
const handleModify = useCallback(async () => {
3840
let trimmedName = name.trim();
@@ -67,16 +69,14 @@ const CommandDialog: React.FC<CommandDialogProps> = ({ workspaceUrl, open, comma
6769
lines = trimmedLongHelp.split("\n").filter((l) => l.length > 0);
6870
}
6971

70-
setUpdating(true);
71-
7272
const leafUrl =
7373
`${workspaceUrl}/CommandTree/Nodes/aaz/` +
7474
command.names.slice(0, -1).join("/") +
7575
"/Leaves/" +
7676
command.names[command.names.length - 1];
7777

7878
try {
79-
const commandData = await commandApi.updateCommand(leafUrl, {
79+
const commandData = await updateOperation.execute(leafUrl, {
8080
help: {
8181
short: trimmedShortHelp,
8282
lines: lines,
@@ -88,18 +88,15 @@ const CommandDialog: React.FC<CommandDialogProps> = ({ workspaceUrl, open, comma
8888
const commandName = names.join(" ");
8989
if (commandName === command.names.join(" ")) {
9090
const cmd = DecodeResponseCommand(commandData);
91-
setUpdating(false);
9291
onClose(cmd);
9392
} else {
94-
const renamedData = await commandApi.renameCommand(leafUrl, commandName);
93+
const renamedData = await renameOperation.execute(leafUrl, commandName);
9594
const cmd = DecodeResponseCommand(renamedData);
96-
setUpdating(false);
9795
onClose(cmd);
9896
}
9997
} catch (err: any) {
10098
console.error(err);
10199
setInvalidText(errorHandlerApi.getErrorMessage(err));
102-
setUpdating(false);
103100
}
104101
}, [name, shortHelp, longHelp, confirmation, stage, workspaceUrl, command, onClose]);
105102

@@ -108,14 +105,19 @@ const CommandDialog: React.FC<CommandDialogProps> = ({ workspaceUrl, open, comma
108105
onClose();
109106
}, [onClose]);
110107

108+
const isLoading = updateOperation.loading || renameOperation.loading;
109+
const error = updateOperation.error || renameOperation.error;
110+
111111
return (
112112
<Dialog disableEscapeKeyDown open={open} sx={{ "& .MuiDialog-paper": { width: "80%" } }}>
113113
<DialogTitle>Command</DialogTitle>
114114
<DialogContent dividers={true}>
115-
{invalidText && (
115+
<AsyncOperationBanner operation={updateOperation} />
116+
<AsyncOperationBanner operation={renameOperation} />
117+
{(invalidText || error) && (
116118
<Alert variant="filled" severity="error">
117119
{" "}
118-
{invalidText}{" "}
120+
{invalidText || errorHandlerApi.getErrorMessage(error!)}{" "}
119121
</Alert>
120122
)}
121123
<InputLabel required shrink sx={{ font: "inherit" }}>
@@ -191,17 +193,12 @@ const CommandDialog: React.FC<CommandDialogProps> = ({ workspaceUrl, open, comma
191193
/>
192194
</DialogContent>
193195
<DialogActions>
194-
{updating && (
195-
<Box sx={{ width: "100%" }}>
196-
<LinearProgress color="secondary" />
197-
</Box>
198-
)}
199-
{!updating && (
200-
<React.Fragment>
201-
<Button onClick={handleClose}>Cancel</Button>
202-
<Button onClick={handleModify}>Save</Button>
203-
</React.Fragment>
204-
)}
196+
<Button onClick={handleClose} disabled={isLoading}>
197+
Cancel
198+
</Button>
199+
<Button onClick={handleModify} disabled={isLoading}>
200+
Save
201+
</Button>
205202
</DialogActions>
206203
</Dialog>
207204
);

src/web/src/views/workspace/components/WSEditorCommandContent/ExampleDialog.tsx

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
Input,
1212
InputAdornment,
1313
InputLabel,
14-
LinearProgress,
1514
TextField,
1615
Typography,
1716
TypographyProps,
@@ -22,6 +21,8 @@ import DoDisturbOnRoundedIcon from "@mui/icons-material/DoDisturbOnRounded";
2221
import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
2322
import CloseIcon from "@mui/icons-material/Close";
2423
import { commandApi, errorHandlerApi } from "../../../../services";
24+
import { useAsyncOperation } from "../../../../services/hooks";
25+
import { AsyncOperationBanner } from "../../../../components";
2526
import { COMMAND_PREFIX } from "../../../../constants";
2627
import { ExampleItemSelector } from "./ExampleItemSelector";
2728
import { DecodeResponseCommand } from "../../utils/decodeResponseCommand";
@@ -43,11 +44,13 @@ const ExampleCommandTypography = styled(Typography)<TypographyProps>(({ theme })
4344
}));
4445

4546
const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, command, idx, onClose }) => {
47+
const updateExamplesOperation = useAsyncOperation(commandApi.updateCommandExamples);
48+
const generateExamplesOperation = useAsyncOperation(commandApi.generateSwaggerExamples);
49+
4650
const [name, setName] = useState<string>("");
4751
const [exampleCommands, setExampleCommands] = useState<string[]>([""]);
4852
const [isAdd, setIsAdd] = useState<boolean>(true);
4953
const [invalidText, setInvalidText] = useState<string | undefined>(undefined);
50-
const [updating, setUpdating] = useState<boolean>(false);
5154
const [source, setSource] = useState<string | undefined>(undefined);
5255
const [exampleOptions, setExampleOptions] = useState<Example[]>([]);
5356

@@ -58,7 +61,6 @@ const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, comma
5861
setExampleCommands([""]);
5962
setIsAdd(true);
6063
setInvalidText(undefined);
61-
setUpdating(false);
6264
setSource(undefined);
6365
setExampleOptions([]);
6466
} else {
@@ -67,7 +69,6 @@ const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, comma
6769
setExampleCommands(example.commands);
6870
setIsAdd(false);
6971
setInvalidText(undefined);
70-
setUpdating(false);
7172
setSource(undefined);
7273
setExampleOptions([]);
7374
}
@@ -81,21 +82,17 @@ const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, comma
8182
"/Leaves/" +
8283
command.names[command.names.length - 1];
8384

84-
setUpdating(true);
85-
8685
try {
87-
const responseData = await commandApi.updateCommandExamples(leafUrl, examples);
86+
const responseData = await updateExamplesOperation.execute(leafUrl, examples);
8887
const cmd = DecodeResponseCommand(responseData);
89-
setUpdating(false);
9088
onClose(cmd);
9189
} catch (err: any) {
9290
console.error(err);
9391
const message = errorHandlerApi.getErrorMessage(err);
9492
setInvalidText(`ResponseError: ${message}`);
95-
setUpdating(false);
9693
}
9794
},
98-
[workspaceUrl, command.names, onClose],
95+
[workspaceUrl, command.names, onClose, updateExamplesOperation],
9996
);
10097

10198
const handleDelete = useCallback(() => {
@@ -205,19 +202,18 @@ const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, comma
205202
command.names[command.names.length - 1];
206203

207204
setSource("swagger");
208-
setUpdating(true);
209-
const examples = await commandApi.generateSwaggerExamples(leafUrl);
210-
setExampleOptions(examples);
211-
setUpdating(false);
212-
if (examples.length > 0) {
213-
onExampleSelectorUpdate(examples[0].name);
205+
const examples = await generateExamplesOperation.execute(leafUrl);
206+
if (examples) {
207+
setExampleOptions(examples);
208+
if (examples.length > 0) {
209+
onExampleSelectorUpdate(examples[0].name);
210+
}
214211
}
215212
} catch (err: any) {
216213
console.error(err.response);
217-
setUpdating(false);
218214
setInvalidText(errorHandlerApi.getErrorMessage(err));
219215
}
220-
}, [workspaceUrl, command.names]);
216+
}, [workspaceUrl, command.names, generateExamplesOperation]);
221217

222218
const onExampleSelectorUpdate = useCallback(
223219
(exampleDisplayName: string | null) => {
@@ -359,12 +355,9 @@ const ExampleDialog: React.FC<ExampleDialogProps> = ({ workspaceUrl, open, comma
359355
</DialogContent>
360356
{(!isAdd || source != undefined) && (
361357
<DialogActions>
362-
{updating && (
363-
<Box sx={{ width: "100%" }}>
364-
<LinearProgress color="secondary" />
365-
</Box>
366-
)}
367-
{!updating && (
358+
<AsyncOperationBanner operation={updateExamplesOperation} />
359+
<AsyncOperationBanner operation={generateExamplesOperation} />
360+
{!updateExamplesOperation.loading && !generateExamplesOperation.loading && (
368361
<React.Fragment>
369362
{!isAdd && (
370363
<React.Fragment>

0 commit comments

Comments
 (0)