Skip to content

Commit 3162e87

Browse files
committed
wip
1 parent 3e84d2d commit 3162e87

11 files changed

+471
-103
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@
265265
"@emotion/styled": "^11.11.5",
266266
"@fontsource/dm-sans": "^5.0.20",
267267
"@grpc/grpc-js": "^1.9.15",
268+
"@mui/icons-material": "^6.4.1",
268269
"@mui/lab": "^5.0.0-alpha.161",
269270
"@mui/material": "^5.15.5",
270271
"@mui/styles": "^5.15.5",

src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import SnackbarCloseButton from './renderer/components/SnackbarCloseButton';
2525
import { THEMES } from './shared/constants';
2626
import createCustomTheme, { ThemeConfig } from './shared/theme';
2727
import Layout from './renderer/pages/Layout';
28+
import LoadForm from './renderer/pages/LoadForm';
2829

2930
const RouteListener: FC = () => {
3031
const navigate = useNavigate();
@@ -74,6 +75,7 @@ const App: FC = () => {
7475
element={<Navigate to="/manage" replace />}
7576
/>
7677
<Route path="/manage" element={<ManageConnections />} />
78+
<Route path="/loadForm" element={<LoadForm />} />
7779
<Route
7880
path="/view_connection/:connectionID"
7981
element={<ConnectionView />}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Button, FormHelperText, Grid, Typography } from '@mui/material';
2+
import React, { FC, useState } from 'react';
3+
import TextArea from './TextArea';
4+
import ManualClientCertSelection from './ManualClientCertSelection';
5+
6+
export type ClientCertSelectionProps = {
7+
onChangeCert: (cert: string) => void;
8+
onChangeKey: (key: string) => void;
9+
};
10+
11+
const ClientCertSelection: FC<ClientCertSelectionProps> = ({
12+
onChangeCert,
13+
onChangeKey,
14+
}) => {
15+
const supportsClientCertFromStore =
16+
process.platform === 'win32' || process.platform === 'darwin';
17+
if (!supportsClientCertFromStore) {
18+
return (
19+
<ManualClientCertSelection
20+
onChangeCert={onChangeCert}
21+
onChangeKey={onChangeKey}
22+
/>
23+
);
24+
}
25+
return (
26+
<>
27+
<Grid item xs={12}>
28+
<FormControlLabel
29+
control={
30+
<Switch
31+
checked={clientCertFromStoreEnabled}
32+
color="primary"
33+
onChange={(evt): void =>
34+
saveClientCertFromStore(evt.target.checked ? {} : undefined)
35+
}
36+
/>
37+
}
38+
label="Search OS certificate store"
39+
/>
40+
<FormHelperText sx={{ pl: 2 }}>
41+
Searches for a client certificate based on the trusted CA names
42+
provided in the TLS connection handshake.
43+
</FormHelperText>
44+
</Grid>
45+
<NestedAccordion
46+
sx={{ mt: 2 }}
47+
disabled={!clientCertFromStoreEnabled}
48+
expanded={clientCertFiltersExpanded}
49+
onChange={(evt, expanded) => setClientCertFiltersExpanded(expanded)}
50+
>
51+
<NestedAccordionSummary>
52+
<Typography>
53+
Additional OS certificate store filters
54+
{!clientCertFiltersExpanded && clientCertFiltersSummary && (
55+
<>
56+
:<br />
57+
{clientCertFiltersSummary}
58+
</>
59+
)}
60+
</Typography>
61+
</NestedAccordionSummary>
62+
<NestedAccordionDetails>
63+
{clientCertFromStoreEnabled && (
64+
<>
65+
<CertFilter
66+
label="Issuer Name"
67+
data={connection?.clientCertFromStore?.issuerFilter}
68+
onChange={saveClientCertIssuerFilter}
69+
disabled={!clientCertFromStoreEnabled}
70+
/>
71+
<CertFilter
72+
label="Subject Name"
73+
data={connection?.clientCertFromStore?.subjectFilter}
74+
onChange={saveClientCertSubjectFilter}
75+
disabled={!clientCertFromStoreEnabled}
76+
/>
77+
</>
78+
)}
79+
</NestedAccordionDetails>
80+
</NestedAccordion>
81+
82+
<NestedAccordion sx={{ my: 2 }}>
83+
<NestedAccordionSummary>
84+
<Typography>Set client certificate manually</Typography>
85+
</NestedAccordionSummary>
86+
<NestedAccordionDetails>
87+
<Grid container spacing={2} sx={{ pt: 1 }}>
88+
{manualClientCertSection}
89+
</Grid>
90+
</NestedAccordionDetails>
91+
</NestedAccordion>
92+
</>
93+
);
94+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Button, FormHelperText, Grid, Typography } from '@mui/material';
2+
import React, { FC, useState } from 'react';
3+
import TextArea from './TextArea';
4+
5+
export type ManualClientCertSelectionProps = {
6+
onChangeCert: (cert: string) => void;
7+
onChangeKey: (key: string) => void;
8+
};
9+
10+
const ManualClientCertSelection: FC<ManualClientCertSelectionProps> = ({
11+
onChangeCert,
12+
onChangeKey,
13+
}) => {
14+
const [certText, setCertText] = useState('');
15+
const [keyText, setKeyText] = useState('');
16+
17+
const handleCertText = (evt: React.ChangeEvent<HTMLInputElement>) => {
18+
setCertText(evt.target.value);
19+
onChangeCert(evt.target.value);
20+
};
21+
22+
const handleKeyText = (evt: React.ChangeEvent<HTMLInputElement>) => {
23+
setKeyText(evt.target.value);
24+
onChangeKey(evt.target.value);
25+
};
26+
27+
const handleCertFile = (evt: React.ChangeEvent<HTMLInputElement>) => {
28+
const file = evt.target.files?.[0];
29+
if (file) {
30+
const reader = new FileReader();
31+
reader.onload = (e) => {
32+
setCertText(e?.target?.result as string);
33+
onChangeCert(e?.target?.result as string);
34+
};
35+
reader.readAsText(file);
36+
}
37+
};
38+
39+
const handleKeyFile = (evt: React.ChangeEvent<HTMLInputElement>) => {
40+
const file = evt.target.files?.[0];
41+
if (file) {
42+
const reader = new FileReader();
43+
reader.onload = (e) => {
44+
setKeyText(e?.target?.result as string);
45+
onChangeKey(e?.target?.result as string);
46+
};
47+
reader.readAsText(file);
48+
}
49+
};
50+
51+
return (
52+
<>
53+
<Grid item xs={12}>
54+
<label htmlFor="cert-file">
55+
<input
56+
style={{ display: 'none' }}
57+
id="cert-file"
58+
type="file"
59+
onChange={handleCertFile}
60+
/>
61+
<Button variant="contained" color="primary" component="span">
62+
Client Certificate from File
63+
</Button>
64+
</label>
65+
</Grid>
66+
<Grid item xs={12}>
67+
<Typography variant="body2">Client Certificate Text</Typography>
68+
<TextArea
69+
fullWidth
70+
variant="filled"
71+
value={certText}
72+
multiline
73+
required={!!keyText}
74+
rows={5}
75+
placeholder="e.g. copy/paste the cert in PEM format"
76+
onChange={handleCertText}
77+
spellCheck={false}
78+
/>
79+
<FormHelperText sx={{ pl: 2 }}>
80+
Add a Client Certificate with the File Selector or Copy/Paste to the
81+
Text Area. Key is required if the Certificate is present.
82+
</FormHelperText>
83+
</Grid>
84+
<Grid item xs={12}>
85+
<label htmlFor="key-file">
86+
<input
87+
style={{ display: 'none' }}
88+
id="key-file"
89+
type="file"
90+
onChange={handleKeyFile}
91+
/>
92+
<Button variant="contained" color="primary" component="span">
93+
Client Certificate Key from File
94+
</Button>
95+
</label>
96+
</Grid>
97+
<Grid item xs={12}>
98+
<Typography variant="body2">Client Certificate Key Text</Typography>
99+
<TextArea
100+
fullWidth
101+
variant="filled"
102+
value={keyText}
103+
required={!!certText}
104+
multiline
105+
rows={5}
106+
placeholder="e.g. copy/paste the key in PEM format"
107+
onChange={handleKeyText}
108+
/>
109+
<FormHelperText sx={{ pl: 2 }}>
110+
Add a Client Certificate Key with the File Selector or Copy/Paste to
111+
the Text Area. Certificate is required if the Key is present.
112+
</FormHelperText>
113+
</Grid>
114+
</>
115+
);
116+
};
117+
118+
export default ManualClientCertSelection;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
Button,
3+
ClickAwayListener,
4+
Grow,
5+
MenuItem,
6+
MenuList,
7+
Paper,
8+
Popper,
9+
Stack,
10+
Typography,
11+
} from '@mui/material';
12+
import React, { FC, useRef } from 'react';
13+
import { useNavigate } from 'react-router-dom';
14+
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
15+
import { usePopover } from '../hooks/use-popover';
16+
17+
const NewConnectionButton: FC = () => {
18+
const popover = usePopover<HTMLButtonElement>();
19+
const navigate = useNavigate();
20+
const anchorRef = useRef<HTMLDivElement>(null);
21+
22+
const handleClose = (event: Event) => {
23+
if (
24+
anchorRef.current &&
25+
anchorRef.current.contains(event.target as HTMLElement)
26+
) {
27+
return;
28+
}
29+
popover.handleClose();
30+
};
31+
32+
const handleMenuItemClick = (key: string) => {
33+
popover.handleClose();
34+
switch (key) {
35+
case 'load':
36+
navigate(`/loadForm`, {
37+
replace: true,
38+
});
39+
break;
40+
case 'add':
41+
navigate(`/connectForm`, {
42+
replace: true,
43+
});
44+
break;
45+
}
46+
};
47+
48+
const options = [
49+
{
50+
key: 'load',
51+
title: 'Load Connections',
52+
},
53+
{
54+
key: 'add',
55+
title: 'Add Connecton',
56+
},
57+
];
58+
59+
return (
60+
<>
61+
<Button
62+
variant="contained"
63+
color="primary"
64+
ref={popover.anchorRef}
65+
onClick={popover.handleOpen}
66+
startIcon={<AddCircleOutlineIcon />}
67+
>
68+
<Typography variant="button">New Connection</Typography>
69+
</Button>
70+
<Popper
71+
sx={{ zIndex: 1 }}
72+
open={popover.open}
73+
anchorEl={popover.anchorRef.current}
74+
role={undefined}
75+
transition
76+
disablePortal
77+
placement="bottom-end"
78+
>
79+
{({ TransitionProps, placement }) => (
80+
<Grow
81+
{...TransitionProps}
82+
style={{
83+
transformOrigin:
84+
placement === 'bottom' ? 'center top' : 'center bottom',
85+
}}
86+
>
87+
<Paper>
88+
<ClickAwayListener onClickAway={handleClose}>
89+
<MenuList id="split-button-menu" autoFocusItem disablePadding>
90+
{options.map((option) => (
91+
<MenuItem
92+
key={option.key}
93+
onClick={(_event) => handleMenuItemClick(option.key)}
94+
sx={{ borderRadius: 1 }}
95+
divider={true}
96+
>
97+
<Stack
98+
direction="row"
99+
spacing={1}
100+
justifyContent="center"
101+
>
102+
<Typography variant="button">{option.title}</Typography>
103+
</Stack>
104+
</MenuItem>
105+
))}
106+
</MenuList>
107+
</ClickAwayListener>
108+
</Paper>
109+
</Grow>
110+
)}
111+
</Popper>
112+
</>
113+
);
114+
};
115+
116+
export default NewConnectionButton;

src/renderer/components/TextArea.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { styled, TextField } from '@mui/material';
2+
3+
const TextArea = styled(TextField)({
4+
'& div.MuiFilledInput-root': {
5+
background: `rgba(110, 67, 232, 0.05)`,
6+
padding: `0px`,
7+
marginTop: `10px`,
8+
display: `flex`,
9+
flexFlow: `row nowrap`,
10+
boxShadow: `0 0 0 1px rgb(63 63 68 / 5%), 0 1px 2px 0 rgb(63 63 68 / 15%)`,
11+
12+
'& div.MuiFilledInput-root': {
13+
margin: `2px 0px 0px 6px`,
14+
height: `100%`,
15+
},
16+
'& input.MuiInputBase-input': {
17+
padding: `6px`,
18+
margin: `6px`,
19+
},
20+
21+
'& .MuiFilledInput-inputMultiline': {
22+
padding: `10px`,
23+
},
24+
},
25+
'& div.MuiFilledInput-underline:before': {
26+
borderBottom: `0px`,
27+
},
28+
'& div.MuiFilledInput-underline:after': {
29+
border: `0px`,
30+
},
31+
});
32+
export default TextArea;

0 commit comments

Comments
 (0)