diff --git a/api/file/file.go b/api/file/file.go index 63837d525..8337f3c15 100644 --- a/api/file/file.go +++ b/api/file/file.go @@ -36,6 +36,7 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) { // Checking if a mount directory exists is broken with Go on Windows. // This will need to be reviewed after the issue has been fixed in Go. + // See: https://github.com/portainer/portainer/issues/474 // err := createDirectoryIfNotExist(dataStorePath, 0755) // if err != nil { // return nil, err diff --git a/api/http/server.go b/api/http/server.go index 046e58ee9..974174b5c 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -42,7 +42,7 @@ func (server *Server) Start() error { var settingsHandler = NewSettingsHandler(middleWareService) settingsHandler.settings = server.Settings var templatesHandler = NewTemplatesHandler(middleWareService) - templatesHandler.templatesURL = server.TemplatesURL + templatesHandler.containerTemplatesURL = server.TemplatesURL var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService) dockerHandler.EndpointService = server.EndpointService var websocketHandler = NewWebSocketHandler() diff --git a/api/http/templates_handler.go b/api/http/templates_handler.go index 510605021..be994ddc2 100644 --- a/api/http/templates_handler.go +++ b/api/http/templates_handler.go @@ -12,10 +12,14 @@ import ( // TemplatesHandler represents an HTTP API handler for managing templates. type TemplatesHandler struct { *mux.Router - Logger *log.Logger - templatesURL string + Logger *log.Logger + containerTemplatesURL string } +const ( + containerTemplatesURLLinuxServerIo = "http://tools.linuxserver.io/portainer.json" +) + // NewTemplatesHandler returns a new instance of TemplatesHandler. func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler { h := &TemplatesHandler{ @@ -27,14 +31,30 @@ func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler { return h } -// handleGetTemplates handles GET requests on /templates +// handleGetTemplates handles GET requests on /templates?key= func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { handleNotAllowed(w, []string{http.MethodGet}) return } - resp, err := http.Get(handler.templatesURL) + key := r.FormValue("key") + if key == "" { + Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger) + return + } + + var templatesURL string + if key == "containers" { + templatesURL = handler.containerTemplatesURL + } else if key == "linuxserver.io" { + templatesURL = containerTemplatesURLLinuxServerIo + } else { + Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger) + return + } + + resp, err := http.Get(templatesURL) if err != nil { Error(w, err, http.StatusInternalServerError, handler.Logger) return diff --git a/api/portainer.go b/api/portainer.go index 7feaab60a..801b89e9f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -176,7 +176,7 @@ type ( const ( // APIVersion is the version number of Portainer API. - APIVersion = "1.12.2" + APIVersion = "1.12.3" // DBVersion is the version number of Portainer database. DBVersion = 1 ) diff --git a/app/app.js b/app/app.js index 21ed9207a..b51d1c871 100644 --- a/app/app.js +++ b/app/app.js @@ -456,6 +456,27 @@ angular.module('portainer', [ }) .state('templates', { url: '/templates/', + params: { + key: 'containers', + hide_descriptions: false + }, + views: { + "content@": { + templateUrl: 'app/components/templates/templates.html', + controller: 'TemplatesController' + }, + "sidebar@": { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('templates_linuxserver', { + url: '^/templates/linuxserver.io', + params: { + key: 'linuxserver.io', + hide_descriptions: true + }, views: { "content@": { templateUrl: 'app/components/templates/templates.html', @@ -573,4 +594,4 @@ angular.module('portainer', [ .constant('ENDPOINTS_ENDPOINT', 'api/endpoints') .constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('PAGINATION_MAX_ITEMS', 10) - .constant('UI_VERSION', 'v1.12.2'); + .constant('UI_VERSION', 'v1.12.3'); diff --git a/app/components/createNetwork/createnetwork.html b/app/components/createNetwork/createnetwork.html index c4379cc92..98992d438 100644 --- a/app/components/createNetwork/createnetwork.html +++ b/app/components/createNetwork/createnetwork.html @@ -111,7 +111,7 @@ Restrict external access to the network diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 93d5b047c..f7b1d100a 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -83,8 +83,15 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication, function preparePortsConfig(config, input) { var ports = []; input.Ports.forEach(function (binding) { - if (binding.PublishedPort && binding.TargetPort) { - ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol }); + var port = { + Protocol: binding.Protocol + }; + if (binding.TargetPort) { + port.TargetPort = +binding.TargetPort; + if (binding.PublishedPort) { + port.PublishedPort = +binding.PublishedPort; + } + ports.push(port); } }); config.EndpointSpec.Ports = ports; diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index a89a1fb46..60f556d5d 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -239,7 +239,7 @@
container - +
@@ -261,7 +261,7 @@
volume - @@ -270,7 +270,7 @@
host - +
diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index 61874d1a7..fca49bcc1 100644 --- a/app/components/createVolume/createVolumeController.js +++ b/app/components/createVolume/createVolumeController.js @@ -1,15 +1,13 @@ angular.module('createVolume', []) -.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'ResourceControlService', 'Authentication', 'Messages', -function ($scope, $state, Volume, ResourceControlService, Authentication, Messages) { +.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'InfoService', 'ResourceControlService', 'Authentication', 'Messages', +function ($scope, $state, VolumeService, InfoService, ResourceControlService, Authentication, Messages) { $scope.formValues = { Ownership: $scope.applicationState.application.authentication ? 'private' : '', + Driver: 'local', DriverOptions: [] }; - - $scope.config = { - Driver: 'local' - }; + $scope.availableVolumeDrivers = []; $scope.addDriverOption = function() { $scope.formValues.DriverOptions.push({ name: '', value: '' }); @@ -19,52 +17,51 @@ function ($scope, $state, Volume, ResourceControlService, Authentication, Messag $scope.formValues.DriverOptions.splice(index, 1); }; - function createVolume(config) { - $('#createVolumeSpinner').show(); - Volume.create(config, function (d) { - if (d.message) { - $('#createVolumeSpinner').hide(); - Messages.error('Unable to create volume', {}, d.message); - } else { - if ($scope.formValues.Ownership === 'private') { - ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, d.Name) - .then(function success() { - Messages.send("Volume created", d.Name); - $('#createVolumeSpinner').hide(); - $state.go('volumes', {}, {reload: true}); - }) - .catch(function error(err) { - $('#createVolumeSpinner').hide(); - Messages.error("Failure", err, 'Unable to apply resource control on volume'); - }); - } else { - Messages.send("Volume created", d.Name); - $('#createVolumeSpinner').hide(); - $state.go('volumes', {}, {reload: true}); - } - } - }, function (e) { - $('#createVolumeSpinner').hide(); - Messages.error("Failure", e, 'Unable to create volume'); - }); - } - - function prepareDriverOptions(config) { - var options = {}; - $scope.formValues.DriverOptions.forEach(function (option) { - options[option.name] = option.value; - }); - config.DriverOpts = options; - } - - function prepareConfiguration() { - var config = angular.copy($scope.config); - prepareDriverOptions(config); - return config; - } - $scope.create = function () { - var config = prepareConfiguration(); - createVolume(config); + $('#createVolumeSpinner').show(); + + var name = $scope.formValues.Name; + var driver = $scope.formValues.Driver; + var driverOptions = $scope.formValues.DriverOptions; + var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions); + + VolumeService.createVolume(volumeConfiguration) + .then(function success(data) { + if ($scope.formValues.Ownership === 'private') { + ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, data.Name) + .then(function success() { + Messages.send("Volume created", data.Name); + $state.go('volumes', {}, {reload: true}); + }) + .catch(function error(err) { + Messages.error("Failure", err, 'Unable to apply resource control on volume'); + }); + } else { + Messages.send("Volume created", data.Name); + $state.go('volumes', {}, {reload: true}); + } + }) + .catch(function error(err) { + Messages.error('Failure', err, 'Unable to create volume'); + }) + .finally(function final() { + $('#createVolumeSpinner').hide(); + }); }; + + function initView() { + $('#loadingViewSpinner').show(); + InfoService.getVolumePlugins() + .then(function success(data) { + $scope.availableVolumeDrivers = data; + }) + .catch(function error(err) { + Messages.error("Failure", err, 'Unable to retrieve volume plugin information'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); }]); diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html index 289d46bfd..d44ad2608 100644 --- a/app/components/createVolume/createvolume.html +++ b/app/components/createVolume/createvolume.html @@ -1,5 +1,7 @@ - + + + Volumes > Add volume @@ -14,7 +16,7 @@
- +
@@ -25,7 +27,10 @@
- + +
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 438038087..fff240cc1 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', -function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination) { +.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', 'ModalService', +function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination, ModalService) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -197,6 +197,13 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi MaxAttempts: service.RestartMaxAttempts, Window: service.RestartWindow }; + + service.Ports.forEach(function (binding) { + if (binding.PublishedPort === null || binding.PublishedPort === '') { + delete binding.PublishedPort; + } + }); + config.EndpointSpec = { Mode: config.EndpointSpec.Mode || 'vip', Ports: service.Ports @@ -213,8 +220,17 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi }); }; + $scope.removeService = function() { + ModalService.confirmDeletion( + 'Do you want to delete this service? All the containers associated to this service will be removed too.', + function onConfirm(confirmed) { + if(!confirmed) { return; } + removeService(); + } + ); + }; - $scope.removeService = function removeService() { + function removeService() { $('#loadingViewSpinner').show(); Service.remove({id: $stateParams.id}, function (d) { if (d.message) { @@ -229,7 +245,7 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi $('#loadingViewSpinner').hide(); Messages.error("Failure", e, "Unable to remove service"); }); - }; + } function translateServiceArrays(service) { service.ServiceSecrets = service.Secrets; diff --git a/app/components/services/services.html b/app/components/services/services.html index 5406148b4..486a47cd0 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -131,10 +131,10 @@ - Loading... + Loading... - No services available. + No services available. diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js index 8adf51998..2a0522db6 100644 --- a/app/components/services/servicesController.js +++ b/app/components/services/servicesController.js @@ -68,7 +68,17 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa }); }; - $scope.removeAction = function () { + $scope.removeAction = function() { + ModalService.confirmDeletion( + 'Do you want to delete the selected service(s)? All the containers associated to the selected service(s) will be removed too.', + function onConfirm(confirmed) { + if(!confirmed) { return; } + removeServices(); + } + ); + }; + + function removeServices() { $('#loadServicesSpinner').show(); var counter = 0; var complete = function () { @@ -108,7 +118,11 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa }); } }); - }; + } + + // $scope.removeAction = function () { + // + // }; function mapUsersToServices(users) { angular.forEach($scope.services, function (service) { diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 5515fa69e..85d60dc83 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -21,6 +21,9 @@
@@ -228,7 +231,7 @@
{{ tpl.Title }}
-
{{ tpl.Description }}
+
{{ tpl.Description }}
Loading... diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 1db8927af..003ddba7d 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,9 +1,10 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication', -function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication', +function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, + hideDescriptions: $stateParams.hide_descriptions, pagination_count: Pagination.getPaginationCount('templates') }; $scope.formValues = { @@ -122,7 +123,11 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container function filterNetworksBasedOnProvider(networks) { var endpointProvider = $scope.applicationState.endpoint.mode.provider; if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') { - networks = NetworkService.filterGlobalNetworks(networks); + if (endpointProvider === 'DOCKER_SWARM') { + networks = NetworkService.filterGlobalNetworks(networks); + } else { + networks = NetworkService.filterSwarmModeAttachableNetworks(networks); + } $scope.globalNetworkCount = networks.length; NetworkService.addPredefinedLocalNetworks(networks); } @@ -130,15 +135,20 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container } function initTemplates() { + var templatesKey = $stateParams.key; Config.$promise.then(function (c) { $q.all({ - templates: TemplateService.getTemplates(), + templates: TemplateService.getTemplates(templatesKey), containers: ContainerService.getContainers(0, c.hiddenLabels), networks: NetworkService.getNetworks(), volumes: VolumeService.getVolumes() }) .then(function success(data) { - $scope.templates = data.templates; + var templates = data.templates; + if (templatesKey === 'linuxserver.io') { + templates = TemplateService.filterLinuxServerIOTemplates(templates); + } + $scope.templates = templates; $scope.runningContainers = data.containers; $scope.availableNetworks = filterNetworksBasedOnProvider(data.networks); $scope.availableVolumes = data.volumes.Volumes; diff --git a/app/helpers/templateHelper.js b/app/helpers/templateHelper.js index 0df432495..4556195f9 100644 --- a/app/helpers/templateHelper.js +++ b/app/helpers/templateHelper.js @@ -93,5 +93,22 @@ angular.module('portainer.helpers') return count; }; + helper.filterLinuxServerIOTemplates = function(templates) { + return templates.filter(function f(template) { + var valid = false; + if (template.Category) { + angular.forEach(template.Category, function(category) { + if (_.startsWith(category, 'Network')) { + valid = true; + } + }); + } + return valid; + }).map(function(template, idx) { + template.index = idx; + return template; + }); + }; + return helper; }]); diff --git a/app/helpers/volumeHelper.js b/app/helpers/volumeHelper.js new file mode 100644 index 000000000..b462fad85 --- /dev/null +++ b/app/helpers/volumeHelper.js @@ -0,0 +1,15 @@ +angular.module('portainer.helpers') +.factory('VolumeHelper', [function VolumeHelperFactory() { + 'use strict'; + var helper = {}; + + helper.createDriverOptions = function(optionArray) { + var options = {}; + optionArray.forEach(function (option) { + options[option.name] = option.value; + }); + return options; + }; + + return helper; +}]); diff --git a/app/models/service.js b/app/models/service.js index c60c3faab..12c97971c 100644 --- a/app/models/service.js +++ b/app/models/service.js @@ -53,8 +53,8 @@ function ServiceViewModel(data, runningTasks, nodes) { this.Command = containerSpec.Command; this.Secrets = containerSpec.Secrets; } - if (data.Spec.EndpointSpec) { - this.Ports = data.Spec.EndpointSpec.Ports; + if (data.Endpoint) { + this.Ports = data.Endpoint.Ports; } this.Mounts = []; diff --git a/app/models/template.js b/app/models/template.js index e7e5ae4a6..3456e8c9d 100644 --- a/app/models/template.js +++ b/app/models/template.js @@ -1,6 +1,7 @@ function TemplateViewModel(data) { this.Title = data.title; this.Description = data.description; + this.Category = data.category; this.Logo = data.logo; this.Image = data.image; this.Registry = data.registry ? data.registry : ''; diff --git a/app/services/containerService.js b/app/services/containerService.js index c3316c549..41ff5c9f1 100644 --- a/app/services/containerService.js +++ b/app/services/containerService.js @@ -9,7 +9,7 @@ angular.module('portainer.services') .then(function success(data) { var containers = data; if (hiddenLabels) { - containers = ContainerHelper.hideContainers(d, hiddenLabels); + containers = ContainerHelper.hideContainers(data, hiddenLabels); } deferred.resolve(data); }) diff --git a/app/services/infoService.js b/app/services/infoService.js new file mode 100644 index 000000000..1b5f0fd4a --- /dev/null +++ b/app/services/infoService.js @@ -0,0 +1,20 @@ +angular.module('portainer.services') +.factory('InfoService', ['$q', 'Info', function InfoServiceFactory($q, Info) { + 'use strict'; + var service = {}; + + service.getVolumePlugins = function() { + var deferred = $q.defer(); + Info.get({}).$promise + .then(function success(data) { + var plugins = data.Plugins.Volume; + deferred.resolve(plugins); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err}); + }); + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/networkService.js b/app/services/networkService.js index 256be1141..fb25eead7 100644 --- a/app/services/networkService.js +++ b/app/services/networkService.js @@ -15,6 +15,14 @@ angular.module('portainer.services') }); }; + service.filterSwarmModeAttachableNetworks = function(networks) { + return networks.filter(function (network) { + if (network.Scope === 'swarm' && network.Attachable === true) { + return network; + } + }); + }; + service.addPredefinedLocalNetworks = function(networks) { networks.push({Scope: "local", Name: "bridge"}); networks.push({Scope: "local", Name: "host"}); diff --git a/app/services/templateService.js b/app/services/templateService.js index fb1c86414..c7be012c0 100644 --- a/app/services/templateService.js +++ b/app/services/templateService.js @@ -3,9 +3,9 @@ angular.module('portainer.services') 'use strict'; var service = {}; - service.getTemplates = function() { + service.getTemplates = function(key) { var deferred = $q.defer(); - Template.get().$promise + Template.get({key: key}).$promise .then(function success(data) { var templates = data.map(function (tpl, idx) { var template = new TemplateViewModel(tpl); @@ -20,6 +20,10 @@ angular.module('portainer.services') return deferred.promise; }; + service.filterLinuxServerIOTemplates = function(templates) { + return TemplateHelper.filterLinuxServerIOTemplates(templates); + }; + service.createTemplateConfiguration = function(template, containerName, network, containerMapping) { var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry); var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping); diff --git a/app/services/volumeService.js b/app/services/volumeService.js index d889ce493..0f2a97a68 100644 --- a/app/services/volumeService.js +++ b/app/services/volumeService.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('VolumeService', ['$q', 'Volume', function VolumeServiceFactory($q, Volume) { +.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', function VolumeServiceFactory($q, Volume, VolumeHelper) { 'use strict'; var service = {}; @@ -7,27 +7,14 @@ angular.module('portainer.services') return Volume.query({}).$promise; }; - function prepareVolumeQueries(template, containerConfig) { - var volumeQueries = []; - if (template.volumes) { - template.volumes.forEach(function (vol) { - volumeQueries.push( - Volume.create({}, function (d) { - if (d.message) { - Messages.error("Unable to create volume", {}, d.message); - } else { - Messages.send("Volume created", d.Name); - containerConfig.Volumes[vol] = {}; - containerConfig.HostConfig.Binds.push(d.Name + ':' + vol); - } - }, function (e) { - Messages.error("Failure", e, "Unable to create volume"); - }).$promise - ); - }); - } - return volumeQueries; - } + service.createVolumeConfiguration = function(name, driver, driverOptions) { + var volumeConfiguration = { + Name: name, + Driver: driver, + DriverOpts: VolumeHelper.createDriverOptions(driverOptions) + }; + return volumeConfiguration; + }; service.createVolume = function(volumeConfiguration) { var deferred = $q.defer(); @@ -45,9 +32,9 @@ angular.module('portainer.services') return deferred.promise; }; - service.createVolumes = function(volumes) { - var createVolumeQueries = volumes.map(function(volume) { - return service.createVolume(volume); + service.createVolumes = function(volumeConfigurations) { + var createVolumeQueries = volumeConfigurations.map(function(volumeConfiguration) { + return service.createVolume(volumeConfiguration); }); return $q.all(createVolumeQueries); }; diff --git a/assets/css/app.css b/assets/css/app.css index b26bc7bc5..ed58a6d81 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -301,6 +301,10 @@ ul.sidebar { bottom: 40px; } +ul.sidebar .sidebar-title { + height: auto; +} + ul.sidebar .sidebar-list a.active { color: #fff; text-indent: 22px; @@ -308,6 +312,19 @@ ul.sidebar .sidebar-list a.active { background: #2d3e63; } +ul.sidebar .sidebar-list .sidebar-sublist a { + text-indent: 35px; + font-size: 12px; + color: #b2bfdc; + line-height: 40px; +} + +ul.sidebar .sidebar-list .sidebar-sublist a.active { + color: #fff; + border-left: 3px solid #fff; + background: #2d3e63; +} + @media(min-width: 768px) and (max-width: 992px) { .margin-sm-top { margin-top: 5px; diff --git a/bower.json b/bower.json index 5fc36af9e..993ec2765 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "portainer", - "version": "1.12.2", + "version": "1.12.3", "homepage": "https://github.com/portainer/portainer", "authors": [ "Anthony Lapenna " diff --git a/package.json b/package.json index 8b72e0f64..cce68414c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.12.2", + "version": "1.12.3", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"