[release] v0.4.0-unstable3
This commit is contained in:
parent
8ce9d52fbd
commit
ac6fbe64e7
|
@ -1,7 +1,8 @@
|
||||||
## Version 0.4.0
|
## Version 0.4.0
|
||||||
- Protect server against direct IP access
|
- Protect server against direct IP access
|
||||||
|
- Improvements to installer to make it more robust
|
||||||
|
- Fix bug where you can't complete the setup if you don't have a database
|
||||||
- Stop / Start / Restart / Remove / Kill containers
|
- Stop / Start / Restart / Remove / Kill containers
|
||||||
-
|
|
||||||
|
|
||||||
## Version 0.3.0
|
## Version 0.3.0
|
||||||
- Implement 2 FA
|
- Implement 2 FA
|
||||||
|
|
|
@ -9,6 +9,70 @@ function list() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get(containerName) {
|
||||||
|
return wrap(fetch('/cosmos/api/servapps/' + containerName, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerLogs(containerId, searchQuery, limit, lastReceivedLogs, errorOnly) {
|
||||||
|
|
||||||
|
if(limit < 50) limit = 50;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
search: searchQuery || "",
|
||||||
|
limit: limit || "",
|
||||||
|
lastReceivedLogs: lastReceivedLogs || "",
|
||||||
|
errorOnly: errorOnly || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrap(fetch(`/cosmos/api/servapps/${containerId}/logs?${queryParams}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeList() {
|
||||||
|
return wrap(fetch('/cosmos/api/volumes', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeDelete(name) {
|
||||||
|
return wrap(fetch(`/cosmos/api/volume/${name}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function networkList() {
|
||||||
|
return wrap(fetch('/cosmos/api/networks', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function networkDelete(name) {
|
||||||
|
return wrap(fetch(`/cosmos/api/network/${name}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function secure(id, res) {
|
function secure(id, res) {
|
||||||
return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
|
return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -38,7 +102,13 @@ const manageContainer = (id, action) => {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
list,
|
list,
|
||||||
|
get,
|
||||||
newDB,
|
newDB,
|
||||||
secure,
|
secure,
|
||||||
manageContainer
|
manageContainer,
|
||||||
|
volumeList,
|
||||||
|
volumeDelete,
|
||||||
|
networkList,
|
||||||
|
networkDelete,
|
||||||
|
getContainerLogs
|
||||||
};
|
};
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
||||||
import { getOrigin, getFullOrigin } from "../utils/routes";
|
import { getOrigin, getFullOrigin } from "../utils/routes";
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
const HostChip = ({route, settings}) => {
|
const HostChip = ({route, settings, style}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === 'dark';
|
const isDark = theme.palette.mode === 'dark';
|
||||||
const [isOnline, setIsOnline] = useState(null);
|
const [isOnline, setIsOnline] = useState(null);
|
||||||
|
@ -27,6 +27,7 @@ const HostChip = ({route, settings}) => {
|
||||||
style={{
|
style={{
|
||||||
paddingRight: '4px',
|
paddingRight: '4px',
|
||||||
textDecoration: isOnline ? 'none' : 'underline wavy red',
|
textDecoration: isOnline ? 'none' : 'underline wavy red',
|
||||||
|
...style
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(route.UseHost)
|
if(route.UseHost)
|
||||||
|
|
103
client/src/components/logLine.jsx
Normal file
103
client/src/components/logLine.jsx
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function decodeUnicode(str) {
|
||||||
|
return str.replace(/\\u([0-9a-zA-Z]{3-5})/g, (match, p1) => {
|
||||||
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogLine = ({ message, docker, isMobile }) => {
|
||||||
|
let html = decodeUnicode(message)
|
||||||
|
.replace('\u0001\u0000\u0000\u0000\u0000\u0000\u0000', '')
|
||||||
|
.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/<2F>/g, '')
|
||||||
|
.replace(/\x1b\[([0-9]{1,2}(?:;[0-9]{1,2})*)?m/g, (match, p1) => {
|
||||||
|
if (!p1) {
|
||||||
|
return '</span>';
|
||||||
|
}
|
||||||
|
const codes = p1.split(';');
|
||||||
|
const styles = [];
|
||||||
|
for (const code of codes) {
|
||||||
|
switch (code) {
|
||||||
|
case '1':
|
||||||
|
styles.push('font-weight:bold');
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
styles.push('font-style:italic');
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
styles.push('text-decoration:underline');
|
||||||
|
break;
|
||||||
|
case '30':
|
||||||
|
case '31':
|
||||||
|
case '32':
|
||||||
|
case '33':
|
||||||
|
case '34':
|
||||||
|
case '35':
|
||||||
|
case '36':
|
||||||
|
case '37':
|
||||||
|
case '90':
|
||||||
|
case '91':
|
||||||
|
case '92':
|
||||||
|
case '93':
|
||||||
|
case '94':
|
||||||
|
case '95':
|
||||||
|
case '96':
|
||||||
|
case '97':
|
||||||
|
styles.push(`color:${getColor(code)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `<span style="${styles.join(';')}">`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(docker) {
|
||||||
|
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
|
||||||
|
let restString = html.replace(parts[0], '')
|
||||||
|
|
||||||
|
return <Stack direction={isMobile ? 'column' : 'row'} spacing={1}>
|
||||||
|
<div style={{color:'grey', fontStyle:'italic', whiteSpace: 'pre'}}>
|
||||||
|
{parts[0].replace('T', ' ').split('.')[0]}
|
||||||
|
</div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: restString }} />
|
||||||
|
</Stack>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColor = (code) => {
|
||||||
|
switch (code) {
|
||||||
|
case '30':
|
||||||
|
case '90':
|
||||||
|
return 'black';
|
||||||
|
case '31':
|
||||||
|
case '91':
|
||||||
|
return 'red';
|
||||||
|
case '32':
|
||||||
|
case '92':
|
||||||
|
return 'green';
|
||||||
|
case '33':
|
||||||
|
case '93':
|
||||||
|
return 'yellow';
|
||||||
|
case '34':
|
||||||
|
case '94':
|
||||||
|
return 'blue';
|
||||||
|
case '35':
|
||||||
|
case '95':
|
||||||
|
return 'magenta';
|
||||||
|
case '36':
|
||||||
|
case '96':
|
||||||
|
return 'cyan';
|
||||||
|
case '37':
|
||||||
|
case '97':
|
||||||
|
return 'white';
|
||||||
|
default:
|
||||||
|
return 'inherit';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogLine;
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery } from '@mui/material';
|
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery, CircularProgress } from '@mui/material';
|
||||||
import { styled } from '@mui/system';
|
import { styled } from '@mui/system';
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)`
|
const StyledTabs = styled(Tabs)`
|
||||||
|
@ -36,7 +36,7 @@ const a11yProps = (index) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const PrettyTabbedView = ({ tabs }) => {
|
const PrettyTabbedView = ({ tabs, isLoading }) => {
|
||||||
const [value, setValue] = useState(0);
|
const [value, setValue] = useState(0);
|
||||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
@ -67,15 +67,32 @@ const PrettyTabbedView = ({ tabs }) => {
|
||||||
aria-label="Vertical tabs"
|
aria-label="Vertical tabs"
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<Tab key={index} label={tab.title} {...a11yProps(index)} />
|
<Tab
|
||||||
|
style={{fontWeight: !tab.children ? '1000' : '', }}
|
||||||
|
disabled={!tab.children} key={index}
|
||||||
|
label={tab.title} {...a11yProps(index)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledTabs>
|
</StyledTabs>
|
||||||
)}
|
)}
|
||||||
{tabs.map((tab, index) => (
|
{!isLoading && tabs.map((tab, index) => (
|
||||||
<TabPanel key={index} value={value} index={index}>
|
<TabPanel key={index} value={value} index={index}>
|
||||||
{tab.children}
|
{tab.children}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
color="text.primary"
|
||||||
|
p={2}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,8 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
||||||
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
|
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
|
||||||
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
|
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
|
||||||
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
|
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
|
||||||
|
xl: useMediaQuery((theme) => theme.breakpoints.up('xl')),
|
||||||
|
xxl: useMediaQuery((theme) => theme.breakpoints.up('xxl')),
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -61,4 +61,8 @@
|
||||||
color:white;
|
color:white;
|
||||||
background-color: rgba(0,0,0,0.8);
|
background-color: rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.darken {
|
||||||
|
filter: brightness(0.5);
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
|
||||||
import * as API from "../../api";
|
import * as API from "../../api";
|
||||||
import RouteSecurity from "./routes/routeSecurity";
|
import RouteSecurity from "./routes/routeSecurity";
|
||||||
import RouteOverview from "./routes/routeoverview";
|
import RouteOverview from "./routes/routeoverview";
|
||||||
|
import IsLoggedIn from "../../isLoggedIn";
|
||||||
|
|
||||||
const RouteConfigPage = () => {
|
const RouteConfigPage = () => {
|
||||||
const { routeName } = useParams();
|
const { routeName } = useParams();
|
||||||
|
@ -28,6 +29,7 @@ const RouteConfigPage = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
|
<IsLoggedIn />
|
||||||
<h2>
|
<h2>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
|
|
@ -7,6 +7,13 @@ import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||||
import { getFaviconURL } from '../../../utils/routes';
|
import { getFaviconURL } from '../../../utils/routes';
|
||||||
import * as API from '../../../api';
|
import * as API from '../../../api';
|
||||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||||
|
import IsLoggedIn from '../../../isLoggedIn';
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
}
|
||||||
|
|
||||||
const RouteOverview = ({ routeConfig }) => {
|
const RouteOverview = ({ routeConfig }) => {
|
||||||
const [openModal, setOpenModal] = React.useState(false);
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
@ -35,7 +42,7 @@ const RouteOverview = ({ routeConfig }) => {
|
||||||
</div>
|
</div>
|
||||||
<Stack spacing={2} >
|
<Stack spacing={2} >
|
||||||
<strong>Description</strong>
|
<strong>Description</strong>
|
||||||
<div>{routeConfig.Description}</div>
|
<div style={info}>{routeConfig.Description}</div>
|
||||||
<strong>URL</strong>
|
<strong>URL</strong>
|
||||||
<div><HostChip route={routeConfig} /></div>
|
<div><HostChip route={routeConfig} /></div>
|
||||||
<strong>Target</strong>
|
<strong>Target</strong>
|
||||||
|
|
|
@ -23,6 +23,9 @@ const NewInstall = () => {
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [counter, setCounter] = useState(0);
|
const [counter, setCounter] = useState(0);
|
||||||
|
let [hostname, setHostname] = useState('');
|
||||||
|
const [databaseEnable, setDatabaseEnable] = useState(true);
|
||||||
|
|
||||||
const refreshStatus = async () => {
|
const refreshStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await API.getStatus()
|
const res = await API.getStatus()
|
||||||
|
@ -34,7 +37,7 @@ const NewInstall = () => {
|
||||||
if (typeof status !== 'undefined') {
|
if (typeof status !== 'undefined') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCounter(counter + 1);
|
setCounter(counter + 1);
|
||||||
}, 2000);
|
}, 2500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@ const NewInstall = () => {
|
||||||
}, [counter]);
|
}, [counter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(activeStep == 4 && status && !status.database) {
|
if(activeStep == 4 && status && !databaseEnable) {
|
||||||
setActiveStep(5);
|
setActiveStep(5);
|
||||||
}
|
}
|
||||||
}, [activeStep, status]);
|
}, [activeStep, status]);
|
||||||
|
@ -122,8 +125,12 @@ const NewInstall = () => {
|
||||||
MongoDBMode: values.DBMode,
|
MongoDBMode: values.DBMode,
|
||||||
MongoDB: values.MongoDB,
|
MongoDB: values.MongoDB,
|
||||||
});
|
});
|
||||||
if(res.status == "OK")
|
if(res.status == "OK") {
|
||||||
|
if(values.DBMode === "DisableUserManagement") {
|
||||||
|
setDatabaseEnable(false);
|
||||||
|
}
|
||||||
setStatus({ success: true });
|
setStatus({ success: true });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus({ success: false });
|
setStatus({ success: false });
|
||||||
setErrors({ submit: error.message });
|
setErrors({ submit: error.message });
|
||||||
|
@ -205,9 +212,14 @@ const NewInstall = () => {
|
||||||
If you enable HTTPS, it will be effective after the next restart.
|
If you enable HTTPS, it will be effective after the next restart.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{status && <div>
|
{status && <>
|
||||||
HTTPS Certificate Mode is currently: <b>{status.HTTPSCertificateMode}</b>
|
<div>
|
||||||
</div>}
|
HTTPS Certificate Mode is currently: <b>{status.HTTPSCertificateMode}</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Hostname is currently: <b>{status.hostname}</b>
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Formik
|
<Formik
|
||||||
|
@ -245,8 +257,10 @@ const NewInstall = () => {
|
||||||
TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '',
|
TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '',
|
||||||
Hostname: values.Hostname,
|
Hostname: values.Hostname,
|
||||||
});
|
});
|
||||||
if(res.status == "OK")
|
if(res.status == "OK") {
|
||||||
setStatus({ success: true });
|
setStatus({ success: true });
|
||||||
|
setHostname((values.HTTPSCertificateMode == "DISABLED" ? "http://" : "https://") + values.Hostname);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus({ success: false });
|
setStatus({ success: false });
|
||||||
setErrors({ submit: "Please check you have filled all the inputs properly" });
|
setErrors({ submit: "Please check you have filled all the inputs properly" });
|
||||||
|
@ -264,11 +278,15 @@ const NewInstall = () => {
|
||||||
["LETSENCRYPT", "Use Let's Encrypt automatic HTTPS (recommended)"],
|
["LETSENCRYPT", "Use Let's Encrypt automatic HTTPS (recommended)"],
|
||||||
["PROVIDED", "Supply my own HTTPS certificate"],
|
["PROVIDED", "Supply my own HTTPS certificate"],
|
||||||
["SELFSIGNED", "Generate a self-signed certificate"],
|
["SELFSIGNED", "Generate a self-signed certificate"],
|
||||||
["DISABLE", "Use HTTP only (not recommended)"],
|
["DISABLED", "Use HTTP only (not recommended)"],
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||||
<>
|
<>
|
||||||
|
<Alert severity="warning">
|
||||||
|
If you are using Cloudflare, make sure the DNS record is <strong>NOT</strong> set to <b>Proxied</b> (you should not see the orange cloud but a grey one).
|
||||||
|
Otherwise Cloudflare will not allow Let's Encrypt to verify your domain.
|
||||||
|
</Alert>
|
||||||
<CosmosInputText
|
<CosmosInputText
|
||||||
name="SSLEmail"
|
name="SSLEmail"
|
||||||
label="Let's Encrypt Email"
|
label="Let's Encrypt Email"
|
||||||
|
@ -457,7 +475,12 @@ const NewInstall = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<LeftOutlined />}
|
startIcon={<LeftOutlined />}
|
||||||
onClick={() => setActiveStep(activeStep - 1)}
|
onClick={() => {
|
||||||
|
if(activeStep == 5 && !databaseEnable) {
|
||||||
|
setActiveStep(activeStep - 2)
|
||||||
|
}
|
||||||
|
setActiveStep(activeStep - 1)
|
||||||
|
}}
|
||||||
disabled={activeStep <= 0}
|
disabled={activeStep <= 0}
|
||||||
>Back</Button>
|
>Back</Button>
|
||||||
|
|
||||||
|
@ -471,7 +494,7 @@ const NewInstall = () => {
|
||||||
step: "5",
|
step: "5",
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/ui/login";
|
window.location.href = hostname + "/ui/login";
|
||||||
}, 500);
|
}, 500);
|
||||||
} else
|
} else
|
||||||
setActiveStep(activeStep + 1)
|
setActiveStep(activeStep + 1)
|
||||||
|
|
93
client/src/pages/servapps/actionBar.jsx
Normal file
93
client/src/pages/servapps/actionBar.jsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { IconButton, Tooltip } from '@mui/material';
|
||||||
|
import { CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
|
||||||
|
import * as API from '../../api';
|
||||||
|
|
||||||
|
const GetActions = ({
|
||||||
|
Id,
|
||||||
|
state,
|
||||||
|
refreshServeApps,
|
||||||
|
setIsUpdatingId
|
||||||
|
}) => {
|
||||||
|
const doTo = (action) => {
|
||||||
|
setIsUpdatingId(Id, true);
|
||||||
|
API.docker.manageContainer(Id, action).then((res) => {
|
||||||
|
refreshServeApps();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let actions = [
|
||||||
|
{
|
||||||
|
t: 'Update Available',
|
||||||
|
if: ['update_available'],
|
||||||
|
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size='large'>
|
||||||
|
<UpCircleOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Start',
|
||||||
|
if: ['exited', 'created'],
|
||||||
|
e: <IconButton onClick={() => {doTo('start')}} size='large'>
|
||||||
|
<PlaySquareOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Unpause',
|
||||||
|
if: ['paused'],
|
||||||
|
e: <IconButton onClick={() => {doTo('unpause')}} size='large'>
|
||||||
|
<PlaySquareOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Pause',
|
||||||
|
if: ['running'],
|
||||||
|
e: <IconButton onClick={() => {doTo('pause')}} size='large'>
|
||||||
|
<PauseCircleOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Stop',
|
||||||
|
if: ['paused', 'restarting', 'running'],
|
||||||
|
e: <IconButton onClick={() => {doTo('stop')}} size='large' variant="outlined">
|
||||||
|
<StopOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Restart',
|
||||||
|
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||||
|
e: <IconButton onClick={() => doTo('restart')} size='large'>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Re-create',
|
||||||
|
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||||
|
e: <IconButton onClick={() => doTo('recreate')} color="error" size='large'>
|
||||||
|
<RollbackOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Kill',
|
||||||
|
if: ['running', 'paused', 'created', 'restarting'],
|
||||||
|
e: <IconButton onClick={() => doTo('kill')} color="error" size='large'>
|
||||||
|
<CloseSquareOutlined />
|
||||||
|
</IconButton>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: 'Delete',
|
||||||
|
if: ['exited', 'created'],
|
||||||
|
e: <IconButton onClick={() => {doTo('remove')}} color="error" size='large'>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return actions.filter((action) => {
|
||||||
|
let updateAvailable = false;
|
||||||
|
return action.if.includes(state) ?? (updateAvailable && action.if.includes('update_available'));
|
||||||
|
}).map((action) => {
|
||||||
|
return <Tooltip title={action.t}>{action.e}</Tooltip>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GetActions;
|
86
client/src/pages/servapps/containers/index.jsx
Normal file
86
client/src/pages/servapps/containers/index.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import RestartModal from '../../config/users/restart';
|
||||||
|
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||||
|
import HostChip from '../../../components/hostChip';
|
||||||
|
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||||
|
import { getFaviconURL } from '../../../utils/routes';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||||
|
import IsLoggedIn from '../../../isLoggedIn';
|
||||||
|
import PrettyTabbedView from '../../../components/tabbedView/tabbedView';
|
||||||
|
import Back from '../../../components/back';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import ContainerOverview from './overview';
|
||||||
|
import Logs from './logs';
|
||||||
|
|
||||||
|
const ContainerIndex = () => {
|
||||||
|
const { containerName } = useParams();
|
||||||
|
const [container, setContainer] = React.useState(null);
|
||||||
|
const [config, setConfig] = React.useState(null);
|
||||||
|
|
||||||
|
const refreshContainer = () => {
|
||||||
|
return Promise.all([API.docker.get(containerName).then((res) => {
|
||||||
|
setContainer(res.data);
|
||||||
|
}),
|
||||||
|
API.config.get().then((res) => {
|
||||||
|
setConfig(res.data);
|
||||||
|
})]);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
refreshContainer();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<Back />
|
||||||
|
<div>{containerName}</div>
|
||||||
|
</Stack>
|
||||||
|
<IsLoggedIn />
|
||||||
|
|
||||||
|
<PrettyTabbedView
|
||||||
|
isLoading={!container || !config}
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
title: 'Overview',
|
||||||
|
children: <ContainerOverview refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Logs',
|
||||||
|
children: <Logs containerInfo={container} config={config}/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Terminal',
|
||||||
|
children: <Logs containerInfo={container} config={config}/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Links',
|
||||||
|
children: <div>Links</div>
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'Advanced'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: 'Setup',
|
||||||
|
children: <div>Image, Restart Policy, Environment Variables, Labels, etc...</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Network',
|
||||||
|
children: <div>Urls, Networks, Ports, etc...</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Volumes',
|
||||||
|
children: <div>Volumes</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Resources',
|
||||||
|
children: <div>Runtime Resources, Capabilities...</div>
|
||||||
|
},
|
||||||
|
]} />
|
||||||
|
</Stack>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerIndex;
|
193
client/src/pages/servapps/containers/logs.jsx
Normal file
193
client/src/pages/servapps/containers/logs.jsx
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Box, Button, Checkbox, CircularProgress, Input, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import { ReactTerminal } from "react-terminal";
|
||||||
|
import LogLine from '../../../components/logLine';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
const Logs = ({ containerInfo }) => {
|
||||||
|
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||||
|
const containerName = Name;
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [errorOnly, setErrorOnly] = useState(false);
|
||||||
|
const [limit, setLimit] = useState(100);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [page, setPage] = useState('');
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [hasScrolled, setHasScrolled] = useState(false);
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [forceUpdate, setForceUpdate] = useState(false);
|
||||||
|
const [lastReceivedLogs, setLastReceivedLogs] = useState('');
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
const [scrollToMe, setScrollToMe] = useState(null);
|
||||||
|
const screenMin = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
|
||||||
|
const bottomRef = useRef(null);
|
||||||
|
const topRef = useRef(null);
|
||||||
|
const terminalRef = useRef(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLogs = async (reset, ignoreState) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await API.docker.getContainerLogs(
|
||||||
|
containerName,
|
||||||
|
searchTerm,
|
||||||
|
limit,
|
||||||
|
ignoreState ? '' : lastReceivedLogs,
|
||||||
|
errorOnly
|
||||||
|
);
|
||||||
|
const { data } = response;
|
||||||
|
if (data.length > 0) {
|
||||||
|
const date = data[0].output.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)[0];
|
||||||
|
if (date) {
|
||||||
|
date.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.)(\d+)Z/, (match, p1, p2) => {
|
||||||
|
const newNumber = parseInt(p2) - 1;
|
||||||
|
const newDate = `${p1}${newNumber}Z`;
|
||||||
|
setLastReceivedLogs(newDate);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Could not parse date from log: ', data[0]);
|
||||||
|
setLastReceivedLogs('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(reset) {
|
||||||
|
setLogs(data);
|
||||||
|
} else {
|
||||||
|
// const current = topRef.current;
|
||||||
|
// setScrollToMe(() => current);
|
||||||
|
setLogs((logs) => [...data, ...logs]);
|
||||||
|
// calculate the height of the new logs and scroll to that position
|
||||||
|
// OK I will fix this later
|
||||||
|
// const newHeight = 999999999;
|
||||||
|
// terminalRef.current.scrollTop = newHeight;
|
||||||
|
}
|
||||||
|
setHasMore(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs(true);
|
||||||
|
}, [searchTerm, errorOnly, limit]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetching) return;
|
||||||
|
fetchLogs();
|
||||||
|
}, [fetching]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasScrolled) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
// scrollToMe && scrollToMe.scrollIntoView({ });
|
||||||
|
// setScrollToMe(null);
|
||||||
|
}
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
const handleScroll = (event) => {
|
||||||
|
const { scrollTop } = event.target;
|
||||||
|
setHasScrolled(true);
|
||||||
|
if (scrollTop === 0) {
|
||||||
|
if(!hasMore) return;
|
||||||
|
setFetching(true);
|
||||||
|
setHasMore(false);
|
||||||
|
} else {
|
||||||
|
setHasMore(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={2} sx={{ width: '100%'}}>
|
||||||
|
<Stack
|
||||||
|
spacing={2}
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
<Stack direction={screenMin ? 'row' : 'column'} spacing={3}>
|
||||||
|
<Stack direction="row" spacing={3}>
|
||||||
|
<Input
|
||||||
|
label="Search"
|
||||||
|
value={searchTerm}
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(e) => {
|
||||||
|
setHasScrolled(false);
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
setLastReceivedLogs('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Checkbox
|
||||||
|
checked={errorOnly}
|
||||||
|
onChange={(e) => {
|
||||||
|
setHasScrolled(false);
|
||||||
|
setErrorOnly(e.target.checked);
|
||||||
|
setLastReceivedLogs('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Error Only
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={3}>
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
label="Limit"
|
||||||
|
type="number"
|
||||||
|
value={limit}
|
||||||
|
onChange={(e) => {
|
||||||
|
setHasScrolled(false);
|
||||||
|
setLimit(e.target.value);
|
||||||
|
setLastReceivedLogs('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setHasScrolled(false);
|
||||||
|
setLastReceivedLogs('');
|
||||||
|
fetchLogs(true, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
{loading && <CircularProgress />}
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
ref={terminalRef}
|
||||||
|
sx={{
|
||||||
|
maxHeight: 'calc(1vh * 80 - 200px)',
|
||||||
|
overflow: 'auto',
|
||||||
|
paddingRight: '10px',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
background: '#272d36',
|
||||||
|
color: '#fff',
|
||||||
|
borderTop: '3px solid ' + theme.palette.primary.main
|
||||||
|
}}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={log.index} style={{paddingTop: (!screenMin) ? '10px' : '2px'}}>
|
||||||
|
<LogLine message={log.output} docker isMobile={!screenMin} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{fetching && <CircularProgress sx={{ mt: 1, mb: 2 }} />}
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logs;
|
153
client/src/pages/servapps/containers/overview.jsx
Normal file
153
client/src/pages/servapps/containers/overview.jsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Checkbox, Chip, CircularProgress, Stack, Typography, useMediaQuery } from '@mui/material';
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import { ContainerOutlined, DesktopOutlined, InfoCircleOutlined, NodeExpandOutlined, PlayCircleOutlined, PlusCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { getFaviconURL, getContainersRoutes } from '../../../utils/routes';
|
||||||
|
import HostChip from '../../../components/hostChip';
|
||||||
|
import ExposeModal from '../exposeModal';
|
||||||
|
import * as API from '../../../api';
|
||||||
|
import RestartModal from '../../config/users/restart';
|
||||||
|
import GetActions from '../actionBar';
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContainerOverview = ({ containerInfo, config, refresh }) => {
|
||||||
|
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||||
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
const [openRestartModal, setOpenRestartModal] = React.useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
|
||||||
|
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||||
|
const Image = Config.Image;
|
||||||
|
const IPAddress = NetworkSettings.Networks?.[Object.keys(NetworkSettings.Networks)[0]]?.IPAddress;
|
||||||
|
const Health = State.Health;
|
||||||
|
const healthStatus = Health ? Health.Status : 'Healthy';
|
||||||
|
const healthIconColor = healthStatus === 'Healthy' ? 'green' : 'red';
|
||||||
|
const routes = getContainersRoutes(config, Name.replace('/', ''));
|
||||||
|
|
||||||
|
let refreshAll = refresh && (() => refresh().then(() => {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateRoutes = (newRoute) => {
|
||||||
|
API.config.addRoute(newRoute).then(() => {
|
||||||
|
refreshAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewRoute = async () => {
|
||||||
|
const apps = (await API.docker.list()).data;
|
||||||
|
const app = apps.find((a) => a.Names[0] === Name);
|
||||||
|
setOpenModal(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '1000px', width: '100%' }}>
|
||||||
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
|
<ExposeModal
|
||||||
|
openModal={openModal}
|
||||||
|
setOpenModal={setOpenModal}
|
||||||
|
container={containerInfo}
|
||||||
|
config={config}
|
||||||
|
updateRoutes={
|
||||||
|
(_newRoute) => {
|
||||||
|
updateRoutes(_newRoute);
|
||||||
|
setOpenModal(false);
|
||||||
|
setOpenRestartModal(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MainCard name={Name} title={<div>{Name}</div>}>
|
||||||
|
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||||
|
<Stack spacing={2} direction={'column'} justifyContent={'center'} alignItems={'center'}>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<img className={isUpdating ? 'darken' : ''} src={getFaviconURL(routes && routes[0])} width="128px" />
|
||||||
|
{isUpdating ? (
|
||||||
|
<CircularProgress
|
||||||
|
style={{ position: 'absolute', top: 'calc(50% - 22.5px)', left: 'calc(50% - 22.5px)' }}
|
||||||
|
width="128px"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{({
|
||||||
|
"created": <Chip label="Created" color="warning" />,
|
||||||
|
"restarting": <Chip label="Restarting" color="warning" />,
|
||||||
|
"running": <Chip label="Running" color="success" />,
|
||||||
|
"removing": <Chip label="Removing" color="error" />,
|
||||||
|
"paused": <Chip label="Paused" color="info" />,
|
||||||
|
"exited": <Chip label="Exited" color="error" />,
|
||||||
|
"dead": <Chip label="Dead" color="error" />,
|
||||||
|
})[State.Status]}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={2} style={{ width: '100%' }} >
|
||||||
|
<Stack spacing={2} direction={'row'} >
|
||||||
|
<GetActions
|
||||||
|
Id={containerInfo.Id}
|
||||||
|
state={State.Status}
|
||||||
|
refreshServeApps={() => {
|
||||||
|
refreshAll()
|
||||||
|
}}
|
||||||
|
setIsUpdatingId={() => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<strong><ContainerOutlined /> Image</strong>
|
||||||
|
<div style={info}>{Image}</div>
|
||||||
|
<strong><DesktopOutlined /> Name</strong>
|
||||||
|
<div style={info}>{Name}</div>
|
||||||
|
<strong><InfoCircleOutlined /> IP Address</strong>
|
||||||
|
<div style={info}>{IPAddress}</div>
|
||||||
|
<strong>
|
||||||
|
<SafetyCertificateOutlined/> Health
|
||||||
|
</strong>
|
||||||
|
<div style={info}>{healthStatus}</div>
|
||||||
|
<strong><SettingOutlined /> Settings {State.Status !== 'running' ? '(Start container to edit)' : ''}</strong>
|
||||||
|
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={Config.Labels['cosmos-force-network-secured'] === 'true'}
|
||||||
|
disabled={State.Status !== 'running' || isUpdating}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
API.docker.secure(Name, e.target.checked).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshAll();
|
||||||
|
}, 3000);
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/> Force Secure Network
|
||||||
|
</Stack>
|
||||||
|
<strong><NodeExpandOutlined /> URLs</strong>
|
||||||
|
<div>
|
||||||
|
{routes.map((route) => {
|
||||||
|
return <HostChip route={route} settings style={{margin: '5px'}}/>
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
<Chip
|
||||||
|
label="New"
|
||||||
|
color="primary"
|
||||||
|
style={{paddingRight: '4px', margin: '5px'}}
|
||||||
|
deleteIcon={<PlusCircleOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
addNewRoute();
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
addNewRoute();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</MainCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContainerOverview;
|
98
client/src/pages/servapps/exposeModal.jsx
Normal file
98
client/src/pages/servapps/exposeModal.jsx
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack } from '@mui/material';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import RouteManagement from '../config/routes/routeman';
|
||||||
|
import { ValidateRoute, getFaviconURL, sanitizeRoute, getContainersRoutes } from '../../utils/routes';
|
||||||
|
import * as API from '../../api';
|
||||||
|
|
||||||
|
const getHostnameFromName = (name) => {
|
||||||
|
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExposeModal = ({ openModal, setOpenModal, config, updateRoutes, container }) => {
|
||||||
|
const [submitErrors, setSubmitErrors] = useState([]);
|
||||||
|
const [newRoute, setNewRoute] = useState(null);
|
||||||
|
|
||||||
|
let containerName = openModal && (openModal.Names[0]);
|
||||||
|
|
||||||
|
const hasCosmosNetwork = () => {
|
||||||
|
return container && container.NetworkSettings.Networks && Object.keys(container.NetworkSettings.Networks).some((network) => {
|
||||||
|
if(network.startsWith('cosmos-network'))
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||||
|
<DialogTitle>Expose ServApp</DialogTitle>
|
||||||
|
{openModal && <>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<div>
|
||||||
|
Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{openModal && !hasCosmosNetwork(containerName) && <Alert severity="warning">This ServApp does not appear to be connected to a Cosmos Network, so the hostname might not be accessible. The easiest way to fix this is to check the box "Force Secure Network" or manually create a sub-network in Docker.</Alert>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<RouteManagement TargetContainer={openModal}
|
||||||
|
routeConfig={{
|
||||||
|
Target: "http://"+containerName.replace('/', '') + ":",
|
||||||
|
Mode: "SERVAPP",
|
||||||
|
Name: containerName.replace('/', ''),
|
||||||
|
Description: "Expose " + containerName.replace('/', '') + " to the internet",
|
||||||
|
UseHost: true,
|
||||||
|
Host: getHostnameFromName(containerName),
|
||||||
|
UsePathPrefix: false,
|
||||||
|
PathPrefix: '',
|
||||||
|
CORSOrigin: '',
|
||||||
|
StripPathPrefix: false,
|
||||||
|
AuthEnabled: false,
|
||||||
|
Timeout: 14400000,
|
||||||
|
ThrottlePerMinute: 10000,
|
||||||
|
BlockCommonBots: true,
|
||||||
|
SmartShield: {
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||||
|
setRouteConfig={(_newRoute) => {
|
||||||
|
setNewRoute(sanitizeRoute(_newRoute));
|
||||||
|
}}
|
||||||
|
up={() => {}}
|
||||||
|
down={() => {}}
|
||||||
|
deleteRoute={() => {}}
|
||||||
|
noControls
|
||||||
|
lockTarget
|
||||||
|
/>
|
||||||
|
</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={() => setOpenModal(false)}>Cancel</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
let errors = ValidateRoute(newRoute, config);
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
errors = errors.map((err) => {
|
||||||
|
return `${err}`;
|
||||||
|
});
|
||||||
|
setSubmitErrors(errors);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
setSubmitErrors([]);
|
||||||
|
updateRoutes(newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
}}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>}
|
||||||
|
</Dialog>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExposeModal;
|
41
client/src/pages/servapps/index.jsx
Normal file
41
client/src/pages/servapps/index.jsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import MainCard from '../../components/MainCard';
|
||||||
|
import RestartModal from '../config/users/restart';
|
||||||
|
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||||
|
import HostChip from '../../components/hostChip';
|
||||||
|
import { RouteMode, RouteSecurity } from '../../components/routeComponents';
|
||||||
|
import { getFaviconURL } from '../../utils/routes';
|
||||||
|
import * as API from '../../api';
|
||||||
|
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||||
|
import IsLoggedIn from '../../isLoggedIn';
|
||||||
|
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||||
|
import ServeApps from './servapps';
|
||||||
|
import VolumeManagementList from './volumes';
|
||||||
|
import NetworkManagementList from './networks';
|
||||||
|
|
||||||
|
const ServappsIndex = () => {
|
||||||
|
return <div>
|
||||||
|
<IsLoggedIn />
|
||||||
|
|
||||||
|
<PrettyTabbedView path="/ui/servapps/:tab" tabs={[
|
||||||
|
{
|
||||||
|
title: 'Containers',
|
||||||
|
children: <ServeApps />,
|
||||||
|
path: 'containers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Volumes',
|
||||||
|
children: <VolumeManagementList />,
|
||||||
|
path: 'volumes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Networks',
|
||||||
|
children: <NetworkManagementList />,
|
||||||
|
path: 'networks'
|
||||||
|
},
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServappsIndex;
|
118
client/src/pages/servapps/networks.jsx
Normal file
118
client/src/pages/servapps/networks.jsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
// material-ui
|
||||||
|
import { CloseSquareOutlined, DeleteOutlined, PlusCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Chip, CircularProgress, Stack, useTheme } from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as API from '../../api';
|
||||||
|
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||||
|
|
||||||
|
const NetworkManagementList = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [rows, setRows] = useState(null);
|
||||||
|
const [tryDelete, setTryDelete] = useState(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
setIsLoading(true);
|
||||||
|
API.docker.networkList()
|
||||||
|
.then(data => {
|
||||||
|
setRows(data.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction='row' spacing={1} style={{ marginBottom: '20px' }}>
|
||||||
|
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={refresh}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{isLoading && (<div style={{ height: '550px' }}>
|
||||||
|
<center>
|
||||||
|
<br />
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && rows && (
|
||||||
|
<PrettyTableView
|
||||||
|
data={rows}
|
||||||
|
onRowClick={() => { }}
|
||||||
|
getKey={(r) => r.Id}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'Network Name',
|
||||||
|
field: (r) => <Stack direction='column'>
|
||||||
|
<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', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Driver} driver</div>
|
||||||
|
</Stack>,
|
||||||
|
search: (r) => r.Name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Properties',
|
||||||
|
screenMin: 'md',
|
||||||
|
field: (r) => (
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Chip label={r.Scope} color="primary" />
|
||||||
|
{r.Internal && <Chip label="Internal" color="secondary" />}
|
||||||
|
{r.Attachable && <Chip label="Attachable" color="success" />}
|
||||||
|
{r.Ingress && <Chip label="Ingress" color="warning" />}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IPAM gateway / mask',
|
||||||
|
screenMin: 'lg',
|
||||||
|
field: (r) => r.IPAM.Config.map((config, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{config.Gateway} / {config.Subnet}
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created At',
|
||||||
|
screenMin: 'lg',
|
||||||
|
field: (r) => new Date(r.Created).toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
clickable: true,
|
||||||
|
field: (r) => (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<DeleteOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
if (tryDelete === r.Id) {
|
||||||
|
setIsLoading(true);
|
||||||
|
API.docker.networkDelete(r.Id).then(() => {
|
||||||
|
refresh();
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTryDelete(r.Id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tryDelete === r.Id ? "Really?" : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NetworkManagementList;
|
|
@ -11,8 +11,11 @@ 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 from '../config/routes/routeman';
|
import RouteManagement from '../config/routes/routeman';
|
||||||
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
import { ValidateRoute, getFaviconURL, sanitizeRoute, getContainersRoutes } from '../../utils/routes';
|
||||||
import HostChip from '../../components/hostChip';
|
import HostChip from '../../components/hostChip';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import ExposeModal from './exposeModal';
|
||||||
|
import GetActions from './actionBar';
|
||||||
|
|
||||||
const Item = styled(Paper)(({ theme }) => ({
|
const Item = styled(Paper)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
||||||
|
@ -39,16 +42,6 @@ const ServeApps = () => {
|
||||||
const [submitErrors, setSubmitErrors] = useState([]);
|
const [submitErrors, setSubmitErrors] = useState([]);
|
||||||
const [openRestartModal, setOpenRestartModal] = useState(false);
|
const [openRestartModal, setOpenRestartModal] = useState(false);
|
||||||
|
|
||||||
const hasCosmosNetwork = (containerName) => {
|
|
||||||
const container = serveApps.find((app) => {
|
|
||||||
return app.Names[0].replace('/', '') === containerName.replace('/', '');
|
|
||||||
});
|
|
||||||
return container && container.NetworkSettings.Networks && Object.keys(container.NetworkSettings.Networks).some((network) => {
|
|
||||||
if(network.startsWith('cosmos-network'))
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshServeApps = () => {
|
const refreshServeApps = () => {
|
||||||
API.docker.list().then((res) => {
|
API.docker.list().then((res) => {
|
||||||
setServeApps(res.data);
|
setServeApps(res.data);
|
||||||
|
@ -66,22 +59,11 @@ const ServeApps = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContainersRoutes = (containerName) => {
|
|
||||||
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
|
||||||
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
|
||||||
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
|
||||||
// (
|
|
||||||
// route.Target.startsWith(containerName) ||
|
|
||||||
// route.Target.split('://')[1].startsWith(containerName)
|
|
||||||
// )
|
|
||||||
})) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshServeApps();
|
refreshServeApps();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function updateRoutes() {
|
function updateRoutes(newRoute) {
|
||||||
let con = {
|
let con = {
|
||||||
...config,
|
...config,
|
||||||
HTTPConfig: {
|
HTTPConfig: {
|
||||||
|
@ -112,12 +94,15 @@ const ServeApps = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHostnameFromName = (name) => {
|
const selectable = {
|
||||||
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
cursor: 'pointer',
|
||||||
|
"&:hover": {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstRouteFavIcon = (app) => {
|
const getFirstRouteFavIcon = (app) => {
|
||||||
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
let routes = getContainersRoutes(config, app.Names[0].replace('/', ''));
|
||||||
if(routes.length > 0) {
|
if(routes.length > 0) {
|
||||||
let url = getFaviconURL(routes[0]);
|
let url = getFaviconURL(routes[0]);
|
||||||
return url;
|
return url;
|
||||||
|
@ -126,162 +111,21 @@ const ServeApps = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActions = (app) => {
|
|
||||||
const doTo = (action) => {
|
|
||||||
setIsUpdatingId(app.Id, true);
|
|
||||||
API.docker.manageContainer(app.Id, action).then((res) => {
|
|
||||||
refreshServeApps();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let actions = [
|
|
||||||
{
|
|
||||||
t: 'Update Available',
|
|
||||||
if: ['update_available'],
|
|
||||||
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size='large'>
|
|
||||||
<UpCircleOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Start',
|
|
||||||
if: ['exited'],
|
|
||||||
e: <IconButton onClick={() => {doTo('start')}} size='large'>
|
|
||||||
<PlaySquareOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Unpause',
|
|
||||||
if: ['paused'],
|
|
||||||
e: <IconButton onClick={() => {doTo('unpause')}} size='large'>
|
|
||||||
<PlaySquareOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Pause',
|
|
||||||
if: ['running'],
|
|
||||||
e: <IconButton onClick={() => {doTo('pause')}} size='large'>
|
|
||||||
<PauseCircleOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Stop',
|
|
||||||
if: ['created', 'paused', 'restarting', 'running'],
|
|
||||||
e: <IconButton onClick={() => {doTo('stop')}} size='large' variant="outlined">
|
|
||||||
<StopOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Restart',
|
|
||||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
|
||||||
e: <IconButton onClick={() => doTo('restart')} size='large'>
|
|
||||||
<ReloadOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Re-create',
|
|
||||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
|
||||||
e: <IconButton onClick={() => doTo('recreate')} color="error" size='large'>
|
|
||||||
<RollbackOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Delete',
|
|
||||||
if: ['exited'],
|
|
||||||
e: <IconButton onClick={() => {doTo('remove')}} color="error" size='large'>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</IconButton>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
t: 'Kill',
|
|
||||||
if: ['running', 'paused', 'created', 'restarting'],
|
|
||||||
e: <IconButton onClick={() => doTo('kill')} color="error" size='large'>
|
|
||||||
<CloseSquareOutlined />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return actions.filter((action) => {
|
|
||||||
let updateAvailable = false;
|
|
||||||
return action.if.includes(app.State) ?? (updateAvailable && action.if.includes('update_available'));
|
|
||||||
}).map((action) => {
|
|
||||||
return <Tooltip title={action.t}>{action.e}</Tooltip>
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<IsLoggedIn />
|
|
||||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
<ExposeModal
|
||||||
<DialogTitle>Expose ServApp</DialogTitle>
|
openModal={openModal}
|
||||||
{openModal && <>
|
setOpenModal={setOpenModal}
|
||||||
<DialogContent>
|
container={serveApps.find((app) => {
|
||||||
<DialogContentText>
|
return app.Names[0].replace('/', '') === openModal && openModal.Names[0].replace('/', '');
|
||||||
<Stack spacing={2}>
|
})}
|
||||||
<div>
|
config={config}
|
||||||
Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL.
|
updateRoutes={
|
||||||
</div>
|
(_newRoute) => {
|
||||||
<div>
|
updateRoutes(_newRoute);
|
||||||
{openModal && !hasCosmosNetwork(openModal.Names[0]) && <Alert severity="warning">This ServApp does not appear to be connected to a Cosmos Network, so the hostname might not be accessible. The easiest way to fix this is to check the box "Force Secure Network" or manually create a sub-network in Docker.</Alert>}
|
}
|
||||||
</div>
|
}
|
||||||
<div>
|
/>
|
||||||
<RouteManagement TargetContainer={openModal}
|
|
||||||
routeConfig={{
|
|
||||||
Target: "http://"+openModal.Names[0].replace('/', '') + ":",
|
|
||||||
Mode: "SERVAPP",
|
|
||||||
Name: openModal.Names[0].replace('/', ''),
|
|
||||||
Description: "Expose " + openModal.Names[0].replace('/', '') + " to the internet",
|
|
||||||
UseHost: true,
|
|
||||||
Host: getHostnameFromName(openModal.Names[0]),
|
|
||||||
UsePathPrefix: false,
|
|
||||||
PathPrefix: '',
|
|
||||||
CORSOrigin: '',
|
|
||||||
StripPathPrefix: false,
|
|
||||||
AuthEnabled: false,
|
|
||||||
Timeout: 14400000,
|
|
||||||
ThrottlePerMinute: 10000,
|
|
||||||
BlockCommonBots: true,
|
|
||||||
SmartShield: {
|
|
||||||
Enabled: true,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
|
||||||
setRouteConfig={(_newRoute) => {
|
|
||||||
setNewRoute(sanitizeRoute(_newRoute));
|
|
||||||
}}
|
|
||||||
up={() => {}}
|
|
||||||
down={() => {}}
|
|
||||||
deleteRoute={() => {}}
|
|
||||||
noControls
|
|
||||||
lockTarget
|
|
||||||
/>
|
|
||||||
</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={() => setOpenModal(false)}>Cancel</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
let errors = ValidateRoute(newRoute, config);
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
errors = errors.map((err) => {
|
|
||||||
return `${err}`;
|
|
||||||
});
|
|
||||||
setSubmitErrors(errors);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
setSubmitErrors([]);
|
|
||||||
updateRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
}}>Confirm</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</>}
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
|
@ -306,7 +150,7 @@ const ServeApps = () => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
{serveApps && serveApps.filter(app => search.length < 2 || app.Names[0].toLowerCase().includes(search.toLowerCase())).map((app) => {
|
{serveApps && serveApps.filter(app => search.length < 2 || app.Names[0].toLowerCase().includes(search.toLowerCase())).map((app) => {
|
||||||
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
||||||
<Item>
|
<Item>
|
||||||
|
@ -342,8 +186,7 @@ const ServeApps = () => {
|
||||||
{/* <Button variant="contained" size="small" onClick={() => {}}>
|
{/* <Button variant="contained" size="small" onClick={() => {}}>
|
||||||
Update
|
Update
|
||||||
</Button> */}
|
</Button> */}
|
||||||
|
<GetActions Id={app.Id} state={app.State} setIsUpdatingId={setIsUpdatingId} refreshServeApps={refreshServeApps} />
|
||||||
{getActions(app)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
|
@ -369,35 +212,39 @@ const ServeApps = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isUpdating[app.Id] ? <div>
|
{isUpdating[app.Id] ? <div>
|
||||||
<CircularProgress color="inherit" />
|
<CircularProgress color="inherit" />
|
||||||
</div> :
|
</div>
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
:
|
||||||
<Typography variant="h6" color="text.secondary">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
Settings
|
<Typography variant="h6" color="text.secondary">
|
||||||
</Typography>
|
Settings {app.State !== 'running' ? '(Start container to edit)' : ''}
|
||||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
</Typography>
|
||||||
<Checkbox
|
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||||
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
|
<Checkbox
|
||||||
onChange={(e) => {
|
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
|
||||||
setIsUpdatingId(app.Id, true);
|
disabled={app.State !== 'running'}
|
||||||
API.docker.secure(app.Id, e.target.checked).then(() => {
|
onChange={(e) => {
|
||||||
setTimeout(() => {
|
setIsUpdatingId(app.Id, true);
|
||||||
setIsUpdatingId(app.Id, false);
|
API.docker.secure(app.Id, e.target.checked).then(() => {
|
||||||
refreshServeApps();
|
setTimeout(() => {
|
||||||
}, 3000);
|
setIsUpdatingId(app.Id, false);
|
||||||
})
|
refreshServeApps();
|
||||||
}}
|
}, 3000);
|
||||||
/> Force Secure Network
|
})
|
||||||
</Stack></Stack>}
|
}}
|
||||||
|
/> Force Secure Network
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
<Typography variant="h6" color="text.secondary">
|
<Typography variant="h6" color="text.secondary">
|
||||||
URLs
|
URLs
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack style={noOver} spacing={2} direction="row">
|
<Stack style={noOver} spacing={2} direction="row">
|
||||||
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
{getContainersRoutes(config, app.Names[0].replace('/', '')).map((route) => {
|
||||||
return <HostChip route={route} settings/>
|
return <HostChip route={route} settings/>
|
||||||
})}
|
})}
|
||||||
{/* {getContainersRoutes(app.Names[0].replace('/', '')).length == 0 && */}
|
{/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */}
|
||||||
<Chip
|
<Chip
|
||||||
label="New"
|
label="New"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -413,6 +260,11 @@ const ServeApps = () => {
|
||||||
{/* } */}
|
{/* } */}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Link to={`/ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
|
||||||
|
<Button variant="outlined" color="primary" fullWidth>Details</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
{/* <Stack>
|
{/* <Stack>
|
||||||
<Button variant="contained" color="primary" onClick={() => {
|
<Button variant="contained" color="primary" onClick={() => {
|
||||||
setOpenModal(app);
|
setOpenModal(app);
|
||||||
|
|
116
client/src/pages/servapps/volumes.jsx
Normal file
116
client/src/pages/servapps/volumes.jsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// material-ui
|
||||||
|
import { AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, SyncOutlined, UpCircleOutlined, UpSquareFilled } from '@ant-design/icons';
|
||||||
|
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Input, InputAdornment, TextField, Tooltip, Typography, useTheme } 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';
|
||||||
|
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||||
|
|
||||||
|
const VolumeManagementList = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [rows, setRows] = useState(null);
|
||||||
|
const [tryDelete, setTryDelete] = useState(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
setIsLoading(true);
|
||||||
|
API.docker.volumeList()
|
||||||
|
.then(data => {
|
||||||
|
setRows(data.data.Volumes);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction='row' spacing={1} style={{ marginBottom: '20px' }}>
|
||||||
|
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={refresh}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{isLoading && (<div style={{height: '550px'}}>
|
||||||
|
<center>
|
||||||
|
<br />
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && rows && (
|
||||||
|
<PrettyTableView
|
||||||
|
data={rows}
|
||||||
|
onRowClick={() => {}}
|
||||||
|
getKey={(r) => r.Name}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'Volume Name',
|
||||||
|
field: (r) => <Stack direction='column'>
|
||||||
|
<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', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Mountpoint}</div>
|
||||||
|
</Stack>,
|
||||||
|
search: (r) => r.Name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Driver',
|
||||||
|
screenMin: 'lg',
|
||||||
|
field: (r) => r.Driver,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Scope',
|
||||||
|
screenMin: 'lg',
|
||||||
|
field: (r) => r.Scope,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created At',
|
||||||
|
screenMin: 'lg',
|
||||||
|
field: (r) => new Date(r.CreatedAt).toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
clickable: true,
|
||||||
|
field: (r) => (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<DeleteOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
if(tryDelete === r.Name) {
|
||||||
|
setIsLoading(true);
|
||||||
|
API.docker.volumeDelete(r.Name).then(() => {
|
||||||
|
refresh();
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTryDelete(r.Name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tryDelete === r.Name ? "Really?" : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VolumeManagementList;
|
|
@ -6,11 +6,12 @@ import MainLayout from '../layout/MainLayout';
|
||||||
import UserManagement from '../pages/config/users/usermanagement';
|
import UserManagement from '../pages/config/users/usermanagement';
|
||||||
import ConfigManagement from '../pages/config/users/configman';
|
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 ServeAppsIndex from '../pages/servapps/';
|
||||||
import { Navigate } from 'react-router';
|
import { Navigate } from 'react-router';
|
||||||
import RouteConfigPage from '../pages/config/routeConfigPage';
|
import RouteConfigPage from '../pages/config/routeConfigPage';
|
||||||
import logo from '../assets/images/icons/cosmos.png';
|
import logo from '../assets/images/icons/cosmos.png';
|
||||||
import HomePage from '../pages/home';
|
import HomePage from '../pages/home';
|
||||||
|
import ContainerIndex from '../pages/servapps/containers';
|
||||||
|
|
||||||
|
|
||||||
// render - dashboard
|
// render - dashboard
|
||||||
|
@ -52,7 +53,7 @@ const MainRoutes = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/ui/servapps',
|
path: '/ui/servapps',
|
||||||
element: <ServeApps />
|
element: <ServeAppsIndex />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/ui/config-users',
|
path: '/ui/config-users',
|
||||||
|
@ -70,6 +71,10 @@ const MainRoutes = {
|
||||||
path: '/ui/config-url/:routeName',
|
path: '/ui/config-url/:routeName',
|
||||||
element: <RouteConfigPage />,
|
element: <RouteConfigPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ui/servapps/containers/:containerName',
|
||||||
|
element: <ContainerIndex />,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,10 @@ export const getFaviconURL = (route) => {
|
||||||
return demoicons[route.Name] || logogray;
|
return demoicons[route.Name] || logogray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!route) {
|
||||||
|
return logogray;
|
||||||
|
}
|
||||||
|
|
||||||
const addRemote = (url) => {
|
const addRemote = (url) => {
|
||||||
return '/cosmos/api/favicon?q=' + encodeURIComponent(url)
|
return '/cosmos/api/favicon?q=' + encodeURIComponent(url)
|
||||||
}
|
}
|
||||||
|
@ -102,4 +106,11 @@ export const ValidateRoute = (routeConfig, config) => {
|
||||||
return ['Route Name already exists. Name must be unique.'];
|
return ['Route Name already exists. Name must be unique.'];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContainersRoutes = (config, containerName) => {
|
||||||
|
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
||||||
|
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
||||||
|
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
||||||
|
})) || [];
|
||||||
}
|
}
|
73
package-lock.json
generated
73
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.3.0-unstable",
|
"version": "0.4.0-unstable2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.3.0-unstable",
|
"version": "0.4.0-unstable2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^6.0.0",
|
"@ant-design/colors": "^6.0.0",
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^4.7.0",
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
"react-router": "^6.4.1",
|
"react-router": "^6.4.1",
|
||||||
"react-router-dom": "^6.4.1",
|
"react-router-dom": "^6.4.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"react-terminal": "^1.3.1",
|
||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"simplebar": "^5.3.8",
|
"simplebar": "^5.3.8",
|
||||||
|
@ -8116,6 +8117,50 @@
|
||||||
"react": ">= 0.14.0"
|
"react": ">= 0.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-terminal": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-lbkrih1be0nlJptZR7uwV6YF8PMuxKJOKhGN+GVuFKp9dY/qpSYF76KMGZZJnWbbxAt5Bkf+aUt4iyy5F8NBdQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-device-detect": "2.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-terminal/node_modules/react-device-detect": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-N42xttwez3ECgu4KpOL2ICesdfoz8NCBfmc1rH9FRYSjH7NmMyANPSrQ3EvAtJyj/6TzJNhrANSO38iXjCB2Ug==",
|
||||||
|
"dependencies": {
|
||||||
|
"ua-parser-js": "^0.7.30"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 0.14.0 < 18.0.0",
|
||||||
|
"react-dom": ">= 0.14.0 < 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-terminal/node_modules/ua-parser-js": {
|
||||||
|
"version": "0.7.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
|
||||||
|
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
@ -15065,6 +15110,30 @@
|
||||||
"refractor": "^3.6.0"
|
"refractor": "^3.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-terminal": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-lbkrih1be0nlJptZR7uwV6YF8PMuxKJOKhGN+GVuFKp9dY/qpSYF76KMGZZJnWbbxAt5Bkf+aUt4iyy5F8NBdQ==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-device-detect": "2.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-device-detect": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-N42xttwez3ECgu4KpOL2ICesdfoz8NCBfmc1rH9FRYSjH7NmMyANPSrQ3EvAtJyj/6TzJNhrANSO38iXjCB2Ug==",
|
||||||
|
"requires": {
|
||||||
|
"ua-parser-js": "^0.7.30"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ua-parser-js": {
|
||||||
|
"version": "0.7.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
|
||||||
|
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-transition-group": {
|
"react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.4.0-unstable2",
|
"version": "0.4.0-unstable3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
"react-router": "^6.4.1",
|
"react-router": "^6.4.1",
|
||||||
"react-router-dom": "^6.4.1",
|
"react-router-dom": "^6.4.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"react-terminal": "^1.3.1",
|
||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"simplebar": "^5.3.8",
|
"simplebar": "^5.3.8",
|
||||||
|
@ -53,11 +54,11 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"client": "vite",
|
"client": "vite",
|
||||||
"client-build": "vite build --base=/ui/",
|
"client-build": "vite build --base=/ui/",
|
||||||
"start": "env CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
|
"start": "env COSMOS_HOSTNAME=localhost CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
|
||||||
"build": " sh build.sh",
|
"build": " sh build.sh",
|
||||||
"dev": "npm run build && npm run start",
|
"dev": "npm run build && npm run start",
|
||||||
"dockerdevbuild": "sh build.sh && npm run client-build && docker build --tag cosmos-dev .",
|
"dockerdevbuild": "sh build.sh && npm run client-build && docker build --tag cosmos-dev .",
|
||||||
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_HOSTNAME=localhost -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
|
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
|
||||||
"dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
|
"dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
|
||||||
"demo": "vite build --base=/ui/ --mode demo",
|
"demo": "vite build --base=/ui/ --mode demo",
|
||||||
"devdemo": "vite --mode demo"
|
"devdemo": "vite --mode demo"
|
||||||
|
|
46
src/docker/api_getcontainers.go
Normal file
46
src/docker/api_getcontainers.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
containerId := vars["containerId"]
|
||||||
|
|
||||||
|
|
||||||
|
if req.Method == "GET" {
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("GetContainerRoute", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get Docker container
|
||||||
|
container, err := DockerClient.ContainerInspect(context.Background(), containerId)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("GetContainerRoute: Error while getting container", err)
|
||||||
|
utils.HTTPError(w, "Container Get Error: " + err.Error(), http.StatusInternalServerError, "LN002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": container,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("GetContainerRoute: Method not allowed " + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
140
src/docker/api_getlogs.go
Normal file
140
src/docker/api_getlogs.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogOutput struct {
|
||||||
|
StreamType byte `json:"streamType"`
|
||||||
|
Size uint32 `json:"size"`
|
||||||
|
Output string `json:"output"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDockerLogHeader parses the first 8 bytes of a Docker log message
|
||||||
|
// and returns the stream type, size, and the rest of the message as output.
|
||||||
|
// It also checks if the message contains a log header and extracts the log message from it.
|
||||||
|
func parseDockerLogHeader(data []byte) (LogOutput) {
|
||||||
|
var logOutput LogOutput
|
||||||
|
logOutput.StreamType = 1 // assume stdout if header not present
|
||||||
|
logOutput.Size = uint32(len(data))
|
||||||
|
logOutput.Output = string(data)
|
||||||
|
|
||||||
|
if len(data) < 8 {
|
||||||
|
return logOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the output contains a log header
|
||||||
|
hasHeader := true
|
||||||
|
streamType := data[0]
|
||||||
|
if(!(streamType >= 0 && streamType <= 2)) {
|
||||||
|
hasHeader = false
|
||||||
|
}
|
||||||
|
if(data[1] != 0 || data[2] != 0 || data[3] != 0) {
|
||||||
|
hasHeader = false
|
||||||
|
}
|
||||||
|
if hasHeader {
|
||||||
|
sizeBytes := data[4:8]
|
||||||
|
size := binary.BigEndian.Uint32(sizeBytes)
|
||||||
|
|
||||||
|
output := string(data[8:])
|
||||||
|
|
||||||
|
logOutput.StreamType = streamType
|
||||||
|
logOutput.Size = size
|
||||||
|
logOutput.Output = output
|
||||||
|
}
|
||||||
|
|
||||||
|
return logOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterLogs(logReader io.Reader, searchQuery string, limit int) []LogOutput {
|
||||||
|
scanner := bufio.NewScanner(logReader)
|
||||||
|
logLines := make([]LogOutput, 0)
|
||||||
|
|
||||||
|
// Read all logs into a slice
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
if len(searchQuery) > 0 && !strings.Contains(strings.ToUpper(line), strings.ToUpper(searchQuery)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logLines = append(logLines, parseDockerLogHeader(([]byte)(line)))
|
||||||
|
}
|
||||||
|
|
||||||
|
from := utils.Max(len(logLines)-limit, 0)
|
||||||
|
logLines = logLines[from:]
|
||||||
|
|
||||||
|
return logLines
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContainerLogsRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
containerId := vars["containerId"]
|
||||||
|
|
||||||
|
if req.Method == "GET" {
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("GetContainerLogsRoute", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
limit := 100
|
||||||
|
lastReceivedLogs := ""
|
||||||
|
|
||||||
|
if query.Get("limit") != "" {
|
||||||
|
limit, _ = strconv.Atoi(query.Get("limit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Get("lastReceivedLogs") != "" {
|
||||||
|
lastReceivedLogs = query.Get("lastReceivedLogs")
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOnly := false
|
||||||
|
if query.Get("errorOnly") != "" {
|
||||||
|
errorOnly, _ = strconv.ParseBool(query.Get("errorOnly"))
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerLogsOptions{
|
||||||
|
ShowStdout: !errorOnly,
|
||||||
|
ShowStderr: true,
|
||||||
|
Timestamps: true,
|
||||||
|
Until: lastReceivedLogs,
|
||||||
|
}
|
||||||
|
|
||||||
|
logReader, err := DockerClient.ContainerLogs(context.Background(), containerId, options)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("GetContainerLogsRoute: Error while getting container logs", err)
|
||||||
|
utils.HTTPError(w, "Container Logs Error: "+err.Error(), http.StatusInternalServerError, "LN002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer logReader.Close()
|
||||||
|
|
||||||
|
lines := FilterLogs(logReader, query.Get("search"), limit)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": lines,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("GetContainerLogsRoute: Method not allowed "+req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/azukaar/cosmos-server/src/utils"
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
// "github.com/docker/docker/client"
|
|
||||||
contstuff "github.com/docker/docker/api/types/container"
|
contstuff "github.com/docker/docker/api/types/container"
|
||||||
doctype "github.com/docker/docker/api/types"
|
doctype "github.com/docker/docker/api/types"
|
||||||
)
|
)
|
||||||
|
|
80
src/docker/api_networks.go
Normal file
80
src/docker/api_networks.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == "GET" {
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("ListNetworksRoute", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Docker networks
|
||||||
|
networks, err := DockerClient.NetworkList(context.Background(), types.NetworkListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("ListNetworksRoute: Error while getting networks", err)
|
||||||
|
utils.HTTPError(w, "Networks Get Error: " + err.Error(), http.StatusInternalServerError, "LN002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": networks,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("ListNetworksRoute: Method not allowed " + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func DeleteNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == "DELETE" {
|
||||||
|
// Get the network ID from URL
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
networkID := vars["networkID"]
|
||||||
|
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("DeleteNetworkRoute", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DN001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the specified Docker network
|
||||||
|
err := DockerClient.NetworkRemove(context.Background(), networkID)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("DeleteNetworkRoute: Error while deleting network", err)
|
||||||
|
utils.HTTPError(w, "Network Deletion Error: " + err.Error(), http.StatusInternalServerError, "DN002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"message": "Network deleted successfully",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("DeleteNetworkRoute: Method not allowed " + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
_, errEdit := EditContainer(container.ID, container)
|
_, errEdit := EditContainer(container.ID, container)
|
||||||
if errEdit != nil {
|
if errEdit != nil {
|
||||||
utils.Error("ContainerSecureEdit", errEdit)
|
utils.Error("ContainerSecureEdit", errEdit)
|
||||||
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS003")
|
utils.HTTPError(w, "Internal server error: " + errEdit.Error(), http.StatusInternalServerError, "DS003")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
79
src/docker/api_volumes.go
Normal file
79
src/docker/api_volumes.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
filters "github.com/docker/docker/api/types/filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == "GET" {
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("ManageContainer", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "LV001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Docker volumes
|
||||||
|
volumes, err := DockerClient.VolumeList(context.Background(), filters.Args{})
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("ListVolumeRoute: Error while getting volumes", err)
|
||||||
|
utils.HTTPError(w, "Volumes Get Error", http.StatusInternalServerError, "LV002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": volumes,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("ListVolumeRoute: Method not allowed " + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == "DELETE" {
|
||||||
|
// Get the volume name from URL
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
volumeName := vars["volumeName"]
|
||||||
|
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("DeleteVolumeRoute", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DV001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the specified Docker volume
|
||||||
|
err := DockerClient.VolumeRemove(context.Background(), volumeName, true)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("DeleteVolumeRoute: Error while deleting volume", err)
|
||||||
|
utils.HTTPError(w, "Volume Deletion Error", http.StatusInternalServerError, "DV002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"message": "Volume deleted successfully",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("DeleteVolumeRoute: Method not allowed " + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ var serverPortHTTP = ""
|
||||||
var serverPortHTTPS = ""
|
var serverPortHTTPS = ""
|
||||||
|
|
||||||
func startHTTPServer(router *mux.Router) {
|
func startHTTPServer(router *mux.Router) {
|
||||||
utils.Log("Listening to HTTP on :" + serverPortHTTP)
|
utils.Log("Listening to HTTP on : 0.0.0.0:" + serverPortHTTP)
|
||||||
|
|
||||||
err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router)
|
err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router)
|
||||||
|
|
||||||
|
@ -35,15 +35,14 @@ func startHTTPServer(router *mux.Router) {
|
||||||
|
|
||||||
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
config := utils.GetMainConfig()
|
config := utils.GetMainConfig()
|
||||||
serverHostname := "0.0.0.0"
|
|
||||||
|
|
||||||
cfg := simplecert.Default
|
cfg := simplecert.Default
|
||||||
|
|
||||||
cfg.Domains = utils.GetAllHostnames()
|
cfg.Domains = utils.GetAllHostnames()
|
||||||
cfg.CacheDir = "/config/certificates"
|
cfg.CacheDir = "/config/certificates"
|
||||||
cfg.SSLEmail = config.HTTPConfig.SSLEmail
|
cfg.SSLEmail = config.HTTPConfig.SSLEmail
|
||||||
cfg.HTTPAddress = serverHostname+":"+serverPortHTTP
|
cfg.HTTPAddress = "0.0.0.0:"+serverPortHTTP
|
||||||
cfg.TLSAddress = serverHostname+":"+serverPortHTTPS
|
cfg.TLSAddress = "0.0.0.0:"+serverPortHTTPS
|
||||||
|
|
||||||
if config.HTTPConfig.DNSChallengeProvider != "" {
|
if config.HTTPConfig.DNSChallengeProvider != "" {
|
||||||
cfg.DNSProvider = config.HTTPConfig.DNSChallengeProvider
|
cfg.DNSProvider = config.HTTPConfig.DNSChallengeProvider
|
||||||
|
@ -59,7 +58,7 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
certReloader, errSimCert = simplecert.Init(cfg, nil)
|
certReloader, errSimCert = simplecert.Init(cfg, nil)
|
||||||
if errSimCert != nil {
|
if errSimCert != nil {
|
||||||
// Temporary before we have a better way to handle this
|
// Temporary before we have a better way to handle this
|
||||||
utils.Error("simplecert init failed, HTTPS wont renew", errSimCert)
|
utils.Error("Failed to Init Let's Encrypt. HTTPS wont renew", errSimCert)
|
||||||
startHTTPServer(router)
|
startHTTPServer(router)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -106,7 +105,7 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
TLSConfig: tlsConf,
|
TLSConfig: tlsConf,
|
||||||
Addr: serverHostname + ":" + serverPortHTTPS,
|
Addr: "0.0.0.0:" + serverPortHTTPS,
|
||||||
ReadTimeout: 0,
|
ReadTimeout: 0,
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
WriteTimeout: 0,
|
WriteTimeout: 0,
|
||||||
|
@ -150,7 +149,6 @@ func StartServer() {
|
||||||
HTTPConfig := config.HTTPConfig
|
HTTPConfig := config.HTTPConfig
|
||||||
serverPortHTTP = HTTPConfig.HTTPPort
|
serverPortHTTP = HTTPConfig.HTTPPort
|
||||||
serverPortHTTPS = HTTPConfig.HTTPSPort
|
serverPortHTTPS = HTTPConfig.HTTPSPort
|
||||||
// serverHostname := HTTPConfig.Hostname
|
|
||||||
|
|
||||||
var tlsCert = HTTPConfig.TLSCert
|
var tlsCert = HTTPConfig.TLSCert
|
||||||
var tlsKey= HTTPConfig.TLSKey
|
var tlsKey= HTTPConfig.TLSKey
|
||||||
|
@ -219,14 +217,22 @@ func StartServer() {
|
||||||
|
|
||||||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||||
|
|
||||||
|
srapi.HandleFunc("/api/volume/{volumeName}", docker.DeleteVolumeRoute)
|
||||||
|
srapi.HandleFunc("/api/volumes", docker.ListVolumeRoute)
|
||||||
|
|
||||||
|
srapi.HandleFunc("/api/network/{networkID}", docker.DeleteNetworkRoute)
|
||||||
|
srapi.HandleFunc("/api/networks", docker.ListNetworksRoute)
|
||||||
|
|
||||||
srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute)
|
srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute)
|
||||||
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
|
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
|
||||||
|
srapi.HandleFunc("/api/servapps/{containerId}/logs", docker.GetContainerLogsRoute)
|
||||||
|
srapi.HandleFunc("/api/servapps/{containerId}/", docker.GetContainerRoute)
|
||||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||||
|
|
||||||
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||||
// srapi.Use(utils.EnsureHostname(serverHostname))
|
srapi.Use(utils.EnsureHostname)
|
||||||
// }
|
}
|
||||||
|
|
||||||
srapi.Use(tokenMiddleware)
|
srapi.Use(tokenMiddleware)
|
||||||
srapi.Use(proxy.SmartShieldMiddleware(
|
srapi.Use(proxy.SmartShieldMiddleware(
|
||||||
|
@ -236,10 +242,10 @@ func StartServer() {
|
||||||
PerUserRequestLimit: 5000,
|
PerUserRequestLimit: 5000,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
|
srapi.Use(utils.MiddlewareTimeout(30 * time.Second))
|
||||||
srapi.Use(utils.BlockPostWithoutReferer)
|
srapi.Use(utils.BlockPostWithoutReferer)
|
||||||
srapi.Use(proxy.BotDetectionMiddleware)
|
srapi.Use(proxy.BotDetectionMiddleware)
|
||||||
srapi.Use(httprate.Limit(60, 1*time.Minute,
|
srapi.Use(httprate.Limit(120, 1*time.Minute,
|
||||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||||
utils.Error("Too many requests. Throttling", nil)
|
utils.Error("Too many requests. Throttling", nil)
|
||||||
|
@ -258,9 +264,9 @@ func StartServer() {
|
||||||
|
|
||||||
fs := spa.SpaHandler(pwd + "/static", "index.html")
|
fs := spa.SpaHandler(pwd + "/static", "index.html")
|
||||||
|
|
||||||
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||||
// fs = utils.EnsureHostname(serverHostname)(fs)
|
fs = utils.EnsureHostname(fs)
|
||||||
// }
|
}
|
||||||
|
|
||||||
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
|
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
"HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode,
|
"HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode,
|
||||||
"needsRestart": utils.NeedsRestart,
|
"needsRestart": utils.NeedsRestart,
|
||||||
"newVersionAvailable": utils.NewVersionAvailable,
|
"newVersionAvailable": utils.NewVersionAvailable,
|
||||||
|
"hostname": utils.GetMainConfig().HTTPConfig.Hostname,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -166,3 +166,37 @@ func BlockPostWithoutReferer(next http.Handler) http.Handler {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnsureHostname(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Debug("Request requested resource from : " + r.Host)
|
||||||
|
|
||||||
|
og := GetMainConfig().HTTPConfig.Hostname
|
||||||
|
ni := GetMainConfig().NewInstall
|
||||||
|
|
||||||
|
if ni || og == "0.0.0.0" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := ""
|
||||||
|
if (IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443") {
|
||||||
|
port = ":" + MainConfig.HTTPConfig.HTTPSPort
|
||||||
|
} else if (!IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80") {
|
||||||
|
port = ":" + MainConfig.HTTPConfig.HTTPPort
|
||||||
|
}
|
||||||
|
|
||||||
|
hostnames := GetAllHostnames()
|
||||||
|
|
||||||
|
for _, hostname := range hostnames {
|
||||||
|
if r.Host != hostname + port {
|
||||||
|
Error("Invalid Hostname " + r.Host + "for request. Expecting " + hostname, nil)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
http.Error(w, "Bad Request: Invalid hostname.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ var DefaultConfig = Config{
|
||||||
GenerateMissingAuthCert: true,
|
GenerateMissingAuthCert: true,
|
||||||
HTTPPort: "80",
|
HTTPPort: "80",
|
||||||
HTTPSPort: "443",
|
HTTPSPort: "443",
|
||||||
Hostname: "localhost",
|
Hostname: "0.0.0.0",
|
||||||
ProxyConfig: ProxyConfig{
|
ProxyConfig: ProxyConfig{
|
||||||
Routes: []ProxyRouteConfig{},
|
Routes: []ProxyRouteConfig{},
|
||||||
},
|
},
|
||||||
|
@ -230,26 +230,6 @@ func GetConfigFileName() string {
|
||||||
return configFile
|
return configFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureHostname(hostname string) func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Debug("Request requested resource from : " + r.Host)
|
|
||||||
port := ""
|
|
||||||
if (IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443") {
|
|
||||||
port = ":" + MainConfig.HTTPConfig.HTTPSPort
|
|
||||||
} else if (!IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80") {
|
|
||||||
port = ":" + MainConfig.HTTPConfig.HTTPPort
|
|
||||||
}
|
|
||||||
if r.Host != hostname + port {
|
|
||||||
Error("Invalid Hostname " + r.Host + "for request. Expecting " + hostname, nil)
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprint(w, "Bad Request.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateDefaultConfigFileIfNecessary() bool {
|
func CreateDefaultConfigFileIfNecessary() bool {
|
||||||
configFile := GetConfigFileName()
|
configFile := GetConfigFileName()
|
||||||
|
@ -420,3 +400,10 @@ func ImageToBase64(path string) (string, error) {
|
||||||
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encodedData)
|
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encodedData)
|
||||||
return dataURI, nil
|
return dataURI, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Max(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
Loading…
Reference in a new issue