v0.2.0-unstable
new UI
This commit is contained in:
parent
963a1c7699
commit
1ad6edf50a
|
@ -13,9 +13,11 @@ import { setSnackit } from './api/wrap';
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [message, setMessage] = React.useState('');
|
const [message, setMessage] = React.useState('');
|
||||||
setSnackit((message) => {
|
const [severity, setSeverity] = React.useState('error');
|
||||||
|
setSnackit((message, severity='error') => {
|
||||||
setMessage(message);
|
setMessage(message);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
setSeverity(severity);
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<ThemeCustomization>
|
<ThemeCustomization>
|
||||||
|
@ -25,7 +27,7 @@ const App = () => {
|
||||||
onClose={() => {setOpen(false)}}
|
onClose={() => {setOpen(false)}}
|
||||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||||
>
|
>
|
||||||
<Alert className={open ? 'shake' : ''} severity="error" sx={{ width: '100%' }}>
|
<Alert className={(open && severity == "error") ? 'shake' : ''} severity={severity} sx={{ width: '100%' }}>
|
||||||
{message}
|
{message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import wrap from './wrap';
|
|
||||||
|
|
||||||
function get() {
|
|
||||||
return wrap(fetch('/cosmos/api/config', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function set(values) {
|
|
||||||
return wrap(fetch('/cosmos/api/config', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function restart() {
|
|
||||||
return wrap(fetch('/cosmos/api/restart', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
restart
|
|
||||||
};
|
|
80
client/src/api/config.ts
Normal file
80
client/src/api/config.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import wrap from './wrap';
|
||||||
|
interface Route {
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete';
|
||||||
|
|
||||||
|
function get() {
|
||||||
|
return wrap(fetch('/cosmos/api/config', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(values) {
|
||||||
|
return wrap(fetch('/cosmos/api/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
return fetch('/cosmos/api/restart', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rawUpdateRoute(routeName: string, operation: Operation, newRoute?: Route): Promise<void> {
|
||||||
|
const payload = {
|
||||||
|
routeName,
|
||||||
|
operation,
|
||||||
|
newRoute,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (operation === 'replace') {
|
||||||
|
if (!newRoute) throw new Error('newRoute must be provided for replace operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap(fetch('/cosmos/api/config', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceRoute(routeName: string, newRoute: Route): Promise<void> {
|
||||||
|
return rawUpdateRoute(routeName, 'replace', newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveRouteUp(routeName: string): Promise<void> {
|
||||||
|
return rawUpdateRoute(routeName, 'move_up');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveRouteDown(routeName: string): Promise<void> {
|
||||||
|
return rawUpdateRoute(routeName, 'move_down');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRoute(routeName: string): Promise<void> {
|
||||||
|
return rawUpdateRoute(routeName, 'delete');
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
restart,
|
||||||
|
rawUpdateRoute,
|
||||||
|
replaceRoute,
|
||||||
|
moveRouteUp,
|
||||||
|
moveRouteDown,
|
||||||
|
deleteRoute,
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import * as auth from './authentication.jsx';
|
import * as auth from './authentication';
|
||||||
import * as users from './users.jsx';
|
import * as users from './users';
|
||||||
import * as config from './config.jsx';
|
import * as config from './config';
|
||||||
import * as docker from './docker.jsx';
|
import * as docker from './docker';
|
||||||
|
|
||||||
import wrap from './wrap';
|
import wrap from './wrap';
|
||||||
|
|
||||||
|
@ -14,6 +14,28 @@ const getStatus = () => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOnline = () => {
|
||||||
|
return fetch('/cosmos/api/status', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (response) => {
|
||||||
|
let rep;
|
||||||
|
try {
|
||||||
|
rep = await response.json();
|
||||||
|
} catch {
|
||||||
|
throw new Error('Server error');
|
||||||
|
}
|
||||||
|
if (response.status == 200) {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
const e = new Error(rep.message);
|
||||||
|
e.status = response.status;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newInstall = (req) => {
|
const newInstall = (req) => {
|
||||||
return wrap(fetch('/cosmos/api/newInstall', {
|
return wrap(fetch('/cosmos/api/newInstall', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -31,4 +53,5 @@ export {
|
||||||
docker,
|
docker,
|
||||||
getStatus,
|
getStatus,
|
||||||
newInstall,
|
newInstall,
|
||||||
|
isOnline
|
||||||
};
|
};
|
|
@ -21,4 +21,8 @@ export default function wrap(apicall) {
|
||||||
|
|
||||||
export function setSnackit(snack) {
|
export function setSnackit(snack) {
|
||||||
snackit = snack;
|
snackit = snack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
snackit
|
||||||
|
};
|
16
client/src/components/back.jsx
Normal file
16
client/src/components/back.jsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { LeftOutlined } from "@ant-design/icons";
|
||||||
|
import { IconButton } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
function Back() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const goBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
}
|
||||||
|
return <IconButton onClick={goBack}>
|
||||||
|
<LeftOutlined />
|
||||||
|
</IconButton>
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Back;
|
|
@ -24,7 +24,10 @@ const HostChip = ({route, settings}) => {
|
||||||
return <Chip
|
return <Chip
|
||||||
label={((isOnline == null) ? "⚪" : (isOnline ? "🟢 " : "🔴 ")) + url}
|
label={((isOnline == null) ? "⚪" : (isOnline ? "🟢 " : "🔴 ")) + url}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
style={{paddingRight: '4px'}}
|
style={{
|
||||||
|
paddingRight: '4px',
|
||||||
|
textDecoration: isOnline ? 'none' : 'underline wavy red',
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(route.UseHost)
|
if(route.UseHost)
|
||||||
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');
|
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');
|
||||||
|
|
|
@ -118,13 +118,13 @@ export const RouteActions = ({route, routeKey, up, down, deleteRoute}) => {
|
||||||
return <>
|
return <>
|
||||||
<Stack direction={'row'} spacing={2} alignItems={'center'} justifyContent={'right'}>
|
<Stack direction={'row'} spacing={2} alignItems={'center'} justifyContent={'right'}>
|
||||||
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
||||||
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={() => deleteRoute()}/>)}
|
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
|
||||||
|
|
||||||
<Tooltip title='Routes with the lowest priority are matched first'>
|
<Tooltip title='Routes with the lowest priority are matched first'>
|
||||||
<Stack direction={'column'} spacing={0}>
|
<Stack direction={'column'} spacing={0}>
|
||||||
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={() => up()}><UpOutlined /></Card>
|
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={(event) => up(event)}><UpOutlined /></Card>
|
||||||
<Card sx={{...miniChip, cursor: 'auto'}}>{routeKey}</Card>
|
<Card sx={{...miniChip, cursor: 'auto'}}>{routeKey}</Card>
|
||||||
<Card sx={{...miniChip, borderTop: 'none'}} onClick={() => down()}><DownOutlined /></Card>
|
<Card sx={{...miniChip, borderTop: 'none'}} onClick={(event) => down(event)}><DownOutlined /></Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
83
client/src/components/tabbedView/tabbedView.jsx
Normal file
83
client/src/components/tabbedView/tabbedView.jsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery } from '@mui/material';
|
||||||
|
import { styled } from '@mui/system';
|
||||||
|
|
||||||
|
const StyledTabs = styled(Tabs)`
|
||||||
|
border-right: 1px solid ${({ theme }) => theme.palette.divider};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabPanel = (props) => {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`vertical-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`vertical-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && (
|
||||||
|
<Box p={3}>
|
||||||
|
<Typography>{children}</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const a11yProps = (index) => {
|
||||||
|
return {
|
||||||
|
id: `vertical-tab-${index}`,
|
||||||
|
'aria-controls': `vertical-tabpanel-${index}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrettyTabbedView = ({ tabs }) => {
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const handleChange = (event, newValue) => {
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (event) => {
|
||||||
|
setValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
|
||||||
|
{isMobile ? (
|
||||||
|
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120 }}>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<MenuItem key={index} value={index}>
|
||||||
|
{tab.title}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<StyledTabs
|
||||||
|
orientation="vertical"
|
||||||
|
variant="scrollable"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label="Vertical tabs"
|
||||||
|
>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<Tab key={index} label={tab.title} {...a11yProps(index)} />
|
||||||
|
))}
|
||||||
|
</StyledTabs>
|
||||||
|
)}
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<TabPanel key={index} value={value} index={index}>
|
||||||
|
{tab.children}
|
||||||
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrettyTabbedView;
|
|
@ -9,8 +9,9 @@ import Paper from '@mui/material/Paper';
|
||||||
import { Input, InputAdornment, Stack, TextField } from '@mui/material';
|
import { Input, InputAdornment, Stack, TextField } 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';
|
||||||
|
|
||||||
const PrettyTableView = ({ getKey, data, columns, onRowClick }) => {
|
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';
|
||||||
|
@ -59,18 +60,28 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick }) => {
|
||||||
key={getKey(row)}
|
key={getKey(row)}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderLeft: 'transparent solid 5px',
|
borderLeft: 'transparent solid 2px',
|
||||||
'&:last-child td, &:last-child th': { border: 0 },
|
'&:last-child td, &:last-child th': { border: 0 },
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
backgroundColor: 'rgba(0, 0, 0, 0.06)',
|
||||||
borderColor: 'gray',
|
borderColor: 'gray',
|
||||||
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell
|
|
||||||
style={column.style}
|
<TableCell sx={column.style}>
|
||||||
>{column.field(row, key)}</TableCell>
|
{!column.clickable ? <Link
|
||||||
|
to={linkTo && linkTo(row, key)}
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{column.field(row, key)}
|
||||||
|
</Link> : column.field(row, key)}
|
||||||
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -25,6 +25,16 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stickyButton {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.50);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.shinyButton:before {
|
.shinyButton:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -44,4 +54,4 @@
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
background-color: rgba(0.2,0.2,0.2,0.2);
|
background-color: rgba(0.2,0.2,0.2,0.2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ const MainLayout = () => {
|
||||||
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
|
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
|
||||||
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
|
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Breadcrumbs navigation={navigation} title divider={false} />
|
<Breadcrumbs navigation={navigation} divider={false} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,194 +1,71 @@
|
||||||
import * as React from 'react';
|
import { useParams } from "react-router";
|
||||||
import MainCard from '../../../components/MainCard';
|
import Back from "../../components/back";
|
||||||
import { Formik } from 'formik';
|
import { CircularProgress, Stack } from "@mui/material";
|
||||||
import * as Yup from 'yup';
|
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
|
||||||
import {
|
import RouteManagement from "./routes/routeman";
|
||||||
Alert,
|
import { useEffect, useState } from "react";
|
||||||
Grid,
|
import * as API from "../../api";
|
||||||
FormHelperText,
|
import RouteSecurity from "./routes/routeSecurity";
|
||||||
Chip,
|
import RouteOverview from "./routes/routeoverview";
|
||||||
|
|
||||||
} from '@mui/material';
|
const RouteConfigPage = () => {
|
||||||
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from './formShortcuts';
|
const { routeName } = useParams();
|
||||||
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
|
const [config, setConfig] = useState(null);
|
||||||
import { CosmosContainerPicker } from './containerPicker';
|
|
||||||
import { ValidateRoute } from './users/routeman';
|
let currentRoute = null;
|
||||||
|
if (config) {
|
||||||
|
currentRoute = config.HTTPConfig.ProxyConfig.Routes.find((r) => r.Name === routeName);
|
||||||
|
}
|
||||||
|
|
||||||
const RouteConfig = ({route, key, lockTarget, TargetContainer, setRouteConfig}) => {
|
const refreshConfig = () => {
|
||||||
return (<div style={{ maxWidth: '1000px', margin: '' }}>
|
API.config.get().then((res) => {
|
||||||
{route && <>
|
setConfig(res.data);
|
||||||
<Formik
|
});
|
||||||
initialValues={{
|
};
|
||||||
Name: route.Name,
|
|
||||||
Description: route.Description,
|
|
||||||
Mode: route.Mode || "SERVAPP",
|
|
||||||
Target: route.Target,
|
|
||||||
UseHost: route.UseHost,
|
|
||||||
AuthEnabled: route.AuthEnabled,
|
|
||||||
Host: route.Host,
|
|
||||||
UsePathPrefix: route.UsePathPrefix,
|
|
||||||
PathPrefix: route.PathPrefix,
|
|
||||||
StripPathPrefix: route.StripPathPrefix,
|
|
||||||
Timeout: route.Timeout,
|
|
||||||
ThrottlePerMinute: route.ThrottlePerMinute,
|
|
||||||
CORSOrigin: route.CORSOrigin,
|
|
||||||
}}
|
|
||||||
validationSchema={ValidateRoute}
|
|
||||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
// validate={(values) => {
|
|
||||||
// setRouteConfig(values);
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
{(formik) => (
|
|
||||||
<form noValidate onSubmit={formik.handleSubmit}>
|
|
||||||
<MainCard name={route.Name} title={route.Name}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{formik.errors.submit && (
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CosmosInputText
|
useEffect(() => {
|
||||||
name="Name"
|
refreshConfig();
|
||||||
label="Name"
|
}, []);
|
||||||
placeholder="Name"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
return <div>
|
||||||
name="Description"
|
<h2>
|
||||||
label="Description"
|
<Stack spacing={1}>
|
||||||
placeholder="Description"
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
formik={formik}
|
<Back />
|
||||||
/>
|
<div>{routeName}</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<CosmosCollapse title="Settings">
|
{config && <PrettyTabbedView tabs={[
|
||||||
<Grid container spacing={2}>
|
{
|
||||||
<CosmosFormDivider title={'Target Type'}/>
|
title: 'Overview',
|
||||||
<Grid item xs={12}>
|
children: <RouteOverview routeConfig={currentRoute} />
|
||||||
<Alert color='info'>What are you trying to access with this route?</Alert>
|
},
|
||||||
</Grid>
|
{
|
||||||
|
title: 'Setup',
|
||||||
|
children: <RouteManagement
|
||||||
|
title="Setup"
|
||||||
|
submitButton
|
||||||
|
routeConfig={currentRoute}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Security',
|
||||||
|
children: <RouteSecurity
|
||||||
|
routeConfig={currentRoute}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Permissions',
|
||||||
|
children: <div>WIP</div>
|
||||||
|
},
|
||||||
|
]}/>}
|
||||||
|
|
||||||
<CosmosSelect
|
{!config && <div style={{textAlign: 'center'}}>
|
||||||
name="Mode"
|
<CircularProgress />
|
||||||
label="Mode"
|
</div>}
|
||||||
formik={formik}
|
</Stack>
|
||||||
disabled={lockTarget}
|
</h2>
|
||||||
options={[
|
</div>
|
||||||
["SERVAPP", "ServApp - Docker Container"],
|
|
||||||
["PROXY", "Proxy"],
|
|
||||||
["STATIC", "Static Folder"],
|
|
||||||
["SPA", "Single Page Application"],
|
|
||||||
["REDIRECT", "Redirection"]
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Target Settings'}/>
|
|
||||||
|
|
||||||
{
|
|
||||||
(formik.values.Mode === "SERVAPP")?
|
|
||||||
<CosmosContainerPicker
|
|
||||||
formik={formik}
|
|
||||||
lockTarget={lockTarget}
|
|
||||||
TargetContainer={TargetContainer}
|
|
||||||
onTargetChange={() => {
|
|
||||||
setRouteConfig(formik.values);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
: <CosmosInputText
|
|
||||||
name="Target"
|
|
||||||
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
|
||||||
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Source'}/>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert color='info'>What URL do you want to access your target from?</Alert>
|
|
||||||
</Grid>
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="UseHost"
|
|
||||||
label="Use Host"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formik.values.UseHost && <CosmosInputText
|
|
||||||
name="Host"
|
|
||||||
label="Host"
|
|
||||||
placeholder="Host"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="UsePathPrefix"
|
|
||||||
label="Use Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formik.values.UsePathPrefix && <CosmosInputText
|
|
||||||
name="PathPrefix"
|
|
||||||
label="Path Prefix"
|
|
||||||
placeholder="Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{formik.values.UsePathPrefix && <CosmosCheckbox
|
|
||||||
name="StripPathPrefix"
|
|
||||||
label="Strip Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Security'}/>
|
|
||||||
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="AuthEnabled"
|
|
||||||
label="Authentication Required"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="Timeout"
|
|
||||||
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
|
||||||
placeholder="Timeout"
|
|
||||||
type="number"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="ThrottlePerMinute"
|
|
||||||
label="Maximum number of requests Per Minute (0 for no limit, at least 100 or less recommended)"
|
|
||||||
placeholder="Throttle Per Minute"
|
|
||||||
type="number"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="CORSOrigin"
|
|
||||||
label="Custom CORS Origin (Recommended to leave blank)"
|
|
||||||
placeholder="CORS Origin"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</CosmosCollapse>
|
|
||||||
</Grid>
|
|
||||||
</MainCard>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RouteConfig;
|
export default RouteConfigPage;
|
119
client/src/pages/config/routes/routeSecurity.jsx
Normal file
119
client/src/pages/config/routes/routeSecurity.jsx
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Stack,
|
||||||
|
|
||||||
|
} from '@mui/material';
|
||||||
|
import RestartModal from '../users/restart';
|
||||||
|
import { CosmosCheckbox, CosmosInputText } from '../users/formShortcuts';
|
||||||
|
import { snackit } from '../../../api/wrap';
|
||||||
|
|
||||||
|
const RouteSecurity = ({ routeConfig }) => {
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
|
||||||
|
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||||
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
|
{routeConfig && <>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
AuthEnabled: routeConfig.AuthEnabled,
|
||||||
|
Host: routeConfig.Host,
|
||||||
|
UsePathPrefix: routeConfig.UsePathPrefix,
|
||||||
|
PathPrefix: routeConfig.PathPrefix,
|
||||||
|
StripPathPrefix: routeConfig.StripPathPrefix,
|
||||||
|
Timeout: routeConfig.Timeout,
|
||||||
|
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
|
||||||
|
CORSOrigin: routeConfig.CORSOrigin,
|
||||||
|
MaxBandwith: routeConfig.MaxBandwith,
|
||||||
|
}}
|
||||||
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
const fullValues = {
|
||||||
|
...routeConfig,
|
||||||
|
...values,
|
||||||
|
}
|
||||||
|
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
||||||
|
if (res.status == "OK") {
|
||||||
|
setStatus({ success: true });
|
||||||
|
snackit('Route updated successfully', 'success');
|
||||||
|
setSubmitting(false);
|
||||||
|
setOpenModal(true);
|
||||||
|
} else {
|
||||||
|
setStatus({ success: false });
|
||||||
|
setErrors({ submit: res.status });
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(formik) => (
|
||||||
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<MainCard name={routeConfig.Name} title={'Security'}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="AuthEnabled"
|
||||||
|
label="Authentication Required"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="Timeout"
|
||||||
|
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
||||||
|
placeholder="Timeout"
|
||||||
|
type="number"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="MaxBandwith"
|
||||||
|
label="Maximum Bandwith limit per user in bytes per seconds (0 for no limit)"
|
||||||
|
placeholder="Maximum Bandwith"
|
||||||
|
type="number"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="ThrottlePerMinute"
|
||||||
|
label="Maximum number of requests Per Minute (0 for no limit, at least 100 or less recommended)"
|
||||||
|
placeholder="Throttle Per Minute"
|
||||||
|
type="number"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="CORSOrigin"
|
||||||
|
label="Custom CORS Origin (Recommended to leave blank)"
|
||||||
|
placeholder="CORS Origin"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</MainCard>
|
||||||
|
<MainCard ><Button
|
||||||
|
fullWidth
|
||||||
|
disableElevation
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button></MainCard>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteSecurity;
|
221
client/src/pages/config/routes/routeman.jsx
Normal file
221
client/src/pages/config/routes/routeman.jsx
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Stack,
|
||||||
|
FormHelperText,
|
||||||
|
} from '@mui/material';
|
||||||
|
import RestartModal from '../users/restart';
|
||||||
|
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
|
||||||
|
import { CosmosContainerPicker } from '../users/containerPicker';
|
||||||
|
import { snackit } from '../../../api/wrap';
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
return h ? <div style={{ display: 'none' }}>
|
||||||
|
{children}
|
||||||
|
</div> : <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false }) => {
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
|
||||||
|
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||||
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
|
{routeConfig && <>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
Name: routeConfig.Name,
|
||||||
|
Description: routeConfig.Description,
|
||||||
|
Mode: routeConfig.Mode || "SERVAPP",
|
||||||
|
Target: routeConfig.Target,
|
||||||
|
UseHost: routeConfig.UseHost,
|
||||||
|
Host: routeConfig.Host,
|
||||||
|
}}
|
||||||
|
validationSchema={ValidateRoute}
|
||||||
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
if(!submitButton) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const fullValues = {
|
||||||
|
...routeConfig,
|
||||||
|
...values,
|
||||||
|
}
|
||||||
|
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
||||||
|
if (res.status == "OK") {
|
||||||
|
setStatus({ success: true });
|
||||||
|
snackit('Route updated successfully', 'success')
|
||||||
|
setSubmitting(false);
|
||||||
|
setOpenModal(true);
|
||||||
|
} else {
|
||||||
|
setStatus({ success: false });
|
||||||
|
setErrors({ submit: res.status });
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={(values) => {
|
||||||
|
setRouteConfig && setRouteConfig(values);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(formik) => (
|
||||||
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<MainCard name={routeConfig.Name} title={
|
||||||
|
noControls ? 'New URL' :
|
||||||
|
<div>{title || routeConfig.Name}</div>
|
||||||
|
}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{formik.errors.submit && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="Name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Name"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="Description"
|
||||||
|
label="Description"
|
||||||
|
placeholder="Description"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Hide h={lockTarget}>
|
||||||
|
<CosmosFormDivider title={'Target Type'} />
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Alert color='info'>What are you trying to access with this route?</Alert>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CosmosSelect
|
||||||
|
name="Mode"
|
||||||
|
label="Mode"
|
||||||
|
formik={formik}
|
||||||
|
disabled={lockTarget}
|
||||||
|
options={[
|
||||||
|
["SERVAPP", "ServApp - Docker Container"],
|
||||||
|
["PROXY", "Proxy"],
|
||||||
|
["STATIC", "Static Folder"],
|
||||||
|
["SPA", "Single Page Application"],
|
||||||
|
["REDIRECT", "Redirection"]
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Hide>
|
||||||
|
<CosmosFormDivider title={'Target Settings'} />
|
||||||
|
|
||||||
|
{
|
||||||
|
(formik.values.Mode === "SERVAPP") ?
|
||||||
|
<CosmosContainerPicker
|
||||||
|
formik={formik}
|
||||||
|
lockTarget={lockTarget}
|
||||||
|
TargetContainer={TargetContainer}
|
||||||
|
onTargetChange={() => {
|
||||||
|
setRouteConfig && setRouteConfig(formik.values);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
: <CosmosInputText
|
||||||
|
name="Target"
|
||||||
|
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
||||||
|
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CosmosFormDivider title={'Source'} />
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Alert color='info'>What URL do you want to access your target from?</Alert>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="UseHost"
|
||||||
|
label="Use Host"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.values.UseHost && <CosmosInputText
|
||||||
|
name="Host"
|
||||||
|
label="Host"
|
||||||
|
placeholder="Host"
|
||||||
|
formik={formik}
|
||||||
|
style={{ paddingLeft: '20px' }}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<CosmosCheckbox
|
||||||
|
name="UsePathPrefix"
|
||||||
|
label="Use Path Prefix"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.values.UsePathPrefix && <CosmosInputText
|
||||||
|
name="PathPrefix"
|
||||||
|
label="Path Prefix"
|
||||||
|
placeholder="Path Prefix"
|
||||||
|
formik={formik}
|
||||||
|
style={{ paddingLeft: '20px' }}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
{formik.values.UsePathPrefix && <CosmosCheckbox
|
||||||
|
name="StripPathPrefix"
|
||||||
|
label="Strip Path Prefix"
|
||||||
|
formik={formik}
|
||||||
|
style={{ paddingLeft: '20px' }}
|
||||||
|
/>}
|
||||||
|
</Grid>
|
||||||
|
</MainCard>
|
||||||
|
{submitButton && <MainCard ><Button
|
||||||
|
fullWidth
|
||||||
|
disableElevation
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button></MainCard>}
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteManagement;
|
38
client/src/pages/config/routes/routeoverview.jsx
Normal file
38
client/src/pages/config/routes/routeoverview.jsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import RestartModal from '../users/restart';
|
||||||
|
import { Chip, Stack, useMediaQuery } from '@mui/material';
|
||||||
|
import HostChip from '../../../components/HostChip';
|
||||||
|
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||||
|
import { getFaviconURL } from '../../../utils/routes';
|
||||||
|
|
||||||
|
const RouteOverview = ({ routeConfig }) => {
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
return <div style={{ maxWidth: '1000px', width: '100%'}}>
|
||||||
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
|
||||||
|
{routeConfig && <>
|
||||||
|
<MainCard name={routeConfig.Name} title={routeConfig.Name}>
|
||||||
|
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||||
|
<div>
|
||||||
|
<img src={getFaviconURL(routeConfig)} width="128px" />
|
||||||
|
</div>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<strong>Description</strong>
|
||||||
|
<div>{routeConfig.Description}</div>
|
||||||
|
<strong>URL</strong>
|
||||||
|
<div><HostChip route={routeConfig} /></div>
|
||||||
|
<strong>Target</strong>
|
||||||
|
<div><RouteMode route={routeConfig} /> <Chip label={routeConfig.Target} /></div>
|
||||||
|
<strong>Security</strong>
|
||||||
|
<div><RouteSecurity route={routeConfig} /></div>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</MainCard>
|
||||||
|
</>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteOverview;
|
|
@ -25,17 +25,19 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Chip,
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
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 './routeman';
|
import RouteManagement, {ValidateRoute} from '../routes/routeman';
|
||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import { getFaviconURL, sanitizeRoute } from '../../../utils/routes';
|
import { getFaviconURL, sanitizeRoute } 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';
|
||||||
|
|
||||||
const stickyButton = {
|
const stickyButton = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
@ -60,6 +62,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();
|
||||||
|
|
||||||
function updateRoutes(routes) {
|
function updateRoutes(routes) {
|
||||||
let con = {
|
let con = {
|
||||||
|
@ -90,7 +93,8 @@ const ProxyManagement = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function up(key) {
|
function up(event, key) {
|
||||||
|
event.stopPropagation();
|
||||||
if (key > 0) {
|
if (key > 0) {
|
||||||
let tmp = routes[key];
|
let tmp = routes[key];
|
||||||
routes[key] = routes[key-1];
|
routes[key] = routes[key-1];
|
||||||
|
@ -98,15 +102,19 @@ const ProxyManagement = () => {
|
||||||
updateRoutes(routes);
|
updateRoutes(routes);
|
||||||
setNeedSave(true);
|
setNeedSave(true);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRoute(key) {
|
function deleteRoute(event, key) {
|
||||||
|
event.stopPropagation();
|
||||||
routes.splice(key, 1);
|
routes.splice(key, 1);
|
||||||
updateRoutes(routes);
|
updateRoutes(routes);
|
||||||
setNeedSave(true);
|
setNeedSave(true);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function down(key) {
|
function down(event, key) {
|
||||||
|
event.stopPropagation();
|
||||||
if (key < routes.length - 1) {
|
if (key < routes.length - 1) {
|
||||||
let tmp = routes[key];
|
let tmp = routes[key];
|
||||||
routes[key] = routes[key+1];
|
routes[key] = routes[key+1];
|
||||||
|
@ -114,6 +122,7 @@ const ProxyManagement = () => {
|
||||||
updateRoutes(routes);
|
updateRoutes(routes);
|
||||||
setNeedSave(true);
|
setNeedSave(true);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -161,6 +170,7 @@ const ProxyManagement = () => {
|
||||||
{routes && <PrettyTableView
|
{routes && <PrettyTableView
|
||||||
data={routes}
|
data={routes}
|
||||||
getKey={(r) => r.Name + r.Target + r.Mode}
|
getKey={(r) => r.Name + r.Target + r.Mode}
|
||||||
|
onRowClick={(r) => {navigate('/ui/config-url/' + r.Name)}}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
|
@ -171,9 +181,12 @@ const ProxyManagement = () => {
|
||||||
},
|
},
|
||||||
{ title: 'URL',
|
{ title: 'URL',
|
||||||
search: (r) => r.Name + ' ' + r.Description,
|
search: (r) => r.Name + ' ' + r.Description,
|
||||||
|
style: {
|
||||||
|
textDecoration: 'inherit',
|
||||||
|
},
|
||||||
field: (r) => <>
|
field: (r) => <>
|
||||||
<div style={{display:'inline-block', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
|
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
|
||||||
<div style={{display:'inline-block', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
// { title: 'Description', field: (r) => shorten(r.Description), style:{fontSize: '90%', opacity: '90%'} },
|
// { title: 'Description', field: (r) => shorten(r.Description), style:{fontSize: '90%', opacity: '90%'} },
|
||||||
|
@ -182,12 +195,12 @@ const ProxyManagement = () => {
|
||||||
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
||||||
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
|
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
|
||||||
style: {minWidth: '70px'} },
|
style: {minWidth: '70px'} },
|
||||||
{ title: '', field: (r, k) => <RouteActions
|
{ title: '', clickable:true, field: (r, k) => <RouteActions
|
||||||
route={r}
|
route={r}
|
||||||
routeKey={k}
|
routeKey={k}
|
||||||
up={() => up(k)}
|
up={(event) => up(event, k)}
|
||||||
down={() => down(k)}
|
down={(event) => down(event, k)}
|
||||||
deleteRoute={() => deleteRoute(k)}
|
deleteRoute={(event) => deleteRoute(event, k)}
|
||||||
/>,
|
/>,
|
||||||
style: {
|
style: {
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
|
@ -195,6 +208,11 @@ const ProxyManagement = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>}
|
/>}
|
||||||
|
{
|
||||||
|
!routes && <div style={{textAlign: 'center'}}>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
{/* {routes && routes.map((route,key) => (<>
|
{/* {routes && routes.map((route,key) => (<>
|
||||||
<RouteManagement key={route.Name} routeConfig={route}
|
<RouteManagement key={route.Name} routeConfig={route}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// material-ui
|
// material-ui
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Button, Typography } from '@mui/material';
|
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||||
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
|
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
@ -23,28 +23,50 @@ import MainCard from '../../../components/MainCard';
|
||||||
import IsLoggedIn from '../../../IsLoggedIn';
|
import IsLoggedIn from '../../../IsLoggedIn';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function checkIsOnline() {
|
||||||
|
API.isOnline().then((res) => {
|
||||||
|
window.location.reload();
|
||||||
|
}).catch((err) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
checkIsOnline();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const RestartModal = ({openModal, setOpenModal}) => {
|
const RestartModal = ({openModal, setOpenModal}) => {
|
||||||
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
|
const [warn, setWarn] = useState(false);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||||
<DialogTitle>Restart Server</DialogTitle>
|
<DialogTitle>{!isRestarting ? 'Restart Server?' : 'Restarting Server...'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
A restart is required to apply changes. Do you want to restart?
|
{warn && <div>
|
||||||
|
<Alert severity="warning" icon={<WarningOutlined />}>
|
||||||
|
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs.
|
||||||
|
</Alert>
|
||||||
|
</div>}
|
||||||
|
{isRestarting ?
|
||||||
|
<div style={{textAlign: 'center', padding: '20px'}}>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
: 'A restart is required to apply changes. Do you want to restart?'}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
{!isRestarting && <DialogActions>
|
||||||
<Button onClick={() => setOpenModal(false)}>Later</Button>
|
<Button onClick={() => setOpenModal(false)}>Later</Button>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
|
setIsRestarting(true);
|
||||||
API.config.restart()
|
API.config.restart()
|
||||||
.then(() => {
|
setTimeout(() => {
|
||||||
refresh();
|
checkIsOnline();
|
||||||
setOpenModal(false);
|
}, 1500)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
setWarn(true);
|
||||||
}, 2000)
|
}, 8000)
|
||||||
})
|
|
||||||
}}>Restart</Button>
|
}}>Restart</Button>
|
||||||
</DialogActions>
|
</DialogActions>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,257 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import IsLoggedIn from '../../../IsLoggedIn';
|
|
||||||
import * as API from '../../../api';
|
|
||||||
import MainCard from '../../../components/MainCard';
|
|
||||||
import { Formik, Field } from 'formik';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
FormControlLabel,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
InputLabel,
|
|
||||||
Link,
|
|
||||||
OutlinedInput,
|
|
||||||
Stack,
|
|
||||||
Typography,
|
|
||||||
FormHelperText,
|
|
||||||
Collapse,
|
|
||||||
TextField,
|
|
||||||
MenuItem,
|
|
||||||
Card,
|
|
||||||
Chip,
|
|
||||||
|
|
||||||
} from '@mui/material';
|
|
||||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
|
||||||
import AnimateButton from '../../../components/@extended/AnimateButton';
|
|
||||||
import RestartModal from './restart';
|
|
||||||
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from './formShortcuts';
|
|
||||||
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);
|
|
||||||
const myRef = React.useRef(null)
|
|
||||||
const currRef = myRef.current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if(currRef && window.location.hash === '#' + routeConfig.Name) {
|
|
||||||
currRef.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
}, [currRef])
|
|
||||||
|
|
||||||
return <div style={{ maxWidth: '1000px', margin: '' }}>
|
|
||||||
{routeConfig && <>
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
Name: routeConfig.Name,
|
|
||||||
Description: routeConfig.Description,
|
|
||||||
Mode: routeConfig.Mode || "SERVAPP",
|
|
||||||
Target: routeConfig.Target,
|
|
||||||
UseHost: routeConfig.UseHost,
|
|
||||||
AuthEnabled: routeConfig.AuthEnabled,
|
|
||||||
Host: routeConfig.Host,
|
|
||||||
UsePathPrefix: routeConfig.UsePathPrefix,
|
|
||||||
PathPrefix: routeConfig.PathPrefix,
|
|
||||||
StripPathPrefix: routeConfig.StripPathPrefix,
|
|
||||||
Timeout: routeConfig.Timeout,
|
|
||||||
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
|
|
||||||
CORSOrigin: routeConfig.CORSOrigin,
|
|
||||||
}}
|
|
||||||
validateOnChange={false}
|
|
||||||
validationSchema={ValidateRoute}
|
|
||||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
validate={(values) => {
|
|
||||||
setRouteConfig(values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(formik) => (
|
|
||||||
<form ref={myRef} noValidate onSubmit={formik.handleSubmit}>
|
|
||||||
<MainCard name={routeConfig.Name} title={
|
|
||||||
noControls ? 'New URL' :
|
|
||||||
<div>{routeConfig.Name}
|
|
||||||
<Chip label={<UpOutlined />} onClick={() => up()}/>
|
|
||||||
<Chip label={<DownOutlined />} onClick={() => down()}/>
|
|
||||||
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
|
||||||
{confirmDelete && (<Chip label={<CheckOutlined />} onClick={() => deleteRoute()}/>)}
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{formik.errors.submit && (
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="Name"
|
|
||||||
label="Name"
|
|
||||||
placeholder="Name"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="Description"
|
|
||||||
label="Description"
|
|
||||||
placeholder="Description"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosCollapse title="Settings">
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<CosmosFormDivider title={'Target Type'}/>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert color='info'>What are you trying to access with this route?</Alert>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<CosmosSelect
|
|
||||||
name="Mode"
|
|
||||||
label="Mode"
|
|
||||||
formik={formik}
|
|
||||||
disabled={lockTarget}
|
|
||||||
options={[
|
|
||||||
["SERVAPP", "ServApp - Docker Container"],
|
|
||||||
["PROXY", "Proxy"],
|
|
||||||
["STATIC", "Static Folder"],
|
|
||||||
["SPA", "Single Page Application"],
|
|
||||||
["REDIRECT", "Redirection"]
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Target Settings'}/>
|
|
||||||
|
|
||||||
{
|
|
||||||
(formik.values.Mode === "SERVAPP")?
|
|
||||||
<CosmosContainerPicker
|
|
||||||
formik={formik}
|
|
||||||
lockTarget={lockTarget}
|
|
||||||
TargetContainer={TargetContainer}
|
|
||||||
onTargetChange={() => {
|
|
||||||
setRouteConfig(formik.values);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
: <CosmosInputText
|
|
||||||
name="Target"
|
|
||||||
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
|
||||||
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Source'}/>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert color='info'>What URL do you want to access your target from?</Alert>
|
|
||||||
</Grid>
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="UseHost"
|
|
||||||
label="Use Host"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formik.values.UseHost && <CosmosInputText
|
|
||||||
name="Host"
|
|
||||||
label="Host"
|
|
||||||
placeholder="Host"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="UsePathPrefix"
|
|
||||||
label="Use Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formik.values.UsePathPrefix && <CosmosInputText
|
|
||||||
name="PathPrefix"
|
|
||||||
label="Path Prefix"
|
|
||||||
placeholder="Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{formik.values.UsePathPrefix && <CosmosCheckbox
|
|
||||||
name="StripPathPrefix"
|
|
||||||
label="Strip Path Prefix"
|
|
||||||
formik={formik}
|
|
||||||
style={{paddingLeft: '20px'}}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<CosmosFormDivider title={'Security'}/>
|
|
||||||
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<CosmosCheckbox
|
|
||||||
name="AuthEnabled"
|
|
||||||
label="Authentication Required"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="Timeout"
|
|
||||||
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
|
||||||
placeholder="Timeout"
|
|
||||||
type="number"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="ThrottlePerMinute"
|
|
||||||
label="Maximum number of requests Per Minute (0 for no limit, at least 100 or less recommended)"
|
|
||||||
placeholder="Throttle Per Minute"
|
|
||||||
type="number"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
name="CORSOrigin"
|
|
||||||
label="Custom CORS Origin (Recommended to leave blank)"
|
|
||||||
placeholder="CORS Origin"
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</CosmosCollapse>
|
|
||||||
</Grid>
|
|
||||||
</MainCard>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RouteManagement;
|
|
|
@ -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/users/routeman';
|
import RouteManagement, { ValidateRoute } from '../config/routes/routeman';
|
||||||
import { sanitizeRoute } from '../../utils/routes';
|
import { 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 }) => ({
|
||||||
|
@ -123,6 +123,17 @@ const ServeApps = () => {
|
||||||
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFirstRouteFavIcon = (app) => {
|
||||||
|
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
||||||
|
console.log(routes)
|
||||||
|
if(routes.length > 0) {
|
||||||
|
let url = getFaviconURL(routes[0]);
|
||||||
|
return url;
|
||||||
|
} else {
|
||||||
|
return getFaviconURL('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<IsLoggedIn />
|
<IsLoggedIn />
|
||||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
|
@ -235,13 +246,16 @@ const ServeApps = () => {
|
||||||
})[app.State]
|
})[app.State]
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="column" spacing={0} alignItems="flex-start">
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
<Typography variant="h5" color="text.secondary">
|
<img src={getFirstRouteFavIcon(app)} width="40px" />
|
||||||
{app.Names[0].replace('/', '')}
|
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px'}}>
|
||||||
</Typography>
|
<Typography variant="h5" color="text.secondary">
|
||||||
<Typography style={{ fontSize: '80%' }} color="text.secondary">
|
{app.Names[0].replace('/', '')}
|
||||||
{app.Image}
|
</Typography>
|
||||||
</Typography>
|
<Typography style={{ fontSize: '80%' }} color="text.secondary">
|
||||||
|
{app.Image}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ConfigManagement from '../pages/config/users/configman';
|
||||||
import ProxyManagement from '../pages/config/users/proxyman';
|
import ProxyManagement from '../pages/config/users/proxyman';
|
||||||
import ServeApps from '../pages/servapps/servapps';
|
import ServeApps from '../pages/servapps/servapps';
|
||||||
import { Navigate } from 'react-router';
|
import { Navigate } from 'react-router';
|
||||||
|
import RouteConfigPage from '../pages/config/RouteConfig';
|
||||||
|
|
||||||
// render - dashboard
|
// render - dashboard
|
||||||
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
|
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
|
||||||
|
@ -52,6 +53,10 @@ const MainRoutes = {
|
||||||
path: '/ui/config-url',
|
path: '/ui/config-url',
|
||||||
element: <ProxyManagement />
|
element: <ProxyManagement />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ui/config-url/:routeName',
|
||||||
|
element: <RouteConfigPage />,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -35,6 +35,7 @@ require (
|
||||||
github.com/docker/docker v23.0.1+incompatible // indirect
|
github.com/docker/docker v23.0.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||||
github.com/exoscale/egoscale v0.40.0 // indirect
|
github.com/exoscale/egoscale v0.40.0 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/foomo/simplecert v1.8.4 // indirect
|
github.com/foomo/simplecert v1.8.4 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -154,6 +154,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY=
|
github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY=
|
||||||
github.com/exoscale/egoscale v0.40.0 h1:fvVKszvqAXNP1ryhC0rwsKHPenyMaV0fGf14oUMNZFw=
|
github.com/exoscale/egoscale v0.40.0 h1:fvVKszvqAXNP1ryhC0rwsKHPenyMaV0fGf14oUMNZFw=
|
||||||
github.com/exoscale/egoscale v0.40.0/go.mod h1:BFi2GNsnsrALev3+gFO/HIQADBQhqJ41S0QrNEB2GJw=
|
github.com/exoscale/egoscale v0.40.0/go.mod h1:BFi2GNsnsrALev3+gFO/HIQADBQhqJ41S0QrNEB2GJw=
|
||||||
|
@ -189,6 +191,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
||||||
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
|
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
@ -343,6 +346,7 @@ github.com/jarcoal/httpmock v1.0.7/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT
|
||||||
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
|
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
|
||||||
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
|
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
@ -391,6 +395,7 @@ github.com/linode/linodego v0.24.2 h1:wj7DO4/8zGEJm6HtGp03L1HLATzmffkwEoifiKIFi0
|
||||||
github.com/linode/linodego v0.24.2/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=
|
github.com/linode/linodego v0.24.2/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=
|
||||||
github.com/liquidweb/liquidweb-go v1.6.1 h1:O51RbJo3ZEWFkZFfP32zIF6MCoZzwuuybuXsvZvVEEI=
|
github.com/liquidweb/liquidweb-go v1.6.1 h1:O51RbJo3ZEWFkZFfP32zIF6MCoZzwuuybuXsvZvVEEI=
|
||||||
github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||||
|
@ -567,6 +572,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
|
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
|
||||||
go.deanishe.net/favicon v0.1.0/go.mod h1:vIKVI+lUh8k3UAzaN4gjC+cpyatLQWmx0hVX4vLE8jU=
|
go.deanishe.net/favicon v0.1.0/go.mod h1:vIKVI+lUh8k3UAzaN4gjC+cpyatLQWmx0hVX4vLE8jU=
|
||||||
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||||
|
|
83
src/configapi/patch.go
Normal file
83
src/configapi/patch.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package configapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateRouteRequest struct {
|
||||||
|
RouteName string `json:"routeName"`
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
NewRoute *utils.ProxyRouteConfig `json:"newRoute,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var configLock sync.Mutex
|
||||||
|
|
||||||
|
func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configLock.Lock()
|
||||||
|
defer configLock.Unlock()
|
||||||
|
|
||||||
|
var updateReq UpdateRouteRequest
|
||||||
|
err := json.NewDecoder(req.Body).Decode(&updateReq)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("SettingsUpdate: Invalid Update Request", err)
|
||||||
|
utils.HTTPError(w, "Invalid Update Request", http.StatusBadRequest, "UR001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := utils.ReadConfigFromFile()
|
||||||
|
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||||
|
routeIndex := -1
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
switch updateReq.Operation {
|
||||||
|
case "replace":
|
||||||
|
if updateReq.NewRoute == 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")
|
||||||
|
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
|
||||||
|
utils.SaveConfigTofile(config)
|
||||||
|
utils.NeedsRestart = true
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ func ConfigRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
ConfigApiGet(w, req)
|
ConfigApiGet(w, req)
|
||||||
} else if (req.Method == "PUT") {
|
} else if (req.Method == "PUT") {
|
||||||
ConfigApiSet(w, req)
|
ConfigApiSet(w, req)
|
||||||
|
} else if (req.Method == "PATCH") {
|
||||||
|
ConfigApiPatch(w, req)
|
||||||
} else {
|
} else {
|
||||||
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
|
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
|
||||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
|
|
@ -38,13 +38,6 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
|
||||||
utils.SaveConfigTofile(request)
|
utils.SaveConfigTofile(request)
|
||||||
utils.NeedsRestart = true
|
utils.NeedsRestart = true
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// utils.Error("SettingsUpdate: Error saving config to file", err)
|
|
||||||
// utils.HTTPError(w, "Error saving config to file",
|
|
||||||
// http.StatusInternalServerError, "CS001")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"status": "OK",
|
"status": "OK",
|
||||||
})
|
})
|
||||||
|
|
|
@ -198,7 +198,11 @@ func StartServer() {
|
||||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||||
|
|
||||||
srapi.Use(tokenMiddleware)
|
srapi.Use(tokenMiddleware)
|
||||||
srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname))
|
srapi.Use(proxy.SmartShieldMiddleware(
|
||||||
|
utils.SmartShieldPolicy{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
))
|
||||||
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
|
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
|
||||||
srapi.Use(httprate.Limit(60, 1*time.Minute,
|
srapi.Use(httprate.Limit(60, 1*time.Minute,
|
||||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||||
|
|
Loading…
Reference in a new issue