Compare commits

...

20 Commits

Author SHA1 Message Date
Anthony Lapenna 8e6272920b Merge branch 'release/1.12.4' 2017-04-06 10:37:32 +02:00
Anthony Lapenna 0cde215259 chore(version): bump version number 2017-04-06 10:37:26 +02:00
Anthony Lapenna 3fc54c095e fix(service-details): fix an update issue when no ports are defined (#765) 2017-04-06 09:35:01 +01:00
Anthony Lapenna 80a0a15490 fix(service-details): display spinner when updating the service (#764) 2017-04-06 09:34:49 +01:00
Anthony Lapenna af49c78498 Merge tag '1.12.3' into develop
Release 1.12.3
2017-04-05 10:15:14 +02:00
Anthony Lapenna 4839c5f313 Merge branch 'release/1.12.3' 2017-04-05 10:15:08 +02:00
Anthony Lapenna e9c6feb3c4 chore(version): bump version number 2017-04-05 10:15:03 +02:00
Anthony Lapenna b8803f380b feat(templates): LinuxServer.io templates integration (#761) 2017-04-05 10:13:32 +02:00
Anthony Lapenna 16166c3367 fix(network-creation): fix internal network switch (#760) 2017-04-05 10:04:29 +02:00
Anthony Lapenna db4b153ce1 fix(service-creation): fix invalid mount specs (#757) 2017-04-04 09:16:13 +02:00
Anthony Lapenna 50305e0eee feat(volume-creation): retrieve available drivers from the engine (#751) 2017-04-01 12:18:46 +02:00
Thomas Krzero 53f31ba3b8 feat(templates): add the ability to connect a template to swarm attachable networks (#642) 2017-03-31 22:12:58 +02:00
Anthony Lapenna ffca440135 fix(services): let Docker automatically assign port when PublishedPort is not defined (#747) 2017-03-30 12:00:16 +02:00
Thomas Krzero 9fda8f9c92 fix(services) - Fix exposed ports (#746) 2017-03-30 11:39:37 +02:00
Anthony Lapenna a48503d821 feat(services): add a confirmation modal before deleting one or multiple services (#742) 2017-03-30 11:22:59 +02:00
Anthony Lapenna f9c1941384 chore(api): update comment 2017-03-30 11:17:54 +02:00
Anthony Lapenna 9520380388 style(services): update empty service list text alignment (#744) 2017-03-29 18:54:27 +02:00
Anthony Lapenna a88d02b0b4 style(templates): update ownership buttons style 2017-03-29 18:47:43 +02:00
Adrian Dimitrov 0a8501fcbb fix(containers): fix an issue with hidden labels (#740) 2017-03-29 17:47:56 +02:00
Anthony Lapenna c9d50641c8 Merge tag '1.12.2' into develop
Release 1.12.2
2017-03-28 15:18:40 +02:00
28 changed files with 295 additions and 127 deletions
+1
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
+1 -1
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()
+24 -4
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
+1 -1
View File
@@ -176,7 +176,7 @@ type (
const (
// APIVersion is the version number of Portainer API.
APIVersion = "1.12.2"
APIVersion = "1.12.4"
// DBVersion is the version number of Portainer database.
DBVersion = 1
)
+22 -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.4');
@@ -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>
@@ -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;
@@ -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 -->
@@ -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();
}]);
@@ -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 -->
+25 -7
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');
@@ -157,7 +157,7 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi
};
$scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show();
$('#loadingViewSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
@@ -197,24 +197,42 @@ function ($scope, $stateParams, $state, $location, $anchorScroll, Service, Servi
MaxAttempts: service.RestartMaxAttempts,
Window: service.RestartWindow
};
if (service.Ports) {
service.Ports.forEach(function (binding) {
if (binding.PublishedPort === null || binding.PublishedPort === '') {
delete binding.PublishedPort;
}
});
}
config.EndpointSpec = {
Mode: config.EndpointSpec.Mode || 'vip',
Ports: service.Ports
};
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
$('#loadingViewSpinner').hide();
Messages.send("Service successfully updated", "Service updated");
$scope.cancelChanges({});
fetchServiceDetails();
}, function (e) {
$('#loadServicesSpinner').hide();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to update service");
});
};
$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 +247,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;
+2 -2
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>
+16 -2
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) {
+3
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>
+14 -11
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...
@@ -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;
+17
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;
}]);
+15
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;
}]);
+2 -2
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 = [];
+1
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 : '';
+1 -1
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);
})
+20
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;
}]);
+8
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"});
+6 -2
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);
+12 -25
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);
};
+17
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;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.12.2",
"version": "1.12.4",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.12.2",
"version": "1.12.4",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"