[release] v0.13.0-unstable0
This commit is contained in:
parent
320b29df55
commit
4884c95a50
|
@ -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
|
## Version 0.12.6
|
||||||
- Fix a security issue with cross-domain APIs availability
|
- Fix a security issue with cross-domain APIs availability
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,15 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [main, setMain] = useState();
|
const [main, setMain] = useState();
|
||||||
const [item, setItem] = 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
|
// set active item state
|
||||||
const getCollapse = (menu) => {
|
const getCollapse = (menu) => {
|
||||||
|
@ -23,7 +32,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
||||||
if (collapse.type && collapse.type === 'collapse') {
|
if (collapse.type && collapse.type === 'collapse') {
|
||||||
getCollapse(collapse);
|
getCollapse(collapse);
|
||||||
} else if (collapse.type && collapse.type === 'item') {
|
} else if (collapse.type && collapse.type === 'item') {
|
||||||
if (location.pathname === collapse.url) {
|
if (location.pathname.startsWith(collapse.url)) {
|
||||||
setMain(menu);
|
setMain(menu);
|
||||||
setItem(collapse);
|
setItem(collapse);
|
||||||
}
|
}
|
||||||
|
@ -82,6 +91,8 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
|
||||||
</Typography>
|
</Typography>
|
||||||
{mainContent}
|
{mainContent}
|
||||||
{itemContent}
|
{itemContent}
|
||||||
|
{subPath && <Typography variant="subtitle1" color="textPrimary">{subPath}</Typography>}
|
||||||
|
{subItem}
|
||||||
</MuiBreadcrumbs>
|
</MuiBreadcrumbs>
|
||||||
</Grid>
|
</Grid>
|
||||||
{title && (
|
{title && (
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
export const DeleteButton = ({onDelete, disabled}) => {
|
export const DeleteButton = ({onDelete, disabled, size}) => {
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
{!confirmDelete && (<Chip label={<DeleteOutlined />}
|
{!confirmDelete && (<Chip label={<DeleteOutlined size={size}/>}
|
||||||
onClick={() => !disabled && setConfirmDelete(true)}/>)}
|
onClick={() => !disabled && setConfirmDelete(true)}/>)}
|
||||||
{confirmDelete && (<Chip label={<CheckOutlined />} color="error"
|
{confirmDelete && (<Chip label={<CheckOutlined size={size}/>} color="error"
|
||||||
onClick={(event) => !disabled && onDelete(event)}/>)}
|
onClick={(event) => !disabled && onDelete(event)}/>)}
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
|
@ -6,12 +6,12 @@ import TableContainer from '@mui/material/TableContainer';
|
||||||
import TableHead from '@mui/material/TableHead';
|
import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
import { CircularProgress, Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
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 [search, setSearch] = React.useState('');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === 'dark';
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
@ -43,7 +43,17 @@ const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, butt
|
||||||
/>
|
/>
|
||||||
{buttons}
|
{buttons}
|
||||||
</Stack>
|
</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">
|
<Table aria-label="simple table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
@ -102,7 +112,7 @@ const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, butt
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useEffect } from 'react';
|
||||||
import { redirectToLocal } from './utils/indexs';
|
import { redirectToLocal } from './utils/indexs';
|
||||||
|
|
||||||
const IsLoggedIn = () => useEffect(() => {
|
const IsLoggedIn = () => useEffect(() => {
|
||||||
console.log("CHECK LOGIN")
|
|
||||||
const urlSearch = encodeURIComponent(window.location.search);
|
const urlSearch = encodeURIComponent(window.location.search);
|
||||||
const redirectToURL = (window.location.pathname + urlSearch);
|
const redirectToURL = (window.location.pathname + urlSearch);
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
|
||||||
PathPrefix: routeConfig.PathPrefix,
|
PathPrefix: routeConfig.PathPrefix,
|
||||||
StripPathPrefix: routeConfig.StripPathPrefix,
|
StripPathPrefix: routeConfig.StripPathPrefix,
|
||||||
AuthEnabled: routeConfig.AuthEnabled,
|
AuthEnabled: routeConfig.AuthEnabled,
|
||||||
|
HideFromDashboard: routeConfig.HideFromDashboard,
|
||||||
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||||
RestrictToConstellation: routeConfig.RestrictToConstellation,
|
RestrictToConstellation: routeConfig.RestrictToConstellation,
|
||||||
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
|
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
|
||||||
|
@ -273,6 +274,13 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
|
||||||
|
|
||||||
<CosmosCollapse title={'Advanced Settings'}>
|
<CosmosCollapse title={'Advanced Settings'}>
|
||||||
<Stack spacing={2}>
|
<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>
|
<Alert severity='info'>These settings are for advanced users only. Please do not change these unless you know what you are doing.</Alert>
|
||||||
<CosmosInputText
|
<CosmosInputText
|
||||||
name="OverwriteHostHeader"
|
name="OverwriteHostHeader"
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import React from 'react';
|
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 { CheckCircleOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
|
||||||
import * as API from '../../api';
|
import * as API from '../../api';
|
||||||
import LogsInModal from '../../components/logsInModal';
|
import LogsInModal from '../../components/logsInModal';
|
||||||
|
import DeleteModal from './deleteModal';
|
||||||
|
|
||||||
const GetActions = ({
|
const GetActions = ({
|
||||||
Id,
|
Id,
|
||||||
|
Ids,
|
||||||
state,
|
state,
|
||||||
image,
|
image,
|
||||||
refreshServApps,
|
refreshServApps,
|
||||||
setIsUpdatingId,
|
setIsUpdatingId,
|
||||||
updateAvailable
|
updateAvailable,
|
||||||
|
isStack,
|
||||||
|
containers,
|
||||||
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||||
const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm'));
|
const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm'));
|
||||||
const [pullRequest, setPullRequest] = React.useState(null);
|
const [pullRequest, setPullRequest] = React.useState(null);
|
||||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
|
||||||
|
|
||||||
const doTo = (action) => {
|
const doTo = (action) => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
@ -27,20 +33,41 @@ const GetActions = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUpdatingId(Id, true);
|
return isStack ?
|
||||||
return API.docker.manageContainer(Id, action).then((res) => {
|
(() => {
|
||||||
setIsUpdating(false);
|
Promise.all(Ids.map((id) => {
|
||||||
refreshServApps();
|
setIsUpdatingId(id, true);
|
||||||
}).catch((err) => {
|
|
||||||
setIsUpdating(false);
|
return API.docker.manageContainer(id, action)
|
||||||
refreshServApps();
|
})).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 = [
|
let actions = [
|
||||||
{
|
{
|
||||||
t: 'Update Available, Click to Update',
|
t: 'Update Available' + (isStack ? ', go the stack details to update' : ', Click to Update'),
|
||||||
if: ['update_available'],
|
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'}>
|
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
|
||||||
<UpCircleOutlined />
|
<UpCircleOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -48,6 +75,7 @@ const GetActions = ({
|
||||||
{
|
{
|
||||||
t: 'No Update Available. Click to Force Pull',
|
t: 'No Update Available. Click to Force Pull',
|
||||||
if: ['update_not_available'],
|
if: ['update_not_available'],
|
||||||
|
hideStack: true,
|
||||||
e: <IconButton onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
|
e: <IconButton onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}>
|
||||||
<UpCircleOutlined />
|
<UpCircleOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -90,6 +118,7 @@ const GetActions = ({
|
||||||
{
|
{
|
||||||
t: 'Re-create',
|
t: 'Re-create',
|
||||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||||
|
hideStack: true,
|
||||||
e: <IconButton onClick={() => doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
|
e: <IconButton onClick={() => doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}>
|
||||||
<RollbackOutlined />
|
<RollbackOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -104,13 +133,7 @@ const GetActions = ({
|
||||||
{
|
{
|
||||||
t: 'Delete',
|
t: 'Delete',
|
||||||
if: ['exited', 'created'],
|
if: ['exited', 'created'],
|
||||||
e: <IconButton onClick={() => {
|
e: <DeleteModal config={config} Ids={Ids} containers={containers} refreshServApps={refreshServApps} setIsUpdatingId={setIsUpdatingId} />
|
||||||
if(confirmDelete) doTo('remove')
|
|
||||||
else setConfirmDelete(true);
|
|
||||||
}} color="error" size='large'>
|
|
||||||
{confirmDelete ? <CheckCircleOutlined />
|
|
||||||
: <DeleteOutlined />}
|
|
||||||
</IconButton>
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -126,7 +149,7 @@ const GetActions = ({
|
||||||
{!isUpdating && actions.filter((action) => {
|
{!isUpdating && actions.filter((action) => {
|
||||||
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')) || (!updateAvailable && action.if.includes('update_not_available'));
|
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')) || (!updateAvailable && action.if.includes('update_not_available'));
|
||||||
}).map((action) => {
|
}).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={{
|
{isUpdating && <Stack sx={{
|
||||||
|
|
|
@ -93,6 +93,7 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
|
||||||
<Stack spacing={2} direction={'row'} >
|
<Stack spacing={2} direction={'row'} >
|
||||||
<GetActions
|
<GetActions
|
||||||
Id={containerInfo.Name}
|
Id={containerInfo.Name}
|
||||||
|
Ids={[containerInfo.Name]}
|
||||||
image={Image}
|
image={Image}
|
||||||
state={State.Status}
|
state={State.Status}
|
||||||
refreshServApps={() => {
|
refreshServApps={() => {
|
||||||
|
@ -103,6 +104,9 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
}}
|
}}
|
||||||
updateAvailable={updatesAvailable && updatesAvailable[Name]}
|
updateAvailable={updatesAvailable && updatesAvailable[Name]}
|
||||||
|
isStack={false}
|
||||||
|
containers={[containerInfo]}
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
{containerInfo.State.Status !== 'running' && (
|
{containerInfo.State.Status !== 'running' && (
|
||||||
|
|
246
client/src/pages/servapps/deleteModal.jsx
Normal file
246
client/src/pages/servapps/deleteModal.jsx
Normal 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;
|
|
@ -12,15 +12,18 @@ import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||||
import ServApps from './servapps';
|
import ServApps from './servapps';
|
||||||
import VolumeManagementList from './volumes';
|
import VolumeManagementList from './volumes';
|
||||||
import NetworkManagementList from './networks';
|
import NetworkManagementList from './networks';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
const ServappsIndex = () => {
|
const ServappsIndex = () => {
|
||||||
|
const { stack } = useParams();
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<IsLoggedIn />
|
<IsLoggedIn />
|
||||||
|
|
||||||
<PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
|
{!stack && <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
|
||||||
{
|
{
|
||||||
title: 'Containers',
|
title: 'Containers',
|
||||||
children: <ServApps />,
|
children: <ServApps stack={stack} />,
|
||||||
path: 'containers'
|
path: 'containers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -34,6 +37,9 @@ const ServappsIndex = () => {
|
||||||
path: 'networks'
|
path: 'networks'
|
||||||
},
|
},
|
||||||
]}/>
|
]}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{stack && <ServApps stack={stack} />}
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,17 +73,10 @@ const NetworkManagementList = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{isLoading && (<div style={{ height: '550px' }}>
|
{rows && (
|
||||||
<center>
|
|
||||||
<br />
|
|
||||||
<CircularProgress />
|
|
||||||
</center>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && rows && (
|
|
||||||
<PrettyTableView
|
<PrettyTableView
|
||||||
data={rows}
|
data={rows}
|
||||||
|
isLoading={isLoading}
|
||||||
buttons={[
|
buttons={[
|
||||||
<NewNetworkButton refresh={refresh} />,
|
<NewNetworkButton refresh={refresh} />,
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { ContainerNetworkWarning } from '../../components/containers';
|
||||||
import { ServAppIcon } from '../../utils/servapp-icon';
|
import { ServAppIcon } from '../../utils/servapp-icon';
|
||||||
import MiniPlotComponent from '../dashboard/components/mini-plot';
|
import MiniPlotComponent from '../dashboard/components/mini-plot';
|
||||||
import { DownloadFile } from '../../api/downloadButton';
|
import { DownloadFile } from '../../api/downloadButton';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
const Item = styled(Paper)(({ theme }) => ({
|
const Item = styled(Paper)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
||||||
|
@ -31,6 +32,15 @@ const Item = styled(Paper)(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
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 = {
|
const noOver = {
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -38,7 +48,7 @@ const noOver = {
|
||||||
height: "50px"
|
height: "50px"
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServApps = () => {
|
const ServApps = ({stack}) => {
|
||||||
const [servApps, setServApps] = useState([]);
|
const [servApps, setServApps] = useState([]);
|
||||||
const [isUpdating, setIsUpdating] = useState({});
|
const [isUpdating, setIsUpdating] = useState({});
|
||||||
const [search, setSearch] = 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>
|
return <div>
|
||||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
|
||||||
<ExposeModal
|
<ExposeModal
|
||||||
|
@ -132,6 +243,9 @@ const ServApps = () => {
|
||||||
|
|
||||||
<Stack spacing={{xs: 1, sm: 1, md: 2 }}>
|
<Stack spacing={{xs: 1, sm: 1, md: 2 }}>
|
||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
|
{stack && <Link to="/cosmos-ui/servapps">
|
||||||
|
<ResponsiveButton variant="secondary" startIcon={<RollbackOutlined />}>Back</ResponsiveButton>
|
||||||
|
</Link>}
|
||||||
<Input placeholder="Search"
|
<Input placeholder="Search"
|
||||||
value={search}
|
value={search}
|
||||||
startAdornment={
|
startAdornment={
|
||||||
|
@ -146,6 +260,7 @@ const ServApps = () => {
|
||||||
<ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
|
<ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
|
||||||
refreshServApps();
|
refreshServApps();
|
||||||
}}>Refresh</ResponsiveButton>
|
}}>Refresh</ResponsiveButton>
|
||||||
|
{!stack && <>
|
||||||
<Link to="/cosmos-ui/servapps/new-service">
|
<Link to="/cosmos-ui/servapps/new-service">
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -158,6 +273,7 @@ const ServApps = () => {
|
||||||
label={'Export Docker Backup'}
|
label={'Export Docker Backup'}
|
||||||
contentGetter={API.config.getBackup}
|
contentGetter={API.config.getBackup}
|
||||||
/>
|
/>
|
||||||
|
</>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid2 container spacing={{xs: 1, sm: 1, md: 2 }}>
|
<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>
|
<Alert severity="info">Update are available for {Object.keys(updatesAvailable).join(', ')}</Alert>
|
||||||
</Item>
|
</Item>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
{servApps && servApps.filter(app => search.length < 2 || app.Names[0].toLowerCase().includes(search.toLowerCase())).map((app) => {
|
{servApps && Object.values(servAppsStacked).filter(app => search.length < 2 || app.name.toLowerCase().includes(search.toLowerCase())).map((app) => {
|
||||||
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4} key={app.Id} item>
|
return <Grid2 sx={{...gridAnim}} xs={12} sm={6} md={6} lg={6} xl={4} key={app.Id} item>
|
||||||
<Item>
|
<Item>
|
||||||
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
|
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
|
||||||
<Stack direction="column" spacing={0} alignItems="flex-start">
|
<Stack direction="column" spacing={0} alignItems="flex-start">
|
||||||
|
@ -182,29 +298,44 @@ const ServApps = () => {
|
||||||
"paused": <Chip label="Paused" color="info" />,
|
"paused": <Chip label="Paused" color="info" />,
|
||||||
"exited": <Chip label="Exited" color="error" />,
|
"exited": <Chip label="Exited" color="error" />,
|
||||||
"dead": <Chip label="Dead" color="error" />,
|
"dead": <Chip label="Dead" color="error" />,
|
||||||
})[app.State]
|
})[app.state]
|
||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
<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'}}>
|
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'no-wrap'}}>
|
||||||
<Typography variant="h5" color="text.secondary">
|
<Typography variant="h5" color="text.secondary">
|
||||||
{app.Names[0].replace('/', '')}
|
{app.name.replace('/', '')} {app.type === 'stack' && <Chip label="Stack" color="primary" size="small" style={{fontSize: '55%'}}/>}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" style={{fontSize: '80%', whiteSpace: 'nowrap', overflow: 'hidden', maxWidth: '100%', textOverflow: 'ellipsis'}}>
|
<Typography color="text.secondary" style={{fontSize: '80%', whiteSpace: 'nowrap', overflow: 'hidden', maxWidth: '100%', textOverflow: 'ellipsis'}}>
|
||||||
{app.Image}
|
{app.app.Image}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" spacing={1} width='100%'>
|
<Stack direction="row" spacing={1} width='100%'>
|
||||||
<GetActions
|
<GetActions
|
||||||
Id={app.Names[0].replace('/', '')}
|
Id={app.app.Names[0].replace('/', '')}
|
||||||
image={app.Image}
|
Ids={app.apps.map((app) => {
|
||||||
state={app.State}
|
return app.Names[0].replace('/', '');
|
||||||
|
})}
|
||||||
|
image={app.app.Image}
|
||||||
|
state={app.app.State}
|
||||||
setIsUpdatingId={setIsUpdatingId}
|
setIsUpdatingId={setIsUpdatingId}
|
||||||
refreshServApps={refreshServApps}
|
refreshServApps={refreshServApps}
|
||||||
updateAvailable={updatesAvailable && updatesAvailable[app.Names[0]]}
|
updateAvailable={app.updateAvailable}
|
||||||
|
isStack={app.type === 'stack'}
|
||||||
|
containers={app.apps}
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -213,7 +344,7 @@ const ServApps = () => {
|
||||||
Ports
|
Ports
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack style={noOver} margin={1} direction="row" spacing={1}>
|
<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' : ''}>
|
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'} />
|
<Chip style={{ fontSize: '80%' }} label={(port.PublicPort ? (port.PublicPort + ":") : '') + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -225,23 +356,23 @@ const ServApps = () => {
|
||||||
Networks
|
Networks
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack style={noOver} margin={1} direction="row" spacing={1}>
|
<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'} />
|
return <Chip style={{ fontSize: '80%' }} label={network} color={network === 'bridge' ? 'warning' : 'default'} />
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isUpdating[app.Names[0].replace('/', '')] ? <div>
|
{app.isUpdating ? <div>
|
||||||
<CircularProgress color="inherit" />
|
<CircularProgress color="inherit" />
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
<Typography variant="h6" color="text.secondary">
|
<Typography variant="h6" color="text.secondary">
|
||||||
Settings {app.State !== 'running' ? '(Start container to edit)' : ''}
|
Settings {app.type == "app" && (app.state !== 'running' ? '(Start container to edit)' : '')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
|
checked={app.labels['cosmos-force-network-secured'] === 'true'}
|
||||||
disabled={app.State !== 'running'}
|
disabled={app.type == "stack" || app.state !== 'running'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const name = app.Names[0].replace('/', '');
|
const name = app.Names[0].replace('/', '');
|
||||||
setIsUpdatingId(name, true);
|
setIsUpdatingId(name, true);
|
||||||
|
@ -255,13 +386,13 @@ const ServApps = () => {
|
||||||
refreshServApps();
|
refreshServApps();
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/> Isolate Container Network <ContainerNetworkWarning container={app} />
|
/> Isolate Container Network <ContainerNetworkWarning container={app.app} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={app.Labels['cosmos-auto-update'] === 'true' ||
|
checked={app.labels['cosmos-auto-update'] === 'true' ||
|
||||||
(selfName && app.Names[0].replace('/', '') == selfName && config.AutoUpdate)}
|
(selfName && app.Names[0].replace('/', '') == selfName && config.AutoUpdate)}
|
||||||
disabled={app.State !== 'running'}
|
disabled={app.type == "stack" || app.state !== 'running'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const name = app.Names[0].replace('/', '');
|
const name = app.Names[0].replace('/', '');
|
||||||
setIsUpdatingId(name, true);
|
setIsUpdatingId(name, true);
|
||||||
|
@ -284,7 +415,7 @@ const ServApps = () => {
|
||||||
URLs
|
URLs
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack style={noOver} spacing={2} direction="row">
|
<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/>
|
return <HostChip route={route} settings/>
|
||||||
})}
|
})}
|
||||||
{/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */}
|
{/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */}
|
||||||
|
@ -294,10 +425,10 @@ const ServApps = () => {
|
||||||
style={{paddingRight: '4px'}}
|
style={{paddingRight: '4px'}}
|
||||||
deleteIcon={<PlusCircleOutlined />}
|
deleteIcon={<PlusCircleOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenModal(app);
|
setOpenModal(app.app);
|
||||||
}}
|
}}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setOpenModal(app);
|
setOpenModal(app.app);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* } */}
|
{/* } */}
|
||||||
|
@ -305,16 +436,21 @@ const ServApps = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
<div>
|
<div>
|
||||||
<MiniPlotComponent agglo metrics={[
|
<MiniPlotComponent agglo metrics={[
|
||||||
"cosmos.system.docker.cpu." + app.Names[0].replace('/', ''),
|
"cosmos.system.docker.cpu." + app.name.replace('/', ''),
|
||||||
"cosmos.system.docker.ram." + app.Names[0].replace('/', ''),
|
"cosmos.system.docker.ram." + app.name.replace('/', ''),
|
||||||
]} labels={{
|
]} labels={{
|
||||||
["cosmos.system.docker.cpu." + app.Names[0].replace('/', '')]: "CPU",
|
["cosmos.system.docker.cpu." + app.name.replace('/', '')]: "CPU",
|
||||||
["cosmos.system.docker.ram." + app.Names[0].replace('/', '')]: "RAM"
|
["cosmos.system.docker.ram." + app.name.replace('/', '')]: "RAM"
|
||||||
}}/>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/cosmos-ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
|
<Link to={app.type === 'stack' ?
|
||||||
<Button variant="outlined" color="primary" fullWidth>Details</Button>
|
`/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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{/* <Stack>
|
{/* <Stack>
|
||||||
|
|
|
@ -44,17 +44,10 @@ const VolumeManagementList = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{isLoading && (<div style={{height: '550px'}}>
|
{rows && (
|
||||||
<center>
|
|
||||||
<br />
|
|
||||||
<CircularProgress />
|
|
||||||
</center>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && rows && (
|
|
||||||
<PrettyTableView
|
<PrettyTableView
|
||||||
data={rows}
|
data={rows}
|
||||||
|
isLoading={isLoading}
|
||||||
onRowClick={() => {}}
|
onRowClick={() => {}}
|
||||||
getKey={(r) => r.Name}
|
getKey={(r) => r.Name}
|
||||||
buttons={[
|
buttons={[
|
||||||
|
|
|
@ -53,6 +53,10 @@ const MainRoutes = {
|
||||||
path: '/cosmos-ui/servapps',
|
path: '/cosmos-ui/servapps',
|
||||||
element: <ServAppsIndex />
|
element: <ServAppsIndex />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/cosmos-ui/servapps/stack/:stack',
|
||||||
|
element: <ServAppsIndex />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/cosmos-ui/config-users',
|
path: '/cosmos-ui/config-users',
|
||||||
element: <UserManagement />
|
element: <UserManagement />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.12.6",
|
"version": "0.13.0-unstable0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<!-- 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/DrMxrcy"><img src="https://avatars.githubusercontent.com/DrMxrcy" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
|
<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/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/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>
|
<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>
|
||||||
|
|
|
@ -106,7 +106,7 @@ func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
utils.RestartHTTPServer()
|
utils.RestartHTTPServer()
|
||||||
|
|
||||||
if updateReq.NewRoute.Mode == "SERVAPP" {
|
if updateReq.NewRoute != nil && updateReq.NewRoute.Mode == "SERVAPP" {
|
||||||
utils.Log("RouteSettingsUpdate: Service needs update: "+updateReq.NewRoute.Target)
|
utils.Log("RouteSettingsUpdate: Service needs update: "+updateReq.NewRoute.Target)
|
||||||
|
|
||||||
target := updateReq.NewRoute.Target
|
target := updateReq.NewRoute.Target
|
||||||
|
|
|
@ -414,7 +414,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||||
utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
|
utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
|
||||||
OnLog(fmt.Sprintf("Forcing secure %s...\n", serviceName))
|
OnLog(fmt.Sprintf("Forcing secure %s...\n", serviceName))
|
||||||
|
|
||||||
newNetwork, errNC := CreateCosmosNetwork()
|
newNetwork, errNC := CreateCosmosNetwork(serviceName)
|
||||||
if errNC != nil {
|
if errNC != nil {
|
||||||
utils.Error("CreateService: Network", err)
|
utils.Error("CreateService: Network", err)
|
||||||
OnLog(utils.DoErr("Network %s cant be created\n", newNetwork))
|
OnLog(utils.DoErr("Network %s cant be created\n", newNetwork))
|
||||||
|
|
|
@ -64,7 +64,7 @@ func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
err := DockerClient.VolumeRemove(context.Background(), volumeName, true)
|
err := DockerClient.VolumeRemove(context.Background(), volumeName, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("DeleteVolumeRoute: Error while deleting volume", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ func findAvailableSubnet() string {
|
||||||
return baseSubnet
|
return baseSubnet
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateCosmosNetwork() (string, error) {
|
func CreateCosmosNetwork(name string) (string, error) {
|
||||||
networks, err := DockerClient.NetworkList(DockerContext, types.NetworkListOptions{})
|
networks, err := DockerClient.NetworkList(DockerContext, types.NetworkListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("Docker Network List", err)
|
utils.Error("Docker Network List", err)
|
||||||
|
@ -79,7 +79,7 @@ func CreateCosmosNetwork() (string, error) {
|
||||||
|
|
||||||
newNeworkName := ""
|
newNeworkName := ""
|
||||||
for {
|
for {
|
||||||
newNeworkName = "cosmos-network-" + utils.GenerateRandomString(9)
|
newNeworkName = "cosmos-" + name + "-" + utils.GenerateRandomString(3)
|
||||||
exists := false
|
exists := false
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
if network.Name == newNeworkName {
|
if network.Name == newNeworkName {
|
||||||
|
@ -144,7 +144,7 @@ func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) {
|
||||||
netName := ""
|
netName := ""
|
||||||
|
|
||||||
if(!HasLabel(containerConfig, "cosmos-network-name")) {
|
if(!HasLabel(containerConfig, "cosmos-network-name")) {
|
||||||
newNetwork, errNC := CreateCosmosNetwork()
|
newNetwork, errNC := CreateCosmosNetwork(containerConfig.Name[1:])
|
||||||
if errNC != nil {
|
if errNC != nil {
|
||||||
utils.Error("DockerSecureNetworkCreate", errNC)
|
utils.Error("DockerSecureNetworkCreate", errNC)
|
||||||
return false, errNC
|
return false, errNC
|
||||||
|
@ -239,7 +239,7 @@ func IsConnectedToASecureCosmosNetwork(self types.ContainerJSON, containerConfig
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Error("Container tries to connect to a non existing Cosmos network, replacing it.", err)
|
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 {
|
if errNC != nil {
|
||||||
utils.Error("DockerSecureNetworkCreate", errNC)
|
utils.Error("DockerSecureNetworkCreate", errNC)
|
||||||
return false, errNC, false
|
return false, errNC, false
|
||||||
|
|
|
@ -58,6 +58,9 @@ func DB() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisconnectDB() {
|
func DisconnectDB() {
|
||||||
|
if client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := client.Disconnect(context.TODO()); err != nil {
|
if err := client.Disconnect(context.TODO()); err != nil {
|
||||||
Fatal("DB", err)
|
Fatal("DB", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue