Skip to content

Commit a3413e5

Browse files
feature: (WIP) loading states for planes
1 parent 98858ad commit a3413e5

File tree

4 files changed

+129
-10
lines changed

4 files changed

+129
-10
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { useAsyncOperation } from "./useAsyncOperation";
2+
export type {
3+
AsyncOperationState,
4+
AsyncOperationActions,
5+
AsyncServiceMethod,
6+
UseAsyncOperationResult,
7+
} from "./useAsyncOperation";
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useState, useCallback } from "react";
2+
3+
export interface AsyncOperationState<T> {
4+
data: T | null;
5+
loading: boolean;
6+
error: Error | null;
7+
loadingMessage: string;
8+
}
9+
10+
export interface AsyncOperationActions<T> {
11+
execute: (...args: any[]) => Promise<T | undefined>;
12+
reset: () => void;
13+
}
14+
15+
export interface AsyncServiceMethod<T> {
16+
loadingMessage: string;
17+
fn: (...args: any[]) => Promise<T>;
18+
}
19+
20+
export interface UseAsyncOperationResult<T> extends AsyncOperationState<T>, AsyncOperationActions<T> {}
21+
22+
/**
23+
* Generic hook for wrapping async service operations with loading states and messages.
24+
*
25+
* @param serviceMethod - Object containing the async function and loading message
26+
* @returns Object with data, loading, error, loadingMessage, execute, and reset
27+
*
28+
* @example
29+
* ```typescript
30+
* const resourceProviders = useAsyncOperation({
31+
* loadingMessage: "Loading resource providers...",
32+
* fn: specsApi.getResourceProviders
33+
* });
34+
*
35+
* // Usage
36+
* await resourceProviders.execute(moduleUrl);
37+
*
38+
* // In JSX
39+
* {resourceProviders.loading && (
40+
* <Box>
41+
* <CircularProgress />
42+
* <Typography>{resourceProviders.loadingMessage}</Typography>
43+
* </Box>
44+
* )}
45+
* ```
46+
*/
47+
export const useAsyncOperation = <T>(serviceMethod?: AsyncServiceMethod<T>): UseAsyncOperationResult<T> => {
48+
const [data, setData] = useState<T | null>(null);
49+
const [loading, setLoading] = useState(false);
50+
const [error, setError] = useState<Error | null>(null);
51+
const [loadingMessage, setLoadingMessage] = useState<string>("");
52+
53+
const execute = useCallback(
54+
async (...args: any[]): Promise<T | undefined> => {
55+
if (!serviceMethod) {
56+
console.warn("useAsyncOperation: No service method provided");
57+
return;
58+
}
59+
60+
setLoading(true);
61+
setError(null);
62+
setLoadingMessage(serviceMethod.loadingMessage);
63+
64+
try {
65+
const result = await serviceMethod.fn(...args);
66+
setData(result);
67+
return result;
68+
} catch (err) {
69+
const error = err instanceof Error ? err : new Error(String(err));
70+
setError(error);
71+
throw error;
72+
} finally {
73+
setLoading(false);
74+
setLoadingMessage("");
75+
}
76+
},
77+
[serviceMethod],
78+
);
79+
80+
const reset = useCallback(() => {
81+
setData(null);
82+
setError(null);
83+
setLoading(false);
84+
setLoadingMessage("");
85+
}, []);
86+
87+
return {
88+
data,
89+
loading,
90+
error,
91+
loadingMessage,
92+
execute,
93+
reset,
94+
};
95+
};

src/web/src/services/specsApi.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ export const specsApi = {
2626
return res.data.map((v: any) => v.name);
2727
},
2828

