v0.1.9 Various bug fixes and UI improvements to route form
This commit is contained in:
parent
bc5743fa05
commit
2eac6fbd3a
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
]}
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 + "")
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue