v0.2.0-unstable4
This commit is contained in:
parent
fd568de0d0
commit
9aa2bc48ea
|
@ -3,7 +3,7 @@ interface Route {
|
||||||
Name: string;
|
Name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete';
|
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete' | 'add';
|
||||||
|
|
||||||
function get() {
|
function get() {
|
||||||
return wrap(fetch('/cosmos/api/config', {
|
return wrap(fetch('/cosmos/api/config', {
|
||||||
|
@ -68,6 +68,10 @@ async function moveRouteDown(routeName: string): Promise<void> {
|
||||||
async function deleteRoute(routeName: string): Promise<void> {
|
async function deleteRoute(routeName: string): Promise<void> {
|
||||||
return rawUpdateRoute(routeName, 'delete');
|
return rawUpdateRoute(routeName, 'delete');
|
||||||
}
|
}
|
||||||
|
async function addRoute(newRoute: Route): Promise<void> {
|
||||||
|
return rawUpdateRoute("", 'add', newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
|
@ -77,4 +81,5 @@ export {
|
||||||
moveRouteUp,
|
moveRouteUp,
|
||||||
moveRouteDown,
|
moveRouteDown,
|
||||||
deleteRoute,
|
deleteRoute,
|
||||||
|
addRoute,
|
||||||
};
|
};
|
|
@ -35,7 +35,7 @@ const HostChip = ({route, settings}) => {
|
||||||
window.open(window.location.origin + route.PathPrefix, '_blank');
|
window.open(window.location.origin + route.PathPrefix, '_blank');
|
||||||
}}
|
}}
|
||||||
onDelete={settings ? () => {
|
onDelete={settings ? () => {
|
||||||
window.open('/ui/config-url#'+route.Name, '_blank');
|
window.open('/ui/config-url/'+route.Name, '_blank');
|
||||||
} : null}
|
} : null}
|
||||||
deleteIcon={settings ? <SettingOutlined /> : null}
|
deleteIcon={settings ? <SettingOutlined /> : null}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
|
||||||
import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
@ -61,6 +61,15 @@ export const RouteMode = ({route}) => {
|
||||||
|
|
||||||
export const RouteSecurity = ({route}) => {
|
export const RouteSecurity = ({route}) => {
|
||||||
return <div style={{fontWeight: 'bold', fontSize: '110%'}}>
|
return <div style={{fontWeight: 'bold', fontSize: '110%'}}>
|
||||||
|
<Tooltip title={route.SmartShield && route.SmartShield.Enabled ? "Smart Shield is enabled" : "Smart Shield is disabled"}>
|
||||||
|
<div style={{display: 'inline-block'}}>
|
||||||
|
{route.SmartShield && route.SmartShield.Enabled ?
|
||||||
|
<SafetyOutlined style={{color: 'green'}} /> :
|
||||||
|
<SafetyOutlined style={{color: 'red'}} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
|
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
|
||||||
<div style={{display: 'inline-block'}}>
|
<div style={{display: 'inline-block'}}>
|
||||||
{route.AuthEnabled ?
|
{route.AuthEnabled ?
|
||||||
|
|
|
@ -6,7 +6,7 @@ import TableContainer from '@mui/material/TableContainer';
|
||||||
import TableHead from '@mui/material/TableHead';
|
import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { Input, InputAdornment, Stack, TextField } from '@mui/material';
|
import { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -15,6 +15,12 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === 'dark';
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
const screenMin = {
|
||||||
|
xs: useMediaQuery((theme) => theme.breakpoints.up('xs')),
|
||||||
|
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
|
||||||
|
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
|
||||||
|
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing={2}>
|
<Stack direction="column" spacing={2}>
|
||||||
|
@ -34,11 +40,11 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableContainer style={{background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
|
<TableContainer style={{background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
|
||||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
<Table aria-label="simple table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell>{column.title}</TableCell>
|
(!column.screenMin || screenMin[column.screenMin]) && <TableCell>{column.title}</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
@ -73,15 +79,15 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
||||||
<TableCell
|
(!column.screenMin || screenMin[column.screenMin]) && <TableCell
|
||||||
component={(linkTo && !column.clickable) ? Link : 'td'}
|
component={(linkTo && !column.clickable) ? Link : 'td'}
|
||||||
to={linkTo(row, key)}
|
to={linkTo && linkTo(row, key)}
|
||||||
className={column.underline ? 'emphasis' : ''}
|
className={column.underline ? 'emphasis' : ''}
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
...column.style,
|
...column.style,
|
||||||
}}>
|
}}>
|
||||||
{column.field(row, key)}
|
{column.field(row, key)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import Back from "../../components/back";
|
import Back from "../../components/back";
|
||||||
import { CircularProgress, Stack } from "@mui/material";
|
import { Alert, CircularProgress, Stack } from "@mui/material";
|
||||||
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
|
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
|
||||||
import RouteManagement from "./routes/routeman";
|
import RouteManagement from "./routes/routeman";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
@ -35,7 +35,11 @@ const RouteConfigPage = () => {
|
||||||
<div>{routeName}</div>
|
<div>{routeName}</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{config && <PrettyTabbedView tabs={[
|
{config && !currentRoute && <div>
|
||||||
|
<Alert severity="error">Route not found</Alert>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{config && currentRoute && <PrettyTabbedView tabs={[
|
||||||
{
|
{
|
||||||
title: 'Overview',
|
title: 'Overview',
|
||||||
children: <RouteOverview routeConfig={currentRoute} />
|
children: <RouteOverview routeConfig={currentRoute} />
|
||||||
|
@ -46,6 +50,7 @@ const RouteConfigPage = () => {
|
||||||
title="Setup"
|
title="Setup"
|
||||||
submitButton
|
submitButton
|
||||||
routeConfig={currentRoute}
|
routeConfig={currentRoute}
|
||||||
|
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||||
/>
|
/>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
94
client/src/pages/config/routes/newRoute.jsx
Normal file
94
client/src/pages/config/routes/newRoute.jsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// material-ui
|
||||||
|
import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
|
||||||
|
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
|
||||||
|
import { Stack } from '@mui/system';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import IsLoggedIn from '../../../isLoggedIn';
|
||||||
|
import RestartModal from '../../config/users/restart';
|
||||||
|
import RouteManagement from '../../config/routes/routeman';
|
||||||
|
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../../utils/routes';
|
||||||
|
import HostChip from '../../../components/hostChip';
|
||||||
|
|
||||||
|
|
||||||
|
const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
||||||
|
const [openRestartModal, setOpenRestartModal] = useState(false);
|
||||||
|
const [submitErrors, setSubmitErrors] = useState([]);
|
||||||
|
const [newRoute, setNewRoute] = useState(null);
|
||||||
|
|
||||||
|
function addRoute() {
|
||||||
|
return API.config.addRoute(newRoute).then((res) => {
|
||||||
|
setOpenNewModal(false);
|
||||||
|
setOpenRestartModal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
|
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
|
||||||
|
<DialogTitle>New URL</DialogTitle>
|
||||||
|
{openNewModal && <>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<div>
|
||||||
|
<RouteManagement
|
||||||
|
routeConfig={{
|
||||||
|
Target: "",
|
||||||
|
Mode: "SERVAPP",
|
||||||
|
Name: "New Route",
|
||||||
|
Description: "New Route",
|
||||||
|
UseHost: false,
|
||||||
|
Host: "",
|
||||||
|
UsePathPrefix: false,
|
||||||
|
PathPrefix: '',
|
||||||
|
CORSOrigin: '',
|
||||||
|
StripPathPrefix: false,
|
||||||
|
AuthEnabled: false,
|
||||||
|
Timeout: 14400000,
|
||||||
|
ThrottlePerMinute: 10000,
|
||||||
|
SmartShield: {
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||||
|
setRouteConfig={(_newRoute) => {
|
||||||
|
setNewRoute(sanitizeRoute(_newRoute));
|
||||||
|
}}
|
||||||
|
up={() => {}}
|
||||||
|
down={() => {}}
|
||||||
|
deleteRoute={() => {}}
|
||||||
|
noControls
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
|
||||||
|
<Alert severity="error">{submitErrors.map((err) => {
|
||||||
|
return <div>{err}</div>
|
||||||
|
})}</Alert>
|
||||||
|
</Stack>}
|
||||||
|
<Button onClick={() => setOpenNewModal(false)}>Cancel</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
let errors = ValidateRoute(newRoute, config);
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
setSubmitErrors(errors);
|
||||||
|
} else {
|
||||||
|
setSubmitErrors([]);
|
||||||
|
addRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
}}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>}
|
||||||
|
</Dialog>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewRouteCreate;
|
|
@ -23,20 +23,24 @@ const RouteSecurity = ({ routeConfig }) => {
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
AuthEnabled: routeConfig.AuthEnabled,
|
AuthEnabled: routeConfig.AuthEnabled,
|
||||||
Host: routeConfig.Host,
|
|
||||||
UsePathPrefix: routeConfig.UsePathPrefix,
|
|
||||||
PathPrefix: routeConfig.PathPrefix,
|
|
||||||
StripPathPrefix: routeConfig.StripPathPrefix,
|
|
||||||
Timeout: routeConfig.Timeout,
|
Timeout: routeConfig.Timeout,
|
||||||
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
|
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
|
||||||
CORSOrigin: routeConfig.CORSOrigin,
|
CORSOrigin: routeConfig.CORSOrigin,
|
||||||
MaxBandwith: routeConfig.MaxBandwith,
|
MaxBandwith: routeConfig.MaxBandwith,
|
||||||
|
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||||
}}
|
}}
|
||||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
const fullValues = {
|
const fullValues = {
|
||||||
...routeConfig,
|
...routeConfig,
|
||||||
...values,
|
...values,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!fullValues.SmartShield) {
|
||||||
|
fullValues.SmartShield = {};
|
||||||
|
}
|
||||||
|
fullValues.SmartShield.Enabled = values._SmartShield_Enabled;
|
||||||
|
delete fullValues._SmartShield_Enabled;
|
||||||
|
|
||||||
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
||||||
if (res.status == "OK") {
|
if (res.status == "OK") {
|
||||||
setStatus({ success: true });
|
setStatus({ success: true });
|
||||||
|
@ -66,6 +70,12 @@ const RouteSecurity = ({ routeConfig }) => {
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="_SmartShield_Enabled"
|
||||||
|
label="Smart Shield Protection"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
<CosmosInputText
|
<CosmosInputText
|
||||||
name="Timeout"
|
name="Timeout"
|
||||||
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
||||||
|
|
|
@ -14,32 +14,7 @@ import RestartModal from '../users/restart';
|
||||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
||||||
import { CosmosContainerPicker } from '../users/containerPicker';
|
import { CosmosContainerPicker } from '../users/containerPicker';
|
||||||
import { snackit } from '../../../api/wrap';
|
import { snackit } from '../../../api/wrap';
|
||||||
|
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
|
||||||
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 Hide = ({ children, h }) => {
|
const Hide = ({ children, h }) => {
|
||||||
return h ? <div style={{ display: 'none' }}>
|
return h ? <div style={{ display: 'none' }}>
|
||||||
|
@ -47,12 +22,21 @@ const Hide = ({ children, h }) => {
|
||||||
</div> : <>{children}</>
|
</div> : <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false }) => {
|
const debounce = (func, wait) => {
|
||||||
const [openModal, setOpenModal] = React.useState(false);
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
const context = this;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
|
||||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
{routeConfig && <>
|
{routeConfig && <>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
@ -62,17 +46,32 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
||||||
Target: routeConfig.Target,
|
Target: routeConfig.Target,
|
||||||
UseHost: routeConfig.UseHost,
|
UseHost: routeConfig.UseHost,
|
||||||
Host: routeConfig.Host,
|
Host: routeConfig.Host,
|
||||||
|
UsePathPrefix: routeConfig.UsePathPrefix,
|
||||||
|
PathPrefix: routeConfig.PathPrefix,
|
||||||
|
StripPathPrefix: routeConfig.StripPathPrefix,
|
||||||
|
AuthEnabled: routeConfig.AuthEnabled,
|
||||||
|
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||||
}}
|
}}
|
||||||
validationSchema={ValidateRoute}
|
validationSchema={ValidateRouteSchema}
|
||||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
if(!submitButton) {
|
if(!submitButton) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
const fullValues = {
|
let fullValues = {
|
||||||
...routeConfig,
|
...routeConfig,
|
||||||
...values,
|
...values,
|
||||||
}
|
}
|
||||||
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
|
||||||
|
fullValues = sanitizeRoute(fullValues);
|
||||||
|
|
||||||
|
let op;
|
||||||
|
if(newRoute) {
|
||||||
|
op = API.config.newRoute(routeConfig.Name, fullValues)
|
||||||
|
} else {
|
||||||
|
op = API.config.replaceRoute(routeConfig.Name, fullValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
op.then((res) => {
|
||||||
if (res.status == "OK") {
|
if (res.status == "OK") {
|
||||||
setStatus({ success: true });
|
setStatus({ success: true });
|
||||||
snackit('Route updated successfully', 'success')
|
snackit('Route updated successfully', 'success')
|
||||||
|
@ -87,7 +86,17 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
validate={(values) => {
|
validate={(values) => {
|
||||||
setRouteConfig && setRouteConfig(values);
|
let fullValues = {
|
||||||
|
...routeConfig,
|
||||||
|
...values,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check name is unique
|
||||||
|
if (newRoute && routeNames.includes(fullValues.Name)) {
|
||||||
|
return { Name: 'Name must be unique' }
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouteConfig && debounce(() => setRouteConfig(fullValues), 500)();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
|
@ -198,6 +207,20 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
||||||
formik={formik}
|
formik={formik}
|
||||||
style={{ paddingLeft: '20px' }}
|
style={{ paddingLeft: '20px' }}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
|
<CosmosFormDivider title={'Basic Security'} />
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="AuthEnabled"
|
||||||
|
label="Authentication Required"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="_SmartShield_Enabled"
|
||||||
|
label="Smart Shield Protection"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</MainCard>
|
</MainCard>
|
||||||
{submitButton && <MainCard ><Button
|
{submitButton && <MainCard ><Button
|
||||||
|
|
|
@ -1,25 +1,39 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MainCard from '../../../components/MainCard';
|
import MainCard from '../../../components/MainCard';
|
||||||
import RestartModal from '../users/restart';
|
import RestartModal from '../users/restart';
|
||||||
import { Chip, Stack, useMediaQuery } from '@mui/material';
|
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||||
import HostChip from '../../../components/hostChip';
|
import HostChip from '../../../components/hostChip';
|
||||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||||
import { getFaviconURL } from '../../../utils/routes';
|
import { getFaviconURL } from '../../../utils/routes';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
const RouteOverview = ({ routeConfig }) => {
|
const RouteOverview = ({ routeConfig }) => {
|
||||||
const [openModal, setOpenModal] = React.useState(false);
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||||
|
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||||
|
|
||||||
|
function deleteRoute(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
API.config.deleteRoute(routeConfig.Name).then(() => {
|
||||||
|
setOpenModal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return <div style={{ maxWidth: '1000px', width: '100%'}}>
|
return <div style={{ maxWidth: '1000px', width: '100%'}}>
|
||||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
{routeConfig && <>
|
{routeConfig && <>
|
||||||
<MainCard name={routeConfig.Name} title={routeConfig.Name}>
|
<MainCard name={routeConfig.Name} title={<div>
|
||||||
|
{routeConfig.Name}
|
||||||
|
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
||||||
|
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
|
||||||
|
</div>}>
|
||||||
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||||
<div>
|
<div>
|
||||||
<img src={getFaviconURL(routeConfig)} width="128px" />
|
<img src={getFaviconURL(routeConfig)} width="128px" />
|
||||||
</div>
|
</div>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2} >
|
||||||
<strong>Description</strong>
|
<strong>Description</strong>
|
||||||
<div>{routeConfig.Description}</div>
|
<div>{routeConfig.Description}</div>
|
||||||
<strong>URL</strong>
|
<strong>URL</strong>
|
||||||
|
|
|
@ -31,13 +31,14 @@ 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, {ValidateRoute} from '../routes/routeman';
|
import RouteManagement from '../routes/routeman';
|
||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import { getFaviconURL, sanitizeRoute } from '../../../utils/routes';
|
import { getFaviconURL, sanitizeRoute, ValidateRoute } from '../../../utils/routes';
|
||||||
import PrettyTableView from '../../../components/tableView/prettyTableView';
|
import PrettyTableView from '../../../components/tableView/prettyTableView';
|
||||||
import HostChip from '../../../components/hostChip';
|
import HostChip from '../../../components/hostChip';
|
||||||
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
|
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import NewRouteCreate from '../routes/newRoute';
|
||||||
|
|
||||||
const stickyButton = {
|
const stickyButton = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
@ -62,7 +63,7 @@ const ProxyManagement = () => {
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
const [submitErrors, setSubmitErrors] = React.useState([]);
|
const [submitErrors, setSubmitErrors] = React.useState([]);
|
||||||
const [needSave, setNeedSave] = React.useState(false);
|
const [needSave, setNeedSave] = React.useState(false);
|
||||||
const navigate = useNavigate();
|
const [openNewModal, setOpenNewModal] = React.useState(false);
|
||||||
|
|
||||||
function updateRoutes(routes) {
|
function updateRoutes(routes) {
|
||||||
let con = {
|
let con = {
|
||||||
|
@ -129,43 +130,22 @@ 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={{ }}>
|
return <div style={{ }}>
|
||||||
<IsLoggedIn />
|
<IsLoggedIn />
|
||||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
|
||||||
refresh();
|
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||||
}}>Refresh</Button>
|
refresh();
|
||||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
}}>Refresh</Button>
|
||||||
routes.unshift({
|
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||||
Name: 'New URL',
|
setOpenNewModal(true);
|
||||||
Description: 'New URL',
|
}}>Create</Button>
|
||||||
Mode: "SERVAPP",
|
</Stack>
|
||||||
UseHost: false,
|
|
||||||
Host: '',
|
|
||||||
UsePathPrefix: false,
|
|
||||||
PathPrefix: '',
|
|
||||||
Timeout: 30000,
|
|
||||||
ThrottlePerMinute: 0,
|
|
||||||
CORSOrigin: '',
|
|
||||||
StripPathPrefix: false,
|
|
||||||
AuthEnabled: false,
|
|
||||||
});
|
|
||||||
updateRoutes(routes);
|
|
||||||
setNeedSave(true);
|
|
||||||
}}>Create</Button>
|
|
||||||
|
|
||||||
<br /><br />
|
|
||||||
|
|
||||||
{config && <>
|
{config && <>
|
||||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
<NewRouteCreate openNewModal={openNewModal} setOpenNewModal={setOpenNewModal} config={config}/>
|
||||||
|
|
||||||
{routes && <PrettyTableView
|
{routes && <PrettyTableView
|
||||||
data={routes}
|
data={routes}
|
||||||
|
@ -190,9 +170,9 @@ const ProxyManagement = () => {
|
||||||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
{ title: 'Origin', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
|
{ title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
|
||||||
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
{ title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
||||||
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
|
{ title: 'Security', screenMin: 'lg', field: (r) => <RouteSecurity route={r} />,
|
||||||
style: {minWidth: '70px'} },
|
style: {minWidth: '70px'} },
|
||||||
{ title: '', clickable:true, field: (r, k) => <RouteActions
|
{ title: '', clickable:true, field: (r, k) => <RouteActions
|
||||||
route={r}
|
route={r}
|
||||||
|
@ -213,19 +193,6 @@ const ProxyManagement = () => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* {routes && routes.map((route,key) => (<>
|
|
||||||
<RouteManagement key={route.Name} routeConfig={route}
|
|
||||||
setRouteConfig={(newRoute) => {
|
|
||||||
routes[key] = sanitizeRoute(newRoute);
|
|
||||||
setNeedSave(true);
|
|
||||||
}}
|
|
||||||
up={() => up(key)}
|
|
||||||
down={() => down(key)}
|
|
||||||
deleteRoute={() => deleteRoute(key)}
|
|
||||||
/>
|
|
||||||
<br /><br />
|
|
||||||
</>))} */}
|
|
||||||
|
|
||||||
{routes && needSave && <>
|
{routes && needSave && <>
|
||||||
<div>
|
<div>
|
||||||
<br /><br /><br /><br />
|
<br /><br /><br /><br />
|
||||||
|
@ -249,7 +216,7 @@ const ProxyManagement = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(routes.some((route, key) => {
|
if(routes.some((route, key) => {
|
||||||
let errors = testRoute(route);
|
let errors = ValidateRoute(route, config);
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
errors = errors.map((err) => {
|
errors = errors.map((err) => {
|
||||||
return `${route.Name}: ${err}`;
|
return `${route.Name}: ${err}`;
|
||||||
|
|
|
@ -64,7 +64,7 @@ const RestartModal = ({openModal, setOpenModal}) => {
|
||||||
}, 1500)
|
}, 1500)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setWarn(true);
|
setWarn(true);
|
||||||
}, 8000)
|
}, 20000)
|
||||||
}}>Restart</Button>
|
}}>Restart</Button>
|
||||||
</DialogActions>}
|
</DialogActions>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import * as API from '../../../api';
|
||||||
import MainCard from '../../../components/MainCard';
|
import MainCard from '../../../components/MainCard';
|
||||||
import IsLoggedIn from '../../../isLoggedIn';
|
import IsLoggedIn from '../../../isLoggedIn';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import PrettyTableView from '../../../components/tableView/prettyTableView';
|
||||||
|
|
||||||
const UserManagement = () => {
|
const UserManagement = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
@ -32,7 +33,7 @@ const UserManagement = () => {
|
||||||
|
|
||||||
const roles = ['Guest', 'User', 'Admin']
|
const roles = ['Guest', 'User', 'Admin']
|
||||||
|
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState(null);
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -143,97 +144,99 @@ const UserManagement = () => {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<MainCard title="Users">
|
|
||||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||||
refresh();
|
refresh();
|
||||||
}}>Refresh</Button>
|
}}>Refresh</Button>
|
||||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||||
setOpenCreateForm(true)
|
setOpenCreateForm(true)
|
||||||
}}>Create</Button><br /><br />
|
}}>Create</Button><br /><br />
|
||||||
{isLoading ? <center><br /><CircularProgress color="inherit" /></center>
|
|
||||||
: <TableContainer component={Paper}>
|
|
||||||
<Table aria-label="simple table">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Nickname</TableCell>
|
|
||||||
<TableCell>Status</TableCell>
|
|
||||||
<TableCell>Created At</TableCell>
|
|
||||||
<TableCell>Last Login</TableCell>
|
|
||||||
<TableCell>Actions</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{rows.map((row) => {
|
|
||||||
const isRegistered = new Date(row.registeredAt).getTime() > 0;
|
|
||||||
const inviteExpired = new Date(row.registerKeyExp).getTime() < new Date().getTime();
|
|
||||||
|
|
||||||
const hasLastLogin = new Date(row.lastLogin).getTime() > 0;
|
|
||||||
|
|
||||||
return (
|
{isLoading && <center><br /><CircularProgress /></center>}
|
||||||
<TableRow
|
|
||||||
key={row.nickname}
|
{!isLoading && rows && (<PrettyTableView
|
||||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
data={rows}
|
||||||
>
|
getKey={(r) => r.nickname}
|
||||||
<TableCell component="th" scope="row">
|
columns={[
|
||||||
<strong>{row.nickname}</strong>
|
{
|
||||||
</TableCell>
|
title: 'User',
|
||||||
<TableCell>
|
// underline: true,
|
||||||
{isRegistered ? (row.role > 1 ? <Chip
|
field: (r) => <strong>{r.nickname}</strong>,
|
||||||
icon={<KeyOutlined />}
|
},
|
||||||
label="Admin"
|
{
|
||||||
variant="outlined"
|
title: 'Status',
|
||||||
/> : <Chip
|
screenMin: 'sm',
|
||||||
icon={<UserOutlined />}
|
field: (r) => {
|
||||||
label="User"
|
const isRegistered = new Date(r.registeredAt).getTime() > 0;
|
||||||
variant="outlined"
|
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
|
||||||
/>) : (
|
|
||||||
inviteExpired ? <Chip
|
return <>{isRegistered ? (r.role > 1 ? <Chip
|
||||||
icon={<ExclamationCircleOutlined />}
|
icon={<KeyOutlined />}
|
||||||
label="Invite Expired"
|
label="Admin"
|
||||||
color="error"
|
/> : <Chip
|
||||||
/> : <Chip
|
icon={<UserOutlined />}
|
||||||
icon={<WarningOutlined />}
|
label="User"
|
||||||
label="Invite Pending"
|
/>) : (
|
||||||
color="warning"
|
inviteExpired ? <Chip
|
||||||
/>
|
icon={<ExclamationCircleOutlined />}
|
||||||
)}
|
label="Invite Expired"
|
||||||
</TableCell>
|
color="error"
|
||||||
<TableCell>
|
/> : <Chip
|
||||||
{new Date(row.createdAt).toLocaleDateString()} -
|
icon={<WarningOutlined />}
|
||||||
{new Date(row.createdAt).toLocaleTimeString()}
|
label="Invite Pending"
|
||||||
</TableCell>
|
color="warning"
|
||||||
<TableCell>
|
/>
|
||||||
{hasLastLogin ? <span>
|
)}</>
|
||||||
{new Date(row.lastLogin).toLocaleDateString()} -
|
}
|
||||||
{new Date(row.lastLogin).toLocaleTimeString()}
|
},
|
||||||
</span> : '-'}
|
{
|
||||||
</TableCell>
|
title: 'Email',
|
||||||
<TableCell>
|
screenMin: 'md',
|
||||||
{isRegistered ?
|
field: (r) => r.email,
|
||||||
(<Button variant="contained" color="primary" onClick={
|
},
|
||||||
() => {
|
{
|
||||||
sendlink(row.nickname, 1);
|
title: 'Created At',
|
||||||
}
|
screenMin: 'lg',
|
||||||
}>Send password reset</Button>) :
|
field: (r) => new Date(r.createdAt).toLocaleString(),
|
||||||
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
|
},
|
||||||
() => {
|
{
|
||||||
sendlink(row.nickname, 2);
|
title: 'Last Login',
|
||||||
}
|
screenMin: 'lg',
|
||||||
} color="primary">Re-Send Invite</Button>)
|
field: (r) => {
|
||||||
}
|
const hasLastLogin = new Date(r.lastLogin).getTime() > 0;
|
||||||
<Button variant="contained" color="error" onClick={
|
return <>{hasLastLogin ? new Date(r.lastLogin).toLocaleString() : 'Never'}</>
|
||||||
() => {
|
},
|
||||||
setToAction(row.nickname);
|
},
|
||||||
setOpenDeleteForm(true);
|
{
|
||||||
}
|
title: '',
|
||||||
}>Delete</Button></TableCell>
|
clickable: true,
|
||||||
</TableRow>
|
field: (r) => {
|
||||||
)
|
const isRegistered = new Date(r.registeredAt).getTime() > 0;
|
||||||
})}
|
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
|
||||||
</TableBody>
|
|
||||||
</Table>
|
return <>{isRegistered ?
|
||||||
</TableContainer>}
|
(<Button variant="contained" color="primary" onClick={
|
||||||
</MainCard>
|
() => {
|
||||||
|
sendlink(r.nickname, 1);
|
||||||
|
}
|
||||||
|
}>Send password reset</Button>) :
|
||||||
|
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
|
||||||
|
() => {
|
||||||
|
sendlink(r.nickname, 2);
|
||||||
|
}
|
||||||
|
} color="primary">Re-Send Invite</Button>)
|
||||||
|
}
|
||||||
|
<Button variant="contained" color="error" onClick={
|
||||||
|
() => {
|
||||||
|
setToAction(r.nickname);
|
||||||
|
setOpenDeleteForm(true);
|
||||||
|
}
|
||||||
|
}>Delete</Button></>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>)}
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { styled } from '@mui/material/styles';
|
||||||
import * as API from '../../api';
|
import * as API from '../../api';
|
||||||
import IsLoggedIn from '../../isLoggedIn';
|
import IsLoggedIn from '../../isLoggedIn';
|
||||||
import RestartModal from '../config/users/restart';
|
import RestartModal from '../config/users/restart';
|
||||||
import RouteManagement, { ValidateRoute } from '../config/routes/routeman';
|
import RouteManagement from '../config/routes/routeman';
|
||||||
import { getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
||||||
import HostChip from '../../components/hostChip';
|
import HostChip from '../../components/hostChip';
|
||||||
|
|
||||||
const Item = styled(Paper)(({ theme }) => ({
|
const Item = styled(Paper)(({ theme }) => ({
|
||||||
|
@ -49,14 +49,6 @@ const ServeApps = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const testRoute = (route) => {
|
|
||||||
try {
|
|
||||||
ValidateRoute.validateSync(route);
|
|
||||||
} catch (e) {
|
|
||||||
return e.errors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshServeApps = () => {
|
const refreshServeApps = () => {
|
||||||
API.docker.list().then((res) => {
|
API.docker.list().then((res) => {
|
||||||
setServeApps(res.data);
|
setServeApps(res.data);
|
||||||
|
@ -97,8 +89,8 @@ const ServeApps = () => {
|
||||||
ProxyConfig: {
|
ProxyConfig: {
|
||||||
...config.HTTPConfig.ProxyConfig,
|
...config.HTTPConfig.ProxyConfig,
|
||||||
Routes: [
|
Routes: [
|
||||||
...config.HTTPConfig.ProxyConfig.Routes,
|
|
||||||
newRoute,
|
newRoute,
|
||||||
|
...config.HTTPConfig.ProxyConfig.Routes,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -109,6 +101,7 @@ const ServeApps = () => {
|
||||||
setOpenRestartModal(true);
|
setOpenRestartModal(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridAnim = {
|
const gridAnim = {
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
@ -125,7 +118,6 @@ const ServeApps = () => {
|
||||||
|
|
||||||
const getFirstRouteFavIcon = (app) => {
|
const getFirstRouteFavIcon = (app) => {
|
||||||
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
||||||
console.log(routes)
|
|
||||||
if(routes.length > 0) {
|
if(routes.length > 0) {
|
||||||
let url = getFaviconURL(routes[0]);
|
let url = getFaviconURL(routes[0]);
|
||||||
return url;
|
return url;
|
||||||
|
@ -160,12 +152,16 @@ const ServeApps = () => {
|
||||||
Host: getHostnameFromName(openModal.Names[0]),
|
Host: getHostnameFromName(openModal.Names[0]),
|
||||||
UsePathPrefix: false,
|
UsePathPrefix: false,
|
||||||
PathPrefix: '',
|
PathPrefix: '',
|
||||||
Timeout: 30000,
|
|
||||||
ThrottlePerMinute: 0,
|
|
||||||
CORSOrigin: '',
|
CORSOrigin: '',
|
||||||
StripPathPrefix: false,
|
StripPathPrefix: false,
|
||||||
AuthEnabled: false,
|
AuthEnabled: false,
|
||||||
|
Timeout: 14400000,
|
||||||
|
ThrottlePerMinute: 10000,
|
||||||
|
SmartShield: {
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||||
setRouteConfig={(_newRoute) => {
|
setRouteConfig={(_newRoute) => {
|
||||||
setNewRoute(sanitizeRoute(_newRoute));
|
setNewRoute(sanitizeRoute(_newRoute));
|
||||||
}}
|
}}
|
||||||
|
@ -187,7 +183,7 @@ const ServeApps = () => {
|
||||||
</Stack>}
|
</Stack>}
|
||||||
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
let errors = testRoute(newRoute);
|
let errors = ValidateRoute(newRoute, config);
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
errors = errors.map((err) => {
|
errors = errors.map((err) => {
|
||||||
return `${err}`;
|
return `${err}`;
|
||||||
|
@ -305,7 +301,7 @@ const ServeApps = () => {
|
||||||
<Typography variant="h6" color="text.secondary">
|
<Typography variant="h6" color="text.secondary">
|
||||||
URLs
|
URLs
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={2} direction="row">
|
<Stack style={noOver} spacing={2} direction="row">
|
||||||
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
||||||
return <HostChip route={route} settings/>
|
return <HostChip route={route} settings/>
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
import Folder from '../assets/images/icons/folder(1).svg';
|
import Folder from '../assets/images/icons/folder(1).svg';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
export const sanitizeRoute = (_route) => {
|
||||||
|
let route = {..._route};
|
||||||
|
|
||||||
export const sanitizeRoute = (route) => {
|
|
||||||
if (!route.UseHost) {
|
if (!route.UseHost) {
|
||||||
route.Host = "";
|
route.Host = "";
|
||||||
}
|
}
|
||||||
if (!route.UsePathPrefix) {
|
if (!route.UsePathPrefix) {
|
||||||
route.PathPrefix = "";
|
route.PathPrefix = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
route.Name = route.Name.trim();
|
||||||
|
|
||||||
|
if(!route.SmartShield) {
|
||||||
|
route.SmartShield = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof route._SmartShield_Enabled !== "undefined") {
|
||||||
|
route.SmartShield.Enabled = route._SmartShield_Enabled;
|
||||||
|
delete route._SmartShield_Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,4 +51,44 @@ export const getFaviconURL = (route) => {
|
||||||
} else {
|
} else {
|
||||||
return addRemote(addProtocol(getOrigin(route)));
|
return addRemote(addProtocol(getOrigin(route)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidateRouteSchema = 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')
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ValidateRoute = (routeConfig, config) => {
|
||||||
|
let routeNames= config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ValidateRouteSchema.validateSync(routeConfig);
|
||||||
|
} catch (e) {
|
||||||
|
return e.errors;
|
||||||
|
}
|
||||||
|
if (routeNames.includes(routeConfig.Name)) {
|
||||||
|
return ['Route Name already exists. Name must be unique.'];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.2.0-unstable3",
|
"version": "0.2.0-unstable4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -36,42 +36,57 @@ func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
||||||
routes := config.HTTPConfig.ProxyConfig.Routes
|
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||||
routeIndex := -1
|
routeIndex := -1
|
||||||
|
|
||||||
for i, route := range routes {
|
if updateReq.Operation != "add" {
|
||||||
if route.Name == updateReq.RouteName {
|
if updateReq.RouteName == "" {
|
||||||
routeIndex = i
|
utils.Error("SettingsUpdate: RouteName must be provided", nil)
|
||||||
break
|
utils.HTTPError(w, "RouteName must be provided", http.StatusBadRequest, "UR002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range routes {
|
||||||
|
if route.Name == updateReq.RouteName {
|
||||||
|
routeIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if routeIndex == -1 {
|
||||||
|
utils.Error("SettingsUpdate: Route not found: "+updateReq.RouteName, nil)
|
||||||
|
utils.HTTPError(w, "Route not found", http.StatusNotFound, "UR002")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if routeIndex == -1 {
|
|
||||||
utils.Error("SettingsUpdate: Route not found: "+updateReq.RouteName, nil)
|
|
||||||
utils.HTTPError(w, "Route not found", http.StatusNotFound, "UR002")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch updateReq.Operation {
|
switch updateReq.Operation {
|
||||||
case "replace":
|
case "replace":
|
||||||
if updateReq.NewRoute == nil {
|
if updateReq.NewRoute == nil {
|
||||||
utils.Error("SettingsUpdate: NewRoute must be provided for replace operation", nil)
|
utils.Error("SettingsUpdate: NewRoute must be provided for replace operation", nil)
|
||||||
utils.HTTPError(w, "NewRoute must be provided for replace operation", http.StatusBadRequest, "UR003")
|
utils.HTTPError(w, "NewRoute must be provided for replace operation", http.StatusBadRequest, "UR003")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
routes[routeIndex] = *updateReq.NewRoute
|
||||||
|
case "move_up":
|
||||||
|
if routeIndex > 0 {
|
||||||
|
routes[routeIndex-1], routes[routeIndex] = routes[routeIndex], routes[routeIndex-1]
|
||||||
|
}
|
||||||
|
case "move_down":
|
||||||
|
if routeIndex < len(routes)-1 {
|
||||||
|
routes[routeIndex+1], routes[routeIndex] = routes[routeIndex], routes[routeIndex+1]
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
routes = append(routes[:routeIndex], routes[routeIndex+1:]...)
|
||||||
|
case "add":
|
||||||
|
if updateReq.NewRoute == nil {
|
||||||
|
utils.Error("SettingsUpdate: NewRoute must be provided for add operation", nil)
|
||||||
|
utils.HTTPError(w, "NewRoute must be provided for add operation", http.StatusBadRequest, "UR003")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
routes = append([]utils.ProxyRouteConfig{*updateReq.NewRoute}, routes...)
|
||||||
|
default:
|
||||||
|
utils.Error("SettingsUpdate: Unsupported operation: "+updateReq.Operation, nil)
|
||||||
|
utils.HTTPError(w, "Unsupported operation", http.StatusBadRequest, "UR004")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routes[routeIndex] = *updateReq.NewRoute
|
|
||||||
case "move_up":
|
|
||||||
if routeIndex > 0 {
|
|
||||||
routes[routeIndex-1], routes[routeIndex] = routes[routeIndex], routes[routeIndex-1]
|
|
||||||
}
|
|
||||||
case "move_down":
|
|
||||||
if routeIndex < len(routes)-1 {
|
|
||||||
routes[routeIndex+1], routes[routeIndex] = routes[routeIndex], routes[routeIndex+1]
|
|
||||||
}
|
|
||||||
case "delete":
|
|
||||||
routes = append(routes[:routeIndex], routes[routeIndex+1:]...)
|
|
||||||
default:
|
|
||||||
utils.Error("SettingsUpdate: Unsupported operation: "+updateReq.Operation, nil)
|
|
||||||
utils.HTTPError(w, "Unsupported operation", http.StatusBadRequest, "UR004")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.HTTPConfig.ProxyConfig.Routes = routes
|
config.HTTPConfig.ProxyConfig.Routes = routes
|
||||||
utils.SaveConfigTofile(config)
|
utils.SaveConfigTofile(config)
|
||||||
|
|
Loading…
Reference in a new issue