29-
getModulesForPlane: async (planeName: string): Promise<string[]> => {
30-
const res = await axios.get(`/Swagger/Specs/${planeName}`);
31-
return res.data.map((v: any) => v.url);
29+
getModulesForPlane: {
30+
// @TODO: revisit msg:
31+
loadingMessage: "Loading modules for plane... (this may take up to 40+ seconds)",
32+
fn: async (planeName: string): Promise<string[]> => {
33+
const res = await axios.get(`/Swagger/Specs/${planeName}`);
34+
return res.data.map((v: any) => v.url);
35+
},
3236
},
3337

3438
getResourceProviders: async (moduleUrl: string): Promise<string[]> => {

src/web/src/views/workspace/components/WorkspaceInstruction/WorkspaceCreateDialog.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import React, { useState, useEffect, useCallback } from "react";
1313
import SwaggerItemSelector from "../../common/SwaggerItemSelector";
1414
import styled from "@emotion/styled";
1515
import { workspaceApi, specsApi, errorHandlerApi } from "../../../../services";
16+
import { useAsyncOperation } from "../../../../services/hooks";
1617
import type { Plane } from "../../interfaces";
1718

1819
interface WorkspaceCreateDialogProps {
@@ -26,6 +27,8 @@ const WorkspaceCreateDialog: React.FC<WorkspaceCreateDialogProps> = ({ openDialo
2627
const [invalidText, setInvalidText] = useState<string | undefined>(undefined);
2728
const [workspaceName, setWorkspaceName] = useState<string>(name);
2829

30+
const modulesLoader = useAsyncOperation(specsApi.getModulesForPlane);
31+
2932
const [planes, setPlanes] = useState<Plane[]>([]);
3033
const [planeOptions, setPlaneOptions] = useState<string[]>([]);
3134
const [selectedPlane, setSelectedPlane] = useState<string | null>(null);
@@ -103,21 +106,18 @@ const WorkspaceCreateDialog: React.FC<WorkspaceCreateDialogProps> = ({ openDialo
103106
await onModuleSelectionUpdate(null);
104107
} else {
105108
try {
106-
setLoading(true);
107-
const options = await specsApi.getModulesForPlane(plane.name);
109+
const options = await modulesLoader.execute(plane.name);
108110
setPlanes((prevPlanes) => {
109111
const updatedPlanes = [...prevPlanes];
110112
const index = updatedPlanes.findIndex((v: Plane) => v.name === plane.name);
111-
updatedPlanes[index].moduleOptions = options;
113+
updatedPlanes[index].moduleOptions = options || [];
112114
return updatedPlanes;
113115
});
114-
setLoading(false);
115-
setModuleOptions(options);
116+
setModuleOptions(options || []);
116117
setModuleOptionsCommonPrefix(`/Swagger/Specs/${plane.name}/`);
117118
await onModuleSelectionUpdate(null);
118119
} catch (err: any) {
119120
console.error(err);
120-
setLoading(false);
121121
setInvalidText(errorHandlerApi.getErrorMessage(err));
122122
}
123123
}
@@ -256,6 +256,12 @@ const WorkspaceCreateDialog: React.FC<WorkspaceCreateDialogProps> = ({ openDialo
256256
{invalidText}{" "}
257257
</Alert>
258258
)}
259+
{/* @TODO: revisit msg and component */}
260+
{modulesLoader.loading && (
261+
<Alert variant="outlined" severity="info">
262+
{modulesLoader.loadingMessage}
263+
</Alert>
264+
)}
259265
<InputLabel shrink> API Specs</InputLabel>
260266
<Box
261267
sx={{
@@ -310,7 +316,14 @@ const WorkspaceCreateDialog: React.FC<WorkspaceCreateDialogProps> = ({ openDialo
310316
<Box>
311317
<Button onClick={handleClose}>Cancel</Button>
312318
<Button
313-
disabled={loading || !selectedPlane || !selectedModule || !selectedResourceProvider || !workspaceName}
319+
disabled={
320+
loading ||
321+
modulesLoader.loading ||
322+
!selectedPlane ||
323+
!selectedModule ||
324+
!selectedResourceProvider ||
325+
!workspaceName
326+
}
314327
onClick={handleCreate}
315328
color="success"
316329
>

0 commit comments

Comments
 (0)