mirror of https://github.com/portainer/portainer
commit
4839c5f313
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
23
app/app.js
23
app/app.js
|
@ -456,6 +456,27 @@ angular.module('portainer', [
|
|||
})
|
||||
.state('templates', {
|
||||
url: '/templates/',
|
||||
params: {
|
||||
key: 'containers',
|
||||
hide_descriptions: false
|
||||
},
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('templates_linuxserver', {
|
||||
url: '^/templates/linuxserver.io',
|
||||
params: {
|
||||
key: 'linuxserver.io',
|
||||
hide_descriptions: true
|
||||
},
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
|
@ -573,4 +594,4 @@ angular.module('portainer', [
|
|||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.12.2');
|
||||
.constant('UI_VERSION', 'v1.12.3');
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,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;
|
||||
|
|
|
@ -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;
|
||||
}]);
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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,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 : '';
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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"});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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,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>"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue