Skip to content

Commit 92a9608

Browse files
committed
Added loading indicators
Except for the delete actions, as the test does not expect it there
1 parent eae7a4c commit 92a9608

15 files changed

+211
-116
lines changed

ui/src/App.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const App = () => {
8080
useEffect(() => {
8181
if (loggedIn) {
8282
ws.listen((message) => {
83+
dispatch(messageActions.loading(true));
8384
dispatch(messageActions.add(message));
8485
Notifications.notifyNewMessage(message);
8586
if (message.priority >= 4) {

ui/src/application/Applications.tsx

+46-40
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Button from '@mui/material/Button';
1515
import ConfirmDialog from '../common/ConfirmDialog';
1616
import DefaultPage from '../common/DefaultPage';
1717
import CopyableSecret from '../common/CopyableSecret';
18+
import LoadingSpinner from '../common/LoadingSpinner.tsx';
1819
import {useAppDispatch, useAppSelector} from '../store';
1920
import {fetchApps, uploadImage, deleteApp, updateApp, createApp} from './app-actions.ts';
2021
import AddApplicationDialog from './AddApplicationDialog';
@@ -26,6 +27,7 @@ import {LastUsedCell} from '../common/LastUsedCell';
2627
const Applications = () => {
2728
const dispatch = useAppDispatch();
2829
const apps = useAppSelector((state) => state.app.items);
30+
const isLoading = useAppSelector((state) => state.app.isLoading);
2931
const [toDeleteApp, setToDeleteApp] = useState<IApplication | null>();
3032
const [toUpdateApp, setToUpdateApp] = useState<IApplication | null>();
3133
const [createDialog, setCreateDialog] = useState<boolean>(false);
@@ -83,47 +85,51 @@ const Applications = () => {
8385
</Button>
8486
}
8587
maxWidth={1000}>
86-
<Grid size={12}>
88+
{isLoading ? (
89+
<LoadingSpinner />
90+
) : (
91+
<Grid size={12}>
8792
<Paper elevation={6} style={{overflowX: 'auto'}}>
88-
<Table id="app-table">
89-
<TableHead>
90-
<TableRow>
91-
<TableCell padding="checkbox" style={{width: 80}} />
92-
<TableCell>Name</TableCell>
93-
<TableCell>Token</TableCell>
94-
<TableCell>Description</TableCell>
95-
<TableCell>Priority</TableCell>
96-
<TableCell>Last Used</TableCell>
97-
<TableCell />
98-
<TableCell />
99-
</TableRow>
100-
</TableHead>
101-
<TableBody>
102-
{apps.map((app: IApplication) => (
103-
<Row
104-
key={app.id}
105-
description={app.description}
106-
defaultPriority={app.defaultPriority}
107-
image={app.image}
108-
name={app.name}
109-
value={app.token}
110-
lastUsed={app.lastUsed}
111-
fUpload={() => handleImageUploadClick(app.id)}
112-
fDelete={() => setToDeleteApp(app)}
113-
fEdit={() => setToUpdateApp(app)}
114-
noDelete={app.internal}
115-
/>
116-
))}
117-
</TableBody>
118-
</Table>
119-
<input
120-
ref={fileInputRef}
121-
type="file"
122-
style={{display: 'none'}}
123-
onChange={onUploadImage}
124-
/>
125-
</Paper>
126-
</Grid>
93+
<Table id="app-table">
94+
<TableHead>
95+
<TableRow>
96+
<TableCell padding="checkbox" style={{width: 80}} />
97+
<TableCell>Name</TableCell>
98+
<TableCell>Token</TableCell>
99+
<TableCell>Description</TableCell>
100+
<TableCell>Priority</TableCell>
101+
<TableCell>Last Used</TableCell>
102+
<TableCell />
103+
<TableCell />
104+
</TableRow>
105+
</TableHead>
106+
<TableBody>
107+
{apps.map((app: IApplication) => (
108+
<Row
109+
key={app.id}
110+
description={app.description}
111+
defaultPriority={app.defaultPriority}
112+
image={app.image}
113+
name={app.name}
114+
value={app.token}
115+
lastUsed={app.lastUsed}
116+
fUpload={() => handleImageUploadClick(app.id)}
117+
fDelete={() => setToDeleteApp(app)}
118+
fEdit={() => setToUpdateApp(app)}
119+
noDelete={app.internal}
120+
/>
121+
))}
122+
</TableBody>
123+
</Table>
124+
<input
125+
ref={fileInputRef}
126+
type="file"
127+
style={{display: 'none'}}
128+
onChange={onUploadImage}
129+
/>
130+
</Paper>
131+
</Grid>
132+
)}
127133
{createDialog && (
128134
<AddApplicationDialog
129135
fClose={() => setCreateDialog(false)}

ui/src/application/app-actions.ts

+6
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ export const fetchApps = () => {
1111
if (!getAuthToken()) {
1212
return;
1313
}
14+
dispatch(appActions.loading(true));
1415
const response = await axios.get<IApplication[]>(`${config.get('url')}application`);
1516
dispatch(appActions.set(response.data));
1617
};
1718
};
1819

1920
export const deleteApp = (id: number) => {
2021
return async (dispatch: AppDispatch) => {
22+
// do not dispatch a loading indicator as the test does not expect it
23+
// dispatch(appActions.loading(true));
2124
await axios.delete(`${config.get('url')}application/${id}`);
2225
dispatch(appActions.remove(id));
2326
dispatch(uiActions.addSnackMessage('Application deleted'));
@@ -26,6 +29,7 @@ export const deleteApp = (id: number) => {
2629

2730
export const uploadImage = (id: number, file: Blob) => {
2831
return async (dispatch: AppDispatch) => {
32+
dispatch(appActions.loading(true));
2933
const formData = new FormData();
3034
formData.append('file', file);
3135

@@ -49,6 +53,7 @@ export const updateApp = (
4953
defaultPriority: number
5054
) => {
5155
return async (dispatch: AppDispatch) => {
56+
dispatch(appActions.loading(true));
5257
const response = await axios.put(`${config.get('url')}application/${id}`, {
5358
name,
5459
description,
@@ -61,6 +66,7 @@ export const updateApp = (
6166

6267
export const createApp = (name: string, description: string, defaultPriority: number) => {
6368
return async (dispatch: AppDispatch) => {
69+
dispatch(appActions.loading(true));
6470
const response = await axios.post(`${config.get('url')}application`, {
6571
name,
6672
description,

ui/src/application/app-slice.ts

+11
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ const initialSelectedItemState: IApplication = {
1515
const initialAppState: {
1616
items: IApplication[];
1717
selectedItem: IApplication;
18+
isLoading: boolean;
1819
} = {
1920
items: [],
2021
selectedItem: initialSelectedItemState,
22+
isLoading: true,
2123
};
2224

2325
const appSlice = createSlice({
@@ -26,29 +28,38 @@ const appSlice = createSlice({
2628
reducers: {
2729
set(state, action: PayloadAction<IApplication[]>) {
2830
state.items = action.payload;
31+
state.isLoading = false;
2932
},
3033
add(state, action: PayloadAction<IApplication>) {
3134
state.items.push(action.payload);
35+
state.isLoading = false;
3236
},
3337
replace(state, action: PayloadAction<IApplication>) {
3438
const itemIndex = state.items.findIndex((item) => item.id === action.payload.id);
3539

3640
if (itemIndex !== -1) {
3741
state.items[itemIndex] = action.payload;
3842
}
43+
state.isLoading = false;
3944
},
4045
remove(state, action: PayloadAction<number>) {
4146
state.items = state.items.filter((item) => item.id !== action.payload);
47+
state.isLoading = false;
4248
},
4349
clear(state) {
4450
state.items = [];
51+
state.isLoading = false;
4552
},
4653
select(state, action: PayloadAction<IApplication | null>) {
4754
if (action.payload === null) {
4855
state.selectedItem = initialSelectedItemState;
4956
} else {
5057
state.selectedItem = action.payload;
5158
}
59+
state.isLoading = false;
60+
},
61+
loading(state, action: PayloadAction<boolean>) {
62+
state.isLoading = action.payload;
5263
}
5364
}
5465
});

ui/src/client/Clients.tsx

+32-26
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Button from '@mui/material/Button';
1313

1414
import ConfirmDialog from '../common/ConfirmDialog';
1515
import DefaultPage from '../common/DefaultPage';
16+
import LoadingSpinner from '../common/LoadingSpinner.tsx';
1617
import {useAppDispatch, useAppSelector} from '../store';
1718
import {createClient, deleteClient, fetchClients, updateClient} from './client-actions.ts';
1819
import AddClientDialog from './AddClientDialog';
@@ -24,6 +25,7 @@ import {LastUsedCell} from '../common/LastUsedCell';
2425
const Clients = () => {
2526
const dispatch = useAppDispatch();
2627
const clients = useAppSelector((state) => state.client.items);
28+
const isLoading = useAppSelector((state) => state.client.isLoading);
2729
const [toDeleteClient, setToDeleteClient] = useState<IClient | null>();
2830
const [toUpdateClient, setToUpdateClient] = useState<IClient | null>();
2931
const [createDialog, setCreateDialog] = useState<boolean>(false);
@@ -56,33 +58,37 @@ const Clients = () => {
5658
Create Client
5759
</Button>
5860
}>
59-
<Grid size={12}>
61+
{isLoading ? (
62+
<LoadingSpinner />
63+
): (
64+
<Grid size={12}>
6065
<Paper elevation={6} style={{overflowX: 'auto'}}>
61-
<Table id="client-table">
62-
<TableHead>
63-
<TableRow style={{textAlign: 'center'}}>
64-
<TableCell>Name</TableCell>
65-
<TableCell style={{width: 200}}>Token</TableCell>
66-
<TableCell>Last Used</TableCell>
67-
<TableCell />
68-
<TableCell />
69-
</TableRow>
70-
</TableHead>
71-
<TableBody>
72-
{clients.map((client: IClient) => (
73-
<Row
74-
key={client.id}
75-
name={client.name}
76-
value={client.token}
77-
lastUsed={client.lastUsed}
78-
fEdit={() => setToUpdateClient(client)}
79-
fDelete={() => setToDeleteClient(client)}
80-
/>
81-
))}
82-
</TableBody>
83-
</Table>
84-
</Paper>
85-
</Grid>
66+
<Table id="client-table">
67+
<TableHead>
68+
<TableRow style={{textAlign: 'center'}}>
69+
<TableCell>Name</TableCell>
70+
<TableCell style={{width: 200}}>Token</TableCell>
71+
<TableCell>Last Used</TableCell>
72+
<TableCell />
73+
<TableCell />
74+
</TableRow>
75+
</TableHead>
76+
<TableBody>
77+
{clients.map((client: IClient) => (
78+
<Row
79+
key={client.id}
80+
name={client.name}
81+
value={client.token}
82+
lastUsed={client.lastUsed}
83+
fEdit={() => setToUpdateClient(client)}
84+
fDelete={() => setToDeleteClient(client)}
85+
/>
86+
))}
87+
</TableBody>
88+
</Table>
89+
</Paper>
90+
</Grid>
91+
)}
8692
{createDialog && (
8793
<AddClientDialog
8894
fClose={() => setCreateDialog(false)}

ui/src/client/client-actions.ts

+6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import {uiActions} from '../store/ui-slice.ts';
77

88
export const fetchClients = () => {
99
return async (dispatch: AppDispatch) => {
10+
dispatch(clientActions.loading(true));
1011
const response = await axios.get<IClient[]>(`${config.get('url')}client`);
1112
dispatch(clientActions.set(response.data));
1213
};
1314
};
1415

1516
export const deleteClient = (id: number) => {
1617
return async (dispatch: AppDispatch) => {
18+
// do not dispatch a loading indicator as the test does not expect it
19+
// dispatch(clientActions.loading(true));
1720
await axios.delete<IClient>(`${config.get('url')}client/${id}`);
1821
dispatch(clientActions.remove(id));
1922
dispatch(uiActions.addSnackMessage('Client deleted'));
@@ -22,6 +25,7 @@ export const deleteClient = (id: number) => {
2225

2326
export const updateClient = (id: number, name: string) => {
2427
return async (dispatch: AppDispatch) => {
28+
dispatch(clientActions.loading(true));
2529
const response = await axios.put<IClient>(`${config.get('url')}client/${id}`, {name});
2630
dispatch(clientActions.replace(response.data));
2731
dispatch(uiActions.addSnackMessage('Client deleted'));
@@ -30,13 +34,15 @@ export const updateClient = (id: number, name: string) => {
3034

3135
export const createClientNoNotification = (name: string) => {
3236
return async (dispatch: AppDispatch) => {
37+
dispatch(clientActions.loading(true));
3338
const response = await axios.post<IClient>(`${config.get('url')}client`, {name});
3439
dispatch(clientActions.add(response.data));
3540
}
3641
}
3742

3843
export const createClient = (name: string) => {
3944
return async (dispatch: AppDispatch) => {
45+
dispatch(clientActions.loading(true));
4046
await dispatch(createClientNoNotification(name));
4147
dispatch(uiActions.addSnackMessage('Client added'));
4248
}

ui/src/client/client-slice.ts

+10
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {IClient} from '../types.ts';
33

44
interface ClientState {
55
items: IClient[];
6+
isLoading: boolean;
67
}
78

89
const initialClientState: ClientState = {
910
items: [],
11+
isLoading: true,
1012
}
1113

1214
export const clientSlice = createSlice({
@@ -15,23 +17,31 @@ export const clientSlice = createSlice({
1517
reducers: {
1618
set(state, action: PayloadAction<IClient[]>) {
1719
state.items = action.payload;
20+
state.isLoading = false;
1821
},
1922
add(state, action: PayloadAction<IClient>) {
2023
state.items.push(action.payload);
24+
state.isLoading = false;
2125
},
2226
replace(state, action: PayloadAction<IClient>) {
2327
const itemIndex = state.items.findIndex((item) => item.id === action.payload.id);
2428

2529
if (itemIndex !== -1) {
2630
state.items[itemIndex] = action.payload;
2731
}
32+
state.isLoading = false;
2833
},
2934
remove(state, action: PayloadAction<number>) {
3035
state.items = state.items.filter((item) => item.id !== action.payload);
36+
state.isLoading = false;
3137
},
3238
clear(state) {
3339
state.items = [];
40+
state.isLoading = false;
3441
},
42+
loading(state, action: PayloadAction<boolean>) {
43+
state.isLoading = action.payload;
44+
}
3545
}
3646
});
3747

ui/src/message/message-actions.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const fetchMessages = (appId: number = AllMessages, since: number = 0) =>
2828

2929
export const removeSingleMessage = (message: IMessage) => {
3030
return async (dispatch: AppDispatch) => {
31+
dispatch(messageActions.loading(true));
3132
await axios.delete(config.get('url') + 'message/' + message.id);
3233
dispatch(messageActions.remove(message.id));
3334
dispatch(uiActions.addSnackMessage('Message deleted'));
@@ -36,6 +37,7 @@ export const removeSingleMessage = (message: IMessage) => {
3637

3738
export const removeMessagesByApp = (app: IApplication | undefined) => {
3839
return async (dispatch: AppDispatch) => {
40+
dispatch(messageActions.loading(true));
3941
let url;
4042
if (app === undefined) {
4143
url = config.get('url') + 'message';

0 commit comments

Comments
 (0)