[release] v0.13.0-unstable0

This commit is contained in:
Yann Stepienik 2023-11-21 19:32:02 +00:00
parent 320b29df55
commit 4884c95a50
21 changed files with 529 additions and 85 deletions

View file

@ -1,3 +1,10 @@
## Version 0.13.0
- Display containers as stacks
- new Delete modal to delete services entirely
- cosmos-network now have container names instead for network names
- Fix issue where search bar reset when deleting volume/network
- Fix breadcrumbs in subpaths
## Version 0.12.6
- Fix a security issue with cross-domain APIs availability

View file

@ -15,6 +15,15 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
const location = useLocation();
const [main, setMain] = useState();
const [item, setItem] = useState();
let subItem = '';
// extract /servapps/stack/:stack
const subPath = location.pathname.split('/')[3];
if(subPath && location.pathname.split('/')[4]) {
subItem = <Typography variant="subtitle1" color="textPrimary">
{location.pathname.split('/')[4]}
</Typography>;
}
// set active item state
const getCollapse = (menu) => {
@ -23,7 +32,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse);
} else if (collapse.type && collapse.type === 'item') {
if (location.pathname === collapse.url) {
if (location.pathname.startsWith(collapse.url)) {
setMain(menu);
setItem(collapse);
}
@ -82,6 +91,8 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
</Typography>
{mainContent}
{itemContent}
{subPath && <Typography variant="subtitle1" color="textPrimary">{subPath}</Typography>}
{subItem}
</MuiBreadcrumbs>
</Grid>
{title && (

View file

@ -3,13 +3,13 @@ import { Card, Chip, Stack, Tooltip } from "@mui/material";
import { useState } from "react";
import { useTheme } from '@mui/material/styles';
export const DeleteButton = ({onDelete, disabled}) => {
export const DeleteButton = ({onDelete, disabled, size}) => {
const [confirmDelete, setConfirmDelete] = useState(false);
return (<>
{!confirmDelete && (<Chip label={<DeleteOutlined />}
{!confirmDelete && (<Chip label={<DeleteOutlined size={size}/>}
onClick={() => !disabled && setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error"
{confirmDelete && (<Chip label={<CheckOutlined size={size}/>} color="error"
onClick={(event) => !disabled && onDelete(event)}/>)}
</>);
}

View file

@ -6,12 +6,12 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
import { CircularProgress, Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
import { SearchOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import { Link } from 'react-router-dom';
const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => {
const PrettyTableView = ({ isLoading, getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => {
const [search, setSearch] = React.useState('');
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
@ -43,7 +43,17 @@ const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, butt
/>
{buttons}
</Stack>
<TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
{isLoading && (<div style={{height: '550px'}}>
<center>
<br />
<CircularProgress />
</center>
</div>
)}
{!isLoading && <TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
@ -102,7 +112,7 @@ const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, butt
))}
</TableBody>
</Table>
</TableContainer>
</TableContainer>}
</Stack>
)
}

View file

@ -4,7 +4,6 @@ import { useEffect } from 'react';
import { redirectToLocal } from './utils/indexs';
const IsLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
const urlSearch = encodeURIComponent(window.location.search);
const redirectToURL = (window.location.pathname + urlSearch);

View file

@ -71,6 +71,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
PathPrefix: routeConfig.PathPrefix,
StripPathPrefix: routeConfig.StripPathPrefix,
AuthEnabled: routeConfig.AuthEnabled,
HideFromDashboard: routeConfig.HideFromDashboard,
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
RestrictToConstellation: routeConfig.RestrictToConstellation,
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
@ -273,6 +274,13 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
<CosmosCollapse title={'Advanced Settings'}>
<Stack spacing={2}>
<CosmosCheckbox
name="HideFromDashboard"
label="Hide from Dashboard"
formik={formik}
/>
<CosmosFormDivider />
<Alert severity='info'>These settings are for advanced users only. Please do not change these unless you know what you are doing.</Alert>
<CosmosInputText
name="OverwriteHostHeader"

View file

@ -1,21 +1,27 @@
import React from 'react';
import { Box, IconButton, LinearProgress, Stack, Tooltip, useMediaQuery } from '@mui/material';
import { Box, Chip, IconButton, LinearProgress, Stack, Tooltip, useMediaQuery } from '@mui/material';
import { CheckCircleOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
import * as API from '../../api';
import LogsInModal from '../../components/logsInModal';
import DeleteModal from './deleteModal';
const GetActions = ({
Id,
Ids,
state,
image,
refreshServApps,
setIsUpdatingId,
updateAvailable
updateAvailable,
isStack,
containers,
config,
}) => {
const [confirmDelete, setConfirmDelete] = React.useState(false);
const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm'));
const [pullRequest, setPullRequest] = React.useState(null);
const [isUpdating, setIsUpdating] = React.useState(false);
const doTo = (action) => {
setIsUpdating(true);
@ -27,20 +33,41 @@ const GetActions = ({
return;
}
setIsUpdatingId(Id, true);
return API.docker.manageContainer(Id, action).then((res) => {
setIsUpdating(false);
refreshServApps();
}).catch((err) => {
setIsUpdating(false);
refreshServApps();
});
return isStack ?
(() => {
Promise.all(Ids.map((id) => {
setIsUpdatingId(id, true);
return API.docker.manageContainer(id, action)
})).then((res) => {
setIsUpdating(false);
refreshServApps();
}).catch((err) => {
setIsUpdating(false);
refreshServApps();
})
})()
:
(() => {
setIsUpdatingId(Id, true);
API.docker.manageContainer(Id, action).then((res) => {
setIsUpdating(false);
refreshServApps();
}).catch((err) => {
setIsUpdating(false);
refreshServApps();
});
})()
};
let actions = [
{
t: 'Update Available, Click to Update',
t: 'Update Available' + (isStack ? ', go the stack details to update' : ', Click to Update'),
if: ['update_available'],
es: <IconButton className="shinyButton" style={{cursor: 'not-allowed'}} color='primary' onClick={()=>{}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>,
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>
@ -48,6 +75,7 @@ const GetActions = ({
{
t: 'No Update Available. Click to Force Pull',
if: ['update_not_available'],
hideStack: true,
e: <IconButton onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined />
</IconButton>
@ -90,6 +118,7 @@ const GetActions = ({
{
t: 'Re-create',
if: ['exited', 'running', 'paused', 'created', 'restarting'],
hideStack: true,
e: <IconButton onClick={() => doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
<RollbackOutlined />
</IconButton>
@ -104,13 +133,7 @@ const GetActions = ({
{
t: 'Delete',
if: ['exited', 'created'],
e: <IconButton onClick={() => {
if(confirmDelete) doTo('remove')
else setConfirmDelete(true);
}} color="error" size='large'>
{confirmDelete ? <CheckCircleOutlined />
: <DeleteOutlined />}
</IconButton>
e: <DeleteModal config={config} Ids={Ids} containers={containers} refreshServApps={refreshServApps} setIsUpdatingId={setIsUpdatingId} />
}
];
@ -126,7 +149,7 @@ const GetActions = ({
{!isUpdating && actions.filter((action) => {
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')) || (!updateAvailable && action.if.includes('update_not_available'));
}).map((action) => {
return <Tooltip title={action.t}>{action.e}</Tooltip>
return (!isStack || !action.hideStack) && <Tooltip title={action.t}>{isStack ? (action.es ? action.es : action.e) : action.e}</Tooltip>
})}
{isUpdating && <Stack sx={{

View file

@ -93,6 +93,7 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
<Stack spacing={2} direction={'row'} >
<GetActions
Id={containerInfo.Name}
Ids={[containerInfo.Name]}
image={Image}
state={State.Status}
refreshServApps={() => {
@ -103,6 +104,9 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
setIsUpdating(true);
}}
updateAvailable={updatesAvailable && updatesAvailable[Name]}
isStack={false}
containers={[containerInfo]}
config={config}
/>
</Stack>
{containerInfo.State.Status !== 'running' && (

View file

@ -0,0 +1,246 @@
import React from 'react';
import { Box, Button, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, LinearProgress, Stack, Tooltip, useMediaQuery } from '@mui/material';
import { ApiOutlined, CheckCircleOutlined, CloseSquareOutlined, ContainerOutlined, DatabaseOutlined, DeleteOutlined, LinkOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
import * as API from '../../api';
import LogsInModal from '../../components/logsInModal';
import { DeleteButton } from '../../components/delete';
import { CosmosCheckbox } from '../config/users/formShortcuts';
import { getContainersRoutes } from '../../utils/routes';
const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [confirmDelete, setConfirmDelete] = React.useState(false);
const [failed, setFailed] = React.useState([]);
const [deleted, setDeleted] = React.useState([]);
const [ignored, setIgnored] = React.useState([]);
const [isDeleting, setIsDeleting] = React.useState(false);
containers = containers.map((container) => {
if(container.Names) {
container.Name = container.Names[0];
}
return container;
});
const ShowAction = ({item}) => {
if(!isDeleting) {
return <Checkbox checked={!ignored.includes(item)} size="large" id={item} onChange={(e) => {
if(!e.target.checked) {
setIgnored((prev) => {
if(!prev) return [item];
else return [...prev, item];
});
} else {
setIgnored((prev) => {
if(!prev) return [];
else return prev.filter((i) => i !== item);
});
}
}} />
}
if(failed.includes(item)) {
return "❌"
} else if(deleted.includes(item)) {
return "✔️"
} else {
return <CircularProgress size={18} />
}
}
let networks = isOpen && containers.map((container) => {
return Object.keys(container.NetworkSettings.Networks);
}).flat().filter((network, index, self) => {
return self.indexOf(network) === index;
});
let volumes = isOpen && containers.map((container) => {
return container.Mounts.filter((mount) => {
return mount.Type === 'volume'
}).map((mount) => {
return mount.Name
})
}).flat().filter((volume, index, self) => {
return self.indexOf(volume) === index;
});
let routes = isOpen && containers.map((container) => {
return getContainersRoutes(config, container.Name.replace('/', ''));
}).flat().map((route) => {
return route.Name;
}).filter((route, index, self) => {
return self.indexOf(route) === index;
});
console.log(routes);
const doDelete = () => {
setIsDeleting(true);
const promises = [];
promises.concat(
containers.map((container) => {
let key = container.Name + '-container';
if (ignored.includes(key)) return;
return API.docker.manageContainer(container.Name, 'remove')
.then(() => {
setDeleted((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
})
.catch((err) => {
setFailed((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
});
})
);
Promise.all(promises)
.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
})
.then(() => {
const promises2 = [];
promises2.concat(
networks.map((network) => {
let key = network + '-network';
if (ignored.includes(key)) return;
return API.docker.networkDelete(network)
.then(() => {
setDeleted((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
})
.catch((err) => {
setFailed((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
});
})
);
promises2.concat(
volumes.map((volume) => {
let key = volume + '-volume';
if (ignored.includes(key)) return;
return API.docker.volumeDelete(volume)
.then(() => {
setDeleted((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
})
.catch((err) => {
setFailed((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
});
})
);
promises2.concat(
routes.map((route) => {
let key = route + '-route';
if (ignored.includes(key)) return;
return API.config.deleteRoute(route)
.then(() => {
setDeleted((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
})
.catch((err) => {
setFailed((prev) => {
if(!prev) return [key];
else return [...prev, key];
});
});
})
);
return Promise.all(promises2);
})
}
return <>
{isOpen && <>
<Dialog open={isOpen} onClose={() => {refreshServApps() ; setIsOpen(false)}}>
<DialogTitle>Delete Service</DialogTitle>
<DialogContent>
<DialogContentText>
<Stack spacing={1}>
<div>
{isDeleting && <div>
Deletion status:
</div>}
{!isDeleting && <div>
Select what you wish to delete:
</div>}
</div>
{containers.map((container) => {
return (!isDeleting || (!ignored.includes(container.Name + "-container"))) && <div key={container.Name + "-container"}>
<ShowAction item={container.Name + "-container"} /> <ContainerOutlined /> Container {container.Name}
</div>
})}
{networks.map((network) => {
return (!isDeleting || (!ignored.includes(network + "-network"))) &&<div key={network + "-network"}>
<ShowAction item={network + "-network"} /> <ApiOutlined /> Network {network}
</div>
})}
{volumes.map((mount) => {
return (!isDeleting || (!ignored.includes(mount + "-volume"))) && <div key={mount + "-volume"}>
<ShowAction item={mount + "-volume"} /> <DatabaseOutlined /> Volume {mount}
</div>
})}
{routes.map((route) => {
return (!isDeleting || (!ignored.includes(route + "-route"))) && <div key={route + "-route"}>
<ShowAction item={route + "-route"} /> <LinkOutlined /> Route {route}
</div>
})}
</Stack>
</DialogContentText>
</DialogContent>
{!isDeleting && <DialogActions>
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={() => {
doDelete();
}}>Delete</Button>
</DialogActions>}
{isDeleting && <DialogActions>
<Button onClick={() => {
refreshServApps();
setIsOpen(false);
}}>Done</Button>
</DialogActions>}
</Dialog>
</>}
<IconButton onClick={() => {
setIsOpen(true);
setIsDeleting(false);
setFailed([]);
setDeleted([]);
setIgnored([]);
}} color="error" size='large'>
<DeleteOutlined />
</IconButton>
</>
}
export default DeleteModal;

View file

@ -12,15 +12,18 @@ import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import ServApps from './servapps';
import VolumeManagementList from './volumes';
import NetworkManagementList from './networks';
import { useParams } from 'react-router';
const ServappsIndex = () => {
const { stack } = useParams();
return <div>
<IsLoggedIn />
<PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
{!stack && <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
{
title: 'Containers',
children: <ServApps />,
children: <ServApps stack={stack} />,
path: 'containers'
},
{
@ -34,6 +37,9 @@ const ServappsIndex = () => {
path: 'networks'
},
]}/>
}
{stack && <ServApps stack={stack} />}
</div>;
}

View file

@ -73,17 +73,10 @@ const NetworkManagementList = () => {
</Button>
</Stack>
{isLoading && (<div style={{ height: '550px' }}>
<center>
<br />
<CircularProgress />
</center>
</div>
)}
{!isLoading && rows && (
{rows && (
<PrettyTableView
data={rows}
isLoading={isLoading}
buttons={[
<NewNetworkButton refresh={refresh} />,
]}

View file

@ -22,6 +22,7 @@ import { ContainerNetworkWarning } from '../../components/containers';
import { ServAppIcon } from '../../utils/servapp-icon';
import MiniPlotComponent from '../dashboard/components/mini-plot';
import { DownloadFile } from '../../api/downloadButton';
import { useTheme } from '@mui/material/styles';
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
@ -31,6 +32,15 @@ const Item = styled(Paper)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const StyledBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
right: 5,
top: -10,
border: `2px solid ${theme.palette.background.paper}`,
padding: '0 4px',
},
}));
const noOver = {
overflowX: 'auto',
width: "100%",
@ -38,7 +48,7 @@ const noOver = {
height: "50px"
}
const ServApps = () => {
const ServApps = ({stack}) => {
const [servApps, setServApps] = useState([]);
const [isUpdating, setIsUpdating] = useState({});
const [search, setSearch] = useState("");
@ -114,6 +124,107 @@ const ServApps = () => {
}
}
const statusPriority = [
"running",
"paused",
"created",
"restarting",
"removing",
"exited",
"dead"
]
const servAppsStacked = servApps && servApps.reduce((acc, app) => {
// if has label cosmos-stack, add to stack
if(!stack && (app.Labels['cosmos-stack'] || app.Labels['com.docker.compose.project'])) {
let stackName = app.Labels['cosmos-stack'] || app.Labels['com.docker.compose.project'];
let stackMain = app.Labels['cosmos-stack-main'] || (app.Labels['com.docker.compose.container-number'] == '1' && app.Names[0].replace('/', ''));
if(!acc[stackName]) {
acc[stackName] = {
type: 'stack',
name: stackName,
state: -1,
app: {},
apps: [],
ports: [],
isUpdating: false,
updateAvailable: false,
labels: {
'cosmos-force-network-secured': 'true',
'cosmos-auto-update': 'true',
},
networkSettings: {
Networks: {}
},
};
}
acc[stackName].apps.push(app);
if(statusPriority.indexOf(app.State) > statusPriority.indexOf(acc[stackName].state)) {
acc[stackName].state = app.State;
}
acc[stackName].ports = acc[stackName].ports.concat(app.Ports);
if(!app.Labels['cosmos-force-network-secured']) {
acc[stackName].labels['cosmos-force-network-secured'] = 'false';
}
if(isUpdating[app.Names[0].replace('/', '')]) {
acc[stackName].isUpdating = true;
}
if(!app.Labels['cosmos-auto-update']) {
acc[stackName].labels['cosmos-auto-update'] = 'false';
}
acc[stackName].networkSettings = {
...acc[stackName].networkSettings,
...app.NetworkSettings
};
if(updatesAvailable && updatesAvailable[app.Names[0]]) {
acc[stackName].updateAvailable = true;
}
if(stackMain == app.Names[0].replace('/', '') || !acc[stackName].app) {
acc[stackName].app = app;
}
} else if (!stack || (stack && (app.Labels['cosmos-stack'] === stack || app.Labels['com.docker.compose.project'] === stack))){
// else add to default stack
acc[app.Names[0]] = {
type: 'app',
name: app.Names[0],
state: app.State,
app: app,
apps: [app],
isUpdating: isUpdating[app.Names[0].replace('/', '')],
ports: app.Ports,
networkSettings: app.NetworkSettings,
labels: app.Labels,
updateAvailable: updatesAvailable && updatesAvailable[app.Names[0]],
};
}
return acc;
}, {});
// flatten stacks with single app
Object.keys(servAppsStacked).forEach((key) => {
if(servAppsStacked[key].type === 'stack' && servAppsStacked[key].apps.length === 1) {
servAppsStacked[key] = {
...servAppsStacked[key],
type: 'app',
name: servAppsStacked[key].apps[0].Names[0],
app: servAppsStacked[key].app,
apps: [servAppsStacked[key].app],
isUpdating: isUpdating[servAppsStacked[key].apps[0].Names[0].replace('/', '')],
ports: servAppsStacked[key].apps[0].Ports,
networkSettings: servAppsStacked[key].apps[0].NetworkSettings,
labels: servAppsStacked[key].apps[0].Labels,
updateAvailable: updatesAvailable && updatesAvailable[servAppsStacked[key].apps[0].Names[0]],
};
}
});
return <div>
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
<ExposeModal
@ -132,6 +243,9 @@ const ServApps = () => {
<Stack spacing={{xs: 1, sm: 1, md: 2 }}>
<Stack direction="row" spacing={2}>
{stack && <Link to="/cosmos-ui/servapps">
<ResponsiveButton variant="secondary" startIcon={<RollbackOutlined />}>Back</ResponsiveButton>
</Link>}
<Input placeholder="Search"
value={search}
startAdornment={
@ -146,6 +260,7 @@ const ServApps = () => {
<ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
refreshServApps();
}}>Refresh</ResponsiveButton>
{!stack && <>
<Link to="/cosmos-ui/servapps/new-service">
<ResponsiveButton
variant="contained"
@ -158,6 +273,7 @@ const ServApps = () => {
label={'Export Docker Backup'}
contentGetter={API.config.getBackup}
/>
</>}
</Stack>
<Grid2 container spacing={{xs: 1, sm: 1, md: 2 }}>
@ -166,8 +282,8 @@ const ServApps = () => {
<Alert severity="info">Update are available for {Object.keys(updatesAvailable).join(', ')}</Alert>
</Item>
</Grid2>}
{servApps && servApps.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} key={app.Id} item>
{servApps && Object.values(servAppsStacked).filter(app => search.length < 2 || app.name.toLowerCase().includes(search.toLowerCase())).map((app) => {
return <Grid2 sx={{...gridAnim}} xs={12} sm={6} md={6} lg={6} xl={4} key={app.Id} item>
<Item>
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
<Stack direction="column" spacing={0} alignItems="flex-start">
@ -182,29 +298,44 @@ const ServApps = () => {
"paused": <Chip label="Paused" color="info" />,
"exited": <Chip label="Exited" color="error" />,
"dead": <Chip label="Dead" color="error" />,
})[app.State]
})[app.state]
}
</Typography>
<Stack direction="row" spacing={2} alignItems="center">
<ServAppIcon container={app} route={getFirstRoute(app)} className="loading-image" width="40px"/>
{app.type === 'app' && <ServAppIcon container={app.app} route={getFirstRoute(app.app)} className="loading-image" width="40px"/>}
{app.type === 'stack' && <StyledBadge overlap="circular"
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}color="primary"
badgeContent={app.apps.length} >
<ServAppIcon container={app.app} route={getFirstRoute(app.app)} className="loading-image" width="40px"/>
</StyledBadge>}
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'no-wrap'}}>
<Typography variant="h5" color="text.secondary">
{app.Names[0].replace('/', '')}&nbsp;
{app.name.replace('/', '')}&nbsp; {app.type === 'stack' && <Chip label="Stack" color="primary" size="small" style={{fontSize: '55%'}}/>}
</Typography>
<Typography color="text.secondary" style={{fontSize: '80%', whiteSpace: 'nowrap', overflow: 'hidden', maxWidth: '100%', textOverflow: 'ellipsis'}}>
{app.Image}
{app.app.Image}
</Typography>
</Stack>
</Stack>
</Stack>
<Stack direction="row" spacing={1} width='100%'>
<GetActions
Id={app.Names[0].replace('/', '')}
image={app.Image}
state={app.State}
Id={app.app.Names[0].replace('/', '')}
Ids={app.apps.map((app) => {
return app.Names[0].replace('/', '');
})}
image={app.app.Image}
state={app.app.State}
setIsUpdatingId={setIsUpdatingId}
refreshServApps={refreshServApps}
updateAvailable={updatesAvailable && updatesAvailable[app.Names[0]]}
updateAvailable={app.updateAvailable}
isStack={app.type === 'stack'}
containers={app.apps}
config={config}
/>
</Stack>
</Stack>
@ -213,7 +344,7 @@ const ServApps = () => {
Ports
</Typography>
<Stack style={noOver} margin={1} direction="row" spacing={1}>
{app.Ports.filter(p => p.IP != '::').map((port) => {
{app.ports.filter(p => p.IP != '::').map((port) => {
return <Tooltip title={port.PublicPort ? 'Warning, this port is publicly accessible' : ''}>
<Chip style={{ fontSize: '80%' }} label={(port.PublicPort ? (port.PublicPort + ":") : '') + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
</Tooltip>
@ -225,23 +356,23 @@ const ServApps = () => {
Networks
</Typography>
<Stack style={noOver} margin={1} direction="row" spacing={1}>
{app.NetworkSettings.Networks && Object.keys(app.NetworkSettings.Networks).map((network) => {
{app.networkSettings.Networks && Object.keys(app.networkSettings.Networks).map((network) => {
return <Chip style={{ fontSize: '80%' }} label={network} color={network === 'bridge' ? 'warning' : 'default'} />
})}
</Stack>
</Stack>
{isUpdating[app.Names[0].replace('/', '')] ? <div>
{app.isUpdating ? <div>
<CircularProgress color="inherit" />
</div>
:
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
<Typography variant="h6" color="text.secondary">
Settings {app.State !== 'running' ? '(Start container to edit)' : ''}
Settings {app.type == "app" && (app.state !== 'running' ? '(Start container to edit)' : '')}
</Typography>
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
<Checkbox
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
disabled={app.State !== 'running'}
checked={app.labels['cosmos-force-network-secured'] === 'true'}
disabled={app.type == "stack" || app.state !== 'running'}
onChange={(e) => {
const name = app.Names[0].replace('/', '');
setIsUpdatingId(name, true);
@ -255,13 +386,13 @@ const ServApps = () => {
refreshServApps();
})
}}
/> Isolate Container Network <ContainerNetworkWarning container={app} />
/> Isolate Container Network <ContainerNetworkWarning container={app.app} />
</Stack>
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
<Checkbox
checked={app.Labels['cosmos-auto-update'] === 'true' ||
checked={app.labels['cosmos-auto-update'] === 'true' ||
(selfName && app.Names[0].replace('/', '') == selfName && config.AutoUpdate)}
disabled={app.State !== 'running'}
disabled={app.type == "stack" || app.state !== 'running'}
onChange={(e) => {
const name = app.Names[0].replace('/', '');
setIsUpdatingId(name, true);
@ -284,7 +415,7 @@ const ServApps = () => {
URLs
</Typography>
<Stack style={noOver} spacing={2} direction="row">
{getContainersRoutes(config, app.Names[0].replace('/', '')).map((route) => {
{getContainersRoutes(config, app.name.replace('/', '')).map((route) => {
return <HostChip route={route} settings/>
})}
{/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */}
@ -294,10 +425,10 @@ const ServApps = () => {
style={{paddingRight: '4px'}}
deleteIcon={<PlusCircleOutlined />}
onClick={() => {
setOpenModal(app);
setOpenModal(app.app);
}}
onDelete={() => {
setOpenModal(app);
setOpenModal(app.app);
}}
/>
{/* } */}
@ -305,16 +436,21 @@ const ServApps = () => {
</Stack>
<div>
<MiniPlotComponent agglo metrics={[
"cosmos.system.docker.cpu." + app.Names[0].replace('/', ''),
"cosmos.system.docker.ram." + app.Names[0].replace('/', ''),
"cosmos.system.docker.cpu." + app.name.replace('/', ''),
"cosmos.system.docker.ram." + app.name.replace('/', ''),
]} labels={{
["cosmos.system.docker.cpu." + app.Names[0].replace('/', '')]: "CPU",
["cosmos.system.docker.ram." + app.Names[0].replace('/', '')]: "RAM"
["cosmos.system.docker.cpu." + app.name.replace('/', '')]: "CPU",
["cosmos.system.docker.ram." + app.name.replace('/', '')]: "RAM"
}}/>
</div>
<div>
<Link to={`/cosmos-ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
<Button variant="outlined" color="primary" fullWidth>Details</Button>
<Link to={app.type === 'stack' ?
`/cosmos-ui/servapps/stack/${app.name}` :
`/cosmos-ui/servapps/containers/${app.name.replace('/', '')}`
}>
<Button variant="outlined" color="primary" fullWidth>
{app.type === 'stack' ? 'View Stack' : 'View Details'}
</Button>
</Link>
</div>
{/* <Stack>

View file

@ -44,17 +44,10 @@ const VolumeManagementList = () => {
</Button>
</Stack>
{isLoading && (<div style={{height: '550px'}}>
<center>
<br />
<CircularProgress />
</center>
</div>
)}
{!isLoading && rows && (
{rows && (
<PrettyTableView
data={rows}
isLoading={isLoading}
onRowClick={() => {}}
getKey={(r) => r.Name}
buttons={[

View file

@ -53,6 +53,10 @@ const MainRoutes = {
path: '/cosmos-ui/servapps',
element: <ServAppsIndex />
},
{
path: '/cosmos-ui/servapps/stack/:stack',
element: <ServAppsIndex />
},
{
path: '/cosmos-ui/config-users',
element: <UserManagement />

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.12.6",
"version": "0.13.0-unstable0",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -5,6 +5,7 @@
<!-- sponsors -->
<h3 align="center">Thanks to the sponsors:</h3></br>
<p align="center"><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>
<a href="https://github.com/BakaDalek"><img src="https://avatars.githubusercontent.com/BakaDalek" style="border-radius:48px" width="48" height="48" alt="BakaDalek" title="BakaDalek" /></a>
<a href="https://github.com/soldier1"><img src="https://avatars.githubusercontent.com/soldier1" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
<a href="https://github.com/devcircus"><img src="https://avatars.githubusercontent.com/devcircus" style="border-radius:48px" width="48" height="48" alt="Clayton Stone" title="Clayton Stone" /></a>
<a href="https://github.com/BlackrazorNZ"><img src="https://avatars.githubusercontent.com/BlackrazorNZ" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>

View file

@ -106,7 +106,7 @@ func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
utils.RestartHTTPServer()
if updateReq.NewRoute.Mode == "SERVAPP" {
if updateReq.NewRoute != nil && updateReq.NewRoute.Mode == "SERVAPP" {
utils.Log("RouteSettingsUpdate: Service needs update: "+updateReq.NewRoute.Target)
target := updateReq.NewRoute.Target

View file

@ -414,7 +414,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
OnLog(fmt.Sprintf("Forcing secure %s...\n", serviceName))
newNetwork, errNC := CreateCosmosNetwork()
newNetwork, errNC := CreateCosmosNetwork(serviceName)
if errNC != nil {
utils.Error("CreateService: Network", err)
OnLog(utils.DoErr("Network %s cant be created\n", newNetwork))

View file

@ -64,7 +64,7 @@ func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
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")
utils.HTTPError(w, "Volume Deletion Error " + err.Error(), http.StatusInternalServerError, "DV002")
return
}

View file

@ -70,7 +70,7 @@ func findAvailableSubnet() string {
return baseSubnet
}
func CreateCosmosNetwork() (string, error) {
func CreateCosmosNetwork(name string) (string, error) {
networks, err := DockerClient.NetworkList(DockerContext, types.NetworkListOptions{})
if err != nil {
utils.Error("Docker Network List", err)
@ -79,7 +79,7 @@ func CreateCosmosNetwork() (string, error) {
newNeworkName := ""
for {
newNeworkName = "cosmos-network-" + utils.GenerateRandomString(9)
newNeworkName = "cosmos-" + name + "-" + utils.GenerateRandomString(3)
exists := false
for _, network := range networks {
if network.Name == newNeworkName {
@ -144,7 +144,7 @@ func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) {
netName := ""
if(!HasLabel(containerConfig, "cosmos-network-name")) {
newNetwork, errNC := CreateCosmosNetwork()
newNetwork, errNC := CreateCosmosNetwork(containerConfig.Name[1:])
if errNC != nil {
utils.Error("DockerSecureNetworkCreate", errNC)
return false, errNC
@ -239,7 +239,7 @@ func IsConnectedToASecureCosmosNetwork(self types.ContainerJSON, containerConfig
if err != nil {
utils.Error("Container tries to connect to a non existing Cosmos network, replacing it.", err)
newNetwork, errNC := CreateCosmosNetwork()
newNetwork, errNC := CreateCosmosNetwork(containerConfig.Name[1:])
if errNC != nil {
utils.Error("DockerSecureNetworkCreate", errNC)
return false, errNC, false

View file

@ -58,6 +58,9 @@ func DB() error {
}
func DisconnectDB() {
if client == nil {
return
}
if err := client.Disconnect(context.TODO()); err != nil {
Fatal("DB", err)
}