diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 037cb4dfb..ec3da1e7b 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -29,6 +29,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) h.PathPrefix("/{id}/kubernetes").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI))) + h.PathPrefix("/{id}/agent/docker").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) + h.PathPrefix("/{id}/agent/kubernetes").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI))) h.PathPrefix("/{id}/storidge").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) return h diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index c4671506d..9ac89edbc 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -3,6 +3,7 @@ package endpointproxy import ( "errors" "strconv" + "strings" "time" httperror "github.com/portainer/libhttp/error" @@ -65,6 +66,12 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. } id := strconv.Itoa(endpointID) - http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r) + + prefix := "/" + id + "/agent/docker"; + if !strings.HasPrefix(r.URL.Path, prefix) { + prefix = "/" + id + "/docker"; + } + + http.StripPrefix(prefix, proxy).ServeHTTP(w, r) return nil } diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go index 1f6ed3bfd..88a369486 100644 --- a/api/http/handler/endpointproxy/proxy_kubernetes.go +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -65,17 +65,18 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h } } + // For KubernetesLocalEnvironment requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID) + if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { - if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) { - requestPrefix = fmt.Sprintf("/%d", endpointID) + requestPrefix = fmt.Sprintf("/%d", endpointID) + + agentPrefix := fmt.Sprintf("/%d/agent/kubernetes", endpointID) + if strings.HasPrefix(r.URL.Path, agentPrefix) { + requestPrefix = agentPrefix } } http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r) return nil -} - -func isKubernetesRequest(requestURL string) bool { - return strings.HasPrefix(requestURL, "/api") || strings.HasPrefix(requestURL, "/healthz") -} +} \ No newline at end of file diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index b924ad660..a5c4e1dd5 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -176,6 +176,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/azure/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) + case strings.Contains(r.URL.Path, "/agent/"): + http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/edge/"): http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r) default: diff --git a/api/http/handler/kubernetes/kubernetes_config.go b/api/http/handler/kubernetes/kubernetes_config.go index c9c4c21e4..f2b1ac5d3 100644 --- a/api/http/handler/kubernetes/kubernetes_config.go +++ b/api/http/handler/kubernetes/kubernetes_config.go @@ -34,14 +34,6 @@ import ( // @failure 500 "Server error" // @router /kubernetes/{id}/config [get] func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if r.TLS == nil { - return &httperror.HandlerError{ - StatusCode: http.StatusInternalServerError, - Message: "Kubernetes config generation only supported on portainer instances running with TLS", - Err: errors.New("missing request TLS config"), - } - } - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} @@ -76,17 +68,19 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err} } + filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name) contentAcceptHeader := r.Header.Get("Accept") if contentAcceptHeader == "text/yaml" { yaml, err := kcli.GenerateYAML(config) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err} } - w.Header().Set("Content-Disposition", `attachment; filename=config.yaml`) + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase)) return YAML(w, yaml) } - w.Header().Set("Content-Disposition", `attachment; filename="config.json"`) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase)) return response.JSON(w, config) } diff --git a/app/agent/rest/dockerhub.js b/app/agent/rest/dockerhub.js index eb901b494..369feda3e 100644 --- a/app/agent/rest/dockerhub.js +++ b/app/agent/rest/dockerhub.js @@ -4,7 +4,7 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub); function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) { return $resource( - `${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`, + `${API_ENDPOINT_ENDPOINTS}/:endpointId/agent/:endpointType/v2/dockerhub/:registryId`, {}, { limits: { method: 'GET', params: { registryId: '@registryId' } }, diff --git a/app/app.js b/app/app.js index bf795ccc6..5e94c74f0 100644 --- a/app/app.js +++ b/app/app.js @@ -31,10 +31,6 @@ angular.module('portainer').run([ HttpRequestHelper.resetAgentHeaders(); }); - $state.defaultErrorHandler(function () { - // Do not log transitionTo errors - }); - // Keep-alive Edge endpoints by sending a ping request every minute $interval(function () { ping(EndpointProvider, SystemService); diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 8d946337d..d88bfc110 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -376,98 +376,6 @@ a[ng-click] { margin: 0 auto; } -ul.sidebar { - position: relative; - overflow: hidden; - flex-shrink: 0; -} - -ul.sidebar .sidebar-title { - height: auto; -} - -ul.sidebar .sidebar-title.endpoint-name { - color: #fff; - text-align: center; - text-indent: 0; -} - -ul.sidebar .sidebar-list a { - font-size: 14px; -} - -ul.sidebar .sidebar-list a.active { - color: #fff; - text-indent: 22px; - border-left: 3px solid #fff; - background: #2d3e63; -} - -.sidebar-header { - height: 60px; - list-style: none; - text-indent: 20px; - font-size: 18px; - background: #2d3e63; -} - -.sidebar-header a { - color: #fff; -} -.sidebar-header a:hover { - text-decoration: none; -} - -.sidebar-header .menu-icon { - float: right; - padding-right: 28px; - line-height: 60px; -} - -#page-wrapper:not(.open) .sidebar-footer-content { - display: none; -} - -.sidebar-footer-content { - text-align: center; -} - -.sidebar-footer-content .logo { - width: 100%; - max-width: 100px; - height: 100%; - max-height: 35px; - margin: 2px 0 2px 20px; -} - -.sidebar-footer-content .update-notification { - font-size: 14px; - padding: 12px; - border-radius: 2px; - background-color: #ff851b; - margin-bottom: 5px; -} - -.sidebar-footer-content .version { - font-size: 11px; - margin: 11px 20px 0 7px; - color: #fff; -} - -#sidebar-wrapper { - display: flex; - flex-flow: column; -} - -.sidebar-content { - display: flex; - flex-direction: column; - justify-content: space-between; - overflow-y: auto; - overflow-x: hidden; - height: 100%; -} - #image-layers .btn { padding: 0; } @@ -481,86 +389,6 @@ ul.sidebar .sidebar-list a.active { font-size: 90%; } -ul.sidebar .sidebar-list a.active .menu-icon { - text-indent: 25px; -} - -ul.sidebar .sidebar-list .sidebar-sublist a { - text-indent: 35px; - font-size: 12px; - color: #b2bfdc; - line-height: 36px; -} - -ul.sidebar .sidebar-title { - line-height: 36px; -} -ul.sidebar .sidebar-title .form-control { - height: 36px; - padding: 6px 12px; -} - -ul.sidebar .sidebar-list { - height: 36px; -} - -ul.sidebar .sidebar-list a, -ul.sidebar .sidebar-list .sidebar-sublist a { - line-height: 36px; -} - -ul.sidebar .sidebar-list .menu-icon { - line-height: 36px; -} - -ul.sidebar .sidebar-list .sidebar-sublist a.active { - color: #fff; - border-left: 3px solid #fff; - background: #2d3e63; -} - -@media (max-height: 785px) { - ul.sidebar .sidebar-title { - line-height: 26px; - } - ul.sidebar .sidebar-title .form-control { - height: 26px; - padding: 3px 6px; - } - ul.sidebar .sidebar-list { - height: 26px; - } - ul.sidebar .sidebar-list a, - ul.sidebar .sidebar-list .sidebar-sublist a { - font-size: 12px; - line-height: 26px; - } - ul.sidebar .sidebar-list .menu-icon { - line-height: 26px; - } -} - -@media (min-height: 786px) and (max-height: 924px) { - ul.sidebar .sidebar-title { - line-height: 30px; - } - ul.sidebar .sidebar-title .form-control { - height: 30px; - padding: 5px 10px; - } - ul.sidebar .sidebar-list { - height: 30px; - } - ul.sidebar .sidebar-list a, - ul.sidebar .sidebar-list .sidebar-sublist a { - font-size: 12px; - line-height: 30px; - } - ul.sidebar .sidebar-list .menu-icon { - line-height: 30px; - } -} - @media (min-width: 768px) { .margin-sm-top { margin-top: 5px; diff --git a/app/assets/css/rdash.css b/app/assets/css/rdash.css index 950d8fd8d..a9dc4ff48 100644 --- a/app/assets/css/rdash.css +++ b/app/assets/css/rdash.css @@ -14,9 +14,6 @@ padding-left: 70px; } } -#page-wrapper.open #sidebar-wrapper { - left: 150px; -} /** * Hamburg Menu @@ -254,139 +251,6 @@ div.input-mask { padding-top: 7px; } -/* #592727 RED */ -/* #2f5927 GREEN */ -/* #30426a BLUE (default)*/ -/* Sidebar background color */ -/* Sidebar header and footer color */ -/* Sidebar title text colour */ -/* Sidebar menu item hover color */ -/** - * Sidebar - */ -#sidebar-wrapper { - background: #30426a; -} -ul.sidebar .sidebar-main a, -.sidebar-footer, -ul.sidebar .sidebar-list a:hover, -#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator { - /* Sidebar header and footer color */ - background: #2d3e63; -} -ul.sidebar { - position: absolute; - top: 0; - bottom: 0; - padding: 0; - margin: 0; - list-style: none; - text-indent: 20px; - overflow-x: hidden; - overflow-y: auto; -} -ul.sidebar li a { - color: #fff; - display: block; - float: left; - text-decoration: none; - width: 250px; -} -ul.sidebar .sidebar-main { - height: 65px; -} -ul.sidebar .sidebar-main a { - font-size: 18px; - line-height: 60px; -} -ul.sidebar .sidebar-main a:hover { - cursor: pointer; -} -ul.sidebar .sidebar-main .menu-icon { - float: right; - font-size: 18px; - padding-right: 28px; - line-height: 60px; -} -ul.sidebar .sidebar-title { - color: #738bc0; - font-size: 12px; - height: 35px; - line-height: 40px; - text-transform: uppercase; - transition: all 0.6s ease 0s; -} -ul.sidebar .sidebar-list { - height: 40px; -} -ul.sidebar .sidebar-list a { - text-indent: 25px; - font-size: 15px; - color: #b2bfdc; - line-height: 40px; -} -ul.sidebar .sidebar-list a:hover { - color: #fff; - border-left: 3px solid #e99d1a; - text-indent: 22px; -} -ul.sidebar .sidebar-list a:hover .menu-icon { - text-indent: 25px; -} -ul.sidebar .sidebar-list .menu-icon { - float: right; - padding-right: 29px; - line-height: 40px; - width: 70px; -} -#page-wrapper:not(.open) ul.sidebar { - bottom: 0; -} -#page-wrapper:not(.open) ul.sidebar .sidebar-title { - display: none; - height: 0px; - text-indent: -100px; -} -#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator { - display: block; - height: 2px; - margin: 13px 0; -} -#page-wrapper:not(.open) ul.sidebar .sidebar-list a:hover span { - border-left: 3px solid #e99d1a; - text-indent: 22px; -} -#page-wrapper:not(.open) .sidebar-footer { - display: none; -} -.sidebar-footer { - position: absolute; - height: 40px; - bottom: 0; - width: 100%; - padding: 0; - margin: 0; - transition: all 0.6s ease 0s; - text-align: center; -} -.sidebar-footer div a { - color: #b2bfdc; - font-size: 12px; - line-height: 43px; -} -.sidebar-footer div a:hover { - color: #ffffff; - text-decoration: none; -} - -/* #592727 RED */ -/* #2f5927 GREEN */ -/* #30426a BLUE (default)*/ -/* Sidebar background color */ -/* Sidebar header and footer color */ -/* Sidebar title text colour */ -/* Sidebar menu item hover color */ - /** * Widgets */ diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js deleted file mode 100644 index 1cdf11ca3..000000000 --- a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js +++ /dev/null @@ -1,8 +0,0 @@ -import angular from 'angular'; - -angular.module('portainer.azure').component('azureSidebarContent', { - templateUrl: './azureSidebarContent.html', - bindings: { - endpointId: '<', - }, -}); diff --git a/app/azure/components/azure-sidebar-content/azureSidebarContent.html b/app/azure/components/azure-sidebar-content/azureSidebarContent.html deleted file mode 100644 index d6e68d12b..000000000 --- a/app/azure/components/azure-sidebar-content/azureSidebarContent.html +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/app/azure/components/azure-sidebar/azure-sidebar.html b/app/azure/components/azure-sidebar/azure-sidebar.html new file mode 100644 index 000000000..7f6c5345a --- /dev/null +++ b/app/azure/components/azure-sidebar/azure-sidebar.html @@ -0,0 +1,7 @@ + + Dashboard + + + + Container instances + diff --git a/app/azure/components/azure-sidebar/index.js b/app/azure/components/azure-sidebar/index.js new file mode 100644 index 000000000..5b5b13f8b --- /dev/null +++ b/app/azure/components/azure-sidebar/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +angular.module('portainer.azure').component('azureSidebar', { + templateUrl: './azure-sidebar.html', + bindings: { + endpointId: '<', + }, +}); diff --git a/app/docker/components/docker-sidebar/docker-sidebar.html b/app/docker/components/docker-sidebar/docker-sidebar.html new file mode 100644 index 000000000..59866f008 --- /dev/null +++ b/app/docker/components/docker-sidebar/docker-sidebar.html @@ -0,0 +1,121 @@ + + Dashboard + + + + + Custom Templates + + + + + Stacks + + + + Services + + + + Containers + + + + Images + + + + Networks + + + + Volumes + + + + Configs + + + + Secrets + + + + Events + + + +
+ + Setup + + + + Registries + +
+
+ + +
+ + Setup + + + + Registries + +
+
diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/docker-sidebar/docker-sidebar.js similarity index 62% rename from app/docker/components/dockerSidebarContent/docker-sidebar-content.js rename to app/docker/components/docker-sidebar/docker-sidebar.js index 088165ac3..65e679262 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/docker-sidebar/docker-sidebar.js @@ -1,12 +1,13 @@ -angular.module('portainer.docker').component('dockerSidebarContent', { - templateUrl: './dockerSidebarContent.html', +angular.module('portainer.docker').component('dockerSidebar', { + templateUrl: './docker-sidebar.html', bindings: { + isSidebarOpen: '<', + endpointApiVersion: '<', swarmManagement: '<', standaloneManagement: '<', adminAccess: '<', offlineMode: '<', - toggle: '<', currentRouteName: '<', endpointId: '<', showStacks: '<', diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html deleted file mode 100644 index 086047756..000000000 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - diff --git a/app/docker/services/imageService.js b/app/docker/services/imageService.js index 2f04f8fb7..e22f0a3af 100644 --- a/app/docker/services/imageService.js +++ b/app/docker/services/imageService.js @@ -133,7 +133,7 @@ angular.module('portainer.docker').factory('ImageService', [ Image.create({}, imageConfiguration) .$promise.then(function success(data) { - var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message'); + var err = data.length > 0 && data[data.length - 1].message; if (err) { var detail = data[data.length - 1]; deferred.reject({ msg: detail.message }); diff --git a/app/integrations/storidge/services/nodeService.js b/app/integrations/storidge/services/nodeService.js index 564b50881..1e506c17b 100644 --- a/app/integrations/storidge/services/nodeService.js +++ b/app/integrations/storidge/services/nodeService.js @@ -16,7 +16,7 @@ angular.module('portainer.integrations.storidge').factory('StoridgeNodeService', var nodes = []; for (var key in nodeData) { - if (nodeData.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(nodeData, key)) { nodes.push(new StoridgeNodeModel(key, nodeData[key])); } } diff --git a/app/integrations/storidge/services/snapshotService.js b/app/integrations/storidge/services/snapshotService.js index 8edb18aa4..ae964e12d 100644 --- a/app/integrations/storidge/services/snapshotService.js +++ b/app/integrations/storidge/services/snapshotService.js @@ -20,7 +20,7 @@ angular.module('portainer.integrations.storidge').factory('StoridgeSnapshotServi var snapshotsData = data.snapshots; let snapshotsArray = []; for (const key in snapshotsData) { - if (snapshotsData.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(snapshotsData, key)) { snapshotsArray.push(snapshotsData[key]); } } diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js index 15e31baef..6fa2a5f58 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.controller.js @@ -39,6 +39,7 @@ export default class KubectlShellController { } configureSocketAndTerminal(socket, term) { + var vm = this; socket.onopen = function () { const terminal_container = document.getElementById('terminal-container'); term.open(terminal_container); @@ -55,7 +56,7 @@ export default class KubectlShellController { }); this.$window.onresize = function () { - term.fit(); + vm.TerminalWindow.terminalresize(); }; socket.onmessage = function (msg) { diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.css b/app/kubernetes/components/kubectl-shell/kubectl-shell.css index 969252f19..829baf691 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.css +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.css @@ -60,7 +60,7 @@ ul.sidebar li .shell-item-center a:hover { bottom: 0; left: 0; width: 100vw; - height: 480px; + height: 495px; z-index: 1000; } diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html deleted file mode 100644 index f7439c9a6..000000000 --- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js deleted file mode 100644 index 8a44e8a8d..000000000 --- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesSidebarContent', { - templateUrl: './kubernetesSidebarContent.html', - bindings: { - adminAccess: '<', - endpointId: '<', - currentState: '<', - }, -}); diff --git a/app/kubernetes/components/kubernetes-sidebar/index.js b/app/kubernetes/components/kubernetes-sidebar/index.js new file mode 100644 index 000000000..def39be7a --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar/index.js @@ -0,0 +1,9 @@ +import angular from 'angular'; + +angular.module('portainer.kubernetes').component('kubernetesSidebar', { + templateUrl: './kubernetes-sidebar.html', + bindings: { + endpointId: '<', + isSidebarOpen: '<', + }, +}); diff --git a/app/kubernetes/components/kubernetes-sidebar/kubernetes-sidebar.html b/app/kubernetes/components/kubernetes-sidebar/kubernetes-sidebar.html new file mode 100644 index 000000000..dc6901a06 --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar/kubernetes-sidebar.html @@ -0,0 +1,38 @@ + + Dashboard + + + + Namespaces + + + + Applications + + + + Configurations + + + + Volumes + + + +
+ + Setup + + + + Registries + +
+
diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 0f13b5dd2..fa80b8e85 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -89,122 +89,110 @@ class KubernetesApplicationService { /* #region GET */ async getAsync(namespace, name) { - try { - const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([ - this.KubernetesDeploymentService.get(namespace, name), - this.KubernetesDaemonSetService.get(namespace, name), - this.KubernetesStatefulSetService.get(namespace, name), - this.KubernetesPodService.get(namespace, name), - this.KubernetesPodService.get(namespace), - this.KubernetesHorizontalPodAutoScalerService.get(namespace), - this.KubernetesIngressService.get(namespace), - ]); + const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([ + this.KubernetesDeploymentService.get(namespace, name), + this.KubernetesDaemonSetService.get(namespace, name), + this.KubernetesStatefulSetService.get(namespace, name), + this.KubernetesPodService.get(namespace, name), + this.KubernetesPodService.get(namespace), + this.KubernetesHorizontalPodAutoScalerService.get(namespace), + this.KubernetesIngressService.get(namespace), + ]); - // const pod = _.find(pods.value, ['metadata.namespace', namespace, 'metadata.name', name]); - - let rootItem; - let converterFunc; - if (deployment.status === 'fulfilled') { - rootItem = deployment; - converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication; - } else if (daemonSet.status === 'fulfilled') { - rootItem = daemonSet; - converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication; - } else if (statefulSet.status === 'fulfilled') { - rootItem = statefulSet; - converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication; - } else if (pod.status === 'fulfilled') { - rootItem = pod; - converterFunc = KubernetesApplicationConverter.apiPodToApplication; - } else { - throw new PortainerError('Unable to determine which association to use to convert application'); - } - - const services = await this.KubernetesServiceService.get(namespace); - const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); - const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; - - const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value); - application.Yaml = rootItem.value.Yaml; - application.Raw = rootItem.value.Raw; - application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item)); - application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); - - const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); - const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; - application.AutoScaler = scaler; - - await this.KubernetesHistoryService.get(application); - - if (service.Yaml) { - application.Yaml += '---\n' + service.Yaml; - } - if (scaler && scaler.Yaml) { - application.Yaml += '---\n' + scaler.Yaml; - } - // TODO: refactor @LP - // append ingress yaml ? - return application; - } catch (err) { - throw err; + let rootItem; + let converterFunc; + if (deployment.status === 'fulfilled') { + rootItem = deployment; + converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication; + } else if (daemonSet.status === 'fulfilled') { + rootItem = daemonSet; + converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication; + } else if (statefulSet.status === 'fulfilled') { + rootItem = statefulSet; + converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication; + } else if (pod.status === 'fulfilled') { + rootItem = pod; + converterFunc = KubernetesApplicationConverter.apiPodToApplication; + } else { + throw new PortainerError('Unable to determine which association to use to convert application'); } + + const services = await this.KubernetesServiceService.get(namespace); + const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); + const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; + + const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value); + application.Yaml = rootItem.value.Yaml; + application.Raw = rootItem.value.Raw; + application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item)); + application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); + + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + + await this.KubernetesHistoryService.get(application); + + if (service.Yaml) { + application.Yaml += '---\n' + service.Yaml; + } + if (scaler && scaler.Yaml) { + application.Yaml += '---\n' + scaler.Yaml; + } + // TODO: refactor @LP + // append ingress yaml ? + return application; } async getAllAsync(namespace) { - try { - const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); - const convertToApplication = (item, converterFunc, services, pods, ingresses) => { - const service = KubernetesServiceHelper.findApplicationBoundService(services, item); - const application = converterFunc(item, pods, service, ingresses); - application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); - return application; - }; + const convertToApplication = (item, converterFunc, services, pods, ingresses) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = converterFunc(item, pods, service, ingresses); + application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); + return application; + }; - const res = await Promise.all( - _.map(namespaces, async (ns) => { - const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([ - this.KubernetesDeploymentService.get(ns), - this.KubernetesDaemonSetService.get(ns), - this.KubernetesStatefulSetService.get(ns), - this.KubernetesServiceService.get(ns), - this.KubernetesPodService.get(ns), - this.KubernetesIngressService.get(ns), - this.KubernetesHorizontalPodAutoScalerService.get(ns), - ]); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([ + this.KubernetesDeploymentService.get(ns), + this.KubernetesDaemonSetService.get(ns), + this.KubernetesStatefulSetService.get(ns), + this.KubernetesServiceService.get(ns), + this.KubernetesPodService.get(ns), + this.KubernetesIngressService.get(ns), + this.KubernetesHorizontalPodAutoScalerService.get(ns), + ]); - const deploymentApplications = _.map(deployments, (item) => - convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses) - ); - const daemonSetApplications = _.map(daemonSets, (item) => - convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses) - ); - const statefulSetApplications = _.map(statefulSets, (item) => - convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) - ); + const deploymentApplications = _.map(deployments, (item) => + convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses) + ); + const daemonSetApplications = _.map(daemonSets, (item) => convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses)); + const statefulSetApplications = _.map(statefulSets, (item) => + convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) + ); - const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods')); - const unboundPods = _.without(pods, ...boundPods); - const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses)); + const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods')); + const unboundPods = _.without(pods, ...boundPods); + const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses)); - const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications); - _.forEach(applications, (app) => { - app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item)); - }); - await Promise.all( - _.forEach(applications, async (application) => { - const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); - const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; - application.AutoScaler = scaler; - }) - ); - return applications; - }) - ); - return _.flatten(res); - } catch (err) { - throw err; - } + const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications); + _.forEach(applications, (app) => { + app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item)); + }); + await Promise.all( + _.forEach(applications, async (application) => { + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + }) + ); + return applications; + }) + ); + return _.flatten(res); } get(namespace, name) { @@ -226,42 +214,38 @@ class KubernetesApplicationService { * also be displayed in the summary output (getCreatedApplicationResources) */ async createAsync(formValues) { - try { - let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); - if (service) { - await this.KubernetesServiceService.create(service); - if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name); - await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); - } + if (service) { + await this.KubernetesServiceService.create(service); + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); } - - const apiService = this._getApplicationApiService(app); - - if (app instanceof KubernetesStatefulSet) { - app.VolumeClaims = claims; - headlessService = await this.KubernetesServiceService.create(headlessService); - app.ServiceName = headlessService.metadata.name; - } else { - const claimPromises = _.map(claims, (item) => { - if (!item.PreviousName && !item.Id) { - return this.KubernetesPersistentVolumeClaimService.create(item); - } - }); - await Promise.all(_.without(claimPromises, undefined)); - } - - if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) { - const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); - const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind); - await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler); - } - - await apiService.create(app); - } catch (err) { - throw err; } + + const apiService = this._getApplicationApiService(app); + + if (app instanceof KubernetesStatefulSet) { + app.VolumeClaims = claims; + headlessService = await this.KubernetesServiceService.create(headlessService); + app.ServiceName = headlessService.metadata.name; + } else { + const claimPromises = _.map(claims, (item) => { + if (!item.PreviousName && !item.Id) { + return this.KubernetesPersistentVolumeClaimService.create(item); + } + }); + await Promise.all(_.without(claimPromises, undefined)); + } + + if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) { + const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); + const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind); + await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler); + } + + await apiService.create(app); } create(formValues) { @@ -277,97 +261,89 @@ class KubernetesApplicationService { * in this method should also be displayed in the summary output (getUpdatedApplicationResources) */ async patchAsync(oldFormValues, newFormValues) { - try { - const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); - const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); - const oldApiService = this._getApplicationApiService(oldApp); - const newApiService = this._getApplicationApiService(newApp); + const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); + const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); + const oldApiService = this._getApplicationApiService(oldApp); + const newApiService = this._getApplicationApiService(newApp); - if (oldApiService !== newApiService) { - await this.delete(oldApp); - if (oldService) { - await this.KubernetesServiceService.delete(oldService); - } - return await this.create(newFormValues); - } - - if (newApp instanceof KubernetesStatefulSet) { - await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); - } else { - const claimPromises = _.map(newClaims, (newClaim) => { - if (!newClaim.PreviousName && !newClaim.Id) { - return this.KubernetesPersistentVolumeClaimService.create(newClaim); - } else if (!newClaim.Id) { - const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); - return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); - } - }); - await Promise.all(claimPromises); - } - - await newApiService.patch(oldApp, newApp); - - if (oldService && newService) { - await this.KubernetesServiceService.patch(oldService, newService); - if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name); - const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); - await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses)); - } - } else if (!oldService && newService) { - await this.KubernetesServiceService.create(newService); - if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); - await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses)); - } - } else if (oldService && !newService) { + if (oldApiService !== newApiService) { + await this.delete(oldApp); + if (oldService) { await this.KubernetesServiceService.delete(oldService); - if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name); - await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); - } } + return await this.create(newFormValues); + } - const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); - const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind); - if (!oldFormValues.AutoScaler.IsUsed) { - if (newFormValues.AutoScaler.IsUsed) { - await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); - } - } else { - const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp); - const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind); - if (newFormValues.AutoScaler.IsUsed) { - await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler); - } else { - await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler); + if (newApp instanceof KubernetesStatefulSet) { + await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); + } else { + const claimPromises = _.map(newClaims, (newClaim) => { + if (!newClaim.PreviousName && !newClaim.Id) { + return this.KubernetesPersistentVolumeClaimService.create(newClaim); + } else if (!newClaim.Id) { + const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); + return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); } + }); + await Promise.all(claimPromises); + } + + await newApiService.patch(oldApp, newApp); + + if (oldService && newService) { + await this.KubernetesServiceService.patch(oldService, newService); + if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name); + const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); + await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses)); + } + } else if (!oldService && newService) { + await this.KubernetesServiceService.create(newService); + if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); + await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses)); + } + } else if (oldService && !newService) { + await this.KubernetesServiceService.delete(oldService); + if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + } + + const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); + const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind); + if (!oldFormValues.AutoScaler.IsUsed) { + if (newFormValues.AutoScaler.IsUsed) { + await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); + } + } else { + const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp); + const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind); + if (newFormValues.AutoScaler.IsUsed) { + await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler); + } else { + await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler); } - } catch (err) { - throw err; } } // this function accepts KubernetesApplication as parameters async patchPartialAsync(oldApp, newApp) { - try { - const oldAppPayload = { - Name: oldApp.Name, - Namespace: oldApp.ResourcePool, - StackName: oldApp.StackName, - Note: oldApp.Note, - }; - const newAppPayload = { - Name: newApp.Name, - Namespace: newApp.ResourcePool, - StackName: newApp.StackName, - Note: newApp.Note, - }; - const apiService = this._getApplicationApiService(oldApp); - await apiService.patch(oldAppPayload, newAppPayload); - } catch (err) { - throw err; - } + const oldAppPayload = { + Name: oldApp.Name, + Namespace: oldApp.ResourcePool, + StackName: oldApp.StackName, + Note: oldApp.Note, + }; + const newAppPayload = { + Name: newApp.Name, + Namespace: newApp.ResourcePool, + StackName: newApp.StackName, + Note: newApp.Note, + }; + const apiService = this._getApplicationApiService(oldApp); + await apiService.patch(oldAppPayload, newAppPayload); } // accept either formValues or applications as parameters @@ -384,42 +360,38 @@ class KubernetesApplicationService { /* #region DELETE */ async deleteAsync(application) { - try { - const payload = { - Namespace: application.ResourcePool || application.Namespace, - Name: application.Name, - }; - const servicePayload = angular.copy(payload); - servicePayload.Name = application.Name; + const payload = { + Namespace: application.ResourcePool || application.Namespace, + Name: application.Name, + }; + const servicePayload = angular.copy(payload); + servicePayload.Name = application.Name; - const apiService = this._getApplicationApiService(application); - await apiService.delete(payload); + const apiService = this._getApplicationApiService(application); + await apiService.delete(payload); - if (apiService === this.KubernetesStatefulSetService) { - const headlessServicePayload = angular.copy(payload); - headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName; - await this.KubernetesServiceService.delete(headlessServicePayload); - } + if (apiService === this.KubernetesStatefulSetService) { + const headlessServicePayload = angular.copy(payload); + headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName; + await this.KubernetesServiceService.delete(headlessServicePayload); + } - if (application.ServiceType) { - await this.KubernetesServiceService.delete(servicePayload); - const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length; - if (isIngress) { - const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace); - const formValues = { - OriginalIngresses: originalIngresses, - PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts), - }; - _.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true)); - const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name); - await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); - } + if (application.ServiceType) { + await this.KubernetesServiceService.delete(servicePayload); + const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length; + if (isIngress) { + const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace); + const formValues = { + OriginalIngresses: originalIngresses, + PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts), + }; + _.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true)); + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); } - if (!_.isEmpty(application.AutoScaler)) { - await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler); - } - } catch (err) { - throw err; + } + if (!_.isEmpty(application.AutoScaler)) { + await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler); } } @@ -430,13 +402,9 @@ class KubernetesApplicationService { /* #region ROLLBACK */ async rollbackAsync(application, targetRevision) { - try { - const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision); - const apiService = this._getApplicationApiService(application); - await apiService.rollback(application.ResourcePool, application.Name, payload); - } catch (err) { - throw err; - } + const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision); + const apiService = this._getApplicationApiService(application); + await apiService.rollback(application.ResourcePool, application.Name, payload); } rollback(application, targetRevision) { diff --git a/app/kubernetes/services/configurationService.js b/app/kubernetes/services/configurationService.js index e6600e6cd..fd410e424 100644 --- a/app/kubernetes/services/configurationService.js +++ b/app/kubernetes/services/configurationService.js @@ -26,35 +26,27 @@ class KubernetesConfigurationService { * GET */ async getAsync(namespace, name) { - try { - const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]); - let configuration; - if (secret.status === 'fulfilled') { - configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value); - return configuration; - } - configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value); + const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]); + let configuration; + if (secret.status === 'fulfilled') { + configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value); return configuration; - } catch (err) { - throw err; } + configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value); + return configuration; } async getAllAsync(namespace) { - try { - const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); - const res = await Promise.all( - _.map(namespaces, async (ns) => { - const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]); - const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret)); - const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap)); - return _.concat(configMapsConfigurations, secretsConfigurations); - }) - ); - return _.flatten(res); - } catch (err) { - throw err; - } + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]); + const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret)); + const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap)); + return _.concat(configMapsConfigurations, secretsConfigurations); + }) + ); + return _.flatten(res); } get(namespace, name) { @@ -70,16 +62,12 @@ class KubernetesConfigurationService { async createAsync(formValues) { formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner); - try { - if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { - const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); - await this.KubernetesConfigMapService.create(configMap); - } else { - const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); - await this.KubernetesSecretService.create(secret); - } - } catch (err) { - throw err; + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + await this.KubernetesConfigMapService.create(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + await this.KubernetesSecretService.create(secret); } } @@ -91,18 +79,14 @@ class KubernetesConfigurationService { * UPDATE */ async updateAsync(formValues, configuration) { - try { - if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { - const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); - configMap.ConfigurationOwner = configuration.ConfigurationOwner; - await this.KubernetesConfigMapService.update(configMap); - } else { - const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); - secret.ConfigurationOwner = configuration.ConfigurationOwner; - await this.KubernetesSecretService.update(secret); - } - } catch (err) { - throw err; + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + configMap.ConfigurationOwner = configuration.ConfigurationOwner; + await this.KubernetesConfigMapService.update(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + secret.ConfigurationOwner = configuration.ConfigurationOwner; + await this.KubernetesSecretService.update(secret); } } @@ -114,14 +98,10 @@ class KubernetesConfigurationService { * DELETE */ async deleteAsync(config) { - try { - if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { - await this.KubernetesConfigMapService.delete(config); - } else { - await this.KubernetesSecretService.delete(config); - } - } catch (err) { - throw err; + if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { + await this.KubernetesConfigMapService.delete(config); + } else { + await this.KubernetesSecretService.delete(config); } } diff --git a/app/kubernetes/services/kubeconfigService.js b/app/kubernetes/services/kubeconfigService.js index 752dc1ef3..8b78ca9f7 100644 --- a/app/kubernetes/services/kubeconfigService.js +++ b/app/kubernetes/services/kubeconfigService.js @@ -9,7 +9,11 @@ class KubernetesConfigService { async downloadConfig() { const response = await this.KubernetesConfig.get(); - return this.FileSaver.saveAs(response.data, 'config'); + + const headers = response.headers(); + const contentDispositionHeader = headers['content-disposition']; + const filename = contentDispositionHeader.replace('attachment;', '').trim(); + return this.FileSaver.saveAs(response.data, filename); } } diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index e89069191..a8de30eb4 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -14,39 +14,31 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete }; async function getOne(name) { - try { - const namespace = await KubernetesNamespaceService.get(name); - const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); - const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); - if (quotaAttempt.status === 'fulfilled') { - pool.Quota = quotaAttempt.value; - pool.Yaml += '---\n' + quotaAttempt.value.Yaml; - } - return pool; - } catch (err) { - throw err; + const namespace = await KubernetesNamespaceService.get(name); + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; } + return pool; } async function getAll() { - try { - const namespaces = await KubernetesNamespaceService.get(); - const pools = await Promise.all( - _.map(namespaces, async (namespace) => { - const name = namespace.Name; - const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); - const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); - if (quotaAttempt.status === 'fulfilled') { - pool.Quota = quotaAttempt.value; - pool.Yaml += '---\n' + quotaAttempt.value.Yaml; - } - return pool; - }) - ); - return pools; - } catch (err) { - throw err; - } + const namespaces = await KubernetesNamespaceService.get(); + const pools = await Promise.all( + _.map(namespaces, async (namespace) => { + const name = namespace.Name; + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } + return pool; + }) + ); + return pools; } function get(name) { @@ -58,80 +50,68 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete function create(formValues) { return $async(async () => { - try { - const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues); - await KubernetesNamespaceService.create(namespace); + const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues); + await KubernetesNamespaceService.create(namespace); - if (quota) { - await KubernetesResourceQuotaService.create(quota); - } - const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i)); - await Promise.all(ingressPromises); - - const endpointId = formValues.EndpointId; - const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); - await Promise.all(registriesPromises); - } catch (err) { - throw err; + if (quota) { + await KubernetesResourceQuotaService.create(quota); } + const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i)); + await Promise.all(ingressPromises); + + const endpointId = formValues.EndpointId; + const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); + await Promise.all(registriesPromises); }); } function patch(oldFormValues, newFormValues) { return $async(async () => { - try { - const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); - const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); - void oldNamespace, newNamespace; + const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); + const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); + void oldNamespace, newNamespace; - if (oldQuota && newQuota) { - await KubernetesResourceQuotaService.patch(oldQuota, newQuota); - } else if (!oldQuota && newQuota) { - await KubernetesResourceQuotaService.create(newQuota); - } else if (oldQuota && !newQuota) { - await KubernetesResourceQuotaService.delete(oldQuota); - } - - const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name })); - const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name })); - const patch = _.without(newIngresses, ...create); - - const createPromises = _.map(create, (i) => KubernetesIngressService.create(i)); - const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i.Namespace, i.Name)); - const patchPromises = _.map(patch, (ing) => { - const old = _.find(oldIngresses, { Name: ing.Name }); - ing.Paths = angular.copy(old.Paths); - ing.PreviousHost = old.Host; - return KubernetesIngressService.patch(old, ing); - }); - - const promises = _.flatten([createPromises, delPromises, patchPromises]); - await Promise.all(promises); - - const endpointId = newFormValues.EndpointId; - const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id'); - const removedRegistries = _.without(oldRegistries, ...keptRegistries); - - const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); - const removedRegistriesPromises = _.map(removedRegistries, (r) => { - _.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name); - return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]); - }); - - await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises)); - } catch (err) { - throw err; + if (oldQuota && newQuota) { + await KubernetesResourceQuotaService.patch(oldQuota, newQuota); + } else if (!oldQuota && newQuota) { + await KubernetesResourceQuotaService.create(newQuota); + } else if (oldQuota && !newQuota) { + await KubernetesResourceQuotaService.delete(oldQuota); } + + const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name })); + const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name })); + const patch = _.without(newIngresses, ...create); + + const createPromises = _.map(create, (i) => KubernetesIngressService.create(i)); + const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i.Namespace, i.Name)); + const patchPromises = _.map(patch, (ing) => { + const old = _.find(oldIngresses, { Name: ing.Name }); + ing.Paths = angular.copy(old.Paths); + ing.PreviousHost = old.Host; + return KubernetesIngressService.patch(old, ing); + }); + + const promises = _.flatten([createPromises, delPromises, patchPromises]); + await Promise.all(promises); + + const endpointId = newFormValues.EndpointId; + const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id'); + const removedRegistries = _.without(oldRegistries, ...keptRegistries); + + const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); + const removedRegistriesPromises = _.map(removedRegistries, (r) => { + _.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name); + return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]); + }); + + await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises)); }); } function _delete(pool) { return $async(async () => { - try { - await KubernetesNamespaceService.delete(pool.Namespace); - } catch (err) { - throw err; - } + await KubernetesNamespaceService.delete(pool.Namespace); }); } } diff --git a/app/kubernetes/services/stackService.js b/app/kubernetes/services/stackService.js index 23e09c01f..7caf17a08 100644 --- a/app/kubernetes/services/stackService.js +++ b/app/kubernetes/services/stackService.js @@ -14,13 +14,9 @@ class KubernetesStackService { * GET */ async getAllAsync(namespace) { - try { - const applications = await this.KubernetesApplicationService.get(namespace); - const stacks = _.map(applications, (item) => item.StackName); - return _.uniq(_.without(stacks, '-')); - } catch (err) { - throw err; - } + const applications = await this.KubernetesApplicationService.get(namespace); + const stacks = _.map(applications, (item) => item.StackName); + return _.uniq(_.without(stacks, '-')); } get(namespace) { diff --git a/app/kubernetes/services/volumeService.js b/app/kubernetes/services/volumeService.js index abbf0019d..7b00aa6e2 100644 --- a/app/kubernetes/services/volumeService.js +++ b/app/kubernetes/services/volumeService.js @@ -20,28 +20,20 @@ class KubernetesVolumeService { * GET */ async getAsync(namespace, name) { - try { - const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]); - return KubernetesVolumeConverter.pvcToVolume(pvc, pool); - } catch (err) { - throw err; - } + const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]); + return KubernetesVolumeConverter.pvcToVolume(pvc, pool); } async getAllAsync(namespace) { - try { - const data = await this.KubernetesResourcePoolService.get(namespace); - const pools = data instanceof Array ? data : [data]; - const res = await Promise.all( - _.map(pools, async (pool) => { - const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); - return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool)); - }) - ); - return _.flatten(res); - } catch (err) { - throw err; - } + const data = await this.KubernetesResourcePoolService.get(namespace); + const pools = data instanceof Array ? data : [data]; + const res = await Promise.all( + _.map(pools, async (pool) => { + const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); + return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool)); + }) + ); + return _.flatten(res); } get(namespace, name) { @@ -55,11 +47,7 @@ class KubernetesVolumeService { * DELETE */ async deleteAsync(volume) { - try { - await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim); - } catch (err) { - throw err; - } + await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim); } delete(volume) { diff --git a/app/portainer/components/code-editor/codeEditorController.js b/app/portainer/components/code-editor/codeEditorController.js index 26363c108..2c7499b03 100644 --- a/app/portainer/components/code-editor/codeEditorController.js +++ b/app/portainer/components/code-editor/codeEditorController.js @@ -2,7 +2,7 @@ angular.module('portainer.app').controller('CodeEditorController', function Code var ctrl = this; this.$onChanges = function $onChanges({ value }) { - if (value && value.currentValue && ctrl.editor) { + if (value && value.currentValue && ctrl.editor && ctrl.editor.getValue() !== value.currentValue) { ctrl.editor.setValue(value.currentValue); } }; diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js index f4179817d..717c15333 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.controller.js @@ -68,7 +68,7 @@ class StackRedeployGitFormController { this.state.redeployInProgress = true; await this.StackService.updateGit(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), false, this.formValues); - + this.Notifications.success('Pulled and redeployed stack successfully'); await this.$state.reload(); } catch (err) { this.Notifications.error('Failure', err, 'Failed redeploying stack'); diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js index bef46f4d7..9a660facf 100644 --- a/app/portainer/components/index.js +++ b/app/portainer/components/index.js @@ -1,7 +1,8 @@ import angular from 'angular'; +import sidebarModule from './sidebar'; import gitFormModule from './forms/git-form'; import porAccessManagementModule from './accessManagement'; import formComponentsModule from './form-components'; -export default angular.module('portainer.app.components', [gitFormModule, porAccessManagementModule, formComponentsModule]).name; +export default angular.module('portainer.app.components', [sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule]).name; diff --git a/app/portainer/components/sidebar/index.js b/app/portainer/components/sidebar/index.js new file mode 100644 index 000000000..1d2c18062 --- /dev/null +++ b/app/portainer/components/sidebar/index.js @@ -0,0 +1,12 @@ +import angular from 'angular'; +import './sidebar.css'; + +import { sidebarMenu } from './sidebar-menu'; +import { sidebarSection } from './sidebar-section'; +import { sidebarMenuItem } from './sidebar-menu-item'; + +export default angular + .module('portainer.app.components.sidebar', []) + .component('sidebarMenu', sidebarMenu) + .component('sidebarMenuItem', sidebarMenuItem) + .component('sidebarSection', sidebarSection).name; diff --git a/app/portainer/components/sidebar/sidebar-menu-item/index.js b/app/portainer/components/sidebar/sidebar-menu-item/index.js new file mode 100644 index 000000000..014d6262f --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu-item/index.js @@ -0,0 +1,12 @@ +import './sidebar-menu-item.css'; + +export const sidebarMenuItem = { + templateUrl: './sidebar-menu-item.html', + bindings: { + path: '@', + pathParams: '<', + iconClass: '@', + className: '@', + }, + transclude: true, +}; diff --git a/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.css b/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.css new file mode 100644 index 000000000..4f751ad36 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.css @@ -0,0 +1,4 @@ +.sidebar-menu-item > a { + display: flex; + justify-content: space-between; +} diff --git a/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.html b/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.html new file mode 100644 index 000000000..bfcb509c9 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu-item/sidebar-menu-item.html @@ -0,0 +1,6 @@ + diff --git a/app/portainer/components/sidebar/sidebar-menu/index.js b/app/portainer/components/sidebar/sidebar-menu/index.js new file mode 100644 index 000000000..8c9d24aea --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu/index.js @@ -0,0 +1,18 @@ +import './sidebar-menu.css'; + +import controller from './sidebar-menu.controller.js'; + +export const sidebarMenu = { + templateUrl: './sidebar-menu.html', + controller, + bindings: { + iconClass: '@', + path: '@', // string + pathParams: '<', //object, corresponds to https://ui-router.github.io/ng1/docs/latest/modules/directives.html#uistatedirective + childrenPaths: '<', // []string (globs) + label: '@', // string + isSidebarOpen: '<', + currentState: '@', + }, + transclude: true, +}; diff --git a/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.controller.js b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.controller.js new file mode 100644 index 000000000..8122f7ea9 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.controller.js @@ -0,0 +1,45 @@ +class SidebarMenuController { + /* @ngInject */ + constructor($state) { + this.$state = $state; + + this.state = { + forceOpen: false, + }; + } + + isOpen() { + if (!this.isSidebarOpen) { + return false; + } + + if (this.state.forceOpen) { + return true; + } + + return this.isOpenByPathState(); + } + + isOpenByPathState() { + const currentName = this.$state.current.name; + return currentName.startsWith(this.path) || this.childrenPaths.includes(currentName); + } + + onClickArrow(event) { + event.stopPropagation(); + event.preventDefault(); + + // prevent toggling when menu is open by state + if (this.isOpenByPathState()) { + return; + } + + this.state.forceOpen = !this.state.forceOpen; + } + + $onInit() { + this.childrenPaths = this.childrenPaths || []; + } +} + +export default SidebarMenuController; diff --git a/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.css b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.css new file mode 100644 index 000000000..f28c4ae14 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.css @@ -0,0 +1,15 @@ +.sidebar-menu .sidebar-menu-head { + position: relative; +} + +.sidebar-menu .sidebar-menu-head .sidebar-menu-indicator { + background: none; + border: 0; + width: fit-content; + height: fit-content; + color: white; + margin: 0 10px; + padding: 0; + position: absolute; + left: -5px; +} diff --git a/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.html b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.html new file mode 100644 index 000000000..a9e6243d5 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-menu/sidebar-menu.html @@ -0,0 +1,14 @@ + diff --git a/app/portainer/components/sidebar/sidebar-section/index.js b/app/portainer/components/sidebar/sidebar-section/index.js new file mode 100644 index 000000000..b577b6660 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-section/index.js @@ -0,0 +1,7 @@ +export const sidebarSection = { + templateUrl: './sidebar-section.html', + transclude: true, + bindings: { + title: '@', + }, +}; diff --git a/app/portainer/components/sidebar/sidebar-section/sidebar-section.html b/app/portainer/components/sidebar/sidebar-section/sidebar-section.html new file mode 100644 index 000000000..057db83f8 --- /dev/null +++ b/app/portainer/components/sidebar/sidebar-section/sidebar-section.html @@ -0,0 +1,7 @@ + diff --git a/app/portainer/components/sidebar/sidebar.css b/app/portainer/components/sidebar/sidebar.css new file mode 100644 index 000000000..e5d27df9b --- /dev/null +++ b/app/portainer/components/sidebar/sidebar.css @@ -0,0 +1,326 @@ +#page-wrapper.open #sidebar-wrapper { + left: 150px; +} + +/* #592727 RED */ +/* #2f5927 GREEN */ +/* #30426a BLUE (default)*/ +/* Sidebar background color */ +/* Sidebar header and footer color */ +/* Sidebar title text colour */ +/* Sidebar menu item hover color */ +/** + * Sidebar + */ +#sidebar-wrapper { + background: #30426a; +} + +ul.sidebar .sidebar-main a, +.sidebar-footer, +ul.sidebar .sidebar-list a:hover, +#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator { + /* Sidebar header and footer color */ + background: #2d3e63; +} + +ul.sidebar { + position: absolute; + top: 0; + bottom: 0; + padding: 0; + margin: 0; + list-style: none; + text-indent: 20px; + overflow-x: hidden; + overflow-y: auto; +} + +ul.sidebar li a { + color: #fff; + text-decoration: none; + width: 250px; +} + +ul.sidebar .sidebar-main { + height: 65px; +} + +ul.sidebar .sidebar-main a { + font-size: 18px; + line-height: 60px; +} + +ul.sidebar .sidebar-main a:hover { + cursor: pointer; +} + +ul.sidebar .sidebar-main .menu-icon { + float: right; + font-size: 18px; + padding-right: 28px; + line-height: 60px; +} + +ul.sidebar .sidebar-title { + color: #738bc0; + font-size: 12px; + height: 35px; + line-height: 40px; + text-transform: uppercase; + transition: all 0.6s ease 0s; +} + +ul.sidebar .sidebar-list a { + text-indent: 25px; + font-size: 15px; + color: #b2bfdc; + line-height: 40px; + padding-left: 5px; + border-left: 3px solid transparent; +} + +ul.sidebar .sidebar-list a:hover { + color: #fff; + border-left-color: #e99d1a; +} + +ul.sidebar .sidebar-list a:hover .menu-icon { + text-indent: 25px; +} + +ul.sidebar .sidebar-list .menu-icon { + padding-right: 30px; + line-height: 40px; + width: fit-content; +} + +#page-wrapper:not(.open) ul.sidebar { + bottom: 0; +} + +#page-wrapper:not(.open) ul.sidebar .sidebar-title { + display: none; + height: 0px; + text-indent: -100px; +} + +#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator { + display: block; + height: 2px; + margin: 13px 0; +} + +#page-wrapper:not(.open) ul.sidebar .sidebar-list a:hover span { + border-left: 3px solid #e99d1a; + text-indent: 22px; +} + +#page-wrapper:not(.open) .sidebar-footer { + display: none; +} + +.sidebar-footer { + position: absolute; + height: 40px; + bottom: 0; + width: 100%; + padding: 0; + margin: 0; + transition: all 0.6s ease 0s; + text-align: center; +} + +.sidebar-footer div a { + color: #b2bfdc; + font-size: 12px; + line-height: 43px; +} + +.sidebar-footer div a:hover { + color: #ffffff; + text-decoration: none; +} + +/* #592727 RED */ +/* #2f5927 GREEN */ +/* #30426a BLUE (default)*/ +/* Sidebar background color */ +/* Sidebar header and footer color */ +/* Sidebar title text colour */ +/* Sidebar menu item hover color */ + +ul.sidebar { + position: relative; + overflow: hidden; + flex-shrink: 0; +} + +ul.sidebar .sidebar-title { + height: auto; +} + +ul.sidebar .sidebar-title.endpoint-name { + color: #fff; + text-align: center; + text-indent: 0; +} + +ul.sidebar .sidebar-list a { + font-size: 14px; +} + +ul.sidebar .sidebar-list a.active { + color: #fff; + border-left-color: #fff; + background: #2d3e63; +} + +.sidebar-header { + height: 60px; + list-style: none; + text-indent: 20px; + font-size: 18px; + background: #2d3e63; +} + +.sidebar-header a { + color: #fff; +} + +.sidebar-header a:hover { + text-decoration: none; +} + +.sidebar-header .menu-icon { + float: right; + padding-right: 28px; + line-height: 60px; +} + +#page-wrapper:not(.open) .sidebar-footer-content { + display: none; +} + +.sidebar-footer-content { + text-align: center; +} + +.sidebar-footer-content .logo { + width: 100%; + max-width: 100px; + height: 100%; + max-height: 35px; + margin: 2px 0 2px 20px; +} + +.sidebar-footer-content .update-notification { + font-size: 14px; + padding: 12px; + border-radius: 2px; + background-color: #ff851b; + margin-bottom: 5px; +} + +.sidebar-footer-content .version { + font-size: 11px; + margin: 11px 20px 0 7px; + color: #fff; +} + +.sidebar-footer-content .edition-version { + font-size: 10px; + margin-bottom: 8px; + color: #fff; +} + +#sidebar-wrapper { + display: flex; + flex-flow: column; +} + +.sidebar-content { + display: flex; + flex-direction: column; + justify-content: space-between; + overflow-y: auto; + overflow-x: hidden; + height: 100%; +} + +ul.sidebar .sidebar-list a.active .menu-icon { + text-indent: 25px; +} + +ul.sidebar .sidebar-list .sidebar-sublist a { + text-indent: 35px; + font-size: 12px; + color: #b2bfdc; + line-height: 36px; +} + +ul.sidebar .sidebar-title { + line-height: 36px; +} + +ul.sidebar .sidebar-title .form-control { + height: 36px; + padding: 6px 12px; +} + +ul.sidebar .sidebar-list a, +ul.sidebar .sidebar-list .sidebar-sublist a { + line-height: 36px; +} + +ul.sidebar .sidebar-list .menu-icon { + line-height: 36px; +} + +ul.sidebar .sidebar-list .sidebar-sublist a.active { + color: #fff; + border-left: 3px solid #fff; + background: #2d3e63; +} + +@media (max-height: 785px) { + ul.sidebar .sidebar-title { + line-height: 26px; + } + + ul.sidebar .sidebar-title .form-control { + height: 26px; + padding: 3px 6px; + } + + ul.sidebar .sidebar-list a, + ul.sidebar .sidebar-list .sidebar-sublist a { + font-size: 12px; + line-height: 26px; + } + + ul.sidebar .sidebar-list .menu-icon { + line-height: 26px; + } +} + +@media (min-height: 786px) and (max-height: 924px) { + ul.sidebar .sidebar-title { + line-height: 30px; + } + + ul.sidebar .sidebar-title .form-control { + height: 30px; + padding: 5px 10px; + } + + ul.sidebar .sidebar-list a, + ul.sidebar .sidebar-list .sidebar-sublist a { + font-size: 12px; + line-height: 30px; + } + + ul.sidebar .sidebar-list .menu-icon { + line-height: 30px; + } +} diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js index 40018afd3..711f052f5 100644 --- a/app/portainer/services/api/accessService.js +++ b/app/portainer/services/api/accessService.js @@ -77,26 +77,22 @@ angular.module('portainer.app').factory('AccessService', [ } async function accessesAsync(entity, parent) { - try { - if (!entity) { - throw new Error('Unable to retrieve accesses'); - } - if (!entity.UserAccessPolicies) { - entity.UserAccessPolicies = {}; - } - if (!entity.TeamAccessPolicies) { - entity.TeamAccessPolicies = {}; - } - if (parent && !parent.UserAccessPolicies) { - parent.UserAccessPolicies = {}; - } - if (parent && !parent.TeamAccessPolicies) { - parent.TeamAccessPolicies = {}; - } - return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}); - } catch (err) { - throw err; + if (!entity) { + throw new Error('Unable to retrieve accesses'); } + if (!entity.UserAccessPolicies) { + entity.UserAccessPolicies = {}; + } + if (!entity.TeamAccessPolicies) { + entity.TeamAccessPolicies = {}; + } + if (parent && !parent.UserAccessPolicies) { + parent.UserAccessPolicies = {}; + } + if (parent && !parent.TeamAccessPolicies) { + parent.TeamAccessPolicies = {}; + } + return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}); } function accesses(entity, parent) { diff --git a/app/portainer/services/terminal-window.js b/app/portainer/services/terminal-window.js index 0464fa1d8..5bd10dbf7 100644 --- a/app/portainer/services/terminal-window.js +++ b/app/portainer/services/terminal-window.js @@ -1,6 +1,6 @@ angular.module('portainer').service('TerminalWindow', function ($window) { + const terminalHeight = 495; this.terminalopen = function () { - const terminalHeight = 480; const contentWrapperHeight = $window.innerHeight; const newContentWrapperHeight = contentWrapperHeight - terminalHeight; document.getElementById('content-wrapper').style.height = newContentWrapperHeight + 'px'; @@ -16,4 +16,11 @@ angular.module('portainer').service('TerminalWindow', function ($window) { document.getElementById('content-wrapper').style.overflowY = wrapperCSS.overflowY; document.getElementById('sidebar-wrapper').style.height = wrapperCSS.height; }; + this.terminalresize = function () { + const contentWrapperHeight = $window.innerHeight; + const newContentWrapperHeight = contentWrapperHeight - terminalHeight; + document.getElementById('content-wrapper').style.height = newContentWrapperHeight + 'px'; + document.getElementById('content-wrapper').style.overflowY = 'auto'; + document.getElementById('sidebar-wrapper').style.height = newContentWrapperHeight + 'px'; + }; }); diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index 219a1886f..742b3122f 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -7,6 +7,12 @@ Registry management + + + View registries via an endpoint to manage access for user(s) and/or team(s) + + +
-