Merge branch 'release/1.12.3'

pull/765/head 1.12.3
Anthony Lapenna 2017-04-05 10:15:08 +02:00
commit 4839c5f313
28 changed files with 290 additions and 124 deletions

View File

@ -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

View File

@ -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()

View File

@ -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=<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

View File

@ -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
)

View File

@ -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');

View File

@ -111,7 +111,7 @@
Restrict external access to the network
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
<input type="checkbox" ng-model="config.Internal"><i></i>
</label>
</div>
</div>

View File

@ -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;

View File

@ -239,7 +239,7 @@
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/in/container">
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
</div>
<!-- !container-path -->
<!-- volume-type -->
@ -261,7 +261,7 @@
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Target">
<select class="form-control" ng-model="volume.Source">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select>
@ -270,7 +270,7 @@
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/on/host">
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
</div>
<!-- !bind -->
<!-- read-only -->

View File

@ -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();
}]);

View File

@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-title title="Create volume">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > Add volume
</rd-header-content>
@ -14,7 +16,7 @@
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. myVolume">
<input type="text" class="form-control" ng-model="formValues.Name" id="volume_name" placeholder="e.g. myVolume">
</div>
</div>
<!-- !name-input -->
@ -25,7 +27,10 @@
<div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
<select class="form-control" ng-options="driver for driver in availableVolumeDrivers" ng-model="formValues.Driver" ng-if="availableVolumeDrivers.length > 0">
<option disabled hidden value="">Select a driver</option>
</select>
<input type="text" class="form-control" ng-model="formValues.Driver" id="volume_driver" placeholder="e.g. driverName" ng-if="availableVolumeDrivers.length === 0">
</div>
</div>
<!-- !driver-input -->

View File

@ -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;

View File

@ -131,10 +131,10 @@
</td>
</tr>
<tr ng-if="!services">
<td colspan="5" class="text-center text-muted">Loading...</td>
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="services.length == 0">
<td colspan="5" class="text-center text-muted">No services available.</td>
<td colspan="7" class="text-center text-muted">No services available.</td>
</tr>
</tbody>
</table>

View File

@ -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) {

View File

@ -21,6 +21,9 @@
</li>
<li class="sidebar-list">
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>

View File

@ -14,17 +14,13 @@
</rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<!-- description -->
<div class="form-group" ng-if="state.selectedTemplate.Description">
<div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
<span class="small" style="margin-left: 5px;">{{ state.selectedTemplate.Description }}</span>
</div>
</div>
<!-- !description -->
<!-- name-and-network-inputs -->
<div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
@ -61,8 +57,8 @@
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'">Private</label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'">Public</label>
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">Private</label>
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">Public</label>
</div>
</div>
</div>
@ -200,6 +196,13 @@
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
</span>
<span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" style="margin-left: 10px">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted" style="margin-left: 5px;">App templates cannot be deployed as Swarm Mode services for the moment. You can still use them to quickly deploy containers on the Docker host.</span>
</span>
</div>
</div>
</form>
@ -228,7 +231,7 @@
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
<img class="logo" ng-src="{{ tpl.Logo }}" />
<div class="title">{{ tpl.Title }}</div>
<div class="description">{{ tpl.Description }}</div>
<div class="description" ng-if="tpl.Description && !state.hideDescriptions">{{ tpl.Description }}</div>
</div>
<div ng-if="!templates" class="text-center text-muted">
Loading...

View File

@ -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;

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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 = [];

View File

@ -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 : '';

View File

@ -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);
})

View File

@ -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;
}]);

View File

@ -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"});

View File

@ -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);

View File

@ -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);
};

View File

@ -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;

View File

@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.12.2",
"version": "1.12.3",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"

View File

@ -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"