Skip to content

Commit 9967291

Browse files
committed
added mails and table to inspect sent mails
Signed-off-by: Tobias Winkler <[email protected]>
1 parent fe84473 commit 9967291

File tree

10 files changed

+316
-19
lines changed

10 files changed

+316
-19
lines changed

npm-shrinkwrap.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"husky": "4.2.3",
3131
"jest-styled-components": "7.0.2",
3232
"lint-staged": "10.0.8",
33+
"lodash": "4.17.15",
34+
"moment": "2.27.0",
3335
"node-plop": "0.25.0",
3436
"plop": "2.6.0",
3537
"prettier": "2.0.1",
@@ -113,5 +115,8 @@
113115
"statements": 90
114116
}
115117
}
118+
},
119+
"devDependencies": {
120+
"@types/lodash": "4.14.157"
116121
}
117122
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[
2+
{
3+
"id": "mail1",
4+
"date": "2020-07-01T10:00:00.000Z",
5+
"from": "[email protected]",
6+
7+
"subject": "subject 1",
8+
"body": "body 1",
9+
"raw": "some raw content for mail 1"
10+
},
11+
{
12+
"id": "mail2",
13+
"date": "2020-07-02T10:00:00.000Z",
14+
"from": "[email protected]",
15+
16+
"subject": "subject 2",
17+
"body": "body 2",
18+
"raw": "some raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\nsome raw content for mail 2\n"
19+
},
20+
{
21+
"id": "mail3",
22+
"date": "2020-07-03T10:00:00.000Z",
23+
"from": "[email protected]",
24+
25+
"subject": "subject 3",
26+
"body": "body 3",
27+
"raw": "some raw content for mail 3"
28+
}
29+
]

src/apis.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export const getDockerData = isProduction
99
? 'localhost/getDockerData'
1010
: `${mockPath}/getDockerData.json`;
1111

12+
export const getMails = isProduction
13+
? 'localhost/getMails'
14+
: `${mockPath}/getMails.json`;
15+
1216
export const makeGetContainerData = (id: string) =>
1317
isProduction
1418
? `localhost/getContainerData/${id}`

src/app/pages/Mails/index.tsx

