1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

feat(templates): introduce templates management (#2017)

This commit is contained in:
Anthony Lapenna 2018-07-03 20:31:02 +02:00 committed by GitHub
parent e7939a5384
commit 61c285bd2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 3489 additions and 637 deletions

View file

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

View file

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

View file

@ -4,6 +4,11 @@ angular.module('portainer.docker').component('porImageRegistry', {
bindings: {
'image': '=',
'registry': '=',
'autoComplete': '<'
'autoComplete': '<',
'labelClass': '@',
'inputClass': '@'
},
require: {
form: '^form'
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,469 +0,0 @@
<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}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div class="row">
<!-- stack-form -->
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'stack'">
<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-body classes="padding">
<form class="form-horizontal">
<!-- description -->
<div ng-if="state.selectedTemplate.Note">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
</div>
</div>
</div>
<!-- !description -->
<div class="col-sm-12 form-section-title">
Configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. myStack" required>
</div>
</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 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>
<option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
</select>
</div>
</div>
<!-- !env -->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<!-- 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-disabled="state.actionInProgress || !formValues.name" ng-click="createTemplate()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the stack</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
<!-- !stack-form -->
<!-- container-form -->
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'container'">
<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-body classes="padding">
<form class="form-horizontal">
<!-- description -->
<div ng-if="state.selectedTemplate.Note">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
</div>
</div>
</div>
<!-- !description -->
<div class="col-sm-12 form-section-title">
Configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
</div>
</div>
<!-- !name-input -->
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-2 control-label text-left">Network</label>
<div class="col-sm-10">
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</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 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>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div>
</div>
<!-- !env -->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<div class="form-group">
<div class="col-sm-12">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
</a>
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
<div ng-if="state.showAdvancedOptions">
<!-- 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="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="state.selectedTemplate.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 -->
<!-- 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 state.selectedTemplate.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="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
<!-- volume-mapping -->
<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="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="state.selectedTemplate.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 state.selectedTemplate.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.containerPath" 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>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="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>
<!-- 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">
<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>
</div>
<!-- !volume -->
<!-- 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">
</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>
<!-- !volume-mapping -->
<!-- extra-host -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Hosts file entries</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraHost()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
</span>
</div>
<!-- extra-host-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="(idx, host) in state.selectedTemplate.Hosts track by $index" style="margin-top: 2px;">
<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="state.selectedTemplate.Hosts[idx]" placeholder="e.g. host:IP">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraHost($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<!-- !extra-host -->
<!-- Label -->
<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="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 state.selectedTemplate.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="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !Label -->
<!-- hostname -->
<div class="form-group">
<label for="container_hostname" class="col-sm-2 control-label text-left">Hostname</label>
<div class="col-sm-10">
<input type="text" name="container_hostname" class="form-control" ng-model="state.selectedTemplate.Hostname" placeholder="leave empty to use docker default">
</div>
</div>
<!-- !hostname -->
</div>
<!-- !advanced-options -->
<!-- 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-disabled="state.actionInProgress || !formValues.network" ng-click="createTemplate()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
<!-- 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>
</div>

View file

@ -1,274 +0,0 @@
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) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
hideDescriptions: $transition$.params().hide_descriptions,
formValidationError: '',
showDeploymentSelector: false,
actionInProgress: false,
filters: {
Categories: '!',
Platform: '!',
Type: 'container'
}
};
$scope.formValues = {
network: '',
name: '',
AccessControlData: new AccessControlFormData()
};
$scope.addVolume = function () {
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', name: '', readOnly: false, type: 'auto' });
};
$scope.removeVolume = function(index) {
$scope.state.selectedTemplate.Volumes.splice(index, 1);
};
$scope.addPortBinding = function() {
$scope.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.state.selectedTemplate.Ports.splice(index, 1);
};
$scope.addExtraHost = function() {
$scope.state.selectedTemplate.Hosts.push('');
};
$scope.removeExtraHost = function(index) {
$scope.state.selectedTemplate.Hosts.splice(index, 1);
};
$scope.addLabel = function () {
$scope.state.selectedTemplate.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.state.selectedTemplate.Labels.splice(index, 1);
};
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
function createContainerFromTemplate(template, userId, accessControlData) {
var templateConfiguration = createTemplateConfiguration(template);
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
var generatedVolumeIds = [];
VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount)
.then(function success(data) {
var volumeResourceControlQueries = [];
angular.forEach(data, function (volume) {
var volumeId = volume.Id;
generatedVolumeIds.push(volumeId);
});
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
return ImageService.pullImage(template.Image, { URL: template.Registry }, true);
})
.then(function success(data) {
return ContainerService.createAndStartContainer(templateConfiguration);
})
.then(function success(data) {
var containerIdentifier = data.Id;
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
})
.then(function success() {
Notifications.success('Container successfully created');
$state.go('docker.containers', {}, {reload: true});
})
.catch(function error(err) {
Notifications.error('Failure', err, err.msg);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function createStackFromTemplate(template, userId, accessControlData) {
var stackName = $scope.formValues.name;
for (var i = 0; i < template.Env.length; i++) {
var envvar = template.Env[i];
if (envvar.set) {
envvar.value = envvar.set;
}
}
var repositoryOptions = {
RepositoryURL: template.Repository.url,
ComposeFilePathInRepository: template.Repository.stackfile
};
var endpointId = EndpointProvider.endpointID();
StackService.createSwarmStackFromGitRepository(stackName, repositoryOptions, template.Env, 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.err.data.err);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
$scope.createTemplate = function() {
var userDetails = Authentication.getUserDetails();
var userId = userDetails.ID;
var accessControlData = $scope.formValues.AccessControlData;
var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) {
return;
}
var template = $scope.state.selectedTemplate;
var templatesKey = $scope.templatesKey;
$scope.state.actionInProgress = true;
if (template.Type === 'stack') {
createStackFromTemplate(template, userId, accessControlData);
} else {
createContainerFromTemplate(template, userId, accessControlData);
}
};
$scope.unselectTemplate = function() {
var currentTemplateIndex = $scope.state.selectedTemplate.index;
$('#template_' + currentTemplateIndex).toggleClass('template-container--selected');
$scope.state.selectedTemplate = null;
};
$scope.selectTemplate = function(index, pos) {
if ($scope.state.selectedTemplate && $scope.state.selectedTemplate.index !== index) {
$scope.unselectTemplate();
}
var templates = $filter('filter')($scope.templates, $scope.state.filters, true);
var template = templates[pos];
if (template === $scope.state.selectedTemplate) {
$scope.unselectTemplate();
} else {
selectTemplate(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; });
} 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 = '';
}
$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);
}
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);
};
function updateCategories(templates, type) {
var availableCategories = [];
angular.forEach(templates, function(template) {
if (template.Type === type) {
availableCategories = availableCategories.concat(template.Categories);
}
});
$scope.availableCategories = _.sortBy(_.uniq(availableCategories));
}
function initTemplates(templatesKey, type, provider, apiVersion) {
$q.all({
templates: TemplateService.getTemplates(templatesKey),
containers: ContainerService.containers(0),
volumes: VolumeService.getVolumes(),
networks: NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
false,
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;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occured during apps initialization.');
});
}
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();
}]);