Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 287652cf

Přidáno uživatelem Michal Schwob před asi 2 roky(ů)

Refactor of administration page and user registration
re #9627

Zobrazit rozdíly:

backend/docker-compose.yml
5 5
    image: postgres:14.2
6 6
    container_name: postgres-db
7 7
    environment:
8
      - POSTGRES_DB=backend-db                # database name
9
      - POSTGRES_USER=backend-db              # database user
10
      - POSTGRES_PASSWORD=gLt7*6d@pL!kAC8A8j8w  # database password
8
      - POSTGRES_DB=test                # database name
9
      - POSTGRES_USER=test              # database user
10
      - POSTGRES_PASSWORD=Password.123  # database password
11 11
    expose:
12 12
      - 5432
13 13
    ports:
14 14
      - "5432:5432"                # expose port 5432 (PostgreSQL) out of the docker container to the local machine
15 15
    volumes:
16
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
17
      - db-data:/var/lib/postgresql/dat
16
      - db-data:/var/lib/postgresql/data
18 17

  
19 18

  
20
#  app: # Spring boot application
21
#    build: .
22
#    container_name: app-backend  # name of the container
23
#    image: schwobik/backend-app:1.5
24
#    ports:
25
#      - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
26
#    depends_on:
27
#      - db
28
#    environment:
29
#      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/test # overwrites application.properties datasource url to connect to the database
30
#
31
#  frontend:
19
  app: # Spring boot application
20
    build: .
21
    container_name: app-backend  # name of the container
22
    image: schwobik/backend-app:1.7
23
    ports:
24
      - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
25
    depends_on:
26
      - db
27
    environment:
28
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/test # overwrites application.properties datasource url to connect to the database
29

  
30
  frontend:
32 31
#     Since our Dockerfile for web-server is located in react-app folder, our build context is ./react-app
33
#    build: ../frontend
34
#    container_name: frontend
35
#    image: schwobik/frontend-app:1.5
36
#    ports:
37
#      - "80:80"
32
    build: ../frontend
33
    container_name: frontend
34
    image: schwobik/frontend-app:1.7
35
    ports:
36
      - "80:80"
38 37

  
39 38
volumes:
40 39
  db-data:
frontend/src/features/Administration/Administration.tsx
1 1
import {
2 2
    Button,
3
    Container, Grid,
4
    Link,
3
    Grid,
5 4
    List,
6 5
    ListItem,
7
    ListItemIcon,
8 6
    ListItemText,
9
    NativeSelect,
10 7
    Paper,
11
    Select, SelectChangeEvent
12 8
} from '@mui/material'
13
import {Fragment, ReactNode, useEffect, useState} from 'react'
9
import {Fragment, useEffect, useState} from 'react'
14 10
import { useDispatch, useSelector } from 'react-redux'
15 11
import { RootState } from '../redux/store'
16
import axiosInstance from "../../api/api"
17 12
import 'react-quill/dist/quill.snow.css'
18 13
import ContentLoading from "../Reusables/ContentLoading"
19 14
import {UserDto} from "../../swagger/data-contracts"
20 15
import UserDetail from "./UserDetail"
21
import EditIcon from "@mui/icons-material/Edit"
22 16
import {Link as RouterLink} from "react-router-dom"
23 17
import AddIcon from '@mui/icons-material/Add';
24

  
25

  
18
import {fetchUsers} from "./userDetailThunks"
19
import {
20
    clear,
21
    consumeError,
22
    setLoading,
23
    setSelectedUser
24
} from "./userDetailSlice"
25
import ShowErrorIfPresent from "../Reusables/ShowErrorIfPresent"
26
import {number} from "yup"
26 27

  
27 28
const apiError =
28 29
    'Error while fetching data from the server, please try again later.'
29 30

  
30 31
const Administration = () => {
31
    const [users, setUsers] = useState<UserDto[] | undefined>(undefined)
32
    const [selectedUser, setSelectedUser] = useState<UserDto | undefined>(undefined)
33
    const [selectedUserID, setSelectedUserID] = useState<number | undefined>(undefined)
34
    const [isContentLoading, setIsContentLoading] = useState(true)
35
    const [err, setErr] = useState<string | undefined>(undefined)
32
    // Items, loading and error from api
33
    const users = useSelector((state: RootState) => state.usersDetail.users)
34
    const loading = useSelector((state: RootState) => state.usersDetail.loading)
35
    const apiError = useSelector((state: RootState) => state.usersDetail.error)
36
    const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
37
    const [selectedUserId, setSelectedUserId] = useState<number | undefined>(undefined)
38

  
39
    // Local state to display any error relevant error
40
    const [displayError, setDisplayError] = useState<string | undefined>(
41
        undefined
42
    )
43

  
44
    const dispatch = useDispatch()
36 45

  
37 46
    const isAdmin = useSelector(
38 47
        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
39 48
    )
40 49

  
41 50
    const handleClick = (userID: number) => {
42
        console.log("clicked " + userID)
51
        console.log("clicked "+ userID)
43 52
        if (users) {
44
            setSelectedUser(users.at(userID))
45
            setSelectedUserID(userID)
53
            setSelectedUserId(userID)
54
            dispatch(setSelectedUser(users.at(userID)))
55
            console.log("after " + selectedUser)
46 56
        }
47 57
    }
48 58

  
49
    // Function to fetch the item from the api
50
    const fetchItem = async () => {
51
        try {
52
            const { data, status } = await axiosInstance.get(
53
                `/users`
54
            )
55
            if (status !== 200) {
56
                setErr(apiError)
57
                return
58
            }
59
    // Use effect to read the error and consume it
60
    useEffect(() => {
61
        if (users.length > 0) {
62
            setSelectedUserId(0)
63
            dispatch(setSelectedUser(users.at(0)))
64
            console.log("selected id: " + selectedUser)
65
        }
66
    }, [users])
67

  
59 68

  
60
            setUsers(data)
61
            setIsContentLoading(false)
62
        } catch (err: any) {
63
            setErr(apiError)
69
    // Use effect to read the error and consume it
70
    useEffect(() => {
71
        if (apiError) {
72
            setDisplayError(apiError)
73
            dispatch(consumeError())
64 74
        }
65
    }
75
    }, [apiError, dispatch])
66 76

  
67 77
    // Fetch the item from the api after mounting the component
68 78
    useEffect(() => {
69
        fetchItem()
70
    }, [])
79
        console.log("before fetch")
80
        dispatch(fetchUsers())
81
        console.log("after fetch: " + loading)
82

  
83
        return () => {
84
            // Invalidate the state when unmounting so that the old list is not rerendered when the user returns to the page
85
            dispatch(setLoading())
86
            console.log("set loading: " + loading)
87
        }
88
    }, [dispatch])
71 89

  
72 90
    const childClosed = () => {
73
        setSelectedUser(undefined)
74
        setSelectedUserID(undefined)
75
        fetchItem()
91
        dispatch(clear())
92
        dispatch(fetchUsers())
93
        setSelectedUserId(0)
94
        dispatch(setSelectedUser(users.at(0)))
76 95
    }