Lines changed: 181 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,185 @@
1-
import React from 'react';
1+
import React, { useEffect, useState, Fragment } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
23
import { Helmet } from 'react-helmet-async';
3-
import { Container, Typography } from '@material-ui/core';
4-
5-
const Mails = () => (
6-
<>
7-
<Helmet>
8-
<title>Mails</title>
9-
<meta name="description" content="Devilbox Web-UI" />
10-
</Helmet>
11-
<Container>
12-
<Typography variant="h1" component="h2" gutterBottom>
13-
Mails
14-
</Typography>
15-
16-
<Typography variant="body1" gutterBottom>
17-
Content
18-
</Typography>
19-
</Container>
20-
</>
4+
import {
5+
Container,
6+
Typography,
7+
Input,
8+
Button,
9+
Grid,
10+
Box,
11+
TableContainer,
12+
Table,
13+
TableHead,
14+
TableRow,
15+
TableCell,
16+
TableBody,
17+
Divider,
18+
TextField,
19+
} from '@material-ui/core';
20+
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
21+
import { without } from 'lodash';
22+
import moment from 'moment';
23+
import { useInjectReducer, useInjectSaga } from 'utils/redux-injectors';
24+
import saga from './saga';
25+
import { makeDataIsFetchedSelector, makeMailsSelector } from './selectors';
26+
import { sliceKey, reducer, actions } from './slice';
27+
28+
const useStyles = makeStyles((theme: Theme) =>
29+
createStyles({
30+
isClickable: {
31+
cursor: 'pointer',
32+
},
33+
}),
2134
);
2235

36+
const Mails = () => {
37+
useInjectReducer({ key: sliceKey, reducer });
38+
useInjectSaga({ key: sliceKey, saga });
39+
40+
const classes = useStyles();
41+
const [mailsOpened, setMailsOpened] = useState<string[]>([]);
42+
const [rawSourcesOpened, setRawSourcesOpened] = useState<string[]>([]);
43+
const dispatch = useDispatch();
44+
45+
const dataIsFetched = useSelector(makeDataIsFetchedSelector);
46+
const mails = useSelector(makeMailsSelector);
47+
48+
useEffect(() => {
49+
if (!dataIsFetched) {
50+
dispatch(actions.fetchData());
51+
}
52+
}, [dataIsFetched, dispatch]);
53+
54+
return (
55+
<>
56+
<Helmet>
57+
<title>Mails</title>
58+
<meta name="description" content="Devilbox Web-UI" />
59+
</Helmet>
60+
<Container>
61+
<Typography variant="h1" component="h2" gutterBottom>
62+
Mails
63+
</Typography>
64+
65+
<Typography variant="h2" component="h2" gutterBottom>
66+
Send test email
67+
</Typography>
68+
69+
<form>
70+
<Grid container spacing={2}>
71+
<Grid item xs={3}>
72+
<Input
73+
placeholder="recipient"
74+
name="recipient"
75+
value=""
76+
fullWidth
77+
/>
78+
</Grid>
79+
<Grid item xs={3}>
80+
<Input placeholder="subject" name="subject" value="" fullWidth />
81+
</Grid>
82+
<Grid item xs={4}>
83+
<Input placeholder="message" name="message" value="" fullWidth />
84+
</Grid>
85+
<Grid item xs={2}>
86+
<Button>Send email</Button>
87+
</Grid>
88+
</Grid>
89+
</form>
90+
91+
<Box mt={6}>
92+
<Typography variant="h2" component="h2" gutterBottom>
93+
Received emails
94+
</Typography>
95+
</Box>
96+
97+
{mails.length > 0 && (
98+
<TableContainer>
99+
<Table>
100+
<TableHead>
101+
<TableRow>
102+
<TableCell component="th" scope="row">
103+
<Typography variant="button">#</Typography>
104+
</TableCell>
105+
<TableCell component="th" scope="row">
106+
<Typography variant="button">Date</Typography>
107+
</TableCell>
108+
<TableCell component="th" scope="row">
109+
<Typography variant="button">From</Typography>
110+
</TableCell>
111+
<TableCell component="th" scope="row">
112+
<Typography variant="button">To</Typography>
113+
</TableCell>
114+
<TableCell component="th" scope="row">
115+
<Typography variant="button">Subject</Typography>
116+
</TableCell>
117+
</TableRow>
118+
</TableHead>
119+
<TableBody>
120+
{mails.map((mail, index) => (
121+
<Fragment key={mail.id}>
122+
<TableRow
123+
className={classes.isClickable}
124+
onClick={() => {
125+
setMailsOpened(
126+
mailsOpened.includes(mail.id)
127+
? without(mailsOpened, mail.id)
128+
: [...mailsOpened, mail.id],
129+
);
130+
}}
131+
>
132+
<TableCell>{index + 1}</TableCell>
133+
<TableCell>{moment(mail.date).format('llll')}</TableCell>
134+
<TableCell>{mail.from}</TableCell>
135+
<TableCell>{mail.to}</TableCell>
136+
<TableCell>{mail.subject}</TableCell>
137+
</TableRow>
138+
{mailsOpened.includes(mail.id) && (
139+
<TableRow>
140+
<TableCell colSpan={5}>
141+
<Typography variant="body1">{mail.body}</Typography>
142+
<Box mt={2} mb={2}>
143+
<Divider />
144+
</Box>
145+
<Button
146+
onClick={() => {
147+
setRawSourcesOpened(
148+
rawSourcesOpened.includes(mail.id)
149+
? without(rawSourcesOpened, mail.id)
150+
: [...rawSourcesOpened, mail.id],
151+
);
152+
}}
153+
variant="outlined"
154+
>
155+
Raw source
156+
</Button>
157+
158+
{rawSourcesOpened.includes(mail.id) && (
159+
<Box mt={2}>
160+
<TextField
161+
multiline
162+
value={mail.raw}
163+
InputProps={{
164+
readOnly: true,
165+
}}
166+
fullWidth
167+
variant="filled"
168+
/>
169+
</Box>
170+
)}
171+
</TableCell>
172+
</TableRow>
173+
)}
174+
</Fragment>
175+
))}
176+
</TableBody>
177+
</Table>
178+
</TableContainer>
179+
)}
180+
</Container>
181+
</>
182+
);
183+
};
184+
23185
export default Mails;

src/app/pages/Mails/saga.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { takeLatest, put } from 'redux-saga/effects';
2+
import axios from 'axios';
3+
import { actions } from './slice';
4+
import { Mail } from './types';
5+
import { getMails as GET_MAILS_API } from '../../../apis';
6+
7+
function* fetchAppData() {
8+
const data: Mail[] = yield axios
9+
.get(GET_MAILS_API)
10+
.then(response => response.data);
11+
12+
if (data) {
13+
yield put(actions.setSentMails(data));
14+
}
15+
}
16+
17+
export default function* appSaga() {
18+
yield takeLatest(actions.fetchData.type, fetchAppData);
19+
}

src/app/pages/Mails/selectors.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createSelector } from '@reduxjs/toolkit';
2+
import RootState from '../../../types/RootState';
3+
import { Mail } from './types';
4+
5+
export const mails = (state: RootState) => state.mails;
6+
7+
export const makeDataIsFetchedSelector = createSelector(
8+
mails,
9+
mailsState => mailsState.__meta.fetch === 'done',
10+
);
11+
12+
export const makeMailsSelector = createSelector(
13+
mails,
14+
mailsState => mailsState.sent as Mail[],
15+
);

