mirror of https://github.com/portainer/portainer
feat(templates): new templates capabilities (#862)
parent
c3363604ac
commit
0579251c70
|
@ -19,7 +19,7 @@ function ($scope, $state, Settings, Config, EndpointService, StateManager, Endpo
|
||||||
$state.go('dashboard');
|
$state.go('dashboard');
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error("Failure", err, "Unable to connect to the Docker endpoint");
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
EndpointProvider.setEndpointID(activeEndpointID);
|
EndpointProvider.setEndpointID(activeEndpointID);
|
||||||
EndpointProvider.setEndpointPublicURL(activeEndpointPublicURL);
|
EndpointProvider.setEndpointPublicURL(activeEndpointPublicURL);
|
||||||
StateManager.updateEndpointState(true)
|
StateManager.updateEndpointState(true)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<rd-header>
|
<rd-header id="view-top">
|
||||||
<rd-header-title title="Application templates list">
|
<rd-header-title title="Application templates list">
|
||||||
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||||
|
@ -7,34 +7,52 @@
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>Templates</rd-header-content>
|
<rd-header-content>Templates</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
<div id="selectedTemplate" class="row" ng-if="state.selectedTemplate">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="row" style="height: 90%">
|
||||||
|
|
||||||
|
<div class="col-sm-12" ng-if="state.selectedTemplate">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Image">
|
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title="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>
|
||||||
<rd-widget-body classes="padding">
|
<rd-widget-body classes="padding">
|
||||||
|
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
<div class="form-group" ng-if="state.selectedTemplate.Note">
|
<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="col-sm-12">
|
||||||
<span class="small" style="margin-left: 5px;" ng-bind-html="state.selectedTemplate.Note"></span>
|
<span class="template-note" ng-bind-html="state.selectedTemplate.Note"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !description -->
|
<!-- !description -->
|
||||||
<!-- name-and-network-inputs -->
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-10">
|
||||||
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||||
</div>
|
</div>
|
||||||
<label for="container_network" class="col-sm-2 col-lg-1 control-label text-right">Network</label>
|
</div>
|
||||||
<div class="col-sm-4 col-lg-5">
|
<!-- !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">
|
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||||
<option disabled hidden value="">Select a network</option>
|
<option disabled hidden value="">Select a network</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-and-network-inputs -->
|
<!-- !network-input -->
|
||||||
<!-- env -->
|
<!-- env -->
|
||||||
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.set" class="form-group">
|
<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>
|
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||||
|
@ -63,6 +81,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !ownership -->
|
<!-- !ownership -->
|
||||||
|
<!-- advanced-options -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
|
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
|
||||||
|
@ -73,7 +92,6 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- advanced-options -->
|
|
||||||
<div ng-if="state.showAdvancedOptions">
|
<div ng-if="state.showAdvancedOptions">
|
||||||
<!-- port-mapping -->
|
<!-- port-mapping -->
|
||||||
<div class="form-group" >
|
<div class="form-group" >
|
||||||
|
@ -120,7 +138,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- !port-mapping-input-list -->
|
<!-- !port-mapping-input-list -->
|
||||||
<!-- volume-mapping -->
|
<!-- volume-mapping -->
|
||||||
|
@ -192,6 +209,7 @@
|
||||||
<!-- !volume-mapping -->
|
<!-- !volume-mapping -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !advanced-options -->
|
<!-- !advanced-options -->
|
||||||
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||||
|
@ -205,45 +223,90 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="col-sm-12" style="height: 100%">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<rd-template-widget>
|
||||||
<rd-widget>
|
<rd-widget-header icon="fa-rocket" title="Templates">
|
||||||
<rd-widget-header icon="fa-rocket" title="Available templates">
|
<div ng-if="availableCategories.length > 0" class="pull-right">
|
||||||
<div class="pull-right">
|
Category
|
||||||
Items per page:
|
<select ng-model="state.filters.Categories">
|
||||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
<option value="!">All</option>
|
||||||
<option value="0">All</option>
|
<option ng-repeat="category in availableCategories" value="{{ category }}">{{ category }}</option>
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-body classes="padding">
|
<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="fa fa-windows" aria-hidden="true"></i>
|
||||||
|
Windows
|
||||||
|
</label>
|
||||||
|
<label class="btn btn-primary" ng-model="state.filters.Platform" uib-btn-radio="'linux'">
|
||||||
|
<i class="fa fa-linux" aria-hidden="true"></i>
|
||||||
|
Linux
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</rd-widget-taskbar>
|
||||||
|
<rd-widget-body classes="padding template-widget-body">
|
||||||
<div class="template-list">
|
<div class="template-list">
|
||||||
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
<!-- template -->
|
||||||
<img class="logo" ng-src="{{ tpl.Logo }}" />
|
<div dir-paginate="tpl in templates | filter:state.filters:true | itemsPerPage: state.pagination_count" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
|
||||||
<div class="title">{{ tpl.Title }}</div>
|
<div class="template-main">
|
||||||
<div class="description" ng-if="tpl.Description && !state.hideDescriptions">{{ tpl.Description }}</div>
|
<!-- 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="fa fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
|
||||||
|
<i class="fa 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>
|
||||||
<div ng-if="!templates" class="text-center text-muted">
|
<div ng-if="!templates" class="text-center text-muted">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="templates.length == 0" class="text-center text-muted">
|
<div ng-if="(templates | filter:state.filters:true | itemsPerPage: state.pagination_count).length == 0" class="text-center text-muted">
|
||||||
No templates available.
|
No templates available.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="templates">
|
|
||||||
<dir-pagination-controls></dir-pagination-controls>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-template-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
angular.module('templates', [])
|
angular.module('templates', [])
|
||||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication',
|
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication',
|
||||||
function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication) {
|
function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
showAdvancedOptions: false,
|
showAdvancedOptions: false,
|
||||||
hideDescriptions: $stateParams.hide_descriptions,
|
hideDescriptions: $stateParams.hide_descriptions,
|
||||||
pagination_count: Pagination.getPaginationCount('templates')
|
pagination_count: Pagination.getPaginationCount('templates'),
|
||||||
|
filters: {
|
||||||
|
Categories: '!',
|
||||||
|
Platform: '!'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
|
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
|
||||||
network: "",
|
network: '',
|
||||||
name: "",
|
name: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.changePaginationCount = function() {
|
$scope.changePaginationCount = function() {
|
||||||
|
@ -74,32 +78,38 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var selectedItem = -1;
|
$scope.unselectTemplate = function() {
|
||||||
$scope.selectTemplate = function(idx) {
|
var currentTemplateIndex = $scope.state.selectedTemplate.index;
|
||||||
$('#template_' + idx).toggleClass("container-template--selected");
|
$('#template_' + currentTemplateIndex).toggleClass('template-container--selected');
|
||||||
if (selectedItem === idx) {
|
$scope.state.selectedTemplate = null;
|
||||||
unselectTemplate();
|
};
|
||||||
|
|
||||||
|
$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 {
|
} else {
|
||||||
selectTemplate(idx);
|
selectTemplate(index, pos, templates);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function unselectTemplate() {
|
function selectTemplate(index, pos, filteredTemplates) {
|
||||||
selectedItem = -1;
|
$('#template_' + index).toggleClass('template-container--selected');
|
||||||
$scope.state.selectedTemplate = null;
|
var selectedTemplate = filteredTemplates[pos];
|
||||||
}
|
|
||||||
|
|
||||||
function selectTemplate(idx) {
|
|
||||||
$('#template_' + selectedItem).toggleClass("container-template--selected");
|
|
||||||
selectedItem = idx;
|
|
||||||
var selectedTemplate = $scope.templates[idx];
|
|
||||||
$scope.state.selectedTemplate = selectedTemplate;
|
$scope.state.selectedTemplate = selectedTemplate;
|
||||||
|
|
||||||
if (selectedTemplate.Network) {
|
if (selectedTemplate.Network) {
|
||||||
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === selectedTemplate.Network; });
|
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === selectedTemplate.Network; });
|
||||||
} else {
|
} else {
|
||||||
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === "bridge"; });
|
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
|
||||||
}
|
}
|
||||||
$anchorScroll('selectedTemplate');
|
|
||||||
|
$anchorScroll('view-top');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTemplateConfiguration(template) {
|
function createTemplateConfiguration(template) {
|
||||||
|
@ -114,7 +124,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ
|
||||||
var containerMapping = 'BY_CONTAINER_IP';
|
var containerMapping = 'BY_CONTAINER_IP';
|
||||||
if (endpointProvider === 'DOCKER_SWARM' && network.Scope === 'global') {
|
if (endpointProvider === 'DOCKER_SWARM' && network.Scope === 'global') {
|
||||||
containerMapping = 'BY_SWARM_CONTAINER_NAME';
|
containerMapping = 'BY_SWARM_CONTAINER_NAME';
|
||||||
} else if (network.Name !== "bridge") {
|
} else if (network.Name !== 'bridge') {
|
||||||
containerMapping = 'BY_CONTAINER_NAME';
|
containerMapping = 'BY_CONTAINER_NAME';
|
||||||
}
|
}
|
||||||
return containerMapping;
|
return containerMapping;
|
||||||
|
@ -144,18 +154,19 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerServ
|
||||||
volumes: VolumeService.getVolumes()
|
volumes: VolumeService.getVolumes()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var templates = data.templates;
|
$scope.templates = data.templates;
|
||||||
if (templatesKey === 'linuxserver.io') {
|
var availableCategories = [];
|
||||||
templates = TemplateService.filterLinuxServerIOTemplates(templates);
|
angular.forEach($scope.templates, function(template) {
|
||||||
}
|
availableCategories = availableCategories.concat(template.Categories);
|
||||||
$scope.templates = templates;
|
});
|
||||||
|
$scope.availableCategories = _.sortBy(_.uniq(availableCategories));
|
||||||
$scope.runningContainers = data.containers;
|
$scope.runningContainers = data.containers;
|
||||||
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
||||||
$scope.availableVolumes = data.volumes.Volumes;
|
$scope.availableVolumes = data.volumes.Volumes;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.templates = [];
|
$scope.templates = [];
|
||||||
Notifications.error("Failure", err, "An error occured during apps initialization.");
|
Notifications.error('Failure', err, 'An error occured during apps initialization.');
|
||||||
})
|
})
|
||||||
.finally(function final(){
|
.finally(function final(){
|
||||||
$('#loadTemplatesSpinner').hide();
|
$('#loadTemplatesSpinner').hide();
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular
|
||||||
|
.module('portainer')
|
||||||
|
.directive('rdTemplateWidget', function rdWidget() {
|
||||||
|
var directive = {
|
||||||
|
scope: {
|
||||||
|
'ngModel': '='
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
template: '<div class="widget template-widget" id="template-widget" ng-transclude></div>',
|
||||||
|
restrict: 'EA'
|
||||||
|
};
|
||||||
|
return directive;
|
||||||
|
});
|
|
@ -3,7 +3,7 @@ angular
|
||||||
.directive('rdWidget', function rdWidget() {
|
.directive('rdWidget', function rdWidget() {
|
||||||
var directive = {
|
var directive = {
|
||||||
scope: {
|
scope: {
|
||||||
"ngModel": "="
|
'ngModel': '='
|
||||||
},
|
},
|
||||||
transclude: true,
|
transclude: true,
|
||||||
template: '<div class="widget" ng-transclude></div>',
|
template: '<div class="widget" ng-transclude></div>',
|
||||||
|
|
|
@ -28,7 +28,7 @@ angular.module('portainer.helpers')
|
||||||
};
|
};
|
||||||
ports.forEach(function (p) {
|
ports.forEach(function (p) {
|
||||||
if (p.containerPort) {
|
if (p.containerPort) {
|
||||||
var key = p.containerPort + "/" + p.protocol;
|
var key = p.containerPort + '/' + p.protocol;
|
||||||
var binding = {};
|
var binding = {};
|
||||||
if (p.hostPort) {
|
if (p.hostPort) {
|
||||||
binding.HostPort = p.hostPort;
|
binding.HostPort = p.hostPort;
|
||||||
|
@ -60,7 +60,7 @@ angular.module('portainer.helpers')
|
||||||
value = $filter('swarmcontainername')(envvar.value);
|
value = $filter('swarmcontainername')(envvar.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
env.push(envvar.name + "=" + value);
|
env.push(envvar.name + '=' + value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return env;
|
return env;
|
||||||
|
@ -108,8 +108,8 @@ angular.module('portainer.helpers')
|
||||||
helper.filterLinuxServerIOTemplates = function(templates) {
|
helper.filterLinuxServerIOTemplates = function(templates) {
|
||||||
return templates.filter(function f(template) {
|
return templates.filter(function f(template) {
|
||||||
var valid = false;
|
var valid = false;
|
||||||
if (template.Category) {
|
if (template.Categories) {
|
||||||
angular.forEach(template.Category, function(category) {
|
angular.forEach(template.Categories, function(category) {
|
||||||
if (_.startsWith(category, 'Network')) {
|
if (_.startsWith(category, 'Network')) {
|
||||||
valid = true;
|
valid = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
function TemplateViewModel(data) {
|
function TemplateViewModel(data) {
|
||||||
this.Title = data.title;
|
this.Title = data.title;
|
||||||
this.Description = data.description;
|
this.Description = data.description;
|
||||||
this.Note = data.note ? data.note : data.description;
|
this.Note = data.note;
|
||||||
this.Category = data.category;
|
this.Categories = data.categories ? data.categories : [];
|
||||||
|
this.Platform = data.platform ? data.platform : '';
|
||||||
this.Logo = data.logo;
|
this.Logo = data.logo;
|
||||||
this.Image = data.image;
|
this.Image = data.image;
|
||||||
this.Registry = data.registry ? data.registry : '';
|
this.Registry = data.registry ? data.registry : '';
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
function TemplateLSIOViewModel(data) {
|
||||||
|
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.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]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,10 +8,18 @@ angular.module('portainer.services')
|
||||||
Template.get({key: key}).$promise
|
Template.get({key: key}).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var templates = data.map(function (tpl, idx) {
|
var templates = data.map(function (tpl, idx) {
|
||||||
var template = new TemplateViewModel(tpl);
|
var template;
|
||||||
|
if (key === 'linuxserver.io') {
|
||||||
|
template = new TemplateLSIOViewModel(tpl);
|
||||||
|
} else {
|
||||||
|
template = new TemplateViewModel(tpl);
|
||||||
|
}
|
||||||
template.index = idx;
|
template.index = idx;
|
||||||
return template;
|
return template;
|
||||||
});
|
});
|
||||||
|
if (key === 'linuxserver.io') {
|
||||||
|
templates = TemplateHelper.filterLinuxServerIOTemplates(templates);
|
||||||
|
}
|
||||||
deferred.resolve(templates);
|
deferred.resolve(templates);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -20,10 +28,6 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.filterLinuxServerIOTemplates = function(templates) {
|
|
||||||
return TemplateHelper.filterLinuxServerIOTemplates(templates);
|
|
||||||
};
|
|
||||||
|
|
||||||
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
||||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
||||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
||||||
|
|
|
@ -64,6 +64,13 @@ html, body, #content-wrapper, .page-content, #view {
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section-title {
|
||||||
|
border-bottom: 1px solid #777;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
.form-horizontal .control-label.text-left{
|
.form-horizontal .control-label.text-left{
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
@ -149,11 +156,6 @@ a[ng-click]{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-header-ico {
|
.custom-header-ico {
|
||||||
max-width: 32px;
|
max-width: 32px;
|
||||||
max-height: 32px;
|
max-height: 32px;
|
||||||
|
@ -176,81 +178,6 @@ a[ng-click]{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Underline From Center */
|
|
||||||
.hvr-underline-from-center {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
|
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.hvr-underline-from-center:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
left: 50%;
|
|
||||||
right: 50%;
|
|
||||||
bottom: 0;
|
|
||||||
background: #85898b;
|
|
||||||
height: 2px;
|
|
||||||
-webkit-transition-property: left, right;
|
|
||||||
transition-property: left, right;
|
|
||||||
-webkit-transition-duration: 0.3s;
|
|
||||||
transition-duration: 0.3s;
|
|
||||||
-webkit-transition-timing-function: ease-out;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
.hvr-underline-from-center:hover:before, .hvr-underline-from-center:focus:before, .hvr-underline-from-center:active:before {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template {
|
|
||||||
font-size: 1em;
|
|
||||||
width: 256px;
|
|
||||||
height: 128px;
|
|
||||||
margin: 15px;
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #f6f6f6;
|
|
||||||
color: #30426a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template--selected {
|
|
||||||
background-color: #ececec;
|
|
||||||
color: #2d3e63;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template:hover {
|
|
||||||
background-color: #ececec;
|
|
||||||
color: #2d3e63;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template .logo {
|
|
||||||
max-width: 48px;
|
|
||||||
max-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template .title {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-template .description {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-wrapper {
|
.page-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -418,3 +345,68 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
#toast-container > div {
|
#toast-container > div {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-widget {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-widget-body {
|
||||||
|
max-height: 86%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-logo {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60px;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container {
|
||||||
|
padding: 0.7rem;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container--selected {
|
||||||
|
border: 2px solid #333333;
|
||||||
|
background-color: #ececec;
|
||||||
|
color: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container:hover {
|
||||||
|
background-color: #ececec;
|
||||||
|
color: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-note {
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-title {
|
||||||
|
font-size: 1.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-description {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue