From 4884c95a50f8fe9db05dd5fcb38ea7e9a4463bd7 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Tue, 21 Nov 2023 19:32:02 +0000 Subject: [PATCH] [release] v0.13.0-unstable0 --- changelog.md | 7 + .../src/components/@extended/Breadcrumbs.jsx | 13 +- client/src/components/delete.jsx | 6 +- .../components/tableView/prettyTableView.jsx | 18 +- client/src/isLoggedIn.jsx | 1 - client/src/pages/config/routes/routeman.jsx | 8 + client/src/pages/servapps/actionBar.jsx | 61 +++-- .../pages/servapps/containers/overview.jsx | 4 + client/src/pages/servapps/deleteModal.jsx | 246 ++++++++++++++++++ client/src/pages/servapps/index.jsx | 10 +- client/src/pages/servapps/networks.jsx | 11 +- client/src/pages/servapps/servapps.jsx | 194 +++++++++++--- client/src/pages/servapps/volumes.jsx | 11 +- client/src/routes/MainRoutes.jsx | 4 + package.json | 2 +- readme.md | 1 + src/configapi/patch.go | 2 +- src/docker/api_blueprint.go | 2 +- src/docker/api_volumes.go | 2 +- src/docker/network.go | 8 +- src/utils/db.go | 3 + 21 files changed, 529 insertions(+), 85 deletions(-) create mode 100644 client/src/pages/servapps/deleteModal.jsx diff --git a/changelog.md b/changelog.md index 23d021a..1e4a9bf 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/client/src/components/@extended/Breadcrumbs.jsx b/client/src/components/@extended/Breadcrumbs.jsx index 613639b..4bffd68 100644 --- a/client/src/components/@extended/Breadcrumbs.jsx +++ b/client/src/components/@extended/Breadcrumbs.jsx @@ -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 = + {location.pathname.split('/')[4]} + ; + } // 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 }) => { {mainContent} {itemContent} + {subPath && {subPath}} + {subItem} {title && ( diff --git a/client/src/components/delete.jsx b/client/src/components/delete.jsx index 362182c..e7db626 100644 --- a/client/src/components/delete.jsx +++ b/client/src/components/delete.jsx @@ -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 && (} + {!confirmDelete && (} onClick={() => !disabled && setConfirmDelete(true)}/>)} - {confirmDelete && (} color="error" + {confirmDelete && (} color="error" onClick={(event) => !disabled && onDelete(event)}/>)} ); } \ No newline at end of file diff --git a/client/src/components/tableView/prettyTableView.jsx b/client/src/components/tableView/prettyTableView.jsx index d3f8200..a283133 100644 --- a/client/src/components/tableView/prettyTableView.jsx +++ b/client/src/components/tableView/prettyTableView.jsx @@ -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} - + + {isLoading && (
+
+
+ +
+
+ )} + + + {!isLoading && @@ -102,7 +112,7 @@ const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, butt ))}
-
+
} ) } diff --git a/client/src/isLoggedIn.jsx b/client/src/isLoggedIn.jsx index 1dcbf35..29744c7 100644 --- a/client/src/isLoggedIn.jsx +++ b/client/src/isLoggedIn.jsx @@ -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); diff --git a/client/src/pages/config/routes/routeman.jsx b/client/src/pages/config/routes/routeman.jsx index 48037f4..62b1d91 100644 --- a/client/src/pages/config/routes/routeman.jsx +++ b/client/src/pages/config/routes/routeman.jsx @@ -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 + + + These settings are for advanced users only. Please do not change these unless you know what you are doing. { 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: {}} size={isMiniMobile ? 'medium' : 'large'}> + + , e: {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}> @@ -48,6 +75,7 @@ const GetActions = ({ { t: 'No Update Available. Click to Force Pull', if: ['update_not_available'], + hideStack: true, e: {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}> @@ -90,6 +118,7 @@ const GetActions = ({ { t: 'Re-create', if: ['exited', 'running', 'paused', 'created', 'restarting'], + hideStack: true, e: doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}> @@ -104,13 +133,7 @@ const GetActions = ({ { t: 'Delete', if: ['exited', 'created'], - e: { - if(confirmDelete) doTo('remove') - else setConfirmDelete(true); - }} color="error" size='large'> - {confirmDelete ? - : } - + e: } ]; @@ -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 {action.e} + return (!isStack || !action.hideStack) && {isStack ? (action.es ? action.es : action.e) : action.e} })} {isUpdating && { @@ -103,6 +104,9 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s setIsUpdating(true); }} updateAvailable={updatesAvailable && updatesAvailable[Name]} + isStack={false} + containers={[containerInfo]} + config={config} /> {containerInfo.State.Status !== 'running' && ( diff --git a/client/src/pages/servapps/deleteModal.jsx b/client/src/pages/servapps/deleteModal.jsx new file mode 100644 index 0000000..bb25c57 --- /dev/null +++ b/client/src/pages/servapps/deleteModal.jsx @@ -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 { + 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 + } + } + + 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 && <> + {refreshServApps() ; setIsOpen(false)}}> + Delete Service + + + +
+ {isDeleting &&
+ Deletion status: +
} + {!isDeleting &&
+ Select what you wish to delete: +
} +
+ {containers.map((container) => { + return (!isDeleting || (!ignored.includes(container.Name + "-container"))) &&
+ Container {container.Name} +
+ })} + {networks.map((network) => { + return (!isDeleting || (!ignored.includes(network + "-network"))) &&
+ Network {network} +
+ })} + {volumes.map((mount) => { + return (!isDeleting || (!ignored.includes(mount + "-volume"))) &&
+ Volume {mount} +
+ })} + {routes.map((route) => { + return (!isDeleting || (!ignored.includes(route + "-route"))) &&
+ Route {route} +
+ })} +
+
+
+ {!isDeleting && + + + } + {isDeleting && + + } +
+ } + + { + setIsOpen(true); + setIsDeleting(false); + setFailed([]); + setDeleted([]); + setIgnored([]); + }} color="error" size='large'> + + + +} + +export default DeleteModal; \ No newline at end of file diff --git a/client/src/pages/servapps/index.jsx b/client/src/pages/servapps/index.jsx index 95bb54d..339e6b7 100644 --- a/client/src/pages/servapps/index.jsx +++ b/client/src/pages/servapps/index.jsx @@ -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
- , + children: , path: 'containers' }, { @@ -34,6 +37,9 @@ const ServappsIndex = () => { path: 'networks' }, ]}/> + } + + {stack && }
; } diff --git a/client/src/pages/servapps/networks.jsx b/client/src/pages/servapps/networks.jsx index c7b448f..af5f00c 100644 --- a/client/src/pages/servapps/networks.jsx +++ b/client/src/pages/servapps/networks.jsx @@ -73,17 +73,10 @@ const NetworkManagementList = () => {
- {isLoading && (
-
-
- -
-
- )} - - {!isLoading && rows && ( + {rows && ( , ]} diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index 2a9a627..03225bb 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -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
{ + {stack && + }>Back + } { } onClick={() => { refreshServApps(); }}>Refresh + {!stack && <> { label={'Export Docker Backup'} contentGetter={API.config.getBackup} /> + } @@ -166,8 +282,8 @@ const ServApps = () => { Update are available for {Object.keys(updatesAvailable).join(', ')} } - {servApps && servApps.filter(app => search.length < 2 || app.Names[0].toLowerCase().includes(search.toLowerCase())).map((app) => { - return + {servApps && Object.values(servAppsStacked).filter(app => search.length < 2 || app.name.toLowerCase().includes(search.toLowerCase())).map((app) => { + return }> @@ -182,29 +298,44 @@ const ServApps = () => { "paused": , "exited": , "dead": , - })[app.State] + })[app.state] } - + {app.type === 'app' && } + {app.type === 'stack' && + + } + - {app.Names[0].replace('/', '')}  + {app.name.replace('/', '')}  {app.type === 'stack' && } - {app.Image} + {app.app.Image} { + 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} /> @@ -213,7 +344,7 @@ const ServApps = () => { Ports - {app.Ports.filter(p => p.IP != '::').map((port) => { + {app.ports.filter(p => p.IP != '::').map((port) => { return @@ -225,23 +356,23 @@ const ServApps = () => { Networks - {app.NetworkSettings.Networks && Object.keys(app.NetworkSettings.Networks).map((network) => { + {app.networkSettings.Networks && Object.keys(app.networkSettings.Networks).map((network) => { return })} - {isUpdating[app.Names[0].replace('/', '')] ?
+ {app.isUpdating ?
: - Settings {app.State !== 'running' ? '(Start container to edit)' : ''} + Settings {app.type == "app" && (app.state !== 'running' ? '(Start container to edit)' : '')} { const name = app.Names[0].replace('/', ''); setIsUpdatingId(name, true); @@ -255,13 +386,13 @@ const ServApps = () => { refreshServApps(); }) }} - /> Isolate Container Network + /> Isolate Container Network { const name = app.Names[0].replace('/', ''); setIsUpdatingId(name, true); @@ -284,7 +415,7 @@ const ServApps = () => { URLs - {getContainersRoutes(config, app.Names[0].replace('/', '')).map((route) => { + {getContainersRoutes(config, app.name.replace('/', '')).map((route) => { return })} {/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */} @@ -294,10 +425,10 @@ const ServApps = () => { style={{paddingRight: '4px'}} deleteIcon={} onClick={() => { - setOpenModal(app); + setOpenModal(app.app); }} onDelete={() => { - setOpenModal(app); + setOpenModal(app.app); }} /> {/* } */} @@ -305,16 +436,21 @@ const ServApps = () => {
- - + +
{/* diff --git a/client/src/pages/servapps/volumes.jsx b/client/src/pages/servapps/volumes.jsx index 5161aa9..72e7705 100644 --- a/client/src/pages/servapps/volumes.jsx +++ b/client/src/pages/servapps/volumes.jsx @@ -44,17 +44,10 @@ const VolumeManagementList = () => { - {isLoading && (
-
-
- -
-
- )} - - {!isLoading && rows && ( + {rows && ( {}} getKey={(r) => r.Name} buttons={[ diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx index bec75ba..ec7cd10 100644 --- a/client/src/routes/MainRoutes.jsx +++ b/client/src/routes/MainRoutes.jsx @@ -53,6 +53,10 @@ const MainRoutes = { path: '/cosmos-ui/servapps', element: }, + { + path: '/cosmos-ui/servapps/stack/:stack', + element: + }, { path: '/cosmos-ui/config-users', element: diff --git a/package.json b/package.json index f68ca5b..c046155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.12.6", + "version": "0.13.0-unstable0", "description": "", "main": "test-server.js", "bugs": { diff --git a/readme.md b/readme.md index 2c30469..1d8a23c 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,7 @@

Thanks to the sponsors:


null +BakaDalek null Clayton Stone null diff --git a/src/configapi/patch.go b/src/configapi/patch.go index cf1c1e0..91303df 100644 --- a/src/configapi/patch.go +++ b/src/configapi/patch.go @@ -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 diff --git a/src/docker/api_blueprint.go b/src/docker/api_blueprint.go index d669e10..b255dda 100644 --- a/src/docker/api_blueprint.go +++ b/src/docker/api_blueprint.go @@ -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)) diff --git a/src/docker/api_volumes.go b/src/docker/api_volumes.go index 6c8a6ca..eefb77f 100644 --- a/src/docker/api_volumes.go +++ b/src/docker/api_volumes.go @@ -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 } diff --git a/src/docker/network.go b/src/docker/network.go index 6f37d70..17a9f18 100644 --- a/src/docker/network.go +++ b/src/docker/network.go @@ -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 diff --git a/src/utils/db.go b/src/utils/db.go index 26c6531..1f60d35 100644 --- a/src/utils/db.go +++ b/src/utils/db.go @@ -58,6 +58,9 @@ func DB() error { } func DisconnectDB() { + if client == nil { + return + } if err := client.Disconnect(context.TODO()); err != nil { Fatal("DB", err) }