src/app/pages/Mails/slice.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PayloadAction } from '@reduxjs/toolkit';
2+
import { createSlice } from 'utils/@reduxjs/toolkit';
3+
import { ContainerState, Mail } from './types';
4+
5+
export const initialState: ContainerState = {
6+
sent: [],
7+
error: undefined,
8+
__meta: {
9+
fetch: 'unstarted',
10+
},
11+
};
12+
13+
const mailSlice = createSlice({
14+
name: 'mails',
15+
initialState,
16+
reducers: {
17+
fetchData(state) {
18+
state.__meta.fetch = 'started';
19+
state.error = undefined;
20+
},
21+
setSentMails(state, action: PayloadAction<Mail[]>) {
22+
state.sent = action.payload;
23+
state.__meta.fetch = 'done';
24+
},
25+
},
26+
});
27+
28+
export const { actions, reducer, name: sliceKey } = mailSlice;

src/app/pages/Mails/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export interface Mail {
2+
id: string;
3+
date: string;
4+
from: string;
5+
to: string;
6+
subject: string;
7+
body: string;
8+
raw: string;
9+
}
10+
11+
export interface MailStateData {
12+
sent: Mail[];
13+
}
14+
15+
interface MailState extends MailStateData {
16+
__meta: {
17+
fetch: 'unstarted' | 'started' | 'done';
18+
};
19+
error?: string;
20+
}
21+
22+
export type ContainerState = MailState;

src/types/RootState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { ContainerState as AppState } from 'app/types';
22
import { ContainerState as DockerState } from 'app/pages/Home/types';
33
import { ContainerState as VhostsState } from 'app/pages/Vhosts/types';
4+
import { ContainerState as MailState } from 'app/pages/Mails/types';
45

56
export default interface RootState {
67
app: AppState;
78
docker: DockerState;
89
vhosts: VhostsState;
10+
mails: MailState;
911
}

0 commit comments

Comments
 (0)