[release] v0.7.0-unstable3

This commit is contained in:
Yann Stepienik 2023-06-13 02:03:18 +01:00
parent 8d6329f8d5
commit 6bc9e02e28
33 changed files with 736 additions and 323 deletions

View file

@ -36,7 +36,7 @@ const HostChip = ({route, settings, style}) => {
window.open(window.location.origin + route.PathPrefix, '_blank'); window.open(window.location.origin + route.PathPrefix, '_blank');
}} }}
onDelete={settings ? () => { onDelete={settings ? () => {
window.open('/ui/config-url/'+route.Name, '_blank'); window.open('/cosmos-ui/config-url/'+route.Name, '_blank');
} : null} } : null}
deleteIcon={settings ? <SettingOutlined /> : null} deleteIcon={settings ? <SettingOutlined /> : null}
/> />

View file

@ -1,7 +1,7 @@
// ==============================|| THEME CONFIG ||============================== // // ==============================|| THEME CONFIG ||============================== //
const config = { const config = {
defaultPath: '/ui', defaultPath: '/cosmos-ui',
fontFamily: `'Public Sans', sans-serif`, fontFamily: `'Public Sans', sans-serif`,
i18n: 'en', i18n: 'en',
miniDrawer: false, miniDrawer: false,

View file

@ -10,13 +10,13 @@ const IsLoggedIn = () => useEffect(() => {
API.auth.me().then((data) => { API.auth.me().then((data) => {
if(data.status != 'OK') { if(data.status != 'OK') {
if(data.status == 'NEW_INSTALL') { if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall'; window.location.href = '/cosmos-ui/newInstall';
} else if (data.status == 'error' && data.code == "HTTP004") { } else if (data.status == 'error' && data.code == "HTTP004") {
window.location.href = '/ui/login?redirect=' + redirectTo; window.location.href = '/cosmos-ui/login?redirect=' + redirectTo;
} else if (data.status == 'error' && data.code == "HTTP006") { } else if (data.status == 'error' && data.code == "HTTP006") {
window.location.href = '/ui/loginmfa?redirect=' + redirectTo; window.location.href = '/cosmos-ui/loginmfa?redirect=' + redirectTo;
} else if (data.status == 'error' && data.code == "HTTP007") { } else if (data.status == 'error' && data.code == "HTTP007") {
window.location.href = '/ui/newmfa?redirect=' + redirectTo; window.location.href = '/cosmos-ui/newmfa?redirect=' + redirectTo;
} }
} }
}) })

View file

@ -18,7 +18,7 @@ const HeaderContent = () => {
{!matchesXs && <Search />} {!matchesXs && <Search />}
{matchesXs && <Box sx={{ width: '100%', ml: 1 }} />} {matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
<Link href="/ui/logout" underline="none"> <Link href="/cosmos-ui/logout" underline="none">
<Chip label="Logout" /> <Chip label="Logout" />
</Link> </Link>
{/* <Notification /> */} {/* <Notification /> */}

View file

@ -17,7 +17,7 @@ const dashboard = {
id: 'home', id: 'home',
title: 'Home', title: 'Home',
type: 'item', type: 'item',
url: '/ui/', url: '/cosmos-ui/',
icon: icons.HomeOutlined, icon: icons.HomeOutlined,
breadcrumbs: false breadcrumbs: false
}, },
@ -25,7 +25,7 @@ const dashboard = {
id: 'dashboard', id: 'dashboard',
title: 'Dashboard', title: 'Dashboard',
type: 'item', type: 'item',
url: '/ui/dashboard', url: '/cosmos-ui/dashboard',
icon: DashboardOutlined, icon: DashboardOutlined,
breadcrumbs: false breadcrumbs: false
}, },
@ -33,7 +33,7 @@ const dashboard = {
id: 'market', id: 'market',
title: 'Market', title: 'Market',
type: 'item', type: 'item',
url: '/ui/market-listing', url: '/cosmos-ui/market-listing',
icon: AppstoreAddOutlined, icon: AppstoreAddOutlined,
breadcrumbs: false breadcrumbs: false
}, },

View file

@ -19,35 +19,35 @@ const pages = {
id: 'servapps', id: 'servapps',
title: 'ServApps', title: 'ServApps',
type: 'item', type: 'item',
url: '/ui/servapps', url: '/cosmos-ui/servapps',
icon: AppstoreOutlined icon: AppstoreOutlined
}, },
{ {
id: 'url', id: 'url',
title: 'URLs', title: 'URLs',
type: 'item', type: 'item',
url: '/ui/config-url', url: '/cosmos-ui/config-url',
icon: icons.NodeExpandOutlined, icon: icons.NodeExpandOutlined,
}, },
{ {
id: 'users', id: 'users',
title: 'Users', title: 'Users',
type: 'item', type: 'item',
url: '/ui/config-users', url: '/cosmos-ui/config-users',
icon: icons.ProfileOutlined, icon: icons.ProfileOutlined,
}, },
{ {
id: 'openid', id: 'openid',
title: 'OpenID', title: 'OpenID',
type: 'item', type: 'item',
url: '/ui/openid-manage', url: '/cosmos-ui/openid-manage',
icon: PicLeftOutlined, icon: PicLeftOutlined,
}, },
{ {
id: 'config', id: 'config',
title: 'Configuration', title: 'Configuration',
type: 'item', type: 'item',
url: '/ui/config-general', url: '/cosmos-ui/config-general',
icon: icons.SettingOutlined, icon: icons.SettingOutlined,
} }
] ]

View file

@ -17,7 +17,7 @@ const Logout = () => {
API.auth.logout() API.auth.logout()
.then(() => { .then(() => {
setTimeout(() => { setTimeout(() => {
window.location.href = '/ui/login'; window.location.href = '/cosmos-ui/login';
}, 2000); }, 2000);
}); });
},[]); },[]);

View file

@ -15,7 +15,7 @@ const Signup = () => (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}> <Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Sign up</Typography> <Typography variant="h3">Sign up</Typography>
<Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary"> <Typography component={Link} to="/cosmos-ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Already have an account? Already have an account?
</Typography> </Typography>
</Stack> </Stack>

View file

@ -53,14 +53,14 @@ const AuthLogin = () => {
const notLogged = urlSearchParams.get('notlogged') == 1; const notLogged = urlSearchParams.get('notlogged') == 1;
const notLoggedAdmin = urlSearchParams.get('notlogged') == 2; const notLoggedAdmin = urlSearchParams.get('notlogged') == 2;
const invalid = urlSearchParams.get('invalid') == 1; const invalid = urlSearchParams.get('invalid') == 1;
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui'; const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
useEffect(() => { useEffect(() => {
API.auth.me().then((data) => { API.auth.me().then((data) => {
if(data.status == 'OK') { if(data.status == 'OK') {
window.location.href = redirectTo; window.location.href = redirectTo;
} else if(data.status == 'NEW_INSTALL') { } else if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall'; window.location.href = '/cosmos-ui/newInstall';
} }
}); });
@ -190,7 +190,7 @@ const AuthLogin = () => {
} }
label={<Typography variant="h6">Keep me sign in</Typography>} label={<Typography variant="h6">Keep me sign in</Typography>}
/>*/} />*/}
{showResetPassword && <Link variant="h6" component={RouterLink} to="/ui/forgot-password" color="primary"> {showResetPassword && <Link variant="h6" component={RouterLink} to="/cosmos-ui/forgot-password" color="primary">
Forgot Your Password? Forgot Your Password?
</Link>} </Link>}
{!showResetPassword && <Typography variant="h6"> {!showResetPassword && <Typography variant="h6">

View file

@ -85,7 +85,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
}).then((res) => { }).then((res) => {
setStatus({ success: true }); setStatus({ success: true });
setSubmitting(false); setSubmitting(false);
window.location.href = '/ui/login'; window.location.href = '/cosmos-ui/login';
}).catch((err) => { }).catch((err) => {
setStatus({ success: false }); setStatus({ success: false });
setErrors({ submit: err.message }); setErrors({ submit: err.message });

View file

@ -109,7 +109,7 @@ const ForgotPassword = () => {
variant="contained" variant="contained"
color="primary" color="primary"
component={Link} component={Link}
to="/ui/login" to="/cosmos-ui/login"
> >
Back to login Back to login
</Button> </Button>

View file

@ -35,14 +35,14 @@ import { CosmosCollapse } from '../config/users/formShortcuts';
const MFALoginForm = () => { const MFALoginForm = () => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui'; const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
useEffect(() => { useEffect(() => {
API.auth.me().then((data) => { API.auth.me().then((data) => {
if(data.status == 'OK') { if(data.status == 'OK') {
window.location.href = redirectTo; window.location.href = redirectTo;
} else if(data.status == 'NEW_INSTALL') { } else if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall'; window.location.href = '/cosmos-ui/newInstall';
} }
}); });
}); });
@ -149,7 +149,7 @@ const MFASetup = () => {
<MFALoginForm /> <MFALoginForm />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Link to="/ui/logout"> <Link to="/cosmos-ui/logout">
<Typography variant="h5">Logout</Typography> <Typography variant="h5">Logout</Typography>
</Link> </Link>
</Grid> </Grid>

View file

@ -150,7 +150,7 @@ const ProxyManagement = () => {
{routes && <PrettyTableView {routes && <PrettyTableView
data={routes} data={routes}
getKey={(r) => r.Name + r.Target + r.Mode} getKey={(r) => r.Name + r.Target + r.Mode}
linkTo={(r) => '/ui/config-url/' + r.Name} linkTo={(r) => '/cosmos-ui/config-url/' + r.Name}
columns={[ columns={[
{ {
title: '', title: '',

View file

@ -57,7 +57,7 @@ const UserManagement = () => {
formType: ""+formType, formType: ""+formType,
}) })
.then((values) => { .then((values) => {
let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey; let sendLink = window.location.origin + '/cosmos-ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'}); setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'});
setOpenInviteForm(true); setOpenInviteForm(true);
}); });

View file

@ -1,4 +1,4 @@
import { Box, Stack } from "@mui/material"; import { Box, CircularProgress, Stack } from "@mui/material";
import { HomeBackground, TransparentHeader } from "../home"; import { HomeBackground, TransparentHeader } from "../home";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import * as API from "../../api"; import * as API from "../../api";
@ -70,7 +70,7 @@ function ShowcasesItem({ isDark, item }) {
<Button className="CheckButton" color="primary" variant="contained"> <Button className="CheckButton" color="primary" variant="contained">
Install Install
</Button> </Button>
<Link to={"/ui/market-listing/" + item.name} style={{ <Link to={"/cosmos-ui/market-listing/" + item.name} style={{
textDecoration: 'none', textDecoration: 'none',
}}> }}>
<Button className="CheckButton" color="primary" variant="outlined"> <Button className="CheckButton" color="primary" variant="outlined">
@ -133,7 +133,6 @@ const MarketPage = () => {
return <> return <>
<HomeBackground /> <HomeBackground />
<TransparentHeader /> <TransparentHeader />
{openedApp && <Box style={{ {openedApp && <Box style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@ -143,7 +142,7 @@ const MarketPage = () => {
zIndex: 1300, zIndex: 1300,
backgroundColor: 'rgba(0,0,0,0.5)', backgroundColor: 'rgba(0,0,0,0.5)',
}}> }}>
<Link to="/ui/market-listing" as={Box} <Link to="/cosmos-ui/market-listing" as={Box}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@ -169,7 +168,7 @@ const MarketPage = () => {
} }
}}> }}>
<Link to="/ui/market-listing" style={{ <Link to="/cosmos-ui/market-listing" style={{
textDecoration: 'none', textDecoration: 'none',
}}> }}>
<Button className="CheckButton" color="primary" variant="outlined"> <Button className="CheckButton" color="primary" variant="outlined">
@ -199,7 +198,7 @@ const MarketPage = () => {
}}></div> }}></div>
<div> <div>
<DockerComposeImport installer defaultName={openedApp.name} dockerComposeInit={openedApp.compose} /> <DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
</div> </div>
</Stack> </Stack>
</Stack> </Stack>
@ -207,7 +206,19 @@ const MarketPage = () => {
<Stack style={{ position: 'relative' }} spacing={1}> <Stack style={{ position: 'relative' }} spacing={1}>
<Stack style={{ height: '35vh' }} spacing={1}> <Stack style={{ height: '35vh' }} spacing={1}>
<Showcases showcase={showcase} isDark={isDark}/> {(!showcase || !Object.keys(showcase).length) && <Box style={{
width: '100%',
height: '100%',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<CircularProgress
size={100}
/>
</Box>}
{showcase && showcase.length && <Showcases showcase={showcase} isDark={isDark}/>}
</Stack> </Stack>
<Stack spacing={1} style={{ <Stack spacing={1} style={{
@ -218,12 +229,26 @@ const MarketPage = () => {
minHeight: 'calc(65vh - 80px)', minHeight: 'calc(65vh - 80px)',
padding: '24px', padding: '24px',
}}> }}>
<Grid2 container spacing={{ xs: 1, sm: 1, md: 2 }}> {(!apps || !Object.keys(apps).length) && <Box style={{
{apps && Object.keys(apps).length > 0 && apps[Object.keys(apps)[0]].map((app) => { width: '100%',
height: '100%',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginTop: '150px',
}}>
<CircularProgress
size={100}
/>
</Box>}
{apps && Object.keys(apps).length > 0 && <Grid2 container spacing={{ xs: 1, sm: 1, md: 2 }}>
{apps[Object.keys(apps)[0]].map((app) => {
return <Grid2 style={{ return <Grid2 style={{
...gridAnim, ...gridAnim,
cursor: 'pointer', cursor: 'pointer',
}} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name} item><Link to={"/ui/market-listing/" + app.name} style={{ }} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name} item><Link to={"/cosmos-ui/market-listing/" + app.name} style={{
textDecoration: 'none', textDecoration: 'none',
}}> }}>
<div key={app.name} style={appCardStyle(theme)}> <div key={app.name} style={appCardStyle(theme)}>
@ -248,7 +273,7 @@ const MarketPage = () => {
</Link> </Link>
</Grid2> </Grid2>
})} })}
</Grid2> </Grid2>}
</Stack> </Stack>
</Stack> </Stack>
</> </>

View file

@ -63,7 +63,7 @@ const NewInstall = () => {
setStatus(res.data); setStatus(res.data);
} catch(error) { } catch(error) {
if(error.status == 401) if(error.status == 401)
window.location.href = "/ui/login"; window.location.href = "/cosmos-ui/login";
} }
if (typeof status !== 'undefined') { if (typeof status !== 'undefined') {
setTimeout(() => { setTimeout(() => {
@ -577,7 +577,7 @@ const NewInstall = () => {
step: "5", step: "5",
}) })
setTimeout(() => { setTimeout(() => {
window.location.href = hostname + "/ui/login"; window.location.href = hostname + "/cosmos-ui/login";
}, 500); }, 500);
} else } else
setActiveStep(activeStep + 1) setActiveStep(activeStep + 1)

View file

@ -1,7 +1,7 @@
// material-ui // material-ui
import * as React from 'react'; import * as React from 'react';
import { Alert, Button, FormLabel, Stack, Typography } from '@mui/material'; import { Alert, Button, Checkbox, FormControlLabel, FormLabel, Stack, Typography } from '@mui/material';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined, ArrowDownOutlined } from '@ant-design/icons'; import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined, ArrowDownOutlined, ConsoleSqlOutlined } from '@ant-design/icons';
import Table from '@mui/material/Table'; import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody'; import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell'; import TableCell from '@mui/material/TableCell';
@ -26,6 +26,10 @@ import ResponsiveButton from '../../../components/responseiveButton';
import UploadButtons from '../../../components/fileUpload'; import UploadButtons from '../../../components/fileUpload';
import NewDockerService from './newService'; import NewDockerService from './newService';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { CosmosCollapse, CosmosFormDivider, CosmosInputText } from '../../config/users/formShortcuts';
import VolumeContainerSetup from './volumes';
import DockerContainerSetup from './setup';
import whiskers from 'whiskers';
function checkIsOnline() { function checkIsOnline() {
API.isOnline().then((res) => { API.isOnline().then((res) => {
@ -63,11 +67,15 @@ const preStyle = {
marginRight: '0', marginRight: '0',
} }
const isNewerVersion = (v1, v2) => {
return false;
}
const getHostnameFromName = (name) => { const getHostnameFromName = (name) => {
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1] return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
} }
const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultName }) => { const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaultName }) => {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
@ -75,254 +83,371 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
const [service, setService] = useState({}); const [service, setService] = useState({});
const [ymlError, setYmlError] = useState(''); const [ymlError, setYmlError] = useState('');
const [serviceName, setServiceName] = useState(defaultName || 'my-service'); const [serviceName, setServiceName] = useState(defaultName || 'my-service');
const [hostnames, setHostnames] = useState([]); const [hostnames, setHostnames] = useState({});
const [overrides, setOverrides] = useState({});
const [context, setContext] = useState({});
const [installer, setInstaller] = useState(installerInit);
useEffect(() => { useEffect(() => {
const text = fetch(dockerComposeInit) if (!openModal) {
.then((res) => res.text())
.then((text) => {
setDockerCompose(text);
});
}, [dockerComposeInit]);
useEffect(() => {
if(!openModal) {
return; return;
} }
// fetch(dockerComposeInit)
// .then((res) => res.text())
// .then((text) => {
// setDockerCompose(text);
// });
if(dockerComposeInit)
setDockerCompose(`
{
"cosmos-installer": {
"form": [
{
"name": "caca",
"label": "Caca?",
"type": "text"
}
]
},
"services": {
"{ServiceName}": {
"image": "lscr.io/linuxserver/jellyfin:latest",
"container_name": "{ServiceName}",
"restart": "unless-stopped",
"environment": [
"PUID=1000",
"PGID=1000",
"TZ=auto"
],
"labels": {
"cosmos-force-network-secured": "true",
"caca": "{Context.caca}"
},
"volumes": [{
"source": "{ServiceName}-config",
"target": "/config",
"type": "volume"
}],
"routes": [
{
"name": "{ServiceName}",
"description": "Expose {ServiceName} to the web",
"useHost": true,
"target": "http://{ServiceName}:8096",
"mode": "SERVAPP"
}
]
}
}
}
`);
}, [openModal, dockerComposeInit]);
useEffect(() => {
if (!openModal || installer) {
return;
}
setYmlError(''); setYmlError('');
if (dockerCompose === '') { if (dockerCompose === '') {
return; return;
} }
let isJson = dockerCompose.trim().startsWith('{') && dockerCompose.trim().endsWith('}'); let isJson = dockerCompose && dockerCompose.trim().startsWith('{') && dockerCompose.trim().endsWith('}');
// if Json // if Json
if (isJson) { if (isJson) {
try { try {
let doc = JSON.parse(dockerCompose); let doc = JSON.parse(dockerCompose);
if(typeof doc['cosmos-installer'] === 'object') {
if(installer) { setInstaller(true);
doc = JSON.parse(dockerCompose.replace(/\{\{self\}\}/gi, serviceName));
// check hostnames for each service
let hostnames = [];
if(doc.services)
Object.keys(doc.services).forEach((key) => {
if (doc.services[key].routes) {
let routeId = 0;
doc.services[key].routes.forEach((route) => {
if (route.useHost) {
let newRoute = Object.assign({}, route);
if(route.useHost === true) {
newRoute.host = getHostnameFromName(key + (routeId > 0 ? '-' + routeId : ''))
}
hostnames.push(newRoute);
}
});
}
});
setHostnames(hostnames);
} }
setService(doc); setService(doc);
} catch (e) { } catch (e) {
setYmlError(e.message); setYmlError(e.message);
} }
} }
else { else {
// if Yml // if Yml
let doc; let doc;
let newService = {}; let newService = {};
try { try {
doc = yaml.load(dockerCompose); doc = yaml.load(dockerCompose);
if (typeof doc === 'object' && doc !== null && Object.keys(doc).length > 0 && if (typeof doc === 'object' && doc !== null && Object.keys(doc).length > 0 &&
!doc.services && !doc.networks && !doc.volumes) { !doc.services && !doc.networks && !doc.volumes) {
doc = { doc = {
services: Object.assign({}, doc) services: Object.assign({}, doc)
}
} }
}
// convert to the proper format // convert to the proper format
if (doc.services) { if (doc.services) {
Object.keys(doc.services).forEach((key) => { Object.keys(doc.services).forEach((key) => {
// convert volumes // convert volumes
if (doc.services[key].volumes) { if (doc.services[key].volumes) {
if(Array.isArray(doc.services[key].volumes)) { if (Array.isArray(doc.services[key].volumes)) {
let volumes = []; let volumes = [];
doc.services[key].volumes.forEach((volume) => { doc.services[key].volumes.forEach((volume) => {
if (typeof volume === 'object') { if (typeof volume === 'object') {
volumes.push(volume); volumes.push(volume);
} else { } else {
let volumeSplit = volume.split(':'); let volumeSplit = volume.split(':');
let volumeObj = { let volumeObj = {
Source: volumeSplit[0], Source: volumeSplit[0],
Target: volumeSplit[1], Target: volumeSplit[1],
Type: volume[0] === '/' ? 'bind' : 'volume', Type: volume[0] === '/' ? 'bind' : 'volume',
}; };
volumes.push(volumeObj); volumes.push(volumeObj);
} }
}); });
doc.services[key].volumes = volumes; doc.services[key].volumes = volumes;
}
} }
}
// convert expose // convert expose
if (doc.services[key].expose) { if (doc.services[key].expose) {
doc.services[key].expose = doc.services[key].expose.map((port) => { doc.services[key].expose = doc.services[key].expose.map((port) => {
return '' + port; return '' + port;
}) })
}
//convert user
if (doc.services[key].user) {
doc.services[key].user = '' + doc.services[key].user;
}
// convert labels:
if (doc.services[key].labels) {
if (Array.isArray(doc.services[key].labels)) {
let labels = {};
doc.services[key].labels.forEach((label) => {
const [key, value] = label.split('=');
labels['' + key] = '' + value;
});
doc.services[key].labels = labels;
} }
if (typeof doc.services[key].labels == 'object') {
let labels = {};
Object.keys(doc.services[key].labels).forEach((keylabel) => {
labels['' + keylabel] = '' + doc.services[key].labels[keylabel];
});
doc.services[key].labels = labels;
}
}
// convert environment //convert user
if (doc.services[key].environment) { if (doc.services[key].user) {
if (!Array.isArray(doc.services[key].environment)) { doc.services[key].user = '' + doc.services[key].user;
let environment = [];
Object.keys(doc.services[key].environment).forEach((keyenv) => {
environment.push(keyenv + '=' + doc.services[key].environment[keyenv]);
});
doc.services[key].environment = environment;
} }
}
// convert network // convert labels:
if (doc.services[key].networks) { if (doc.services[key].labels) {
if (Array.isArray(doc.services[key].networks)) { if (Array.isArray(doc.services[key].labels)) {
let networks = {}; let labels = {};
doc.services[key].networks.forEach((network) => { doc.services[key].labels.forEach((label) => {
if (typeof network === 'object') { const [key, value] = label.split('=');
networks['' + network.name] = network; labels['' + key] = '' + value;
} });
else doc.services[key].labels = labels;
networks['' + network] = {}; }
}); if (typeof doc.services[key].labels == 'object') {
doc.services[key].networks = networks; let labels = {};
Object.keys(doc.services[key].labels).forEach((keylabel) => {
labels['' + keylabel] = '' + doc.services[key].labels[keylabel];
});
doc.services[key].labels = labels;
}
} }
}
// ensure container_name // convert environment
if (!doc.services[key].container_name) { if (doc.services[key].environment) {
doc.services[key].container_name = key; if (!Array.isArray(doc.services[key].environment)) {
} let environment = [];
}); Object.keys(doc.services[key].environment).forEach((keyenv) => {
} environment.push(keyenv + '=' + doc.services[key].environment[keyenv]);
});
// convert networks doc.services[key].environment = environment;
if (doc.networks) { }
if (Array.isArray(doc.networks)) { }
let networks = {};
doc.networks.forEach((network) => { // convert network
if (typeof network === 'object') { if (doc.services[key].networks) {
networks['' + network.name] = network; if (Array.isArray(doc.services[key].networks)) {
let networks = {};
doc.services[key].networks.forEach((network) => {
if (typeof network === 'object') {
networks['' + network.name] = network;
}
else
networks['' + network] = {};
});
doc.services[key].networks = networks;
}
}
// ensure container_name
if (!doc.services[key].container_name) {
doc.services[key].container_name = key;
} }
else
networks['' + network] = {};
}); });
doc.networks = networks;
} else {
let networks = {};
Object.keys(doc.networks).forEach((key) => {
networks['' + key] = doc.networks[key] || {};
});
doc.networks = networks;
} }
}
// convert volumes // convert networks
if (doc.volumes) { if (doc.networks) {
if (Array.isArray(doc.volumes)) { if (Array.isArray(doc.networks)) {
let volumes = {}; let networks = {};
doc.volumes.forEach((volume) => { doc.networks.forEach((network) => {
if(!volume) { if (typeof network === 'object') {
volume = {}; networks['' + network.name] = network;
} }
if (typeof volume === 'object') { else
volumes['' + volume.name] = volume; networks['' + network] = {};
} });
else doc.networks = networks;
volumes['' + volume] = {}; } else {
}); let networks = {};
doc.volumes = volumes; Object.keys(doc.networks).forEach((key) => {
} else { networks['' + key] = doc.networks[key] || {};
let volumes = {}; });
Object.keys(doc.volumes).forEach((key) => { doc.networks = networks;
volumes['' + key] = doc.volumes[key] || {}; }
});
doc.volumes = volumes;
} }
// convert volumes
if (doc.volumes) {
if (Array.isArray(doc.volumes)) {
let volumes = {};
doc.volumes.forEach((volume) => {
if (!volume) {
volume = {};
}
if (typeof volume === 'object') {
volumes['' + volume.name] = volume;
}
else
volumes['' + volume] = {};
});
doc.volumes = volumes;
} else {
let volumes = {};
Object.keys(doc.volumes).forEach((key) => {
volumes['' + key] = doc.volumes[key] || {};
});
doc.volumes = volumes;
}
}
} catch (e) {
console.log(e);
setYmlError(e.message);
return;
} }
} catch (e) { setService(doc);
console.log(e);
setYmlError(e.message);
return;
} }
setService(doc);
}
}, [openModal, dockerCompose]); }, [openModal, dockerCompose]);
useEffect(() => { useEffect(() => {
if(!openModal) { setOverrides({});
}, [serviceName, hostnames, context]);
useEffect(() => {
if (!openModal || dockerCompose === '') {
return; return;
} }
try { try {
if (installer) { if (installer) {
let doc = JSON.parse(dockerCompose.replace(/\{\{self\}\}/gi, serviceName)); const rendered = whiskers.render(dockerCompose, {
ServiceName: serviceName,
Hostnames: hostnames,
Context: context,
});
if(doc.services) const jsoned = JSON.parse(rendered);
Object.keys(doc.services).forEach((key) => {
if (doc.services[key].routes) { if (jsoned.services) {
doc.services[key].routes.forEach((route, index) => { // GENERATE HOSTNAMES FORM
doc.services[key].routes[index] = { let newHostnames = {};
...hostnames[index], Object.keys(jsoned.services).forEach((key) => {
name: route.name, if (jsoned.services[key].routes) {
description: route.description, let routeId = 0;
}; jsoned.services[key].routes.forEach((route) => {
if (route.useHost) {
let newRoute = Object.assign({}, route);
if (route.useHost === true) {
newRoute.host = getHostnameFromName(key + (routeId > 0 ? '-' + routeId : ''))
}
if(!newHostnames[key]) newHostnames[key] = {};
if(!newHostnames[key][route.name]) {
newHostnames[key][route.name] = newRoute;
if(hostnames[key] && hostnames[key][route.name]) {
newHostnames[key][route.name].host = hostnames[key][route.name].host;
}
}
}
}); });
} }
}); });
setService(doc); if(JSON.stringify(newHostnames) !== JSON.stringify(hostnames))
setHostnames({...newHostnames});
let newVolumes = [];
Object.keys(jsoned.services).forEach((key) => {
// APPLY OVERRIDE
if (overrides[key]) {
// prevent customizing static volumes
if (jsoned.services[key].volumes && jsoned['cosmos-installer'] && jsoned['cosmos-installer']['frozen-volumes']) {
jsoned['cosmos-installer']['frozen-volumes'].forEach((volume) => {
delete overrides[key].volumes;
});
}
jsoned.services[key] = {
...jsoned.services[key],
...overrides[key],
};
}
// APPLY HOSTNAMES
if (hostnames[key]) {
if (jsoned.services[key].routes) {
jsoned.services[key].routes.forEach((route) => {
if (hostnames[key][route.name]) {
route.host = hostnames[key][route.name].host;
}
});
}
}
// CREATE NEW VOLUMES
if (jsoned.services[key].volumes) {
jsoned.services[key].volumes.forEach((volume) => {
if (typeof volume === 'object' && !volume.existing) {
newVolumes.push(volume);
} else if (typeof volume === 'object' && volume.existing) {
delete volume.existing;
}
});
}
});
if (newVolumes.length > 0) {
jsoned.volumes = jsoned.volumes || {};
newVolumes.forEach((volume) => {
jsoned.volumes[volume.source] = {
};
});
}
}
setService(jsoned);
} }
} catch (e) { } catch (e) {
setYmlError(e.message); setYmlError(e.message);
return; return;
} }
}, [openModal, installer, serviceName, hostnames]); }, [openModal, dockerCompose, serviceName, hostnames, overrides]);
const openModalFunc = () => {
setOpenModal(true);
setStep(0);
setService({});
setYmlError(null);
setOverrides({});
setHostnames({});
setDockerCompose('');
setInstaller(installerInit);
setServiceName(defaultName || 'default-name');
}
return <> return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)} > <Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>{installer ? "Installation" : "Import Compose File"}</DialogTitle> <DialogTitle>{installer ? "Installation" : "Import Compose File"}</DialogTitle>
<DialogContent style={{width: '800px', maxWidth: '100%'}}> <DialogContent style={{ width: '800px', maxWidth: '100%' }}>
<DialogContentText> <DialogContentText>
{step === 0 && !installer && <><Stack spacing={2}> {step === 0 && !installer && <><Stack spacing={2}>
<Alert severity="warning" icon={<WarningOutlined />}> <Alert severity="warning" icon={<WarningOutlined />}>
@ -359,27 +484,119 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
}} }}
rows={20}></TextField> rows={20}></TextField>
</Stack></>} </Stack></>}
{step === 0 && installer && <><Stack spacing={2}> {step === 0 && installer && <><Stack spacing={2}>
<div style={{ color: 'red' }}> <div style={{ color: 'red' }}>
{ymlError} {ymlError}
</div> </div>
{!ymlError && (<><FormLabel>Choose your service name</FormLabel>
{!ymlError && (<><FormLabel>Choose your service name</FormLabel>
<TextField label="Service Name" value={serviceName} onChange={(e) => setServiceName(e.target.value)} /> <TextField label="Service Name" value={serviceName} onChange={(e) => setServiceName(e.target.value)} />
{hostnames.map((hostname, index) => {
return <> {service['cosmos-installer'] && service['cosmos-installer'].form.map((formElement) => {
<FormLabel>Choose URL for {hostname.name}</FormLabel> return formElement.type === 'checkbox' ? <FormControlLabel
<div style={{ opacity: 0.9, fontSize: '0.8em', textDecoration: 'italic'}} control={<Checkbox checked={context[formElement.name] || formElement.initialValue} onChange={(e) => {
>{hostname.description}</div> setContext({ ...context, [formElement.name]: e.target.checked });
<TextField key={index} label="Hostname" value={hostname.host} onChange={(e) => { }
const newHostnames = [...hostnames]; } />}
newHostnames[index].host = e.target.value; label={formElement.label}
setHostnames(newHostnames); /> : <TextField
}} /> label={formElement.label}
</> value={context[formElement.name] || formElement.initialValue}
onChange={(e) => {
setContext({ ...context, [formElement.name]: e.target.value });
}
} />
})} })}
</>)}
{Object.keys(hostnames).map((serviceIndex) => {
const service = hostnames[serviceIndex];
return Object.keys(service).map((hostIndex) => {
const hostname = service[hostIndex];
return <>
<FormLabel>Choose URL for {hostname.name}</FormLabel>
<div style={{ opacity: 0.9, fontSize: '0.8em', textDecoration: 'italic' }}
>{hostname.description}</div>
<TextField key={serviceIndex + hostIndex} label="Hostname" value={hostname.host} onChange={(e) => {
hostnames[serviceIndex][hostname.name].host = e.target.value;
setHostnames({...hostnames});
}} />
</>
})
})}
{service && service.services && Object.values(service.services).map((value) => {
return <CosmosCollapse title={`Customize ${value.container_name}`}>
<Stack spacing={2}>
<DockerContainerSetup
newContainer
containerInfo={{
Name: '',
Image: '',
Config: {
Env: value.environment || [],
Labels: value.labels || {},
},
HostConfig: {
RestartPolicy: {},
}
}}
OnChange={(containerInfo) => {
setOverrides({
...overrides,
[value.container_name]: {
environment: containerInfo.envVars,
labels: containerInfo.labels,
}
})
}}
noCard
installer
/>
<CosmosFormDivider title="Volumes" />
<VolumeContainerSetup
newContainer
frozenVolumes={service['cosmos-installer'] && service['cosmos-installer']['frozen-volumes'] || []}
containerInfo={{
HostConfig: {
Binds: [],
Mounts: Object.keys(value.volumes).map(k => {
return {
Type: value.volumes[k].type || (k.startsWith('/') ? 'bind' : 'volume'),
Source: value.volumes[k].source || "",
Target: value.volumes[k].target || "",
}
}) || [],
}
}}
OnChange={(containerInfo, volumes) => {
setOverrides({
...overrides,
[value.container_name]: {
volumes: containerInfo.volumes.map((v, k) => {
return {
type: v.Type,
source: v.Source,
target: v.Target,
existing: v.Type == 'volume' && volumes.find(v2 => v2.Source === v.Name),
}
})
}
})
}}
noCard
/>
</Stack>
</CosmosCollapse>
})}
</>)}
</Stack></>} </Stack></>}
{step === 0 && dockerComposeInit && dockerCompose == '' && <Stack spacing={2} alignItems={'center'} style={{paddingTop: '20px'}}>
<CircularProgress />
</Stack>}
{step === 1 && <Stack spacing={2}> {step === 1 && <Stack spacing={2}>
<NewDockerService service={service} refresh={refresh} /> <NewDockerService service={service} refresh={refresh} />
@ -392,6 +609,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
setStep(0); setStep(0);
setDockerCompose(''); setDockerCompose('');
setYmlError(''); setYmlError('');
setInstaller(false);
}}>Cancel</Button> }}>Cancel</Button>
<Button disabled={!dockerCompose || ymlError} onClick={() => { <Button disabled={!dockerCompose || ymlError} onClick={() => {
if (step === 0) { if (step === 0) {
@ -406,14 +624,21 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installer, defaultNam
</DialogActions>} </DialogActions>}
</Dialog> </Dialog>
<ResponsiveButton {(installerInit && service.minVersion && isNewerVersion(service.minVersion)) ?
color="primary" <Alert severity="error" icon={<WarningOutlined />}>
onClick={() => setOpenModal(true)} This service requires a newer version of Cosmos. Please update Cosmos to install this service.
variant={(installer ? "contained" : "outlined")} </Alert>
startIcon={(installer ? <ArrowDownOutlined /> : <ArrowUpOutlined />)} : <ResponsiveButton
> color="primary"
{installer ? 'Install' : 'Import Compose File'} onClick={() => {
</ResponsiveButton> openModalFunc();
}}
variant={(installerInit ? "contained" : "outlined")}
startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
>
{installerInit ? 'Install' : 'Import Compose File'}
</ResponsiveButton>
}
</>; </>;
}; };

View file

@ -60,6 +60,8 @@ const NewDockerService = ({service, refresh}) => {
const [openModal, setOpenModal] = React.useState(false); const [openModal, setOpenModal] = React.useState(false);
const preRef = React.useRef(null); const preRef = React.useRef(null);
delete service['cosmos-installer'];
React.useEffect(() => { React.useEffect(() => {
// refreshContainer(); // refreshContainer();
}, []); }, []);

View file

@ -8,6 +8,7 @@ import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
import * as API from '../../../api'; import * as API from '../../../api';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import LogsInModal from '../../../components/logsInModal'; import LogsInModal from '../../../components/logsInModal';
import ResponsiveButton from '../../../components/responseiveButton';
const containerInfoFrom = (values) => { const containerInfoFrom = (values) => {
const labels = {}; const labels = {};
@ -27,7 +28,7 @@ const containerInfoFrom = (values) => {
return realvalues; return realvalues;
} }
const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newContainer, OnForceSecure}) => { const DockerContainerSetup = ({noCard, containerInfo, installer, OnChange, refresh, newContainer, OnForceSecure}) => {
const restartPolicies = [ const restartPolicies = [
['no', 'No Restart'], ['no', 'No Restart'],
['always', 'Always Restart'], ['always', 'Always Restart'],
@ -39,6 +40,13 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const padding = isMobile ? '6px 4px' : '12px 10px'; const padding = isMobile ? '6px 4px' : '12px 10px';
const [latestImage, setLatestImage] = React.useState(containerInfo.Config.Image); const [latestImage, setLatestImage] = React.useState(containerInfo.Config.Image);
const wrapCard = (children) => {
if (noCard) return children;
return <MainCard title="Docker Container Setup">
{children}
</MainCard>;
};
return ( return (
<div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}> <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
@ -56,6 +64,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
}), }),
interactive: containerInfo.Config.Tty && containerInfo.Config.OpenStdin, interactive: containerInfo.Config.Tty && containerInfo.Config.OpenStdin,
}} }}
enableReinitialize
validate={(values) => { validate={(values) => {
const errors = {}; const errors = {};
if (!values.image) { if (!values.image) {
@ -115,13 +124,14 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
}} }}
/> />
<Stack spacing={2}> <Stack spacing={2}>
<MainCard title={'Docker Container Setup'}> {wrapCard(<>
{containerInfo.State && containerInfo.State.Status !== 'running' && ( {containerInfo.State && containerInfo.State.Status !== 'running' && (
<Alert severity="warning" style={{ marginBottom: '15px' }}> <Alert severity="warning" style={{ marginBottom: '15px' }}>
This container is not running. Editing any settings will cause the container to start again. This container is not running. Editing any settings will cause the container to start again.
</Alert> </Alert>
)} )}
<Grid container spacing={4}> <Grid container spacing={4}>
{!installer && <>
{newContainer && <CosmosInputText {newContainer && <CosmosInputText
name="name" name="name"
label="Name" label="Name"
@ -161,6 +171,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
}} }}
/> />
</Grid>} </Grid>}
</>}
<CosmosFormDivider title={'Environment Variables'} /> <CosmosFormDivider title={'Environment Variables'} />
<Grid item xs={12}> <Grid item xs={12}>
@ -206,8 +217,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
</Grid> </Grid>
</Grid> </Grid>
))} ))}
<IconButton <ResponsiveButton
fullWidth
variant="outlined" variant="outlined"
color="primary" color="primary"
size='large' size='large'
@ -216,9 +226,10 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
newEnvVars.push({ key: '', value: '' }); newEnvVars.push({ key: '', value: '' });
formik.setFieldValue('envVars', newEnvVars); formik.setFieldValue('envVars', newEnvVars);
}} }}
startIcon={<PlusCircleOutlined />}
> >
<PlusCircleOutlined /> Add
</IconButton> </ResponsiveButton>
</Grid> </Grid>
<CosmosFormDivider title={'Labels'} /> <CosmosFormDivider title={'Labels'} />
<Grid item xs={12}> <Grid item xs={12}>
@ -264,8 +275,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
</Grid> </Grid>
</Grid> </Grid>
))} ))}
<IconButton <ResponsiveButton
fullWidth
variant="outlined" variant="outlined"
color="primary" color="primary"
size='large' size='large'
@ -274,12 +284,13 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
newLabels.push({ key: '', value: '' }); newLabels.push({ key: '', value: '' });
formik.setFieldValue('labels', newLabels); formik.setFieldValue('labels', newLabels);
}} }}
startIcon={<PlusCircleOutlined />}
> >
<PlusCircleOutlined /> Add
</IconButton> </ResponsiveButton>
</Grid> </Grid>
</Grid> </Grid>
</MainCard> </>)}
{!newContainer && <MainCard> {!newContainer && <MainCard>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
{formik.errors.submit && ( {formik.errors.submit && (

View file

@ -12,7 +12,7 @@ import { NetworksColumns } from '../networks';
import NewNetworkButton from '../createNetwork'; import NewNetworkButton from '../createNetwork';
import ResponsiveButton from '../../../components/responseiveButton'; import ResponsiveButton from '../../../components/responseiveButton';
const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange }) => { const VolumeContainerSetup = ({ noCard, containerInfo, frozenVolumes = [], refresh, newContainer, OnChange }) => {
const restartPolicies = [ const restartPolicies = [
['no', 'No Restart'], ['no', 'No Restart'],
['always', 'Always Restart'], ['always', 'Always Restart'],
@ -44,6 +44,13 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
}); });
}; };
const wrapCard = (children) => {
if (noCard) return children;
return <MainCard title="Volume Mounts">
{children}
</MainCard>
};
return (<Stack spacing={2}> return (<Stack spacing={2}>
<div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}> <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<Formik <Formik
@ -60,6 +67,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
}) })
]) ])
}} }}
enableReinitialize
validate={(values) => { validate={(values) => {
const errors = {}; const errors = {};
// check unique // check unique
@ -70,7 +78,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
if (unique.length !== volumes.length) { if (unique.length !== volumes.length) {
errors.submit = 'Mounts must have unique targets'; errors.submit = 'Mounts must have unique targets';
} }
OnChange && OnChange(values); OnChange && OnChange(values, volumes);
return errors; return errors;
}} }}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
@ -95,8 +103,8 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
> >
{(formik) => ( {(formik) => (
<form noValidate onSubmit={formik.handleSubmit}> <form noValidate onSubmit={formik.handleSubmit}>
<MainCard title={'Volume Mounts'}> {wrapCard(<>
{containerInfo.State && containerInfo.State.Status !== 'running' && ( {!newContainer && containerInfo.State && containerInfo.State.Status !== 'running' && (
<Alert severity="warning" style={{ marginBottom: '15px' }}> <Alert severity="warning" style={{ marginBottom: '15px' }}>
This container is not running. Editing any settings will cause the container to start again. This container is not running. Editing any settings will cause the container to start again.
</Alert> </Alert>
@ -129,6 +137,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
<div style={{fontWeight: 'bold', wordSpace: 'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth: '100px'}}> <div style={{fontWeight: 'bold', wordSpace: 'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth: '100px'}}>
<TextField <TextField
className="px-2 my-2" className="px-2 my-2"
disabled={frozenVolumes.includes(r.Source)}
variant="outlined" variant="outlined"
name='Type' name='Type'
id='Type' id='Type'
@ -155,6 +164,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
variant="outlined" variant="outlined"
name='Source' name='Source'
id='Source' id='Source'
disabled={frozenVolumes.includes(r.Source)}
style={{minWidth: '200px'}} style={{minWidth: '200px'}}
value={r.Source} value={r.Source}
onChange={(e) => { onChange={(e) => {
@ -168,6 +178,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
variant="outlined" variant="outlined"
name='Source' name='Source'
id='Source' id='Source'
disabled={frozenVolumes.includes(r.Source)}
select select
style={{minWidth: '200px'}} style={{minWidth: '200px'}}
value={r.Source} value={r.Source}
@ -177,9 +188,9 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
formik.setFieldValue('volumes', newVolumes); formik.setFieldValue('volumes', newVolumes);
}} }}
> >
{volumes.map((volume) => ( {[...volumes, r].map((volume) => (
<MenuItem key={volume.Id} value={volume.Name}> <MenuItem key={volume.Id || "last"} value={volume.Name || volume.Source}>
{volume.Name} {volume.Name || volume.Source}
</MenuItem> </MenuItem>
))} ))}
</TextField> </TextField>
@ -195,6 +206,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
variant="outlined" variant="outlined"
name='Target' name='Target'
id='Target' id='Target'
disabled={frozenVolumes.includes(r.Source)}
style={{minWidth: '200px'}} style={{minWidth: '200px'}}
value={r.Target} value={r.Target}
onChange={(e) => { onChange={(e) => {
@ -211,6 +223,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
return (<Button return (<Button
variant="outlined" variant="outlined"
color="primary" color="primary"
disabled={frozenVolumes.includes(r.Source)}
onClick={() => { onClick={() => {
const newVolumes = [...formik.values.volumes]; const newVolumes = [...formik.values.volumes];
newVolumes.splice(newVolumes.indexOf(r), 1); newVolumes.splice(newVolumes.indexOf(r), 1);
@ -252,7 +265,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, On
</Stack> </Stack>
</Grid> </Grid>
</Grid> </Grid>
</MainCard> </>)}
</form> </form>
)} )}
</Formik> </Formik>

View file

@ -17,7 +17,7 @@ const ServappsIndex = () => {
return <div> return <div>
<IsLoggedIn /> <IsLoggedIn />
<PrettyTabbedView path="/ui/servapps/:tab" tabs={[ <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
{ {
title: 'Containers', title: 'Containers',
children: <ServeApps />, children: <ServeApps />,

View file

@ -150,7 +150,7 @@ const ServeApps = () => {
<ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => { <ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
refreshServeApps(); refreshServeApps();
}}>Refresh</ResponsiveButton> }}>Refresh</ResponsiveButton>
<Link to="/ui/servapps/new-service"> <Link to="/cosmos-ui/servapps/new-service">
<ResponsiveButton <ResponsiveButton
variant="contained" variant="contained"
startIcon={<AppstoreAddOutlined />} startIcon={<AppstoreAddOutlined />}
@ -303,7 +303,7 @@ const ServeApps = () => {
</Stack> </Stack>
</Stack> </Stack>
<div> <div>
<Link to={`/ui/servapps/containers/${app.Names[0].replace('/', '')}`}> <Link to={`/cosmos-ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
<Button variant="outlined" color="primary" fullWidth>Details</Button> <Button variant="outlined" color="primary" fullWidth>Details</Button>
</Link> </Link>
</div> </div>

View file

@ -22,35 +22,35 @@ const LoginRoutes = {
element: <MinimalLayout />, element: <MinimalLayout />,
children: [ children: [
{ {
path: '/ui/login', path: '/cosmos-ui/login',
element: <AuthLogin /> element: <AuthLogin />
}, },
{ {
path: '/ui/register', path: '/cosmos-ui/register',
element: <AuthRegister /> element: <AuthRegister />
}, },
{ {
path: '/ui/logout', path: '/cosmos-ui/logout',
element: <Logout /> element: <Logout />
}, },
{ {
path: '/ui/newInstall', path: '/cosmos-ui/newInstall',
element: <NewInstall /> element: <NewInstall />
}, },
{ {
path: '/ui/newmfa', path: '/cosmos-ui/newmfa',
element: <NewMFA /> element: <NewMFA />
}, },
{ {
path: '/ui/openid', path: '/cosmos-ui/openid',
element: <OpenID /> element: <OpenID />
}, },
{ {
path: '/ui/loginmfa', path: '/cosmos-ui/loginmfa',
element: <MFALogin /> element: <MFALogin />
}, },
{ {
path: '/ui/forgot-password', path: '/cosmos-ui/forgot-password',
element: <ForgotPassword /> element: <ForgotPassword />
}, },
] ]

View file

@ -39,60 +39,60 @@ const MainRoutes = {
children: [ children: [
{ {
path: '/', path: '/',
// redirect to /ui // redirect to /cosmos-ui
element: <Navigate to="/ui" /> element: <Navigate to="/cosmos-ui" />
}, },
{ {
path: '/ui/logo', path: '/cosmos-ui/logo',
// redirect to /ui // redirect to /cosmos-ui
element: <Navigate to={logo} /> element: <Navigate to={logo} />
}, },
{ {
path: '/ui', path: '/cosmos-ui',
element: <HomePage /> element: <HomePage />
}, },
{ {
path: '/ui/dashboard', path: '/cosmos-ui/dashboard',
element: <DashboardDefault /> element: <DashboardDefault />
}, },
{ {
path: '/ui/servapps', path: '/cosmos-ui/servapps',
element: <ServeAppsIndex /> element: <ServeAppsIndex />
}, },
{ {
path: '/ui/config-users', path: '/cosmos-ui/config-users',
element: <UserManagement /> element: <UserManagement />
}, },
{ {
path: '/ui/config-general', path: '/cosmos-ui/config-general',
element: <ConfigManagement /> element: <ConfigManagement />
}, },
{ {
path: '/ui/servapps/new-service', path: '/cosmos-ui/servapps/new-service',
element: <NewDockerServiceForm /> element: <NewDockerServiceForm />
}, },
{ {
path: '/ui/config-url', path: '/cosmos-ui/config-url',
element: <ProxyManagement /> element: <ProxyManagement />
}, },
{ {
path: '/ui/config-url/:routeName', path: '/cosmos-ui/config-url/:routeName',
element: <RouteConfigPage />, element: <RouteConfigPage />,
}, },
{ {
path: '/ui/servapps/containers/:containerName', path: '/cosmos-ui/servapps/containers/:containerName',
element: <ContainerIndex />, element: <ContainerIndex />,
}, },
{ {
path: '/ui/openid-manage', path: '/cosmos-ui/openid-manage',
element: <OpenIdList />, element: <OpenIdList />,
}, },
{ {
path: '/ui/market-listing/', path: '/cosmos-ui/market-listing/',
element: <MarketPage /> element: <MarketPage />
}, },
{ {
path: '/ui/market-listing/:appName', path: '/cosmos-ui/market-listing/:appName',
element: <MarketPage /> element: <MarketPage />
} }
] ]

View file

@ -25,4 +25,25 @@ export function isDomain(hostname) {
} }
return true; return true;
} }
export function debounce(func, wait, immediate) {
var timeout;
return () => {
var context = this, args = arguments;
var later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};

118
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.0-unstable", "version": "0.7.0-unstable2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.0-unstable", "version": "0.7.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",
@ -24,6 +24,7 @@
"apexcharts": "^3.35.5", "apexcharts": "^3.35.5",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"browserslist": "^4.21.7", "browserslist": "^4.21.7",
"dot": "^1.1.3",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"history": "^5.3.0", "history": "^5.3.0",
@ -52,6 +53,8 @@
"typescript": "4.8.3", "typescript": "4.8.3",
"vite": "^4.2.0", "vite": "^4.2.0",
"web-vitals": "^3.0.2", "web-vitals": "^3.0.2",
"whiskers": "^0.4.0",
"whiskers.js": "^1.0.0",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
@ -4391,6 +4394,11 @@
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
"dev": true "dev": true
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@ -4411,6 +4419,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -4749,6 +4766,17 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": { "node_modules/comma-separated-tokens": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
@ -4997,6 +5025,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/diff-sequences": { "node_modules/diff-sequences": {
"version": "29.4.3", "version": "29.4.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
@ -5048,6 +5084,17 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/dot": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dot/-/dot-1.1.3.tgz",
"integrity": "sha512-/nt74Rm+PcfnirXGEdhZleTwGC2LMnuKTeeTIlI82xb5loBBoXNYzr2ezCroPSMtilK8EZIfcNZwOcHN+ib1Lg==",
"engines": [
"node >=0.2.6"
],
"bin": {
"dottojs": "bin/dot-packer"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.419", "version": "1.4.419",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz",
@ -6107,6 +6154,25 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -6115,6 +6181,19 @@
"is-callable": "^1.1.3" "is-callable": "^1.1.3"
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/format": { "node_modules/format": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@ -7687,6 +7766,25 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": { "node_modules/mimic-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -9923,6 +10021,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/whiskers": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/whiskers/-/whiskers-0.4.0.tgz",
"integrity": "sha512-pTygA/fE6RIMOp3AwUy7E9jrdpqUEa4k5VCdJIBZ/64kNtiMuCTCYC6fzbiUhjxN32zX+qZQlZACMC/un5HS7A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/whiskers.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/whiskers.js/-/whiskers.js-1.0.0.tgz",
"integrity": "sha512-IzdNdA2jTPrTEnjQkySSOW90rtb++vJUjkHdwwsjqIeGndnizM3Mo+5DEVZ3iy7Su3itR92i9+EFtYamIT3Zxw==",
"dependencies": {
"axios": "^0.27.2"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.0-unstable2", "version": "0.7.0-unstable3",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {
@ -24,6 +24,7 @@
"apexcharts": "^3.35.5", "apexcharts": "^3.35.5",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"browserslist": "^4.21.7", "browserslist": "^4.21.7",
"dot": "^1.1.3",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"history": "^5.3.0", "history": "^5.3.0",
@ -52,11 +53,13 @@
"typescript": "4.8.3", "typescript": "4.8.3",
"vite": "^4.2.0", "vite": "^4.2.0",
"web-vitals": "^3.0.2", "web-vitals": "^3.0.2",
"whiskers": "^0.4.0",
"whiskers.js": "^1.0.0",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"scripts": { "scripts": {
"client": "vite", "client": "vite",
"client-build": "vite build --base=/ui/", "client-build": "vite build --base=/cosmos-ui/",
"start": "env CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos", "start": "env 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",
@ -64,7 +67,7 @@
"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 -v /:/mnt/host --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 -v /:/mnt/host --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",
"dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun", "dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
"demo": "vite build --base=/ui/ --mode demo", "demo": "vite build --base=/cosmos-ui/ --mode demo",
"devdemo": "vite --mode demo" "devdemo": "vite --mode demo"
}, },
"eslintConfig": { "eslintConfig": {

View file

@ -5,7 +5,6 @@
<!-- sponsors --> <!-- sponsors -->
<h3 align="center">Thanks to the sponsors:</h3></br> <h3 align="center">Thanks to the sponsors:</h3></br>
<p align="center"><a href="https://github.com/zarevskaya"><img src="https://avatars.githubusercontent.com/zarevskaya" style="border-radius:48px" width="48" height="48" alt="zarev" title="zarev" /></a> <p align="center"><a href="https://github.com/zarevskaya"><img src="https://avatars.githubusercontent.com/zarevskaya" style="border-radius:48px" width="48" height="48" alt="zarev" title="zarev" /></a>
<a href="https://github.com/DrMxrcy"><img src="https://avatars.githubusercontent.com/DrMxrcy" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
</p><!-- /sponsors --> </p><!-- /sponsors -->
--- ---
@ -24,7 +23,7 @@ Cosmos is a self-hosted platform for running server applications securely and wi
<a target="_blank" href="https://cosmos-cloud.io/doc"> <a target="_blank" href="https://cosmos-cloud.io/doc">
<img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/doc.png" /> <img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/doc.png" />
</a> </a>
<a target="_blank" href="https://cosmos-cloud.io/ui"> <a target="_blank" href="https://cosmos-cloud.io/cosmos-ui">
<img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/demo.png" /> <img height="44px" src="https://raw.githubusercontent.com/azukaar/Cosmos-Server/master/icons/demo.png" />
</a> </a>
<br/> <br/>

View file

@ -72,7 +72,7 @@ func discoverEndpoint(rw http.ResponseWriter, req *http.Request) {
json.NewEncoder(rw).Encode(&oidcConfiguration{ json.NewEncoder(rw).Encode(&oidcConfiguration{
Issuer: hostname, Issuer: hostname,
AuthURL: realHostname + "/ui/openid", AuthURL: realHostname + "/cosmos-ui/openid",
TokenURL: hostname + "/oauth2/token", TokenURL: hostname + "/oauth2/token",
JWKsURI: hostname + "/.well-known/jwks.json", JWKsURI: hostname + "/.well-known/jwks.json",
RevocationEndpoint: hostname + "/oauth2/revoke", RevocationEndpoint: hostname + "/oauth2/revoke",
@ -96,7 +96,7 @@ func discoverEndpoint(rw http.ResponseWriter, req *http.Request) {
BackChannelLogoutSessionSupported: true, BackChannelLogoutSessionSupported: true,
FrontChannelLogoutSupported: true, FrontChannelLogoutSupported: true,
FrontChannelLogoutSessionSupported: true, FrontChannelLogoutSessionSupported: true,
EndSessionEndpoint: hostname + "/ui/logout", EndSessionEndpoint: hostname + "/cosmos-ui/logout",
RequestObjectSigningAlgValuesSupported: []string{"RS256"}, RequestObjectSigningAlgValuesSupported: []string{"RS256"},
CodeChallengeMethodsSupported: []string{"plain", "S256"}, CodeChallengeMethodsSupported: []string{"plain", "S256"},
}) })

View file

@ -384,7 +384,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
if containerConfig.Env != nil { if containerConfig.Env != nil {
for i, env := range containerConfig.Env { for i, env := range containerConfig.Env {
if strings.HasPrefix(env, "TZ=") { if strings.HasPrefix(env, "TZ=") {
if strings.TrimPrefix(env, "TZ=") == "" { if strings.TrimPrefix(env, "TZ=") == "auto" {
if os.Getenv("TZ") != "" { if os.Getenv("TZ") != "" {
containerConfig.Env[i] = "TZ=" + os.Getenv("TZ") containerConfig.Env[i] = "TZ=" + os.Getenv("TZ")
} else { } else {

View file

@ -302,12 +302,12 @@ func StartServer() {
fs = utils.EnsureHostname(fs) fs = utils.EnsureHostname(fs)
} }
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs)) router.PathPrefix("/cosmos-ui").Handler(http.StripPrefix("/cosmos-ui", fs))
router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig) router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig)
router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ui", http.StatusMovedPermanently) http.Redirect(w, r, "/cosmos-ui", http.StatusMovedPermanently)
})) }))

View file

@ -152,7 +152,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err
} }
requestURL := req.URL.Path requestURL := req.URL.Path
isSettingMFA := strings.HasPrefix(requestURL, "/ui/loginmfa") || strings.HasPrefix(requestURL, "/ui/newmfa") || strings.HasPrefix(requestURL, "/api/mfa") isSettingMFA := strings.HasPrefix(requestURL, "/cosmos-ui/loginmfa") || strings.HasPrefix(requestURL, "/cosmos-ui/newmfa") || strings.HasPrefix(requestURL, "/api/mfa")
userInBase.MFAState = 0 userInBase.MFAState = 0
@ -201,15 +201,15 @@ func logOutUser(w http.ResponseWriter, req *http.Request) {
} }
func redirectToReLogin(w http.ResponseWriter, req *http.Request) { func redirectToReLogin(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/login?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/login?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
} }
func redirectToLoginMFA(w http.ResponseWriter, req *http.Request) { func redirectToLoginMFA(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
} }
func redirectToNewMFA(w http.ResponseWriter, req *http.Request) { func redirectToNewMFA(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
} }
func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mfaDone bool) { func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mfaDone bool) {

View file

@ -15,15 +15,15 @@ func LoggedInOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
if !isUserLoggedIn || userNickname == "" { if !isUserLoggedIn || userNickname == "" {
Error("LoggedInOnlyWithRedirect: User is not logged in", nil) Error("LoggedInOnlyWithRedirect: User is not logged in", nil)
http.Redirect(w, req, "/ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound) http.Redirect(w, req, "/cosmos-ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
return errors.New("User not logged in") return errors.New("User not logged in")
} }
if(mfa == 1) { if(mfa == 1) {
http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
return errors.New("User requires MFA") return errors.New("User requires MFA")
} else if(mfa == 2) { } else if(mfa == 2) {
http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
return errors.New("User requires MFA Setup") return errors.New("User requires MFA Setup")
} }
@ -39,7 +39,7 @@ func AdminOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
if !isUserLoggedIn || userNickname == "" { if !isUserLoggedIn || userNickname == "" {
Error("AdminLoggedInOnlyWithRedirect: User is not logged in", nil) Error("AdminLoggedInOnlyWithRedirect: User is not logged in", nil)
http.Redirect(w, req, "/ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound) http.Redirect(w, req, "/cosmos-ui/login?notlogged=1&redirect="+req.URL.Path, http.StatusFound)
return errors.New("User is not logged") return errors.New("User is not logged")
} }
@ -50,10 +50,10 @@ func AdminOnlyWithRedirect(w http.ResponseWriter, req *http.Request) error {
} }
if(mfa == 1) { if(mfa == 1) {
http.Redirect(w, req, "/ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/loginmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
return errors.New("User requires MFA") return errors.New("User requires MFA")
} else if(mfa == 2) { } else if(mfa == 2) {
http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) http.Redirect(w, req, "/cosmos-ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect)
return errors.New("User requires MFA Setup") return errors.New("User requires MFA Setup")
} }