Skip to content

Commit

Permalink
Added loading indicators
Browse files Browse the repository at this point in the history
Except for the delete actions, as the test does not expect it there
  • Loading branch information
mfechner committed Jan 9, 2025
1 parent daecf32 commit 4d0f9bf
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 116 deletions.
1 change: 1 addition & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const App = () => {
useEffect(() => {
if (loggedIn) {
ws.listen((message) => {
dispatch(messageActions.loading(true));
dispatch(messageActions.add(message));
Notifications.notifyNewMessage(message);
if (message.priority >= 4) {
Expand Down
86 changes: 46 additions & 40 deletions ui/src/application/Applications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Button from '@mui/material/Button';
import ConfirmDialog from '../common/ConfirmDialog';
import DefaultPage from '../common/DefaultPage';
import CopyableSecret from '../common/CopyableSecret';
import LoadingSpinner from '../common/LoadingSpinner.tsx';
import {useAppDispatch, useAppSelector} from '../store';
import {fetchApps, uploadImage, deleteApp, updateApp, createApp} from './app-actions.ts';
import AddApplicationDialog from './AddApplicationDialog';
Expand All @@ -26,6 +27,7 @@ import {LastUsedCell} from '../common/LastUsedCell';
const Applications = () => {
const dispatch = useAppDispatch();
const apps = useAppSelector((state) => state.app.items);
const isLoading = useAppSelector((state) => state.app.isLoading);
const [toDeleteApp, setToDeleteApp] = useState<IApplication | null>();
const [toUpdateApp, setToUpdateApp] = useState<IApplication | null>();
const [createDialog, setCreateDialog] = useState<boolean>(false);
Expand Down Expand Up @@ -83,47 +85,51 @@ const Applications = () => {
</Button>
}
maxWidth={1000}>
<Grid size={12}>
{isLoading ? (
<LoadingSpinner />
) : (
<Grid size={12}>
<Paper elevation={6} style={{overflowX: 'auto'}}>
<Table id="app-table">
<TableHead>
<TableRow>
<TableCell padding="checkbox" style={{width: 80}} />
<TableCell>Name</TableCell>
<TableCell>Token</TableCell>
<TableCell>Description</TableCell>
<TableCell>Priority</TableCell>
<TableCell>Last Used</TableCell>
<TableCell />
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{apps.map((app: IApplication) => (
<Row
key={app.id}
description={app.description}
defaultPriority={app.defaultPriority}
image={app.image}
name={app.name}
value={app.token}
lastUsed={app.lastUsed}
fUpload={() => handleImageUploadClick(app.id)}
fDelete={() => setToDeleteApp(app)}
fEdit={() => setToUpdateApp(app)}
noDelete={app.internal}
/>
))}
</TableBody>
</Table>
<input
ref={fileInputRef}
type="file"
style={{display: 'none'}}
onChange={onUploadImage}
/>
</Paper>
</Grid>
<Table id="app-table">
<TableHead>
<TableRow>
<TableCell padding="checkbox" style={{width: 80}} />
<TableCell>Name</TableCell>
<TableCell>Token</TableCell>
<TableCell>Description</TableCell>
<TableCell>Priority</TableCell>
<TableCell>Last Used</TableCell>
<TableCell />
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{apps.map((app: IApplication) => (
<Row
key={app.id}
description={app.description}
defaultPriority={app.defaultPriority}
image={app.image}
name={app.name}
value={app.token}
lastUsed={app.lastUsed}
fUpload={() => handleImageUploadClick(app.id)}
fDelete={() => setToDeleteApp(app)}
fEdit={() => setToUpdateApp(app)}
noDelete={app.internal}
/>
))}
</TableBody>
</Table>
<input
ref={fileInputRef}
type="file"
style={{display: 'none'}}
onChange={onUploadImage}
/>
</Paper>
</Grid>
)}
{createDialog && (
<AddApplicationDialog
fClose={() => setCreateDialog(false)}
Expand Down
6 changes: 6 additions & 0 deletions ui/src/application/app-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ export const fetchApps = () => {
if (!getAuthToken()) {
return;
}
dispatch(appActions.loading(true));
const response = await axios.get<IApplication[]>(`${config.get('url')}application`);
dispatch(appActions.set(response.data));
};
};

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

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

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

export const createApp = (name: string, description: string, defaultPriority: number) => {
return async (dispatch: AppDispatch) => {
dispatch(appActions.loading(true));
const response = await axios.post(`${config.get('url')}application`, {
name,
description,
Expand Down
11 changes: 11 additions & 0 deletions ui/src/application/app-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ const initialSelectedItemState: IApplication = {
const initialAppState: {
items: IApplication[];
selectedItem: IApplication;
isLoading: boolean;
} = {
items: [],
selectedItem: initialSelectedItemState,
isLoading: true,
};

const appSlice = createSlice({
Expand All @@ -26,29 +28,38 @@ const appSlice = createSlice({
reducers: {
set(state, action: PayloadAction<IApplication[]>) {
state.items = action.payload;
state.isLoading = false;
},
add(state, action: PayloadAction<IApplication>) {
state.items.push(action.payload);
state.isLoading = false;
},
replace(state, action: PayloadAction<IApplication>) {
const itemIndex = state.items.findIndex((item) => item.id === action.payload.id);

if (itemIndex !== -1) {
state.items[itemIndex] = action.payload;
}
state.isLoading = false;
},
remove(state, action: PayloadAction<number>) {
state.items = state.items.filter((item) => item.id !== action.payload);
state.isLoading = false;
},
clear(state) {
state.items = [];
state.isLoading = false;
},
select(state, action: PayloadAction<IApplication | null>) {
if (action.payload === null) {
state.selectedItem = initialSelectedItemState;
} else {
state.selectedItem = action.payload;
}
state.isLoading = false;
},
loading(state, action: PayloadAction<boolean>) {
state.isLoading = action.payload;
}
}
});
Expand Down
58 changes: 32 additions & 26 deletions ui/src/client/Clients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Button from '@mui/material/Button';

import ConfirmDialog from '../common/ConfirmDialog';
import DefaultPage from '../common/DefaultPage';
import LoadingSpinner from '../common/LoadingSpinner.tsx';
import {useAppDispatch, useAppSelector} from '../store';
import {createClient, deleteClient, fetchClients, updateClient} from './client-actions.ts';
import AddClientDialog from './AddClientDialog';
Expand All @@ -24,6 +25,7 @@ import {LastUsedCell} from '../common/LastUsedCell';
const Clients = () => {
const dispatch = useAppDispatch();
const clients = useAppSelector((state) => state.client.items);
const isLoading = useAppSelector((state) => state.client.isLoading);
const [toDeleteClient, setToDeleteClient] = useState<IClient | null>();
const [toUpdateClient, setToUpdateClient] = useState<IClient | null>();
const [createDialog, setCreateDialog] = useState<boolean>(false);
Expand Down Expand Up @@ -56,33 +58,37 @@ const Clients = () => {
Create Client
</Button>
}>
<Grid size={12}>
{isLoading ? (
<LoadingSpinner />
): (
<Grid size={12}>
<Paper elevation={6} style={{overflowX: 'auto'}}>
<Table id="client-table">
<TableHead>
<TableRow style={{textAlign: 'center'}}>
<TableCell>Name</TableCell>
<TableCell style={{width: 200}}>Token</TableCell>
<TableCell>Last Used</TableCell>
<TableCell />
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{clients.map((client: IClient) => (
<Row
key={client.id}
name={client.name}
value={client.token}
lastUsed={client.lastUsed}
fEdit={() => setToUpdateClient(client)}
fDelete={() => setToDeleteClient(client)}
/>
))}
</TableBody>
</Table>
</Paper>
</Grid>
<Table id="client-table">
<TableHead>
<TableRow style={{textAlign: 'center'}}>
<TableCell>Name</TableCell>
<TableCell style={{width: 200}}>Token</TableCell>
<TableCell>Last Used</TableCell>
<TableCell />
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{clients.map((client: IClient) => (
<Row
key={client.id}
name={client.name}
value={client.token}
lastUsed={client.lastUsed}
fEdit={() => setToUpdateClient(client)}
fDelete={() => setToDeleteClient(client)}
/>
))}
</TableBody>
</Table>
</Paper>
</Grid>
)}
{createDialog && (
<AddClientDialog
fClose={() => setCreateDialog(false)}
Expand Down
6 changes: 6 additions & 0 deletions ui/src/client/client-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import {uiActions} from '../store/ui-slice.ts';

export const fetchClients = () => {
return async (dispatch: AppDispatch) => {
dispatch(clientActions.loading(true));
const response = await axios.get<IClient[]>(`${config.get('url')}client`);
dispatch(clientActions.set(response.data));
};
};

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

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

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

export const createClient = (name: string) => {
return async (dispatch: AppDispatch) => {
dispatch(clientActions.loading(true));
await dispatch(createClientNoNotification(name));
dispatch(uiActions.addSnackMessage('Client added'));
}
Expand Down
10 changes: 10 additions & 0 deletions ui/src/client/client-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {IClient} from '../types.ts';

interface ClientState {
items: IClient[];
isLoading: boolean;
}

const initialClientState: ClientState = {
items: [],
isLoading: true,
}

export const clientSlice = createSlice({
Expand All @@ -15,23 +17,31 @@ export const clientSlice = createSlice({
reducers: {
set(state, action: PayloadAction<IClient[]>) {
state.items = action.payload;
state.isLoading = false;
},
add(state, action: PayloadAction<IClient>) {
state.items.push(action.payload);
state.isLoading = false;
},
replace(state, action: PayloadAction<IClient>) {
const itemIndex = state.items.findIndex((item) => item.id === action.payload.id);

if (itemIndex !== -1) {
state.items[itemIndex] = action.payload;
}
state.isLoading = false;
},
remove(state, action: PayloadAction<number>) {
state.items = state.items.filter((item) => item.id !== action.payload);
state.isLoading = false;
},
clear(state) {
state.items = [];
state.isLoading = false;
},
loading(state, action: PayloadAction<boolean>) {
state.isLoading = action.payload;
}
}
});

Expand Down
2 changes: 2 additions & 0 deletions ui/src/message/message-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const fetchMessages = (appId: number = AllMessages, since: number = 0) =>

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

export const removeMessagesByApp = (app: IApplication | undefined) => {
return async (dispatch: AppDispatch) => {
dispatch(messageActions.loading(true));
let url;
if (app === undefined) {
url = config.get('url') + 'message';
Expand Down
Loading

0 comments on commit 4d0f9bf

Please sign in to comment.