77 96

  
78 97
    return (
79 98
        <Fragment>
80

  
81
            {isContentLoading && !err ? <ContentLoading /> : null}
82
            {!isContentLoading && users && isAdmin ? (
99
            <ShowErrorIfPresent err={displayError} />
100
            {loading && !apiError ? <ContentLoading /> : null}
101
            {!loading && users && isAdmin ? (
83 102
                <Grid container justifyContent="space-around">
84
                    <Grid item xs={3} md={2} sx={{ px: 2 }}>
103
                    <Grid item xs={6} md={3} sx={{ px: 2 }}>
85 104
                        <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
86 105
                            <List>
87 106
                                {users.map((user, id) => (
88 107
                                    <ListItem
89 108
                                        key={id}
90 109
                                        className={
91
                                            selectedUserID === id
110
                                            selectedUserId === id
92 111
                                                ? 'clicked-user'
93 112
                                                : ''
94 113
                                        }
......
108 127
                            </Button>
109 128
                        </Paper>
110 129
                    </Grid>
111
                    <Grid item md={10} xs={9}>
112
                        {selectedUser ?
113
                            <UserDetail user={selectedUser} onClose={childClosed} />
130
                    <Grid item md={9} xs={6}>
131
                        {selectedUserId !== undefined  ?
132
                            <UserDetail user={selectedUser as UserDto} onClose={childClosed} />
114 133
                        : null }
115 134
                    </Grid>
116 135
                </Grid>
frontend/src/features/Administration/UserDetail.tsx
1 1
import {
2 2
    Button,
3
    Container, Divider, Grid,
3
    Grid,
4 4
    Checkbox,
5
    Link,
6
    List,
7
    ListItem,
8
    ListItemIcon,
9
    ListItemText,
10
    NativeSelect,
11 5
    Paper,
12
    Select, SelectChangeEvent, Typography, FormGroup, FormControlLabel
6
    Typography,
7
    FormGroup,
8
    FormControlLabel, Container
13 9
} from '@mui/material'
14
import React, {ChangeEvent, Fragment, ReactNode, useEffect, useState} from 'react'
15
import {useDispatch, useSelector} from 'react-redux'
16
import {RootState} from '../redux/store'
10
import React, {Fragment, useEffect, useState} from 'react'
17 11
import axiosInstance from "../../api/api"
18 12
import 'react-quill/dist/quill.snow.css'
19
import ContentLoading from "../Reusables/ContentLoading"
20
import {Link as RouterLink, useNavigate} from "react-router-dom"
21
import EditIcon from '@mui/icons-material/Edit'
22
import {formatHtmlStringToReactDom} from "../../utils/formatting/HtmlUtils"
23 13
import {PermissionDto, UserDto} from "../../swagger/data-contracts"
24
import {Label} from "@mui/icons-material"
25 14
import ClearIcon from "@mui/icons-material/Clear"
15
import CheckIcon from '@mui/icons-material/Check';
16
import {useDispatch, useSelector} from "react-redux"
17
import {fetchUsers, savePermissions} from "./userDetailThunks"
18
import {RootState} from "../redux/store"
19
import {setSelectedUserPermissions} from "./userDetailSlice"
26 20

  
27 21
export interface UserDetailProps {
28 22
    user: UserDto,
......
30 24
}
31 25

  
32 26
const UserDetail = (props: UserDetailProps) => {
33
    const [user] = useState(props.user)
34
    const [canRead, setCanRead] = useState(user.permissions?.canRead)
35
    const [canWrite, setCanWrite] = useState(user.permissions?.canWrite)
36
    const [canDelete, setCanDelete] = useState(user.permissions?.canDelete)
27
    const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
28
    const [canRead, setCanRead] = useState(selectedUser?.permissions?.canRead)
29
    const [canWrite, setCanWrite] = useState(selectedUser?.permissions?.canWrite)
30
    const [canDelete, setCanDelete] = useState(selectedUser?.permissions?.canDelete)
37 31

  
32
    const dispatch = useDispatch()
33

  
34
    useEffect(()=> {
35
        console.log("user updated:")
36
        setCanRead(selectedUser?.permissions?.canRead)
37
        setCanWrite(selectedUser?.permissions?.canWrite)
38
        setCanDelete(selectedUser?.permissions?.canDelete)
39
    }, [selectedUser]);
38 40

  
39 41
    // Maps user property to corresponding table row
40 42
    const mapToRow = (rowName: string, item: string) => (
41 43
        <Fragment>
42 44
            <Grid sx={{my: 2}} container justifyContent="space-around">
43
                <Grid item xs={4} sx={{px: 1}}>
45
                <Grid item xs={6} sx={{px: 1}}>
44 46
                    <Typography fontWeight={500}>{rowName}</Typography>
45 47
                </Grid>
46
                <Grid item xs={8} sx={{ml: 'auto'}}>
48
                <Grid item xs={6} sx={{ml: 'auto'}}>
47 49
                    <Typography key={item}>{item}</Typography>
48 50
                </Grid>
49 51
            </Grid>
......
57 59
        },
58 60
        {
59 61
            rowName: 'E-mail:',
60
            item: user?.email,
62
            item: selectedUser?.email,
61 63
        }
62 64
    ]
63 65
    const rights = {
......
69 71

  
70 72
    const deleteUser = async () => {
71 73
        const { data, status } = await axiosInstance.delete(
72
            `/users/${user.email}`
74
            `/users/${selectedUser?.email}`
73 75
        )
74 76
        if (status !== 200) {
75 77
            // TODO dodělat zpracování erroru
......
78 80
        props.onClose();
79 81
    }
80 82

  
83
    const prepareSavePermissions = () => {
84
        dispatch(setSelectedUserPermissions({canRead: canRead, canWrite: canWrite, canDelete: canDelete}))
85
        dispatch(savePermissions())
86
        dispatch(fetchUsers())
87
    }
88

  
81 89
    return (
82 90
        <Fragment>
83 91
            <Paper style={{minHeight: '80vh'}} variant="outlined">
84 92
                <Grid container justifyContent="space-around">
85
                    <Grid item xs={6} md={6} sx={{p: 2}}>
93
                    <Grid item xs={12} md={6} sx={{p: 2}}>
86 94
                        <Typography fontWeight={800}>Information</Typography>
87 95
                        {rows.map((row) => (
88 96

  
......
94 102
                            </Fragment>
95 103
                        ))}
96 104
                    </Grid>
97
                    <Grid item xs={6} md={6} sx={{p: 2}}>
98
                        <b>Rights</b>
99
                            <FormGroup>
100
                                <FormControlLabel
101
                                    control={
102
                                        <Checkbox
103
                                            checked={canWrite}
104
                                            onChange={e => setCanWrite(e.target.checked)}
105
                                        />}
106
                                    label={rights.canWrite}/>
107
                                <FormControlLabel
108
                                    control={
109
                                        <Checkbox
110
                                            checked={canRead}
111
                                            onChange={e => setCanRead(e.target.checked)}
112
                                        />}
113
                                    label={rights.canRead}/>
114
                                <FormControlLabel
115
                                    control={
116
                                        <Checkbox
117
                                            checked={canDelete}
118
                                            onChange={e => setCanDelete(e.target.checked)}
119
                                        />}
120
                                    label={rights.canDelete}/>
121
                            </FormGroup>
105
                    <Grid item xs={12} md={6} sx={{p: 2}}>
106
                        <Typography fontWeight={800}>Rights</Typography>
107
                        <FormGroup>
108
                            <FormControlLabel
109
                                control={
110
                                    <Checkbox
111
                                        checked={canWrite}
112
                                        onChange={e => setCanWrite(e.target.checked)}
113
                                    />}
114
                                label={rights.canWrite}/>
115
                            <FormControlLabel
116
                                control={
117
                                    <Checkbox
118
                                        checked={canRead}
119
                                        onChange={e => setCanRead(e.target.checked)}
120
                                    />}
121
                                label={rights.canRead}/>
122
                            <FormControlLabel
123
                                control={
124
                                    <Checkbox
125
                                        checked={canDelete}
126
                                        onChange={e => setCanDelete(e.target.checked)}
127
                                    />}
128
                                label={rights.canDelete}/>
129
                        </FormGroup>
122 130
                    </Grid>
123 131
                </Grid>
132
                <Button startIcon={<CheckIcon />}
133
                        variant="contained"
134
                        color="primary"
135
                        onClick={prepareSavePermissions}
136
                        sx={{ m: 2 }} >
137
                    Save
138
                </Button>
124 139
                <Button startIcon={<ClearIcon />}
125 140
                        variant="contained"
126 141
                        color="primary"
frontend/src/features/Administration/userDetailSlice.tsx
1
import { createSlice } from '@reduxjs/toolkit'
2
import {PermissionDto, UserDto} from '../../swagger/data-contracts'
3
import {fetchUsers} from './userDetailThunks'
4
import {number} from "yup"
5

  
6
export interface UsersDetailState {
7
    users: UserDto[] // list of all fetched items
8
    loading: boolean // whether the users are loading
9
    error?: string,
10
    selectedUser?: UserDto,
11
    permissions?: PermissionDto
12
}
13

  
14
const initialState: UsersDetailState = {
15
    users: [],
16
    loading: true,
17
    error: undefined,
18
    selectedUser: undefined,
19
    permissions: undefined
20
}
21

  
22
const usersDetailSlice = createSlice({
23
    name: 'usersDetail',
24
    initialState,
25
    reducers: {
26
        clear: (state) => ({ ...initialState }),
27
        setLoading: (state) => ({ ...state, loading: true }),
28
        consumeError: (state) => ({ ...state, error: undefined }),
29
        setSelectedUser: (state, action) => ({
30
            ...state,
31
            selectedUser: action.payload
32
        }),
33
        setSelectedUserPermissions: (state, action) => ({
34
            ...state,
35
            permissions: action.payload
36
        }),
37
    },
38
    extraReducers: (builder) => {
39
        builder.addCase(fetchUsers.pending, (state) => ({
40
            ...state,
41
            loading: true,
42
        }))
43
        builder.addCase(fetchUsers.fulfilled, (state, action) => ({
44
            ...state,
45
            users: action.payload,
46
            loading: false,
47
        }))
48
        builder.addCase(fetchUsers.rejected, (state, action) => ({
49
            ...state,
50
            loading: false,
51
            error: action.error.message as string,
52
        }))
53
    },
54
})
55

  
56
export const {
57
    clear,
58
    setLoading,
59
    consumeError,
60
    setSelectedUser,
61
    setSelectedUserPermissions,
62
} = usersDetailSlice.actions
63
const usersDetailReducer = usersDetailSlice.reducer
64
export default usersDetailReducer
frontend/src/features/Administration/userDetailThunks.tsx
1
import { createAsyncThunk } from '@reduxjs/toolkit'
2
import axiosInstance from '../../api/api'
3
import {UsersDetailState} from "./userDetailSlice"
4

  
5

  
6
const apiError = 'Error, server is currently unavailable.'
7

  
8
export const fetchUsers = createAsyncThunk(
9
    'usersDetail/fetchUsers',
10
    async (dispatch, { getState }) => {
11
        try {
12
            console.log("fetch users called")
13
            const {data, status} = await axiosInstance.get(
14
                `/users`
15
            )
16
            if (status === 200) {
17
                console.log(data)
18
                return data
19
            }
20
            console.log("not returning data: " + data)
21
            return Promise.reject(apiError)
22
        } catch (err: any) {
23
            return Promise.reject(apiError)
24
        }
25
    }
26
)
27

  
28
export const savePermissions = createAsyncThunk(
29
    'usersDetail/savePermissions',
30
    async (dispatch, { getState }) => {
31
        try {
32
            const { usersDetail } = getState() as { usersDetail: UsersDetailState }
33
            console.log("savePermissions called")
34

  
35
            const selectedUser = usersDetail.selectedUser
36
            if (selectedUser === undefined) {
37
                return Promise.reject("User is undefined")
38
            }
39

  
40
            const selectedUserPermissions = usersDetail.permissions
41
            if (selectedUserPermissions === undefined) {
42
                return Promise.reject("Permissions is undefined")
43
            }
44

  
45
            const selectedUserIdentifier = selectedUser.email
46

  
47
            const {data, status} = await axiosInstance.patch(
48
                `/users/${selectedUserIdentifier}/permissions`,
49
                selectedUserPermissions
50
            )
51
            if (status === 200) {
52
                console.log("returning data: " + data)
53
                return data
54
            }
55
            console.log("not returning data: " + data)
56
            return Promise.reject(apiError)
57
        } catch (err: any) {
58
            return Promise.reject(apiError)
59
        }
60
    }
61
)
frontend/src/features/redux/store.ts
7 7
import { composeWithDevTools } from 'redux-devtools-extension'
8 8
import notificationReducer from '../Notification/notificationSlice'
9 9
import trackingToolReducer from '../TrackingTool/trackingToolSlice'
10
import usersDetailReducer from '../Administration/userDetailSlice'
10 11
import { enableMapSet } from 'immer'
11 12
import navigationReducer from '../Navigation/navigationSlice'
12 13

  
......
22 23
        catalog: catalogReducer,
23 24
        notification: notificationReducer,
24 25
        trackingTool: trackingToolReducer,
26
        usersDetail: usersDetailReducer,
25 27
        navigation: navigationReducer,
26 28
    }),
27 29
    process.env.REACT_APP_DEV_ENV === 'true'

Také k dispozici: Unified diff