From 0579251c702cea97c45dac95dfae79be3e129888 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 18 May 2017 23:00:08 +0200 Subject: [PATCH] feat(templates): new templates capabilities (#862) --- app/components/sidebar/sidebarController.js | 2 +- app/components/templates/templates.html | 137 +++++++++++----- .../templates/templatesController.js | 69 ++++---- app/directives/template-widget.js | 13 ++ app/directives/widget.js | 2 +- app/helpers/templateHelper.js | 8 +- app/models/template.js | 5 +- app/models/templateLinuxServer.js | 33 ++++ app/services/templateService.js | 14 +- assets/css/app.css | 152 +++++++++--------- 10 files changed, 276 insertions(+), 159 deletions(-) create mode 100644 app/directives/template-widget.js create mode 100644 app/models/templateLinuxServer.js diff --git a/app/components/sidebar/sidebarController.js b/app/components/sidebar/sidebarController.js index eef6a0430..5bd466c70 100644 --- a/app/components/sidebar/sidebarController.js +++ b/app/components/sidebar/sidebarController.js @@ -19,7 +19,7 @@ function ($scope, $state, Settings, Config, EndpointService, StateManager, Endpo $state.go('dashboard'); }) .catch(function error(err) { - Notifications.error("Failure", err, "Unable to connect to the Docker endpoint"); + Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint'); EndpointProvider.setEndpointID(activeEndpointID); EndpointProvider.setEndpointPublicURL(activeEndpointPublicURL); StateManager.updateEndpointState(true) diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index cc33b4cc2..784ef3271 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -1,4 +1,4 @@ - + @@ -7,34 +7,52 @@ Templates -
-
+ + -
-
- - -
- Items per page: - + +
- + +
+ + + + + + +
+
+
-
- -
{{ tpl.Title }}
-
{{ tpl.Description }}
+ +
+
+ + + + + + + + +
+ + {{ tpl.Title }} + + + + + + +
+ + +
+ + {{ tpl.Description }} + + + {{ tpl.Categories.join(', ') }} + +
+ +
+ +
+
Loading...
-
+
No templates available.
-
- -
- +
+
diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 42bbe6de5..9cec4c602 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,16 +1,20 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', -function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', +function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, hideDescriptions: $stateParams.hide_descriptions, - pagination_count: Pagination.getPaginationCount('templates') + pagination_count: Pagination.getPaginationCount('templates'), + filters: { + Categories: '!', + Platform: '!' + } }; $scope.formValues = { Ownership: $scope.applicationState.application.authentication ? 'private' : '', - network: "", - name: "", + network: '', + name: '' }; $scope.changePaginationCount = function() { @@ -74,32 +78,38 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ }); }; - var selectedItem = -1; - $scope.selectTemplate = function(idx) { - $('#template_' + idx).toggleClass("container-template--selected"); - if (selectedItem === idx) { - unselectTemplate(); + $scope.unselectTemplate = function() { + var currentTemplateIndex = $scope.state.selectedTemplate.index; + $('#template_' + currentTemplateIndex).toggleClass('template-container--selected'); + $scope.state.selectedTemplate = null; + }; + + $scope.selectTemplate = function(index, pos) { + if ($scope.state.selectedTemplate && $scope.state.selectedTemplate.index !== index) { + $scope.unselectTemplate(); + } + + var templates = $filter('filter')($scope.templates, $scope.state.filters, true); + var template = templates[pos]; + if (template === $scope.state.selectedTemplate) { + $scope.unselectTemplate(); } else { - selectTemplate(idx); + selectTemplate(index, pos, templates); } }; - function unselectTemplate() { - selectedItem = -1; - $scope.state.selectedTemplate = null; - } - - function selectTemplate(idx) { - $('#template_' + selectedItem).toggleClass("container-template--selected"); - selectedItem = idx; - var selectedTemplate = $scope.templates[idx]; + function selectTemplate(index, pos, filteredTemplates) { + $('#template_' + index).toggleClass('template-container--selected'); + var selectedTemplate = filteredTemplates[pos]; $scope.state.selectedTemplate = selectedTemplate; + if (selectedTemplate.Network) { $scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === selectedTemplate.Network; }); } else { - $scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === "bridge"; }); + $scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; }); } - $anchorScroll('selectedTemplate'); + + $anchorScroll('view-top'); } function createTemplateConfiguration(template) { @@ -114,7 +124,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ var containerMapping = 'BY_CONTAINER_IP'; if (endpointProvider === 'DOCKER_SWARM' && network.Scope === 'global') { containerMapping = 'BY_SWARM_CONTAINER_NAME'; - } else if (network.Name !== "bridge") { + } else if (network.Name !== 'bridge') { containerMapping = 'BY_CONTAINER_NAME'; } return containerMapping; @@ -144,18 +154,19 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ volumes: VolumeService.getVolumes() }) .then(function success(data) { - var templates = data.templates; - if (templatesKey === 'linuxserver.io') { - templates = TemplateService.filterLinuxServerIOTemplates(templates); - } - $scope.templates = templates; + $scope.templates = data.templates; + var availableCategories = []; + angular.forEach($scope.templates, function(template) { + availableCategories = availableCategories.concat(template.Categories); + }); + $scope.availableCategories = _.sortBy(_.uniq(availableCategories)); $scope.runningContainers = data.containers; $scope.availableNetworks = filterNetworksBasedOnProvider(data.networks); $scope.availableVolumes = data.volumes.Volumes; }) .catch(function error(err) { $scope.templates = []; - Notifications.error("Failure", err, "An error occured during apps initialization."); + Notifications.error('Failure', err, 'An error occured during apps initialization.'); }) .finally(function final(){ $('#loadTemplatesSpinner').hide(); diff --git a/app/directives/template-widget.js b/app/directives/template-widget.js new file mode 100644 index 000000000..51f902cbf --- /dev/null +++ b/app/directives/template-widget.js @@ -0,0 +1,13 @@ +angular +.module('portainer') +.directive('rdTemplateWidget', function rdWidget() { + var directive = { + scope: { + 'ngModel': '=' + }, + transclude: true, + template: '
', + restrict: 'EA' + }; + return directive; +}); diff --git a/app/directives/widget.js b/app/directives/widget.js index 56fea70e5..6426fe147 100644 --- a/app/directives/widget.js +++ b/app/directives/widget.js @@ -3,7 +3,7 @@ angular .directive('rdWidget', function rdWidget() { var directive = { scope: { - "ngModel": "=" + 'ngModel': '=' }, transclude: true, template: '
', diff --git a/app/helpers/templateHelper.js b/app/helpers/templateHelper.js index 7cdda76fe..a97bdd978 100644 --- a/app/helpers/templateHelper.js +++ b/app/helpers/templateHelper.js @@ -28,7 +28,7 @@ angular.module('portainer.helpers') }; ports.forEach(function (p) { if (p.containerPort) { - var key = p.containerPort + "/" + p.protocol; + var key = p.containerPort + '/' + p.protocol; var binding = {}; if (p.hostPort) { binding.HostPort = p.hostPort; @@ -60,7 +60,7 @@ angular.module('portainer.helpers') value = $filter('swarmcontainername')(envvar.value); } } - env.push(envvar.name + "=" + value); + env.push(envvar.name + '=' + value); } }); return env; @@ -108,8 +108,8 @@ angular.module('portainer.helpers') helper.filterLinuxServerIOTemplates = function(templates) { return templates.filter(function f(template) { var valid = false; - if (template.Category) { - angular.forEach(template.Category, function(category) { + if (template.Categories) { + angular.forEach(template.Categories, function(category) { if (_.startsWith(category, 'Network')) { valid = true; } diff --git a/app/models/template.js b/app/models/template.js index 0b8dbf3eb..6d8d5497a 100644 --- a/app/models/template.js +++ b/app/models/template.js @@ -1,8 +1,9 @@ function TemplateViewModel(data) { this.Title = data.title; this.Description = data.description; - this.Note = data.note ? data.note : data.description; - this.Category = data.category; + this.Note = data.note; + this.Categories = data.categories ? data.categories : []; + this.Platform = data.platform ? data.platform : ''; this.Logo = data.logo; this.Image = data.image; this.Registry = data.registry ? data.registry : ''; diff --git a/app/models/templateLinuxServer.js b/app/models/templateLinuxServer.js new file mode 100644 index 000000000..ae65c214d --- /dev/null +++ b/app/models/templateLinuxServer.js @@ -0,0 +1,33 @@ +function TemplateLSIOViewModel(data) { + this.Title = data.title; + this.Note = data.description; + this.Categories = data.category ? data.category : []; + this.Platform = data.platform ? data.platform : 'linux'; + this.Logo = data.logo; + this.Image = data.image; + this.Registry = data.registry ? data.registry : ''; + this.Command = data.command ? data.command : ''; + this.Network = data.network ? data.network : ''; + this.Env = data.env ? data.env : []; + this.Privileged = data.privileged ? data.privileged : false; + this.Volumes = []; + if (data.volumes) { + this.Volumes = data.volumes.map(function (v) { + return { + readOnly: false, + containerPath: v, + type: 'auto' + }; + }); + } + this.Ports = []; + if (data.ports) { + this.Ports = data.ports.map(function (p) { + var portAndProtocol = _.split(p, '/'); + return { + containerPort: portAndProtocol[0], + protocol: portAndProtocol[1] + }; + }); + } +} diff --git a/app/services/templateService.js b/app/services/templateService.js index b339135f6..4be7d7cbe 100644 --- a/app/services/templateService.js +++ b/app/services/templateService.js @@ -8,10 +8,18 @@ angular.module('portainer.services') Template.get({key: key}).$promise .then(function success(data) { var templates = data.map(function (tpl, idx) { - var template = new TemplateViewModel(tpl); + var template; + if (key === 'linuxserver.io') { + template = new TemplateLSIOViewModel(tpl); + } else { + template = new TemplateViewModel(tpl); + } template.index = idx; return template; }); + if (key === 'linuxserver.io') { + templates = TemplateHelper.filterLinuxServerIOTemplates(templates); + } deferred.resolve(templates); }) .catch(function error(err) { @@ -20,10 +28,6 @@ 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/assets/css/app.css b/assets/css/app.css index d0d1cd0f5..13c4d0eb0 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -64,6 +64,13 @@ html, body, #content-wrapper, .page-content, #view { color: #777; } +.form-section-title { + border-bottom: 1px solid #777; + margin-top: 5px; + margin-bottom: 15px; + color: #777; +} + .form-horizontal .control-label.text-left{ text-align: left; font-size: 0.9em; @@ -149,11 +156,6 @@ a[ng-click]{ cursor: pointer; } -.template-list { - display: flex; - flex-wrap: wrap; -} - .custom-header-ico { max-width: 32px; max-height: 32px; @@ -176,81 +178,6 @@ a[ng-click]{ } } -/* Underline From Center */ -.hvr-underline-from-center { - display: inline-block; - vertical-align: middle; - -webkit-transform: translateZ(0); - transform: translateZ(0); - box-shadow: 0 0 1px rgba(0, 0, 0, 0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -moz-osx-font-smoothing: grayscale; - position: relative; - overflow: hidden; -} -.hvr-underline-from-center:before { - content: ""; - position: absolute; - z-index: -1; - left: 50%; - right: 50%; - bottom: 0; - background: #85898b; - height: 2px; - -webkit-transition-property: left, right; - transition-property: left, right; - -webkit-transition-duration: 0.3s; - transition-duration: 0.3s; - -webkit-transition-timing-function: ease-out; - transition-timing-function: ease-out; -} -.hvr-underline-from-center:hover:before, .hvr-underline-from-center:focus:before, .hvr-underline-from-center:active:before { - left: 0; - right: 0; -} - -.container-template { - font-size: 1em; - width: 256px; - height: 128px; - margin: 15px; - padding: 5px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - border-radius: 10px; - cursor: pointer; - border: 2px solid #f6f6f6; - color: #30426a; -} - -.container-template--selected { - background-color: #ececec; - color: #2d3e63; -} - -.container-template:hover { - background-color: #ececec; - color: #2d3e63; -} - -.container-template .logo { - max-width: 48px; - max-height: 48px; -} - -.container-template .title { - text-align: center; -} - -.container-template .description { - text-align: center; - font-size: 0.8em; - margin-bottom: 5px; -} - .page-wrapper { height: 100%; width: 100%; @@ -418,3 +345,68 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { #toast-container > div { opacity: 0.9; } + +.template-widget { + height: 100%; +} + +.template-widget-body { + max-height: 86%; + overflow-y: auto; +} + +.template-list { + display: flex; + flex-direction: column; +} + +.template-logo { + width: 100%; + max-width: 60px; + height: 100%; + max-height: 60px; +} + +.template-container { + padding: 0.7rem; + margin-bottom: 0.7rem; + cursor: pointer; + border: 1px solid #333333; + border-radius: 2px; + box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5); +} + +.template-container--selected { + border: 2px solid #333333; + background-color: #ececec; + color: #2d3e63; +} + +.template-container:hover { + background-color: #ececec; + color: #2d3e63; +} + +.template-main { + display: flex; +} + +.template-note { + padding: 0.5em; + font-size: 0.9em; +} + +.template-title { + font-size: 1.8em; + font-weight: bold; +} + +.template-description { + font-size: 0.9em; + padding-right: 1em; +} + +.template-line { + display: flex; + justify-content: space-between; +}