mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(templates): introduce templates management (#2017)
This commit is contained in:
parent
e7939a5384
commit
61c285bd2e
63 changed files with 3489 additions and 637 deletions
|
@ -361,36 +361,6 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
}
|
||||
};
|
||||
|
||||
var templates = {
|
||||
name: 'docker.templates',
|
||||
url: '/templates',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
}
|
||||
},
|
||||
params: {
|
||||
key: 'containers',
|
||||
hide_descriptions: false
|
||||
}
|
||||
};
|
||||
|
||||
var templatesLinuxServer = {
|
||||
name: 'docker.templates.linuxserver',
|
||||
url: '/linuxserver',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/docker/views/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
}
|
||||
},
|
||||
params: {
|
||||
key: 'linuxserver.io',
|
||||
hide_descriptions: true
|
||||
}
|
||||
};
|
||||
|
||||
var volumes = {
|
||||
name: 'docker.volumes',
|
||||
url: '/volumes',
|
||||
|
@ -458,8 +428,6 @@ angular.module('portainer.docker', ['portainer.app'])
|
|||
$stateRegistryProvider.register(tasks);
|
||||
$stateRegistryProvider.register(task);
|
||||
$stateRegistryProvider.register(taskLogs);
|
||||
$stateRegistryProvider.register(templates);
|
||||
$stateRegistryProvider.register(templatesLinuxServer);
|
||||
$stateRegistryProvider.register(volumes);
|
||||
$stateRegistryProvider.register(volume);
|
||||
$stateRegistryProvider.register(volumeCreation);
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.sidebarToggledOn && $ctrl.externalContributions && ($ctrl.currentState === 'docker.templates' || $ctrl.currentState === 'docker.templates.linuxserver')">
|
||||
<a ui-sref="docker.templates.linuxserver" ui-sref-active="active">LinuxServer.io</a>
|
||||
</div>
|
||||
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="portainer.stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
|
||||
|
|
|
@ -4,6 +4,11 @@ angular.module('portainer.docker').component('porImageRegistry', {
|
|||
bindings: {
|
||||
'image': '=',
|
||||
'registry': '=',
|
||||
'autoComplete': '<'
|
||||
'autoComplete': '<',
|
||||
'labelClass': '@',
|
||||
'inputClass': '@'
|
||||
},
|
||||
require: {
|
||||
form: '^form'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
<div>
|
||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11 col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image</label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<input type="text" class="form-control" uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
|
||||
ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
|
||||
ng-model="$ctrl.image" name="image_name" placeholder="e.g. myImage:myTag" required>
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
|
||||
<label for="image_registry" class="margin-sm-top control-label text-right" ng-class="$ctrl.labelClass">
|
||||
Registry
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-4 margin-sm-top">
|
||||
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
|
||||
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry"
|
||||
class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="$ctrl.form.image_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
|
||||
function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
|
||||
var ctrl = this;
|
||||
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
|
||||
function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
function initComponent() {
|
||||
$q.all({
|
||||
registries: RegistryService.registries(),
|
||||
dockerhub: DockerHubService.dockerhub(),
|
||||
availableImages: ctrl.autoComplete ? ImageService.images() : []
|
||||
})
|
||||
.then(function success(data) {
|
||||
var dockerhub = data.dockerhub;
|
||||
var registries = data.registries;
|
||||
ctrl.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
ctrl.availableRegistries = [dockerhub].concat(registries);
|
||||
if (!ctrl.registry.Id) {
|
||||
ctrl.registry = dockerhub;
|
||||
} else {
|
||||
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
});
|
||||
function initComponent() {
|
||||
$q.all({
|
||||
registries: RegistryService.registries(),
|
||||
dockerhub: DockerHubService.dockerhub(),
|
||||
availableImages: ctrl.autoComplete ? ImageService.images() : []
|
||||
})
|
||||
.then(function success(data) {
|
||||
var dockerhub = data.dockerhub;
|
||||
var registries = data.registries;
|
||||
ctrl.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
ctrl.availableRegistries = [dockerhub].concat(registries);
|
||||
if (!ctrl.registry.Id) {
|
||||
ctrl.registry = dockerhub;
|
||||
} else {
|
||||
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
});
|
||||
}
|
||||
|
||||
initComponent();
|
||||
}]);
|
||||
initComponent();
|
||||
}]);
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
function StackTemplateViewModel(data) {
|
||||
this.Type = data.type;
|
||||
this.Name = data.name;
|
||||
this.Title = data.title;
|
||||
this.Description = data.description;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : 'undefined';
|
||||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Env = data.env ? data.env : [];
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
function TemplateViewModel(data) {
|
||||
this.Type = data.type;
|
||||
this.Name = data.name;
|
||||
this.Hostname = data.hostname;
|
||||
this.Title = data.title;
|
||||
this.Description = data.description;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : 'undefined';
|
||||
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.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Volumes = [];
|
||||
|
||||
if (data.volumes) {
|
||||
this.Volumes = data.volumes.map(function (v) {
|
||||
// @DEPRECATED: New volume definition introduced
|
||||
// via https://github.com/portainer/portainer/pull/1154
|
||||
var volume = {
|
||||
readOnly: v.readonly || false,
|
||||
containerPath: v.container || v,
|
||||
type: 'auto'
|
||||
};
|
||||
|
||||
if (v.bind) {
|
||||
volume.name = v.bind;
|
||||
volume.type = 'bind';
|
||||
}
|
||||
|
||||
return volume;
|
||||
});
|
||||
}
|
||||
this.Ports = [];
|
||||
if (data.ports) {
|
||||
this.Ports = data.ports.map(function (p) {
|
||||
var portAndProtocol = _.split(p, '/');
|
||||
return {
|
||||
containerPort: portAndProtocol[0],
|
||||
protocol: portAndProtocol[1]
|
||||
};
|
||||
});
|
||||
}
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
function TemplateLSIOViewModel(data) {
|
||||
this.Type = data.type;
|
||||
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.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
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]
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -27,9 +27,13 @@
|
|||
</div>
|
||||
<div ng-if="formValues.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry" auto-complete="true"></por-image-registry>
|
||||
</div>
|
||||
<por-image-registry
|
||||
image="config.Image"
|
||||
registry="formValues.Registry"
|
||||
ng-if="formValues.Registry"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- always-pull -->
|
||||
<div class="form-group">
|
||||
|
|
|
@ -153,9 +153,12 @@
|
|||
</div>
|
||||
<!-- !tag-description -->
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="config.Image" registry="config.Registry"></por-image-registry>
|
||||
</div>
|
||||
<por-image-registry
|
||||
image="config.Image"
|
||||
registry="config.Registry"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
|
|
|
@ -63,9 +63,11 @@
|
|||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
|
||||
</div>
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
|
|
|
@ -15,9 +15,11 @@
|
|||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
|
||||
</div>
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
|
|
|
@ -22,9 +22,12 @@
|
|||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="formValues.Image" registry="formValues.Registry" auto-complete="true"></por-image-registry>
|
||||
</div>
|
||||
<por-image-registry
|
||||
image="formValues.Image"
|
||||
registry="formValues.Registry"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-1" input-class="col-sm-11 col-md-5"
|
||||
></por-image-registry>
|
||||
<!-- !image-and-registry -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Scheduling
|
||||
|
@ -142,7 +145,7 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Command
|
||||
</div>
|
||||
</div>
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
|
||||
|
@ -202,12 +205,12 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Logging
|
||||
</div>
|
||||
<!-- logging-driver -->
|
||||
<!-- logging-driver -->
|
||||
<div class="form-group">
|
||||
<label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="form-control" ng-model="formValues.LogDriverName" id="log-driver">
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option selected value="">Default logging driver</option>
|
||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
|
@ -247,10 +250,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- logging-opts-input-list -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !logging-opts -->
|
||||
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
|
|
|
@ -351,6 +351,43 @@ angular.module('portainer.app', [])
|
|||
}
|
||||
};
|
||||
|
||||
var templates = {
|
||||
name: 'portainer.templates',
|
||||
url: '/templates',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
}
|
||||
},
|
||||
params: {
|
||||
key: 'containers',
|
||||
hide_descriptions: false
|
||||
}
|
||||
};
|
||||
|
||||
var template = {
|
||||
name: 'portainer.templates.template',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/templates/edit/template.html',
|
||||
controller: 'TemplateController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var templateCreation = {
|
||||
name: 'portainer.templates.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/templates/create/createtemplate.html',
|
||||
controller: 'CreateTemplateController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(root);
|
||||
$stateRegistryProvider.register(portainer);
|
||||
$stateRegistryProvider.register(about);
|
||||
|
@ -382,4 +419,7 @@ angular.module('portainer.app', [])
|
|||
$stateRegistryProvider.register(user);
|
||||
$stateRegistryProvider.register(teams);
|
||||
$stateRegistryProvider.register(team);
|
||||
$stateRegistryProvider.register(templates);
|
||||
$stateRegistryProvider.register(template);
|
||||
$stateRegistryProvider.register(templateCreation);
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
angular.module('portainer.app').component('templateForm', {
|
||||
templateUrl: 'app/portainer/components/forms/template-form/templateForm.html',
|
||||
controller: function() {
|
||||
this.state = {
|
||||
collapseTemplate: false,
|
||||
collapseContainer: false,
|
||||
collapseStack: false,
|
||||
collapseEnv: false
|
||||
};
|
||||
|
||||
this.addPortBinding = function() {
|
||||
this.model.Ports.push({ containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
|
||||
this.removePortBinding = function(index) {
|
||||
this.model.Ports.splice(index, 1);
|
||||
};
|
||||
|
||||
this.addVolume = function () {
|
||||
this.model.Volumes.push({ container: '', bind: '', readonly: false, type: 'auto' });
|
||||
};
|
||||
|
||||
this.removeVolume = function(index) {
|
||||
this.model.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
this.addLabel = function () {
|
||||
this.model.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
this.removeLabel = function(index) {
|
||||
this.model.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
this.addEnvVar = function() {
|
||||
this.model.Env.push({ type: 1, name: '', label: '', description: '', default: '', preset: true, select: [] });
|
||||
};
|
||||
|
||||
this.removeEnvVar = function(index) {
|
||||
this.model.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
this.addEnvVarValue = function(env) {
|
||||
env.select.push({ name: '', value: '' });
|
||||
};
|
||||
|
||||
this.removeEnvVarValue = function(env, index) {
|
||||
env.select.splice(index, 1);
|
||||
};
|
||||
|
||||
this.changeEnvVarType = function(env) {
|
||||
if (env.type === 1) {
|
||||
env.preset = true;
|
||||
} else if (env.type === 2) {
|
||||
env.preset = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
bindings: {
|
||||
model: '=',
|
||||
categories: '<',
|
||||
networks: '<',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<',
|
||||
showTypeSelector: '<'
|
||||
}
|
||||
});
|
569
app/portainer/components/forms/template-form/templateForm.html
Normal file
569
app/portainer/components/forms/template-form/templateForm.html
Normal file
|
@ -0,0 +1,569 @@
|
|||
<form class="form-horizontal" name="templateForm">
|
||||
<!-- title-input -->
|
||||
<div class="form-group" ng-class="{ 'has-error': templateForm.template_title.$invalid }">
|
||||
<label for="template_title" class="col-sm-3 col-lg-2 control-label text-left">Title</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_title" ng-model="$ctrl.model.Title" placeholder="e.g. my-template" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="templateForm.template_title.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="templateForm.template_title.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !title-input -->
|
||||
<!-- description-input -->
|
||||
<div class="form-group" ng-class="{ 'has-error': templateForm.template_description.$invalid }">
|
||||
<label for="template_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_description" ng-model="$ctrl.model.Description" placeholder="e.g. template description..." required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="templateForm.template_description.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="templateForm.template_description.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !description-input -->
|
||||
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseTemplate = !$ctrl.state.collapseTemplate">
|
||||
Template
|
||||
<span class="small space-left">
|
||||
<a ng-if="$ctrl.state.collapseTemplate"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
|
||||
<a ng-if="!$ctrl.state.collapseTemplate"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- template-details -->
|
||||
<div uib-collapse="$ctrl.state.collapseTemplate">
|
||||
|
||||
<div ng-if="$ctrl.showTypeSelector">
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="template_container" ng-model="$ctrl.model.Type" ng-value="1">
|
||||
<label for="template_container">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Container
|
||||
</div>
|
||||
<p>Container template</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="template_swarm_stack" ng-model="$ctrl.model.Type" ng-value="2">
|
||||
<label for="template_swarm_stack">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Swarm stack
|
||||
</div>
|
||||
<p>Stack template (Swarm)</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="template_compose_stack" ng-model="$ctrl.model.Type" ng-value="3">
|
||||
<label for="template_compose_stack">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Compose stack
|
||||
</div>
|
||||
<p>Stack template (Compose)</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- name -->
|
||||
<div class="form-group">
|
||||
<label for="template_name" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Name
|
||||
<portainer-tooltip position="bottom" message="Default name that will be associated to the template"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_name" ng-model="$ctrl.model.Name" placeholder="e.g. myApp">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name -->
|
||||
<!-- logo -->
|
||||
<div class="form-group">
|
||||
<label for="template_logo" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Logo URL
|
||||
<portainer-tooltip position="bottom" message="Recommended size: 60x60"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_logo" ng-model="$ctrl.model.Logo" placeholder="e.g. https://portainer.io/images/logos/nginx.png">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !logo -->
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<label for="template_note" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Note
|
||||
<portainer-tooltip position="bottom" message="Usage / extra information about the template. Supports HTML."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<textarea class="form-control" name="template_note" ng-model="$ctrl.model.Note" placeholder='You can use this field to specify extra information. <br/> It supports <b>HTML</b>.'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<!-- platform -->
|
||||
<div class="form-group">
|
||||
<label for="template_platform" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Platform
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select class="form-control" name="template_platform" ng-model="$ctrl.model.Platform">
|
||||
<option value="">Multi-platform</option>
|
||||
<option value="linux">Linux</option>
|
||||
<option value="windows">Windows</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !platform -->
|
||||
<!-- categories -->
|
||||
<div class="form-group">
|
||||
<label for="template_categories" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Categories
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<ui-select multiple tagging tagging-label="(new category)" ng-model="$ctrl.model.Categories" sortable="true" style="width: 300px;" title="Choose a category">
|
||||
<ui-select-match placeholder="Select categories...">{{ $item }}</ui-select-match>
|
||||
<ui-select-choices repeat="category in $ctrl.categories | filter:$select.search">
|
||||
{{ category }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !categories -->
|
||||
<!-- administrator-only -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
Administrator template
|
||||
<portainer-tooltip position="bottom" message="Should this template be only available to administrator users."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="$ctrl.model.AdministratorOnly"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- administrator-only -->
|
||||
</div>
|
||||
<!-- !template-details -->
|
||||
<div ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
|
||||
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseStack = !$ctrl.state.collapseStack">
|
||||
Stack
|
||||
<span class="small space-left">
|
||||
<a ng-if="$ctrl.state.collapseStack"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
|
||||
<a ng-if="!$ctrl.state.collapseStack"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- stack-details -->
|
||||
<div uib-collapse="$ctrl.state.collapseStack">
|
||||
<!-- repository-url -->
|
||||
<div class="form-group" ng-class="{ 'has-error': templateForm.template_repository_url.$invalid }">
|
||||
<label for="template_repository_url" class="col-sm-3 col-lg-2 control-label text-left">Repository URL</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_repository_url" ng-model="$ctrl.model.Repository.url" placeholder="https://github.com/portainer/portainer-compose" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="templateForm.template_repository_url.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="templateForm.template_repository_url.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !repository-url -->
|
||||
<!-- composefile-path -->
|
||||
<div class="form-group">
|
||||
<label for="template_repository_path" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Compose file path
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_repository_path" ng-model="$ctrl.model.Repository.stackfile" placeholder='docker-compose.yml'>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !composefile-path -->
|
||||
</div>
|
||||
<!-- !stack-details -->
|
||||
</div>
|
||||
<div ng-if="$ctrl.model.Type === 1">
|
||||
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseContainer = !$ctrl.state.collapseContainer">
|
||||
Container
|
||||
<span class="small space-left">
|
||||
<a ng-if="$ctrl.state.collapseContainer"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
|
||||
<a ng-if="!$ctrl.state.collapseContainer"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- container-details -->
|
||||
<div uib-collapse="$ctrl.state.collapseContainer">
|
||||
<por-image-registry
|
||||
image="$ctrl.model.Image"
|
||||
registry="$ctrl.model.Registry"
|
||||
auto-complete="true"
|
||||
label-class="col-sm-2" input-class="col-sm-10 col-md-4"
|
||||
></por-image-registry>
|
||||
<!-- command -->
|
||||
<div class="form-group">
|
||||
<label for="template_command" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Command
|
||||
<portainer-tooltip position="bottom" message="The command to run in the container. If not specified, the container will use the default command specified in its Dockerfile."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_command" ng-model="$ctrl.model.Command" placeholder='/bin/bash -c \"echo hello\" && exit 777'>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command -->
|
||||
<!-- hostname -->
|
||||
<div class="form-group">
|
||||
<label for="template_hostname" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Hostname
|
||||
<portainer-tooltip position="bottom" message="Set the hostname of the container. Will use Docker default if not specified."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="template_hostname" ng-model="$ctrl.model.Hostname" placeholder='mycontainername'>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !hostname -->
|
||||
<!-- network -->
|
||||
<div class="form-group">
|
||||
<label for="template_network" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Network
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" ng-options="net.Name for net in $ctrl.networks" ng-model="$ctrl.model.Network">
|
||||
<option disabled hidden value="">Select a network</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network -->
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Port mapping</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px" ng-if="$ctrl.model.Ports.length > 0">
|
||||
<span class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in $ctrl.model.Ports" style="margin-top: 2px;">
|
||||
<!-- host-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
</div>
|
||||
<!-- !host-port -->
|
||||
<span style="margin: 0 10px 0 10px;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
<!-- container-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<!-- !container-port -->
|
||||
<!-- protocol-actions -->
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
|
||||
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removePortBinding($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !protocol-actions -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- volumes -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Volume mapping</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px" ng-if="$ctrl.model.Volumes.length > 0">
|
||||
<span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
|
||||
</div>
|
||||
<div ng-repeat="volume in $ctrl.model.Volumes">
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<!-- volume-line1 -->
|
||||
<div class="col-sm-12 form-inline">
|
||||
<!-- 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.container" placeholder="e.g. /path/in/container">
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''">Bind</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeVolume($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- !volume-type -->
|
||||
</div>
|
||||
<!-- !volume-line1 -->
|
||||
<!-- volume-line2 -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 5px;" ng-if="volume.type !== 'auto'">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
<!-- 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.bind" placeholder="e.g. /path/on/host">
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !read-only -->
|
||||
</div>
|
||||
<!-- !volume-line2 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !volumes -->
|
||||
<!-- labels -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
<!-- restart_policy -->
|
||||
<div class="form-group">
|
||||
<label for="template_restart_policy" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Restart policy
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select class="form-control" name="template_platform" ng-model="$ctrl.model.RestartPolicy">
|
||||
<option value="always">Always</option>
|
||||
<option value="unless-stopped">Unless stopped</option>
|
||||
<option value="on-failure">On failure</option>
|
||||
<option value="no">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restart_policy -->
|
||||
<!-- privileged-mode -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
Privileged mode
|
||||
<portainer-tooltip position="bottom" message="Start the container in privileged mode."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="$ctrl.model.Privileged"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !privileged-mode -->
|
||||
<!-- interactive-mode -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
Interactive mode
|
||||
<portainer-tooltip position="bottom" message="Start the container in foreground (equivalent of -i -t flags)."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="$ctrl.model.Interactive"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interactive-mode -->
|
||||
</div>
|
||||
<!-- !container-details -->
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseEnv = !$ctrl.state.collapseEnv">
|
||||
Environment
|
||||
<span class="small space-left">
|
||||
<a ng-if="$ctrl.state.collapseEnv"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
|
||||
<a ng-if="!$ctrl.state.collapseEnv"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-details -->
|
||||
<div uib-collapse="$ctrl.state.collapseEnv">
|
||||
<!-- env -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Environment variables</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVar()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- env-var-list -->
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="col-sm-12 template-envvar" ng-repeat="var in $ctrl.model.Env" style="margin-top: 10px;">
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="preset_var_{{$index}}" ng-model="var.type" ng-value="1" ng-change="$ctrl.changeEnvVarType(var)">
|
||||
<label for="preset_var_{{$index}}">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-user-slash" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Preset
|
||||
</div>
|
||||
<p>Preset variable</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="text_var_{{$index}}" ng-model="var.type" ng-value="2" ng-change="$ctrl.changeEnvVarType(var)">
|
||||
<label for="text_var_{{$index}}">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Text
|
||||
</div>
|
||||
<p>Free text value</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="select_var_{{$index}}" ng-model="var.type" ng-value="3">
|
||||
<label for="select_var_{{$index}}">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-list-ol" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Select
|
||||
</div>
|
||||
<p>Choose value from list</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" ng-model="var.name" placeholder="env_var">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-sm btn-danger space-left" type="button" ng-click="$ctrl.removeEnvVar($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="var.type == 2 || var.type == 3">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">
|
||||
Label
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="var.label" placeholder="Choose a label">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left" style="margin-top: 2px;">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10" style="margin-top: 2px;">
|
||||
<input type="text" class="form-control" ng-model="var.description" placeholder="Tooltip">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="var.type === 1 || var.type === 2">
|
||||
<label class="col-sm-2 control-label text-left">
|
||||
Default value
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="var.default" placeholder="default_value">
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="var.type === 3" style="margin-bottom: 5px;" class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Values</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVarValue(var)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add allowed value
|
||||
</span>
|
||||
</div>
|
||||
<!-- envvar-values-list -->
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="val in var.select" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="val.text" placeholder="Yes, I agree">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="val.value" placeholder="Y">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeEnvVarValue(var, $index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
<input style="margin-left: 5px;" type="checkbox" ng-model="val.default" id="val_default_{{$index}}"><label for="val_default_{{$index}}" class="space-left">Default</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- envvar-values-list -->
|
||||
</div>
|
||||
<div class="col-sm-12" ng-show="$ctrl.model.Env.length > 1">
|
||||
<div class="line-separator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !env-var-list -->
|
||||
</div>
|
||||
<!-- !env -->
|
||||
</div>
|
||||
<!-- !environment-details -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !templateForm.$valid" button-spinner="$ctrl.actionInProgress">
|
||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
|
@ -0,0 +1,10 @@
|
|||
angular.module('portainer.app').component('templateItem', {
|
||||
templateUrl: 'app/portainer/components/template-list/template-item/templateItem.html',
|
||||
bindings: {
|
||||
model: '=',
|
||||
onSelect: '<',
|
||||
onDelete: '<',
|
||||
showUpdateAction: '<',
|
||||
showDeleteAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
<!-- template -->
|
||||
<div ng-class="{ 'template-container--selected': $ctrl.model.Selected }" class="template-container" ng-click="$ctrl.onSelect($ctrl.model)">
|
||||
<div class="template-main">
|
||||
<!-- template-image -->
|
||||
<span ng-if="$ctrl.model.Logo">
|
||||
<img class="template-logo" ng-src="{{ $ctrl.model.Logo }}" />
|
||||
</span>
|
||||
<span class="template-logo" ng-if="!$ctrl.model.Logo">
|
||||
<i class="fa fa-rocket fa-4x blue-icon" aria-hidden="true"></i>
|
||||
</span>
|
||||
<!-- !template-image -->
|
||||
<!-- template-details -->
|
||||
<span class="col-sm-12">
|
||||
<!-- template-line1 -->
|
||||
<div class="template-line">
|
||||
<span>
|
||||
<span class="template-title">
|
||||
{{ $ctrl.model.Title }}
|
||||
</span>
|
||||
<span class="space-left template-type">
|
||||
<span>
|
||||
<i class="fab fa-linux" aria-hidden="true" ng-if="$ctrl.model.Platform === 'linux' || !$ctrl.model.Platform"></i>
|
||||
<span ng-if="!$ctrl.model.Platform"> & </span>
|
||||
<i class="fab fa-windows" aria-hidden="true" ng-if="$ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"></i>
|
||||
</span>
|
||||
<span>
|
||||
{{ $ctrl.model.Type === 1 ? 'container' : 'stack' }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-small">
|
||||
<a ui-sref="portainer.templates.template({ id: $ctrl.model.Id })" class="btn btn-xs btn-primary" ng-click="$event.stopPropagation();" ng-if="$ctrl.showUpdateAction">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i>
|
||||
Update
|
||||
</a>
|
||||
<btn class="btn btn-xs btn-danger" ng-click="$event.stopPropagation(); $ctrl.onDelete($ctrl.model)" ng-if="$ctrl.showDeleteAction">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Delete
|
||||
</btn>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !template-line1 -->
|
||||
<!-- template-line2 -->
|
||||
<div class="template-line">
|
||||
<span class="template-description">
|
||||
{{ $ctrl.model.Description }}
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="$ctrl.model.Categories.length > 0">
|
||||
{{ $ctrl.model.Categories.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- !template-line2 -->
|
||||
</span>
|
||||
<!-- !template-details -->
|
||||
</div>
|
||||
<!-- !template -->
|
||||
</div>
|
59
app/portainer/components/template-list/template-list.js
Normal file
59
app/portainer/components/template-list/template-list.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
angular.module('portainer.app').component('templateList', {
|
||||
templateUrl: 'app/portainer/components/template-list/templateList.html',
|
||||
controller: function() {
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
textFilter: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
showContainerTemplates: true
|
||||
};
|
||||
|
||||
this.updateCategories = function() {
|
||||
var availableCategories = [];
|
||||
|
||||
for (var i = 0; i < ctrl.templates.length; i++) {
|
||||
var template = ctrl.templates[i];
|
||||
if ((template.Type === 1 && ctrl.state.showContainerTemplates) || (template.Type === 2 && ctrl.showSwarmStacks) || (template.Type === 3 && !ctrl.showSwarmStacks)) {
|
||||
availableCategories = availableCategories.concat(template.Categories);
|
||||
}
|
||||
}
|
||||
|
||||
this.state.categories = _.sortBy(_.uniq(availableCategories));
|
||||
};
|
||||
|
||||
this.filterByCategory = function(item) {
|
||||
if (!ctrl.state.selectedCategory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.includes(item.Categories, ctrl.state.selectedCategory);
|
||||
};
|
||||
|
||||
this.filterByType = function(item) {
|
||||
if ((item.Type === 1 && ctrl.state.showContainerTemplates) || (item.Type === 2 && ctrl.showSwarmStacks) || (item.Type === 3 && !ctrl.showSwarmStacks)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
if (this.showSwarmStacks) {
|
||||
this.state.showContainerTemplates = false;
|
||||
}
|
||||
this.updateCategories();
|
||||
};
|
||||
},
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
templates: '<',
|
||||
selectAction: '<',
|
||||
deleteAction: '<',
|
||||
showSwarmStacks: '<',
|
||||
showAddAction: '<',
|
||||
showUpdateAction: '<',
|
||||
showDeleteAction: '<'
|
||||
}
|
||||
});
|
62
app/portainer/components/template-list/templateList.html
Normal file
62
app/portainer/components/template-list/templateList.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actionBar">
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.templates.new" ng-if="$ctrl.showAddAction">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add template
|
||||
</button>
|
||||
<span ng-class="{ 'pull-right': $ctrl.showAddAction }" style="width: 25%;">
|
||||
<ui-select ng-model="$ctrl.state.selectedCategory">
|
||||
<ui-select-match placeholder="Select a category" allow-clear="true">
|
||||
<span>{{ $select.selected }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="category in ($ctrl.state.categories | filter: $select.search)">
|
||||
<span>{{ category }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" style="margin: 15px 0 0 5px;">
|
||||
<label for="show_stacks" class="control-label text-left">
|
||||
Show container templates
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="show_stacks" ng-model="$ctrl.state.showContainerTemplates" ng-change="$ctrl.updateCategories()"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
|
||||
<div class="template-list">
|
||||
<template-item
|
||||
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
||||
model="template"
|
||||
show-update-action="$ctrl.showUpdateAction"
|
||||
show-delete-action="$ctrl.showDeleteAction"
|
||||
on-select="$ctrl.selectAction"
|
||||
on-delete="$ctrl.deleteAction"
|
||||
></template-item>
|
||||
<div ng-if="!$ctrl.templates" class="text-center text-muted">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-if="($ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter).length == 0" class="text-center text-muted">
|
||||
No templates available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -7,7 +7,7 @@ angular.module('portainer.app')
|
|||
icon: '='
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}"></img> <span class="text-muted"> {{titleText}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
|
||||
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}" ng-if="icon"></img><i class="fa fa-rocket" aria-hidden="true" ng-if="!icon"></i> <span class="text-muted"> {{titleText}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
|
|
|
@ -62,14 +62,6 @@ angular.module('portainer.app')
|
|||
templateEnvironment.forEach(function(envvar) {
|
||||
if (envvar.value || envvar.set) {
|
||||
var value = envvar.set ? envvar.set : envvar.value;
|
||||
if (envvar.type && envvar.type === 'container') {
|
||||
if (containerMapping === 'BY_CONTAINER_IP') {
|
||||
var container = envvar.value;
|
||||
value = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
|
||||
} else if (containerMapping === 'BY_CONTAINER_NAME') {
|
||||
value = $filter('containername')(envvar.value);
|
||||
}
|
||||
}
|
||||
env.push(envvar.name + '=' + value);
|
||||
}
|
||||
});
|
||||
|
@ -90,14 +82,14 @@ angular.module('portainer.app')
|
|||
|
||||
helper.createVolumeBindings = function(volumes, generatedVolumesPile) {
|
||||
volumes.forEach(function (volume) {
|
||||
if (volume.containerPath) {
|
||||
if (volume.container) {
|
||||
var binding;
|
||||
if (volume.type === 'auto') {
|
||||
binding = generatedVolumesPile.pop().Id + ':' + volume.containerPath;
|
||||
} else if (volume.type !== 'auto' && volume.name) {
|
||||
binding = volume.name + ':' + volume.containerPath;
|
||||
binding = generatedVolumesPile.pop().Id + ':' + volume.container;
|
||||
} else if (volume.type !== 'auto' && volume.bind) {
|
||||
binding = volume.bind + ':' + volume.container;
|
||||
}
|
||||
if (volume.readOnly) {
|
||||
if (volume.readonly) {
|
||||
binding += ':ro';
|
||||
}
|
||||
volume.binding = binding;
|
||||
|
@ -115,21 +107,13 @@ angular.module('portainer.app')
|
|||
return count;
|
||||
};
|
||||
|
||||
helper.filterLinuxServerIOTemplates = function(templates) {
|
||||
return templates.filter(function f(template) {
|
||||
var valid = false;
|
||||
if (template.Categories) {
|
||||
angular.forEach(template.Categories, function(category) {
|
||||
if (_.startsWith(category, 'Network')) {
|
||||
valid = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return valid;
|
||||
}).map(function(template, idx) {
|
||||
template.index = idx;
|
||||
return template;
|
||||
});
|
||||
helper.getUniqueCategories = function(templates) {
|
||||
var categories = [];
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
var template = templates[i];
|
||||
categories = categories.concat(template.Categories);
|
||||
}
|
||||
return _.uniq(categories);
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
function SettingsViewModel(data) {
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.LogoURL = data.LogoURL;
|
||||
this.BlackListedLabels = data.BlackListedLabels;
|
||||
this.DisplayExternalContributors = data.DisplayExternalContributors;
|
||||
this.AuthenticationMethod = data.AuthenticationMethod;
|
||||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
||||
|
|
145
app/portainer/models/template.js
Normal file
145
app/portainer/models/template.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
function TemplateDefaultModel() {
|
||||
this.Type = 1;
|
||||
this.AdministratorOnly = false;
|
||||
this.Title = '';
|
||||
this.Image = '';
|
||||
this.Description = '';
|
||||
this.Volumes = [];
|
||||
this.Ports = [];
|
||||
this.Env = [];
|
||||
this.Labels = [];
|
||||
this.RestartPolicy = 'always';
|
||||
this.Registry = {};
|
||||
}
|
||||
|
||||
function TemplateCreateRequest(model) {
|
||||
this.Type = model.Type;
|
||||
this.Name = model.Name;
|
||||
this.Hostname = model.Hostname;
|
||||
this.Title = model.Title;
|
||||
this.Description = model.Description;
|
||||
this.Note = model.Note;
|
||||
this.Categories = model.Categories;
|
||||
this.Platform = model.Platform;
|
||||
this.Logo = model.Logo;
|
||||
this.Image = model.Image;
|
||||
this.Registry = model.Registry.URL;
|
||||
this.Command = model.Command;
|
||||
this.Network = model.Network;
|
||||
this.Privileged = model.Privileged;
|
||||
this.Interactive = model.Interactive;
|
||||
this.RestartPolicy = model.RestartPolicy;
|
||||
this.Labels = model.Labels;
|
||||
this.Repository = model.Repository;
|
||||
this.Env = model.Env;
|
||||
this.AdministratorOnly = model.AdministratorOnly;
|
||||
|
||||
this.Ports = [];
|
||||
for (var i = 0; i < model.Ports.length; i++) {
|
||||
var binding = model.Ports[i];
|
||||
if (binding.containerPort && binding.protocol) {
|
||||
var port = binding.hostPort ? binding.hostPort + ':' + binding.containerPort + '/' + binding.protocol: binding.containerPort + '/' + binding.protocol;
|
||||
this.Ports.push(port);
|
||||
}
|
||||
}
|
||||
|
||||
this.Volumes = model.Volumes;
|
||||
}
|
||||
|
||||
function TemplateUpdateRequest(model) {
|
||||
TemplateCreateRequest.call(this, model);
|
||||
this.id = model.Id;
|
||||
}
|
||||
|
||||
function TemplateViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Title = data.title;
|
||||
this.Type = data.type;
|
||||
this.Description = data.description;
|
||||
this.AdministratorOnly = data.AdministratorOnly;
|
||||
this.Name = data.name;
|
||||
this.Note = data.note;
|
||||
this.Categories = data.categories ? data.categories : [];
|
||||
this.Platform = data.platform ? data.platform : '';
|
||||
this.Logo = data.logo;
|
||||
this.Repository = data.repository;
|
||||
this.Hostname = data.hostname;
|
||||
this.Registry = data.registry ? { URL: data.registry } : {};
|
||||
this.Image = data.image;
|
||||
this.Registry = data.registry ? data.registry : '';
|
||||
this.Command = data.command ? data.command : '';
|
||||
this.Network = data.network ? data.network : '';
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Labels = data.labels ? data.labels : [];
|
||||
this.Hosts = data.hosts ? data.hosts : [];
|
||||
this.Env = templateEnv(data);
|
||||
this.Volumes = templateVolumes(data);
|
||||
this.Ports = templatePorts(data);
|
||||
}
|
||||
|
||||
function templatePorts(data) {
|
||||
var ports = [];
|
||||
|
||||
if (data.ports) {
|
||||
ports = data.ports.map(function (p) {
|
||||
var portAndProtocol = _.split(p, '/');
|
||||
var hostAndContainerPort = _.split(portAndProtocol[0], ':');
|
||||
|
||||
return {
|
||||
hostPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[0] : undefined,
|
||||
containerPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[1] : hostAndContainerPort[0],
|
||||
protocol: portAndProtocol[1]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function templateVolumes(data) {
|
||||
var volumes = [];
|
||||
|
||||
if (data.volumes) {
|
||||
volumes = data.volumes.map(function (v) {
|
||||
return {
|
||||
container: v.container,
|
||||
readonly: v.readonly || false,
|
||||
type: v.bind ? 'bind' : 'auto',
|
||||
bind : v.bind ? v.bind : null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
function templateEnv(data) {
|
||||
var env = [];
|
||||
|
||||
if (data.env) {
|
||||
env = data.env.map(function(envvar) {
|
||||
envvar.type = 2;
|
||||
envvar.value = envvar.default ? envvar.default : '';
|
||||
|
||||
if (envvar.preset) {
|
||||
envvar.type = 1;
|
||||
}
|
||||
|
||||
if (envvar.select) {
|
||||
envvar.type = 3;
|
||||
for (var i = 0; i < envvar.select.length; i++) {
|
||||
var allowedValue = envvar.select[i];
|
||||
if (allowedValue.default) {
|
||||
envvar.value = allowedValue.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return envvar;
|
||||
});
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('Template', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplateFactory($resource, API_ENDPOINT_TEMPLATES) {
|
||||
return $resource(API_ENDPOINT_TEMPLATES, {}, {
|
||||
get: {method: 'GET', isArray: true}
|
||||
.factory('Templates', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplatesFactory($resource, API_ENDPOINT_TEMPLATES) {
|
||||
return $resource(API_ENDPOINT_TEMPLATES + '/:id', {}, {
|
||||
create: { method: 'POST' },
|
||||
query: { method: 'GET', isArray: true },
|
||||
get: { method: 'GET', params: { id: '@id'} },
|
||||
update: { method: 'PUT', params: { id: '@id'} },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -1,43 +1,64 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('TemplateService', ['$q', 'Template', 'TemplateHelper', 'ImageHelper', 'ContainerHelper', function TemplateServiceFactory($q, Template, TemplateHelper, ImageHelper, ContainerHelper) {
|
||||
.factory('TemplateService', ['$q', 'Templates', 'TemplateHelper', 'ImageHelper', 'ContainerHelper',
|
||||
function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, ContainerHelper) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.getTemplates = function(key) {
|
||||
service.templates = function() {
|
||||
var deferred = $q.defer();
|
||||
Template.get({key: key}).$promise
|
||||
|
||||
Templates.query().$promise
|
||||
.then(function success(data) {
|
||||
var templates = data.map(function (tpl, idx) {
|
||||
var template;
|
||||
if (tpl.type === 'stack') {
|
||||
template = new StackTemplateViewModel(tpl);
|
||||
} else if (tpl.type === 'container' && key === 'linuxserver.io') {
|
||||
template = new TemplateLSIOViewModel(tpl);
|
||||
} else {
|
||||
template = new TemplateViewModel(tpl);
|
||||
}
|
||||
template.index = idx;
|
||||
return template;
|
||||
var templates = data.map(function (item) {
|
||||
return new TemplateViewModel(item);
|
||||
});
|
||||
if (key === 'linuxserver.io') {
|
||||
templates = TemplateHelper.filterLinuxServerIOTemplates(templates);
|
||||
}
|
||||
deferred.resolve(templates);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve templates', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
||||
service.template = function(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Templates.get({ id: id }).$promise
|
||||
.then(function success(data) {
|
||||
var template = new TemplateViewModel(data);
|
||||
deferred.resolve(template);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve template details', err: err });
|
||||
});
|
||||
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.delete = function(id) {
|
||||
return Templates.remove({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.create = function(model) {
|
||||
var payload = new TemplateCreateRequest(model);
|
||||
return Templates.create(payload).$promise;
|
||||
};
|
||||
|
||||
service.update = function(model) {
|
||||
var payload = new TemplateUpdateRequest(model);
|
||||
return Templates.update(payload).$promise;
|
||||
};
|
||||
|
||||
service.createTemplateConfiguration = function(template, containerName, network) {
|
||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network);
|
||||
containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag;
|
||||
return containerConfiguration;
|
||||
};
|
||||
|
||||
service.createContainerConfiguration = function(template, containerName, network, containerMapping) {
|
||||
service.createContainerConfiguration = function(template, containerName, network) {
|
||||
var configuration = TemplateHelper.getDefaultContainerConfiguration();
|
||||
configuration.HostConfig.NetworkMode = network.Name;
|
||||
configuration.HostConfig.Privileged = template.Privileged;
|
||||
|
@ -46,7 +67,7 @@ angular.module('portainer.app')
|
|||
configuration.name = containerName;
|
||||
configuration.Hostname = template.Hostname;
|
||||
configuration.Image = template.Image;
|
||||
configuration.Env = TemplateHelper.EnvToStringArray(template.Env, containerMapping);
|
||||
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
|
||||
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
|
||||
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
|
||||
configuration.HostConfig.PortBindings = portConfiguration.bindings;
|
||||
|
@ -63,7 +84,7 @@ angular.module('portainer.app')
|
|||
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
|
||||
volumes.forEach(function (volume) {
|
||||
if (volume.binding) {
|
||||
configuration.Volumes[volume.containerPath] = {};
|
||||
configuration.Volumes[volume.container] = {};
|
||||
configuration.HostConfig.Binds.push(volume.binding);
|
||||
}
|
||||
});
|
|
@ -25,18 +25,12 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateExternalContributions = function(displayExternalContributors) {
|
||||
state.application.displayExternalContributors = displayExternalContributors;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
function assignStateFromStatusAndSettings(status, settings) {
|
||||
state.application.authentication = status.Authentication;
|
||||
state.application.analytics = status.Analytics;
|
||||
state.application.endpointManagement = status.EndpointManagement;
|
||||
state.application.version = status.Version;
|
||||
state.application.logo = settings.LogoURL;
|
||||
state.application.displayExternalContributors = settings.DisplayExternalContributors;
|
||||
state.application.validity = moment().unix();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Endpoint group details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.groups">Groups</a> > <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a>
|
||||
<a ui-sref="portainer.groups">Groups</a> > {{ ::group.Name }}
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -63,47 +63,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- security -->
|
||||
<!-- app-templates -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
App Templates
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_templates" class="control-label text-left">
|
||||
Use custom templates
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_templates" ng-model="formValues.customTemplates"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="formValues.customTemplates">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can specify the URL to your own template definitions file here. See <a href="https://portainer.readthedocs.io/en/stable/templates.html" target="_blank">Portainer documentation</a> for more details.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group" >
|
||||
<label for="templates_url" class="col-sm-1 control-label text-left">
|
||||
URL
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_external_contrib" class="control-label text-left">
|
||||
Hide external contributions
|
||||
<portainer-tooltip position="bottom" message="When enabled, external contributions such as LinuxServer.io will not be displayed in the sidebar."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_external_contrib" ng-model="formValues.externalContributions"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !app-templates -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -8,8 +8,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
|||
|
||||
$scope.formValues = {
|
||||
customLogo: false,
|
||||
customTemplates: false,
|
||||
externalContributions: false,
|
||||
restrictBindMounts: false,
|
||||
restrictPrivilegedMode: false,
|
||||
labelName: '',
|
||||
|
@ -41,11 +39,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
|||
settings.LogoURL = '';
|
||||
}
|
||||
|
||||
if (!$scope.formValues.customTemplates) {
|
||||
settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
|
||||
}
|
||||
|
||||
settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
|
||||
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
|
||||
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
|
||||
|
||||
|
@ -58,7 +51,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
|||
.then(function success(data) {
|
||||
Notifications.success('Settings updated');
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateExternalContributions(settings.DisplayExternalContributors);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -77,10 +69,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
|||
if (settings.LogoURL !== '') {
|
||||
$scope.formValues.customLogo = true;
|
||||
}
|
||||
if (settings.TemplatesURL !== DEFAULT_TEMPLATES_URL) {
|
||||
$scope.formValues.customTemplates = true;
|
||||
}
|
||||
$scope.formValues.externalContributions = !settings.DisplayExternalContributors;
|
||||
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
|
||||
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
|
||||
})
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('CreateTemplateController', ['$q', '$scope', '$state', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications',
|
||||
function ($q, $scope, $state, TemplateService, TemplateHelper, NetworkService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.create = function() {
|
||||
var model = $scope.model;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
TemplateService.create(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully created', model.Title);
|
||||
$state.go('portainer.templates');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create template');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$scope.model = new TemplateDefaultModel();
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
networks: NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
)
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
|
||||
$scope.networks = data.networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve template details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
24
app/portainer/views/templates/create/createtemplate.html
Normal file
24
app/portainer/views/templates/create/createtemplate.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Create template"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.templates">Templates</a> > Add template
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<template-form
|
||||
model="model"
|
||||
categories="categories"
|
||||
networks="networks"
|
||||
form-action="create"
|
||||
show-type-selector="true"
|
||||
form-action-label="Create the template"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></template-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
28
app/portainer/views/templates/edit/template.html
Normal file
28
app/portainer/views/templates/edit/template.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Template details">
|
||||
<a data-toggle="tooltip" title-text="Refresh" ui-sref="portainer.templates.template({id: template.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.templates">Templates</a> > {{ ::template.Title }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<template-form
|
||||
model="template"
|
||||
categories="categories"
|
||||
networks="networks"
|
||||
form-action="update"
|
||||
show-type-selector="false"
|
||||
form-action-label="Update the template"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></template-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
51
app/portainer/views/templates/edit/templateController.js
Normal file
51
app/portainer/views/templates/edit/templateController.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('TemplateController', ['$q', '$scope', '$state', '$transition$', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, TemplateService, TemplateHelper, NetworkService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
var model = $scope.template;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
TemplateService.update(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully updated', model.Title);
|
||||
$state.go('portainer.templates');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update template');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
var templateId = $transition$.params().id;
|
||||
$q.all({
|
||||
templates: TemplateService.templates(),
|
||||
template: TemplateService.template(templateId),
|
||||
networks: NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
)
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
|
||||
$scope.template = data.template;
|
||||
$scope.networks = data.networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve template details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -1,6 +1,6 @@
|
|||
<rd-header id="view-top">
|
||||
<rd-header-title title-text="Application templates list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker.templates" ui-sref-opts="{reload: true}">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.templates" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
|
@ -9,13 +9,9 @@
|
|||
|
||||
<div class="row">
|
||||
<!-- stack-form -->
|
||||
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'stack'">
|
||||
<div class="col-sm-12" ng-if="state.selectedTemplate && (state.selectedTemplate.Type === 2 || state.selectedTemplate.Type === 3)">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Title">
|
||||
<div class="pull-right">
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="unselectTemplate()">Hide</button>
|
||||
</div>
|
||||
</rd-widget-custom-header>
|
||||
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Title"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
|
||||
<form class="form-horizontal">
|
||||
|
@ -43,10 +39,12 @@
|
|||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- env -->
|
||||
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="var.label && !var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}<portainer-tooltip ng-if="var.description" position="bottom" message="{{ var.description }}"></portainer-tooltip></label>
|
||||
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.preset || var.select" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">
|
||||
{{ var.label }}
|
||||
<portainer-tooltip ng-if="var.description" position="bottom" message="{{ var.description }}"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<!-- <input ng-if="!var.values && (!var.type || !var.type === 'container')" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> -->
|
||||
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<option selected disabled hidden value="">Select value</option>
|
||||
|
@ -68,6 +66,7 @@
|
|||
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="unselectTemplate(state.selectedTemplate)">Hide</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,13 +78,9 @@
|
|||
</div>
|
||||
<!-- !stack-form -->
|
||||
<!-- container-form -->
|
||||
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'container'">
|
||||
<div class="col-sm-12" ng-if="state.selectedTemplate && state.selectedTemplate.Type === 1">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Image">
|
||||
<div class="pull-right">
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="unselectTemplate()">Hide</button>
|
||||
</div>
|
||||
</rd-widget-custom-header>
|
||||
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Image"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
|
||||
<form class="form-horizontal">
|
||||
|
@ -123,13 +118,17 @@
|
|||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- env -->
|
||||
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.preset || var.select" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">
|
||||
{{ var.label }}
|
||||
<portainer-tooltip ng-if="var.description" position="bottom" message="{{ var.description }}"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select ng-if="var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
|
||||
<option selected disabled hidden value="">Select value</option>
|
||||
<option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
|
||||
</select>
|
||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !env -->
|
||||
|
@ -212,15 +211,15 @@
|
|||
<!-- 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.containerPath" placeholder="e.g. /path/in/container">
|
||||
<input type="text" class="form-control" ng-model="volume.container" placeholder="e.g. /path/in/container">
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.name = ''">Auto</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''" ng-if="isAdmin || allowBindMounts">Bind</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.bind = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''" ng-if="isAdmin || allowBindMounts">Bind</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
|
@ -235,7 +234,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.name">
|
||||
<select class="form-control" ng-model="volume.bind">
|
||||
<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>
|
||||
|
@ -244,14 +243,14 @@
|
|||
<!-- 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.name" placeholder="e.g. /path/on/host">
|
||||
<input type="text" class="form-control" ng-model="volume.bind" placeholder="e.g. /path/on/host">
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label>
|
||||
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !read-only -->
|
||||
|
@ -285,7 +284,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !extra-host -->
|
||||
<!-- Label -->
|
||||
<!-- labels -->
|
||||
<div class="form-group" >
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
|
@ -313,7 +312,7 @@
|
|||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !Label -->
|
||||
<!-- !labels -->
|
||||
<!-- hostname -->
|
||||
<div class="form-group">
|
||||
<label for="container_hostname" class="col-sm-2 control-label text-left">Hostname</label>
|
||||
|
@ -334,6 +333,7 @@
|
|||
<span ng-hide="state.actionInProgress">Deploy the container</span>
|
||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="unselectTemplate(state.selectedTemplate)">Hide</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -346,124 +346,17 @@
|
|||
<!-- container-form -->
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" style="height: 100%">
|
||||
<rd-template-widget>
|
||||
<rd-widget-header icon="fa-rocket" title-text="Templates">
|
||||
<div ng-if="availableCategories.length > 0" class="pull-right">
|
||||
Category
|
||||
<select ng-model="state.filters.Categories">
|
||||
<option value="!">All</option>
|
||||
<option ng-repeat="category in availableCategories" value="{{ category }}">{{ category }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar>
|
||||
<div>
|
||||
<!-- Platform -->
|
||||
<span class="btn-group btn-group-sm" style="margin-right: 15px;">
|
||||
<label class="btn btn-primary" ng-model="state.filters.Platform" uib-btn-radio="'!'">
|
||||
All
|
||||
</label>
|
||||
<label class="btn btn-primary" ng-model="state.filters.Platform" uib-btn-radio="'windows'">
|
||||
<i class="fab fa-windows" aria-hidden="true"></i>
|
||||
Windows
|
||||
</label>
|
||||
<label class="btn btn-primary" ng-model="state.filters.Platform" uib-btn-radio="'linux'">
|
||||
<i class="fab fa-linux" aria-hidden="true"></i>
|
||||
Linux
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="padding template-widget-body">
|
||||
<form class="form-horizontal">
|
||||
<div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment method
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div ng-click="updateCategories(templates, state.filters.Type)">
|
||||
<input type="radio" id="template_stack" ng-model="state.filters.Type" value="stack">
|
||||
<label for="template_stack">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Stack
|
||||
</div>
|
||||
<p>Multi-containers deployment</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-click="updateCategories(templates, state.filters.Type)">
|
||||
<input type="radio" id="template_container" ng-model="state.filters.Type" value="container">
|
||||
<label for="template_container">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Container
|
||||
</div>
|
||||
<p>Single container deployment</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Templates
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="template-list">
|
||||
<!-- template -->
|
||||
<div ng-repeat="tpl in templates | filter:state.filters:true" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
|
||||
<div class="template-main">
|
||||
<!-- template-image -->
|
||||
<span class="">
|
||||
<img class="template-logo" ng-src="{{ tpl.Logo }}" />
|
||||
</span>
|
||||
<!-- !template-image -->
|
||||
<!-- template-details -->
|
||||
<span class="col-sm-12">
|
||||
<!-- template-line1 -->
|
||||
<div class="template-line">
|
||||
<span class="template-title">
|
||||
{{ tpl.Title }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fab fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
|
||||
<i class="fab fa-linux" aria-hidden="true" ng-if="tpl.Platform === 'linux'"></i>
|
||||
<!-- Arch / Platform -->
|
||||
</span>
|
||||
</div>
|
||||
<!-- !template-line1 -->
|
||||
<!-- template-line2 -->
|
||||
<div class="template-line">
|
||||
<span class="template-description">
|
||||
{{ tpl.Description }}
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="tpl.Categories.length > 0">
|
||||
{{ tpl.Categories.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- !template-line2 -->
|
||||
</span>
|
||||
<!-- !template-details -->
|
||||
</div>
|
||||
<!-- !template -->
|
||||
</div>
|
||||
<div ng-if="!templates" class="text-center text-muted">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-if="(templates | filter:state.filters:true).length == 0" class="text-center text-muted">
|
||||
No templates available.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-template-widget>
|
||||
<div class="col-sm-12">
|
||||
<template-list ng-if="templates"
|
||||
title-text="Templates" title-icon="fa-rocket"
|
||||
templates="templates"
|
||||
select-action="selectTemplate"
|
||||
delete-action="deleteTemplate"
|
||||
show-add-action="isAdmin"
|
||||
show-update-action="isAdmin"
|
||||
show-delete-action="isAdmin"
|
||||
show-swarm-stacks="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER' && applicationState.endpoint.apiVersion >= 1.25"
|
||||
></template-list>
|
||||
</div>
|
||||
</div>
|
|
@ -1,18 +1,11 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'PaginationService', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService', 'EndpointProvider',
|
||||
function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, PaginationService, ResourceControlService, Authentication, FormValidator, SettingsService, StackService, EndpointProvider) {
|
||||
angular.module('portainer.app')
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService', 'EndpointProvider', 'ModalService',
|
||||
function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, ResourceControlService, Authentication, FormValidator, SettingsService, StackService, EndpointProvider, ModalService) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
hideDescriptions: $transition$.params().hide_descriptions,
|
||||
formValidationError: '',
|
||||
showDeploymentSelector: false,
|
||||
actionInProgress: false,
|
||||
filters: {
|
||||
Categories: '!',
|
||||
Platform: '!',
|
||||
Type: 'container'
|
||||
}
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -22,7 +15,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
|||
};
|
||||
|
||||
$scope.addVolume = function () {
|
||||
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', name: '', readOnly: false, type: 'auto' });
|
||||
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', bind: '', readonly: false, type: 'auto' });
|
||||
};
|
||||
|
||||
$scope.removeVolume = function(index) {
|
||||
|
@ -98,6 +91,31 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
|||
});
|
||||
}
|
||||
|
||||
function createComposeStackFromTemplate(template, userId, accessControlData) {
|
||||
var stackName = $scope.formValues.name;
|
||||
|
||||
var repositoryOptions = {
|
||||
RepositoryURL: template.Repository.url,
|
||||
ComposeFilePathInRepository: template.Repository.stackfile
|
||||
};
|
||||
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
StackService.createComposeStackFromGitRepository(stackName, repositoryOptions, endpointId)
|
||||
.then(function success(data) {
|
||||
return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Stack successfully deployed');
|
||||
$state.go('portainer.stacks');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.warning('Deployment error', err.data.err);
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function createStackFromTemplate(template, userId, accessControlData) {
|
||||
var stackName = $scope.formValues.name;
|
||||
|
||||
|
@ -144,104 +162,88 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
|||
var templatesKey = $scope.templatesKey;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
if (template.Type === 'stack') {
|
||||
if (template.Type === 2) {
|
||||
createStackFromTemplate(template, userId, accessControlData);
|
||||
} if (template.Type === 3) {
|
||||
createComposeStackFromTemplate(template, userId, accessControlData);
|
||||
} else {
|
||||
createContainerFromTemplate(template, userId, accessControlData);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.unselectTemplate = function() {
|
||||
var currentTemplateIndex = $scope.state.selectedTemplate.index;
|
||||
$('#template_' + currentTemplateIndex).toggleClass('template-container--selected');
|
||||
$scope.unselectTemplate = function(template) {
|
||||
template.Selected = false;
|
||||
$scope.state.selectedTemplate = null;
|
||||
};
|
||||
|
||||
$scope.selectTemplate = function(index, pos) {
|
||||
if ($scope.state.selectedTemplate && $scope.state.selectedTemplate.index !== index) {
|
||||
$scope.unselectTemplate();
|
||||
$scope.selectTemplate = function(template) {
|
||||
if ($scope.state.selectedTemplate) {
|
||||
$scope.unselectTemplate($scope.state.selectedTemplate);
|
||||
}
|
||||
|
||||
var templates = $filter('filter')($scope.templates, $scope.state.filters, true);
|
||||
var template = templates[pos];
|
||||
if (template === $scope.state.selectedTemplate) {
|
||||
$scope.unselectTemplate();
|
||||
} else {
|
||||
selectTemplate(index, pos, templates);
|
||||
}
|
||||
};
|
||||
|
||||
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; });
|
||||
template.Selected = true;
|
||||
if (template.Network) {
|
||||
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === template.Network; });
|
||||
} else {
|
||||
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
|
||||
}
|
||||
|
||||
if (selectedTemplate.Name) {
|
||||
$scope.formValues.name = selectedTemplate.Name;
|
||||
} else {
|
||||
$scope.formValues.name = '';
|
||||
}
|
||||
|
||||
$scope.formValues.name = template.Name ? template.Name : '';
|
||||
$scope.state.selectedTemplate = template;
|
||||
$anchorScroll('view-top');
|
||||
}
|
||||
};
|
||||
|
||||
function createTemplateConfiguration(template) {
|
||||
var network = $scope.formValues.network;
|
||||
var name = $scope.formValues.name;
|
||||
var containerMapping = determineContainerMapping(network);
|
||||
return TemplateService.createTemplateConfiguration(template, name, network, containerMapping);
|
||||
return TemplateService.createTemplateConfiguration(template, name, network);
|
||||
}
|
||||
|
||||
function determineContainerMapping(network) {
|
||||
var containerMapping = 'BY_CONTAINER_IP';
|
||||
if (network.Name !== 'bridge') {
|
||||
containerMapping = 'BY_CONTAINER_NAME';
|
||||
}
|
||||
return containerMapping;
|
||||
}
|
||||
|
||||
$scope.updateCategories = function(templates, type) {
|
||||
$scope.state.filters.Categories = '!';
|
||||
updateCategories(templates, type);
|
||||
$scope.deleteTemplate = function(template) {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to delete this template?',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
deleteTemplate(template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function updateCategories(templates, type) {
|
||||
var availableCategories = [];
|
||||
angular.forEach(templates, function(template) {
|
||||
if (template.Type === type) {
|
||||
availableCategories = availableCategories.concat(template.Categories);
|
||||
}
|
||||
function deleteTemplate(template) {
|
||||
TemplateService.delete(template.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Template successfully deleted');
|
||||
var idx = $scope.templates.indexOf(template);
|
||||
$scope.templates.splice(idx, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove template');
|
||||
});
|
||||
$scope.availableCategories = _.sortBy(_.uniq(availableCategories));
|
||||
}
|
||||
|
||||
function initTemplates(templatesKey, type, provider, apiVersion) {
|
||||
function initView() {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
$q.all({
|
||||
templates: TemplateService.getTemplates(templatesKey),
|
||||
containers: ContainerService.containers(0),
|
||||
templates: TemplateService.templates(),
|
||||
volumes: VolumeService.getVolumes(),
|
||||
networks: NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
endpointMode.provider === 'DOCKER_STANDALONE' || endpointMode.provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
|
||||
),
|
||||
settings: SettingsService.publicSettings()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var templates = data.templates;
|
||||
updateCategories(templates, type);
|
||||
$scope.templates = templates;
|
||||
$scope.runningContainers = data.containers;
|
||||
$scope.availableVolumes = data.volumes.Volumes;
|
||||
var networks = data.networks;
|
||||
$scope.availableNetworks = networks;
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
var settings = data.settings;
|
||||
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
|
||||
})
|
||||
|
@ -251,24 +253,5 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
|
|||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var templatesKey = $transition$.params().key;
|
||||
$scope.templatesKey = templatesKey;
|
||||
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
$scope.isAdmin = userDetails.role === 1;
|
||||
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
||||
if (templatesKey !== 'linuxserver.io'
|
||||
&& endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) {
|
||||
$scope.state.filters.Type = 'stack';
|
||||
$scope.state.showDeploymentSelector = true;
|
||||
}
|
||||
|
||||
initTemplates(templatesKey, $scope.state.filters.Type, endpointMode.provider, apiVersion);
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
Loading…
Add table
Add a link
Reference in a new issue