From 0a8501fcbb79f5cb66a28c6501910ded13b0a42f Mon Sep 17 00:00:00 2001 From: Adrian Dimitrov Date: Wed, 29 Mar 2017 18:47:56 +0300 Subject: [PATCH 01/13] fix(containers): fix an issue with hidden labels (#740) --- app/services/containerService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); }) From a88d02b0b496722702a961717a78cb37bdaaa308 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Mar 2017 18:47:43 +0200 Subject: [PATCH 02/13] style(templates): update ownership buttons style --- app/components/templates/templates.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index c6e1468c3..4e943f9e0 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -61,8 +61,8 @@
- - + +
From 95203803886551473cb491a99484b3f256975080 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Mar 2017 18:54:27 +0200 Subject: [PATCH 03/13] style(services): update empty service list text alignment (#744) --- app/components/services/services.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From f9c1941384635ebcf357deb923ee90f295ded745 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 30 Mar 2017 11:17:54 +0200 Subject: [PATCH 04/13] chore(api): update comment --- api/file/file.go | 1 + 1 file changed, 1 insertion(+) 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 From a48503d821f7611601e60cfc0c814dcfabefb283 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 30 Mar 2017 11:22:59 +0200 Subject: [PATCH 05/13] feat(services): add a confirmation modal before deleting one or multiple services (#742) --- app/components/service/serviceController.js | 17 +++++++++++++---- app/components/services/servicesController.js | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 438038087..fa2cea01d 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'); @@ -213,8 +213,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 +238,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/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) { From 9fda8f9c923177857013e7866e8d5bc691cb6b1d Mon Sep 17 00:00:00 2001 From: Thomas Krzero Date: Thu, 30 Mar 2017 11:39:37 +0200 Subject: [PATCH 06/13] fix(services) - Fix exposed ports (#746) --- app/models/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 = []; From ffca4401357e015d4352a3a63087dd53c66334fb Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 30 Mar 2017 12:00:16 +0200 Subject: [PATCH 07/13] fix(services): let Docker automatically assign port when PublishedPort is not defined (#747) --- .../createService/createServiceController.js | 11 +++++++++-- app/components/service/serviceController.js | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) 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/service/serviceController.js b/app/components/service/serviceController.js index fa2cea01d..fff240cc1 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -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 From 53f31ba3b84de48b02bae955962e90e3088a975f Mon Sep 17 00:00:00 2001 From: Thomas Krzero Date: Fri, 31 Mar 2017 22:12:58 +0200 Subject: [PATCH 08/13] feat(templates): add the ability to connect a template to swarm attachable networks (#642) --- app/components/templates/templatesController.js | 6 +++++- app/services/networkService.js | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 1db8927af..625df6044 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -122,7 +122,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); } 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"}); From 50305e0eee3d27879d50b49cd59e7ec724fc7f59 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 1 Apr 2017 12:18:46 +0200 Subject: [PATCH 09/13] feat(volume-creation): retrieve available drivers from the engine (#751) --- .../createVolume/createVolumeController.js | 101 +++++++++--------- app/components/createVolume/createvolume.html | 11 +- app/helpers/volumeHelper.js | 15 +++ app/services/infoService.js | 20 ++++ app/services/volumeService.js | 37 +++---- 5 files changed, 104 insertions(+), 80 deletions(-) create mode 100644 app/helpers/volumeHelper.js create mode 100644 app/services/infoService.js 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/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/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/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); }; From db4b153ce1a362369567faedf7fe86305e646724 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 4 Apr 2017 09:16:13 +0200 Subject: [PATCH 10/13] fix(service-creation): fix invalid mount specs (#757) --- app/components/createService/createservice.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 - +
From 16166c336787f4eb27746b22d15c947d9d63994f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 5 Apr 2017 10:04:29 +0200 Subject: [PATCH 11/13] fix(network-creation): fix internal network switch (#760) --- app/components/createNetwork/createnetwork.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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
From b8803f380b4ae14acceeb35851ca1353cf052e6a Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 5 Apr 2017 10:13:32 +0200 Subject: [PATCH 12/13] feat(templates): LinuxServer.io templates integration (#761) --- api/http/server.go | 2 +- api/http/templates_handler.go | 28 ++++++++++++++++--- app/app.js | 21 ++++++++++++++ app/components/sidebar/sidebar.html | 3 ++ app/components/templates/templates.html | 21 ++++++++------ .../templates/templatesController.js | 16 +++++++---- app/helpers/templateHelper.js | 17 +++++++++++ app/models/template.js | 1 + app/services/templateService.js | 8 ++++-- assets/css/app.css | 17 +++++++++++ 10 files changed, 113 insertions(+), 21 deletions(-) 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/app/app.js b/app/app.js index 21ed9207a..1142ad666 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', 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 @@