v0.1.9 Various bug fixes and UI improvements to route form

This commit is contained in:
Yann Stepienik 2023-04-04 21:54:35 +01:00
parent bc5743fa05
commit 2eac6fbd3a
8 changed files with 148 additions and 71 deletions

View file

@ -36,9 +36,9 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
const [containers, setContainers] = React.useState([]); const [containers, setContainers] = React.useState([]);
const [hasPublicPorts, setHasPublicPorts] = React.useState(false); const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
const [isOnBridge, setIsOnBridge] = React.useState(false); const [isOnBridge, setIsOnBridge] = React.useState(false);
const [options, setOptions] = React.useState([]); const [options, setOptions] = React.useState(null);
const [portsOptions, setPortsOptions] = React.useState([]); const [portsOptions, setPortsOptions] = React.useState([]);
const loading = open && options.length === 0; const loading = options === null;
const name = "Target" const name = "Target"
const label = "Container Name" const label = "Container Name"
@ -50,27 +50,23 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
let preview = formik.values[name]; let preview = formik.values[name];
if(preview && preview.includes("://") && preview.includes(":")) { if(preview) {
let p1_ = preview.split("://")[1] let protocols = preview.split("://")
let p1_ = protocols.length > 1 ? protocols[1] : protocols[0]
console.log("p1_", p1_)
targetResult = { targetResult = {
container: '/' + p1_.split(":")[0], container: '/' + p1_.split(":")[0],
port: p1_.split(":")[1], port: p1_.split(":")[1],
protocol: preview.split("://")[0], protocol: preview.split("://")[0],
containerObject: containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0]), containerObject: !loading && containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0]),
} }
} }
function getTarget() { function getTarget() {
return targetResult.protocol + "://" + targetResult.container.replace("/", "") + ":" + targetResult.port return targetResult.protocol + (targetResult.protocol != '' ? "://" : '') + targetResult.container.replace("/", "") + ":" + targetResult.port
} }
const onContainerChange = (newContainer) => { const postContainerChange = (newContainer) => {
targetResult.container = newContainer.Names[0]
targetResult.containerObject = newContainer
targetResult.port = ''
targetResult.protocol = 'http'
formik.setFieldValue(name, getTarget())
let portsTemp = [] let portsTemp = []
newContainer.Ports.forEach((port) => { newContainer.Ports.forEach((port) => {
portsTemp.push(port.PrivatePort) portsTemp.push(port.PrivatePort)
@ -85,6 +81,17 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
} }
} }
const onContainerChange = (newContainer) => {
if(loading) return;
targetResult.container = newContainer.Names[0]
targetResult.containerObject = newContainer
targetResult.port = ''
targetResult.protocol = 'http'
formik.setFieldValue(name, getTarget())
postContainerChange(newContainer)
}
React.useEffect(() => { React.useEffect(() => {
if(lockTarget) { if(lockTarget) {
onContainerChange(TargetContainer) onContainerChange(TargetContainer)
@ -93,7 +100,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
React.useEffect(() => { React.useEffect(() => {
let active = true; let active = true;
if (!loading) { if (!loading) {
return undefined; return undefined;
} }
@ -101,13 +108,15 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
(async () => { (async () => {
const res = await API.docker.list() const res = await API.docker.list()
setContainers(res.data); setContainers(res.data);
let names = res.data.map((container) => container.Names[0]) let names = res.data.map((container) => container.Names[0])
if (active) { if (active) {
setOptions([...names]); setOptions([...names]);
} }
if (targetResult.container !== 'null') {
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container))
}
})(); })();
return () => { return () => {
@ -124,7 +133,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
return ( <Grid item xs={12}> return ( <Grid item xs={12}>
<Stack spacing={1}> <Stack spacing={1}>
<InputLabel htmlFor={name + "-autocomplete"}>{label}</InputLabel> <InputLabel htmlFor={name + "-autocomplete"}>{label}</InputLabel>
<Autocomplete {!loading && <Autocomplete
id={name + "-autocomplete"} id={name + "-autocomplete"}
open={open} open={open}
disabled={lockTarget} disabled={lockTarget}
@ -147,7 +156,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
loading={loading} loading={loading}
freeSolo={true} freeSolo={true}
placeholder={"Please select a container"} placeholder={"Please select a container"}
defaultValue={lockTarget ? TargetContainer : targetResult.containerObject} value={lockTarget ? TargetContainer : (targetResult.containerObject || {Names: ['...']})}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -162,8 +171,8 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
}} }}
/> />
)} )}
/> />}
{(portsOptions.length > 0) ? (<> {(portsOptions.length > 0) ? (<>
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel> <InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
<TextField <TextField
@ -171,7 +180,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
variant="outlined" variant="outlined"
name={name + "-port"} name={name + "-port"}
id={name + "-port"} id={name + "-port"}
defaultValue={targetResult.port} value={targetResult.port}
select select
placeholder='Select a port' placeholder='Select a port'
onChange={(event) => { onChange={(event) => {
@ -184,24 +193,24 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
{option} {option}
</MenuItem> </MenuItem>
))} ))}
</TextField></>) : ''} </TextField>
{targetResult.port == '' && <FormHelperText error id="standard-weight-helper-text-name-login">
Please select a port
</FormHelperText>}
</>) : ''}
{(portsOptions.length > 0) ? (<> {(portsOptions.length > 0) ? (<>
<Grid item xs={12}> <InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}> <TextField
<FormControlLabel type="text"
type="checkbox"
name={name + "-protocol"} name={name + "-protocol"}
control={<Checkbox size="large" />} defaultValue={targetResult.protocol}
onChange={(event) => { onChange={(event) => {
targetResult.protocol = event.target.checked ? "https" : "http" targetResult.protocol = event.target.value && event.target.value.toLowerCase()
formik.setFieldValue(name, getTarget()) formik.setFieldValue(name, getTarget())
}} }}
label={"Container uses HTTPS internally (leave unchecked if not sure, they usually don't)"}
/> />
</Stack>
</Grid>
</>) : ''} </>) : ''}
<InputLabel htmlFor={name}>Result Target Preview</InputLabel> <InputLabel htmlFor={name}>Result Target Preview</InputLabel>
@ -212,6 +221,12 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
value={formik.values[name]} value={formik.values[name]}
disabled={true} disabled={true}
/> />
{formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
</Stack> </Stack>
</Grid> </Grid>
); );

View file

@ -38,12 +38,7 @@ export const CosmosInputText = ({ name, style, multiline, type, placeholder, onC
name={name} name={name}
multiline={multiline} multiline={multiline}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
onChange={(...e) => { onChange={formik.handleChange}
if (onChange) {
onChange(...e);
}
formik.handleChange(...e);
}}
placeholder={placeholder} placeholder={placeholder}
fullWidth fullWidth
error={Boolean(formik.touched[name] && formik.errors[name])} error={Boolean(formik.touched[name] && formik.errors[name])}
@ -172,6 +167,11 @@ export const CosmosCheckbox = ({ name, label, formik, style }) => {
style={style} style={style}
/> />
</Stack> </Stack>
{formik.touched[name] && formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
</Grid> </Grid>
} }

View file

@ -28,7 +28,8 @@ import {
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton'; import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart'; import RestartModal from './restart';
import RouteManagement from './routeman'; import RouteManagement, {ValidateRoute} from './routeman';
import { map } from 'lodash';
const ProxyManagement = () => { const ProxyManagement = () => {
@ -36,6 +37,7 @@ const ProxyManagement = () => {
const [config, setConfig] = React.useState(null); const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false); const [openModal, setOpenModal] = React.useState(false);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
const [submitErrors, setSubmitErrors] = React.useState([]);
function updateRoutes(routes) { function updateRoutes(routes) {
let con = { let con = {
@ -52,6 +54,14 @@ const ProxyManagement = () => {
return con; return con;
} }
function cleanRoutes(config) {
config.HTTPConfig.ProxyConfig.Routes = config.HTTPConfig.ProxyConfig.Routes.map((r) => {
delete r._hasErrors;
return r;
});
return config;
}
function refresh() { function refresh() {
API.config.get().then((res) => { API.config.get().then((res) => {
setConfig(res.data); setConfig(res.data);
@ -85,6 +95,13 @@ const ProxyManagement = () => {
refresh(); refresh();
}, []); }, []);
const testRoute = (route) => {
try {
ValidateRoute.validateSync(route);
} catch (e) {
return e.errors;
}
}
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []); let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
return <div style={{ maxWidth: '1000px', margin: '' }}> return <div style={{ maxWidth: '1000px', margin: '' }}>
@ -92,7 +109,7 @@ const ProxyManagement = () => {
refresh(); refresh();
}}>Refresh</Button>&nbsp;&nbsp; }}>Refresh</Button>&nbsp;&nbsp;
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => { <Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
routes.push({ routes.unshift({
Name: 'New Route', Name: 'New Route',
Description: 'New Route', Description: 'New Route',
Mode: "SERVAPP", Mode: "SERVAPP",
@ -114,7 +131,7 @@ const ProxyManagement = () => {
{config && <> {config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} /> <RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routes && routes.map((route,key) => (<> {routes && routes.map((route,key) => (<>
<RouteManagement routeConfig={route} <RouteManagement key={key} routeConfig={route}
setRouteConfig={(newRoute) => { setRouteConfig={(newRoute) => {
routes[key] = newRoute; routes[key] = newRoute;
}} }}
@ -133,13 +150,30 @@ const ProxyManagement = () => {
</Grid> </Grid>
)} )}
<Grid item xs={12}> <Grid item xs={12}>
<Stack spacing={1}>
{submitErrors.map((err) => {
return <Alert severity="error">{err}</Alert>
})}
<AnimateButton> <AnimateButton>
<Button <Button
disableElevation disableElevation
disabled={false}
fullWidth fullWidth
onClick={() => { onClick={() => {
API.config.set(updateRoutes(routes)).then(() => { if(routes.some((route, key) => {
let errors = testRoute(route);
if (errors && errors.length > 0) {
errors = errors.map((err) => {
return `${route.Name}: ${err}`;
});
setSubmitErrors(errors);
return true;
}
})) {
return;
} else {
setSubmitErrors([]);
}
API.config.set(cleanRoutes(updateRoutes(routes))).then(() => {
setOpenModal(true); setOpenModal(true);
}).catch((err) => { }).catch((err) => {
console.log(err); console.log(err);
@ -154,6 +188,7 @@ const ProxyManagement = () => {
Save Save
</Button> </Button>
</AnimateButton> </AnimateButton>
</Stack>
</Grid> </Grid>
</MainCard> </MainCard>
} }

View file

@ -33,9 +33,35 @@ import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, Cos
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons'; import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
import { CosmosContainerPicker } from './containerPicker'; import { CosmosContainerPicker } from './containerPicker';
export const ValidateRoute = Yup.object().shape({
Name: Yup.string().required('Name is required'),
Mode: Yup.string().required('Mode is required'),
Target: Yup.string().required('Target is required').when('Mode', {
is: 'SERVAPP',
then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'),
}),
Host: Yup.string().when('UseHost', {
is: true,
then: Yup.string().required('Host is required')
.matches(/[\.|\:]/, 'Host must be full domain ([sub.]domain.com) or an IP')
}),
PathPrefix: Yup.string().when('UsePathPrefix', {
is: true,
then: Yup.string().required('Path Prefix is required').matches(/^\//, 'Path Prefix must start with / (e.g. /api). Do not include a domain/subdomain in it, use the Host for this.')
}),
UseHost: Yup.boolean().when('UsePathPrefix',
{
is: false,
then: Yup.boolean().oneOf([true], 'Source must at least be either Host or Path Prefix')
}),
})
const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => { const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => {
const [confirmDelete, setConfirmDelete] = React.useState(false); const [confirmDelete, setConfirmDelete] = React.useState(false);
return <div style={{ maxWidth: '1000px', margin: '' }}> return <div style={{ maxWidth: '1000px', margin: '' }}>
{routeConfig && <> {routeConfig && <>
<Formik <Formik
@ -54,15 +80,14 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
ThrottlePerMinute: routeConfig.ThrottlePerMinute, ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin, CORSOrigin: routeConfig.CORSOrigin,
}} }}
validationSchema={Yup.object().shape({ validateOnChange={false}
validationSchema={ValidateRoute}
})}
validate={(values) => {
setRouteConfig(values);
}}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false; return false;
}} }}
validate={(values) => {
//setRouteConfig(values);
}}
> >
{(formik) => ( {(formik) => (
<form noValidate onSubmit={formik.handleSubmit}> <form noValidate onSubmit={formik.handleSubmit}>
@ -76,6 +101,11 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
</div> </div>
}> }>
<Grid container spacing={2}> <Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<CosmosInputText <CosmosInputText
name="Name" name="Name"
@ -108,6 +138,7 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
["PROXY", "Proxy"], ["PROXY", "Proxy"],
["STATIC", "Static Folder"], ["STATIC", "Static Folder"],
["SPA", "Single Page Application"], ["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]} ]}
/> />

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.1.8", "version": "0.1.9",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {

View file

@ -70,7 +70,7 @@ Authentication is very hard (how do you check the password match? What encryptio
Installation is simple using Docker: Installation is simple using Docker:
``` ```
docker run -d -p 80:80 -p 443:443 --name cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest docker run -d -p 80:80 -p 443:443 --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest
``` ```
Once installed, simply go to `http://your-ip` and follow the instructions of the setup wizard. Once installed, simply go to `http://your-ip` and follow the instructions of the setup wizard.

View file

@ -63,21 +63,6 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
} }
destination = http.StripPrefix(route.PathPrefix, destination) destination = http.StripPrefix(route.PathPrefix, destination)
} }
timeout := route.Timeout
if(timeout == 0) {
timeout = 10000
}
throttlePerMinute := route.ThrottlePerMinute
throtthleTime := 1*time.Minute
// lets do something better later to disable throttle
if(throttlePerMinute == 0) {
throttlePerMinute = 99999999
throtthleTime = 1*time.Second
}
originCORS := route.CORSOrigin originCORS := route.CORSOrigin
@ -92,12 +77,18 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
if(route.UsePathPrefix && !route.StripPathPrefix && (route.Mode == "STATIC" || route.Mode == "SPA")) { if(route.UsePathPrefix && !route.StripPathPrefix && (route.Mode == "STATIC" || route.Mode == "SPA")) {
utils.Warn("PathPrefix is used, but StripPathPrefix is false. The route mode is " + (string)(route.Mode) + ". This will likely cause issues with the route. Ignore this warning if you know what you are doing.") utils.Warn("PathPrefix is used, but StripPathPrefix is false. The route mode is " + (string)(route.Mode) + ". This will likely cause issues with the route. Ignore this warning if you know what you are doing.")
} }
timeout := route.Timeout
if(timeout > 0) {
destination = utils.MiddlewareTimeout(timeout * time.Millisecond)(destination)
}
origin.Handler( throttlePerMinute := route.ThrottlePerMinute
tokenMiddleware(route.AuthEnabled)(
utils.CORSHeader(originCORS)( if(throttlePerMinute > 0) {
utils.MiddlewareTimeout(timeout * time.Millisecond)( throtthleTime := time.Minute
httprate.Limit(throttlePerMinute, throtthleTime, destination = httprate.Limit(throttlePerMinute, throtthleTime,
httprate.WithKeyFuncs(httprate.KeyByIP), httprate.WithKeyFuncs(httprate.KeyByIP),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil) utils.Error("Too many requests. Throttling", nil)
@ -105,7 +96,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
http.StatusTooManyRequests, "HTTP003") http.StatusTooManyRequests, "HTTP003")
return return
}), }),
)(destination))))) )(destination)
}
origin.Handler(tokenMiddleware(route.AuthEnabled)(utils.CORSHeader(originCORS)((destination))))
utils.Log("Added route: ["+ (string)(route.Mode) + "] " + route.Host + route.PathPrefix + " to " + route.Target + "") utils.Log("Added route: ["+ (string)(route.Mode) + "] " + route.Host + route.PathPrefix + " to " + route.Target + "")

View file

@ -22,6 +22,8 @@ func MiddlewareTimeout(timeout time.Duration) func(next http.Handler) http.Handl
} }
}() }()
w.Header().Set("X-Timeout-Duration", timeout.String())
r = r.WithContext(ctx) r = r.WithContext(ctx)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
} }