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

View file

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

View file

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

View file

@ -33,9 +33,35 @@ import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, Cos
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
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 [confirmDelete, setConfirmDelete] = React.useState(false);
return <div style={{ maxWidth: '1000px', margin: '' }}>
{routeConfig && <>
<Formik
@ -54,15 +80,14 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin,
}}
validationSchema={Yup.object().shape({
})}
validate={(values) => {
setRouteConfig(values);
}}
validateOnChange={false}
validationSchema={ValidateRoute}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false;
}}
validate={(values) => {
//setRouteConfig(values);
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
@ -76,6 +101,11 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
</div>
}>
<Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<CosmosInputText
name="Name"
@ -108,6 +138,7 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockT
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.1.8",
"version": "0.1.9",
"description": "",
"main": "test-server.js",
"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:
```
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.

View file

@ -63,21 +63,6 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
}
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
@ -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")) {
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(
tokenMiddleware(route.AuthEnabled)(
utils.CORSHeader(originCORS)(
utils.MiddlewareTimeout(timeout * time.Millisecond)(
httprate.Limit(throttlePerMinute, throtthleTime,
throttlePerMinute := route.ThrottlePerMinute
if(throttlePerMinute > 0) {
throtthleTime := time.Minute
destination = httprate.Limit(throttlePerMinute, throtthleTime,
httprate.WithKeyFuncs(httprate.KeyByIP),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil)
@ -105,7 +96,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
http.StatusTooManyRequests, "HTTP003")
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 + "")

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)
next.ServeHTTP(w, r)
}