1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-07 14:55:27 +02:00

refactor(templates): migrate list view to react [EE-2296] (#10999)

This commit is contained in:
Chaim Lev-Ari 2024-04-11 09:29:30 +03:00 committed by GitHub
parent d38085a560
commit 6ff4fd3db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 2628 additions and 1315 deletions

View file

@ -1,16 +0,0 @@
import angular from 'angular';
angular.module('portainer.app').component('stackFromTemplateForm', {
templateUrl: './stackFromTemplateForm.html',
bindings: {
template: '=',
formValues: '=',
state: '=',
createTemplate: '<',
unselectTemplate: '<',
nameRegex: '<',
},
transclude: {
advanced: '?advancedForm',
},
});

View file

@ -1,95 +0,0 @@
<div class="col-sm-12">
<rd-widget>
<rd-widget-custom-header icon="$ctrl.template.Logo" title-text="$ctrl.template.Title"></rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal" name="stackTemplateForm">
<!-- description -->
<div ng-if="$ctrl.template.Note">
<div class="form-section-title"> Information </div>
<div class="col-sm-12 form-group">
<div class="template-note" ng-bind-html="$ctrl.template.Note"></div>
</div>
</div>
<!-- !description -->
<div class="form-section-title"> Configuration </div>
<!-- name-input -->
<div class="form-group">
<label for="template_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-6">
<input
type="text"
name="template_name"
class="form-control"
ng-model="$ctrl.formValues.name"
ng-pattern="$ctrl.nameRegex"
placeholder="e.g. myStack"
required
data-cy="stack-name-input"
/>
<div class="form-group" ng-if="stackTemplateForm.template_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="stackTemplateForm.template_name.$error">
<p ng-message="pattern" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
</p>
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This field is required. </p>
</div>
</div>
</div>
</div>
</div>
<!-- !name-input -->
<!-- env -->
<div ng-repeat="var in $ctrl.template.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" message="var.description"></portainer-tooltip>
</label>
<div class="col-sm-6">
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}" data-cy="stackFromTemplateForm-input" />
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}" data-cy="stackFromTemplateForm-select">
<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 -->
<ng-transclude ng-transclude-slot="advanced"></ng-transclude>
<!-- access-control -->
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="form-section-title"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary"
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable || stackTemplateForm.$invalid"
ng-click="$ctrl.createTemplate()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
</button>
<button type="button" class="btn btn-default" ng-click="$ctrl.unselectTemplate($ctrl.template)">Hide</button>
<div class="form-group" ng-if="$ctrl.state.formValidationError">
<div class="col-sm-12 small text-danger" ng-if="$ctrl.state.formValidationError">
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>{{ $ctrl.state.formValidationError }} </p>
</div>
</div>
<div class="form-group" ng-if="!$ctrl.state.deployable">
<div class="col-sm-12 small text-danger" ng-if="!$ctrl.state.deployable">
<p class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>This template type cannot be deployed on this environment. </p>
</div>
</div>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -1,127 +0,0 @@
import _ from 'lodash-es';
angular.module('portainer.app').factory('TemplateHelper', [
function TemplateHelperFactory() {
'use strict';
var helper = {};
helper.getDefaultContainerConfiguration = function () {
return {
Env: [],
OpenStdin: false,
Tty: false,
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no',
},
PortBindings: {},
Binds: [],
Privileged: false,
ExtraHosts: [],
},
Volumes: {},
Labels: {},
};
};
helper.portArrayToPortConfiguration = function (ports) {
var portConfiguration = {
bindings: {},
exposedPorts: {},
};
ports.forEach(function (p) {
if (p.containerPort) {
var key = p.containerPort + '/' + p.protocol;
var binding = {};
if (p.hostPort) {
binding.HostPort = p.hostPort;
if (p.hostPort.indexOf(':') > -1) {
var hostAndPort = p.hostPort.split(':');
binding.HostIp = hostAndPort[0];
binding.HostPort = hostAndPort[1];
}
}
portConfiguration.bindings[key] = [binding];
portConfiguration.exposedPorts[key] = {};
}
});
return portConfiguration;
};
helper.updateContainerConfigurationWithLabels = function (labelsArray) {
var labels = {};
labelsArray.forEach(function (l) {
if (l.name) {
if (l.value) {
labels[l.name] = l.value;
} else {
labels[l.name] = '';
}
}
});
return labels;
};
helper.EnvToStringArray = function (templateEnvironment) {
var env = [];
templateEnvironment.forEach(function (envvar) {
if (envvar.value || envvar.set) {
var value = envvar.set ? envvar.set : envvar.value;
env.push(envvar.name + '=' + value);
}
});
return env;
};
helper.getConsoleConfiguration = function (interactiveFlag) {
var consoleConfiguration = {
openStdin: false,
tty: false,
};
if (interactiveFlag === true) {
consoleConfiguration.openStdin = true;
consoleConfiguration.tty = true;
}
return consoleConfiguration;
};
helper.createVolumeBindings = function (volumes, generatedVolumesPile) {
volumes.forEach(function (volume) {
if (volume.container) {
var binding;
if (volume.type === 'auto') {
binding = generatedVolumesPile.pop().Id + ':' + volume.container;
} else if (volume.type !== 'auto' && volume.bind) {
binding = volume.bind + ':' + volume.container;
}
if (volume.readonly) {
binding += ':ro';
}
volume.binding = binding;
}
});
};
helper.determineRequiredGeneratedVolumeCount = function (volumes) {
var count = 0;
volumes.forEach(function (volume) {
if (volume.type === 'auto') {
++count;
}
});
return count;
};
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;
},
]);

View file

@ -13,7 +13,6 @@ import {
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
import { withFormValidation } from '@/react-tools/withFormValidation';
import { AppTemplatesList } from '@/react/portainer/templates/app-templates/AppTemplatesList';
import { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
import { VariablesFieldAngular } from './variables-field';
@ -39,18 +38,6 @@ export const ngModule = angular
'isVariablesNamesFromParent',
])
)
.component(
'appTemplatesList',
r2a(withUIRouter(withCurrentUser(AppTemplatesList)), [
'onSelect',
'templates',
'selectedId',
'disabledTypes',
'fixedCategories',
'storageKey',
'templateLinkParams',
])
)
.component(
'customTemplatesList',
r2a(withUIRouter(withCurrentUser(CustomTemplatesList)), [

View file

@ -19,6 +19,7 @@ import { updateSchedulesModule } from './update-schedules';
import { environmentGroupModule } from './env-groups';
import { registriesModule } from './registries';
import { activityLogsModule } from './activity-logs';
import { templatesModule } from './templates';
export const viewsModule = angular
.module('portainer.app.react.views', [
@ -28,6 +29,7 @@ export const viewsModule = angular
environmentGroupModule,
registriesModule,
activityLogsModule,
templatesModule,
])
.component(
'homeView',

View file

@ -0,0 +1,23 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView';
import { EditView } from '@/react/portainer/templates/custom-templates/EditView';
import { AppTemplatesView } from '@/react/portainer/templates/app-templates/AppTemplatesView';
export const templatesModule = angular
.module('portainer.app.react.views.templates', [])
.component(
'appTemplatesView',
r2a(withCurrentUser(withUIRouter(AppTemplatesView)), [])
)
.component(
'createCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(CreateView)), [])
)
.component(
'editCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(EditView)), [])
).name;

View file

@ -1,11 +1,10 @@
import { commandStringToArray } from '@/docker/helpers/containers';
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory);
/* @ngInject */
function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, ContainerHelper, EndpointService) {
function TemplateServiceFactory($q, Templates, EndpointService) {
var service = {
templates,
};
@ -45,43 +44,5 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise;
}
service.createTemplateConfiguration = function (template, containerName, network) {
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
var containerConfiguration = createContainerConfiguration(template, containerName, network);
containerConfiguration.Image = imageConfiguration.fromImage;
return containerConfiguration;
};
function createContainerConfiguration(template, containerName, network) {
var configuration = TemplateHelper.getDefaultContainerConfiguration();
configuration.HostConfig.NetworkMode = network.Name;
configuration.HostConfig.Privileged = template.Privileged;
configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
configuration.name = containerName;
configuration.Hostname = template.Hostname;
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
configuration.Cmd = commandStringToArray(template.Command);
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
configuration.HostConfig.PortBindings = portConfiguration.bindings;
configuration.ExposedPorts = portConfiguration.exposedPorts;
var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
configuration.OpenStdin = consoleConfiguration.openStdin;
configuration.Tty = consoleConfiguration.tty;
configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
return configuration;
}
service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) {
var volumes = template.Volumes;
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
volumes.forEach(function (volume) {
if (volume.binding) {
configuration.Volumes[volume.container] = {};
configuration.HostConfig.Binds.push(volume.binding);
}
});
};
return service;
}

View file

@ -1,67 +1,10 @@
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"> </page-header>
<div class="row">
<stack-from-template-form
ng-if="$ctrl.state.selectedTemplate"
template="$ctrl.state.selectedTemplate"
form-values="$ctrl.formValues"
name-regex="$ctrl.state.templateNameRegex"
state="$ctrl.state"
create-template="$ctrl.createStack"
unselect-template="$ctrl.unselectTemplate"
>
<advanced-form>
<custom-templates-variables-field
ng-if="$ctrl.isTemplateVariablesEnabled"
definitions="$ctrl.state.selectedTemplate.Variables"
value="$ctrl.formValues.variables"
on-change="($ctrl.onChangeTemplateVariables)"
></custom-templates-variables-field>
<div class="form-group" ng-if="$ctrl.state.selectedTemplate && !$ctrl.state.templateLoadFailed">
<div class="col-sm-12">
<a class="small interactive vertical-center" ng-show="!$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = true;">
<pr-icon icon="'plus'" class-name="space-right" feather="true"></pr-icon> {{ $ctrl.state.selectedTemplate.GitConfig !== null ? 'View' : 'Customize' }} stack
</a>
<a class="small interactive vertical-center" ng-show="$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = false;">
<pr-icon icon="'minus'" class-name="space-right" feather="true"></pr-icon> Hide {{ $ctrl.state.selectedTemplate.GitConfig === null ? 'custom' : '' }} stack
</a>
</div>
</div>
<span ng-if="$ctrl.state.selectedTemplate && $ctrl.state.templateLoadFailed">
<p class="small vertical-center text-danger mb-5" ng-if="$ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
<a ui-sref="docker.templates.custom.edit({id: $ctrl.state.selectedTemplate.Id})">click here</a> for configuration.</p
>
<p class="small vertical-center text-danger mb-5" ng-if="!($ctrl.currentUser.isAdmin || $ctrl.currentUser.id === $ctrl.state.selectedTemplate.CreatedByUserId)">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
>
</span>
<!-- web-editor -->
<web-editor-form
ng-if="$ctrl.state.showAdvancedOptions"
identifier="custom-template-creation-editor"
value="$ctrl.formValues.fileContent"
on-change="($ctrl.editorUpdate)"
ng-required="true"
yml="true"
placeholder="Define or paste the content of your docker compose file here"
read-only="$ctrl.state.isEditorReadOnly"
>
<editor-description>
<p>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank"> official documentation </a>
.
</p>
</editor-description>
</web-editor-form>
<!-- !web-editor -->
</advanced-form>
</stack-from-template-form>
</div>
<stack-from-custom-template-form-widget
ng-if="$ctrl.state.selectedTemplate"
template="$ctrl.state.selectedTemplate"
unselect="$ctrl.unselectTemplate"
></stack-from-custom-template-form-widget>
<custom-templates-list
templates="$ctrl.templates"

View file

@ -228,6 +228,8 @@ class CustomTemplatesViewController {
const variables = getVariablesFieldDefaultValues(template.Variables);
this.onChangeTemplateVariables(variables);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
}
getNetworks(provider, apiVersion) {

View file

@ -1,292 +0,0 @@
<page-header id="'view-top'" title="'Application templates list'" breadcrumbs="['Templates']"> </page-header>
<div class="row">
<!-- stack-form -->
<stack-from-template-form
ng-if="state.selectedTemplate && (state.selectedTemplate.Type === 2 || state.selectedTemplate.Type === 3)"
template="state.selectedTemplate"
form-values="formValues"
state="state"
create-template="createTemplate"
unselect-template="unselectTemplate"
>
</stack-from-template-form>
<!-- !stack-form -->
<!-- container-form -->
<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.Title"> </rd-widget-custom-header>
<rd-widget-body classes="padding">
<form class="form-horizontal" name="selectedTemplateType1">
<!-- description -->
<div ng-if="state.selectedTemplate.Note">
<div class="form-section-title"> Information </div>
<div class="col-sm-12 form-group">
<div class="template-note" ng-bind-html="state.selectedTemplate.Note"></div>
</div>
</div>
<!-- !description -->
<div class="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-6">
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)" data-cy="container-name-input" />
</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-6">
<select class="form-control" ng-options="net.Name for net in availableNetworks | orderBy: 'Name'" ng-model="formValues.network" data-cy="network-select">
<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.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" message="var.description"></portainer-tooltip>
</label>
<div class="col-sm-6">
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}" data-cy="env-input-{{ $index}" />
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}" data-cy="env-select-{{ $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"></por-access-control-form>
<!-- !access-control -->
<div class="form-group col-sm-12">
<a class="small interactive vertical-center" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<pr-icon icon="'plus'"></pr-icon> Show advanced options
</a>
<a class="small interactive vertical-center" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<pr-icon icon="'minus'"></pr-icon> Hide advanced options
</a>
</div>
<div ng-if="state.showAdvancedOptions">
<!-- port-mapping -->
<div class="form-group mt-2">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
<div class="mt-1" ng-if="state.selectedTemplate.Ports.length > 0">
<div class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</div>
<div class="form-inline mt-2" ng-repeat="portBinding in state.selectedTemplate.Ports">
<!-- 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)"
data-cy="host-port-input-{{ $index }}"
/>
</div>
<!-- !host-port -->
<pr-icon icon="'arrow-right'"></pr-icon>
<!-- 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" data-cy="container-port-input-{{ $index }}" />
</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-light" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-light" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-light" type="button" ng-click="removePortBinding($index)">
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
<div class="col-sm-12">
<span class="form-group small interactive text-muted vertical-center mt-2" ng-click="addPortBinding()">
<pr-icon icon="'plus'"></pr-icon> Add map additional port
</span>
</div>
</div>
</div>
<!-- !port-mapping -->
<!-- volume-mapping -->
<div class="form-group mt-4">
<div class="col-sm-12">
<label class="control-label text-left">Volume mapping</label>
<div class="mt-1" ng-if="state.selectedTemplate.Volumes.length > 0">
<div class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</div>
<div class="mt-2" ng-repeat="volume in state.selectedTemplate.Volumes">
<!-- volume-line1 -->
<div class="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" data-cy="container-path-input-{{ $index }}" />
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5 space-left">
<div class="btn-group btn-group-sm">
<label class="btn btn-light" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
<label class="btn btn-light" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.bind = ''">Volume</label>
<label class="btn btn-light" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''" ng-if="isAdmin || allowBindMounts">Bind</label>
</div>
<button class="btn btn-light" type="button" ng-click="removeVolume($index)">
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
</button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="form-inline mt-1" ng-if="volume.type !== 'auto'">
<pr-icon icon="'arrow-right'"></pr-icon>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'volume'">
<div class="col-sm-12 input-group">
<span class="input-group-addon">volume</span>
<div class="col-sm-12 input-group">
<select class="form-control" ng-model="volume.bind" ng-options="vol.Name as vol.Name for vol in availableVolumes" data-cy="volume-bind-select">
<option value="" disabled selected>Select a volume</option>
</select>
</div>
</div>
</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.bind" placeholder="e.g. /path/on/host" data-cy="host-path-input-{{ $index }}" />
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5 space-left">
<div class="btn-group btn-group-sm">
<label class="btn btn-light" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
<label class="btn btn-light" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div>
</div>
<div class="col-sm-12">
<span class="form-group small interactive text-muted vertical-center mt-2" ng-click="addVolume()">
<pr-icon icon="'plus'"></pr-icon> Add map additional volume
</span>
</div>
</div>
</div>
<!-- !volume-mapping -->
<!-- extra-host -->
<div class="form-group mt-4">
<div class="col-sm-12">
<label class="control-label text-left">Hosts file entries</label>
<!-- extra-host-input-list -->
<div class="mt-1" ng-if="state.selectedTemplate.Hosts.length > 0">
<div class="form-inline mt-2" ng-repeat="(idx, host) in state.selectedTemplate.Hosts track by $index">
<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" data-cy="host-input-{{ $index }}" />
</div>
<button class="btn btn-light" type="button" ng-click="removeExtraHost($index)">
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
</button>
</div>
</div>
<div class="col-sm-12">
<span class="form-group small interactive text-muted vertical-center mt-2" ng-click="addExtraHost()">
<pr-icon icon="'plus'"></pr-icon> Add additional entry
</span>
</div>
</div>
</div>
<!-- !extra-host -->
<!-- labels -->
<div class="form-group mt-4">
<div class="col-sm-12">
<label class="control-label text-left">Labels</label>
<!-- labels-input-list -->
<div class="mt-1" ng-if="state.selectedTemplate.Labels.length > 0">
<div class="form-inline mt-2" ng-repeat="label in state.selectedTemplate.Labels">
<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" data-cy="label-name-input-{{ $index }}" />
</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" data-cy="label-value-input-{{ $index }}" />
</div>
<button class="btn btn-light" type="button" ng-click="removeLabel($index)">
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
</button>
</div>
</div>
<!-- !labels-input-list -->
<div class="col-sm-12">
<span class="form-group small interactive text-muted vertical-center mt-2" ng-click="addLabel()"> <pr-icon icon="'plus'"></pr-icon> Add label </span>
</div>
</div>
</div>
<!-- !labels -->
<!-- hostname -->
<div class="form-group mt-4">
<label for="container_hostname" class="col-sm-2 control-label text-left">Hostname</label>
<div class="col-sm-6">
<input
type="text"
name="container_hostname"
class="form-control"
ng-model="state.selectedTemplate.Hostname"
placeholder="leave empty to use docker default"
data-cy="hostname-input"
/>
</div>
</div>
<!-- !hostname -->
</div>
<!-- !advanced-options -->
<!-- actions -->
<div class="form-section-title"> Actions </div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary"
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>
<button type="button" class="btn btn-default" ng-click="unselectTemplate(state.selectedTemplate)">Hide</button>
<span class="text-danger space-left" ng-if="state.formValidationError">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
<!-- container-form -->
</div>
<app-templates-list
storage-key="'docker-app-templates'"
templates="templates"
on-select="(selectTemplate)"
selected-id="state.selectedTemplate.Id"
disabled-types="disabledTypes"
></app-templates-list>

View file

@ -1,317 +0,0 @@
import _ from 'lodash-es';
import { TemplateType } from '@/react/portainer/templates/app-templates/types';
import { AccessControlFormData } from '../../components/accessControlForm/porAccessControlFormModel';
angular.module('portainer.app').controller('TemplatesController', [
'$scope',
'$q',
'$state',
'$anchorScroll',
'ContainerService',
'ImageService',
'NetworkService',
'TemplateService',
'TemplateHelper',
'VolumeService',
'Notifications',
'ResourceControlService',
'Authentication',
'FormValidator',
'StackService',
'endpoint',
'$async',
function (
$scope,
$q,
$state,
$anchorScroll,
ContainerService,
ImageService,
NetworkService,
TemplateService,
TemplateHelper,
VolumeService,
Notifications,
ResourceControlService,
Authentication,
FormValidator,
StackService,
endpoint,
$async
) {
const DOCKER_STANDALONE = 'DOCKER_STANDALONE';
const DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
formValidationError: '',
actionInProgress: false,
};
$scope.enabledTypes = [TemplateType.Container, TemplateType.ComposeStack];
$scope.formValues = {
network: '',
name: '',
AccessControlData: new AccessControlFormData(),
};
$scope.addVolume = function () {
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', bind: '', 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) {
angular.forEach(data, function (volume) {
var volumeId = volume.Id;
generatedVolumeIds.push(volumeId);
});
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
return ImageService.pullImage(template.RegistryModel, true);
})
.then(function success() {
return ContainerService.createAndStartContainer(endpoint.Id, templateConfiguration);
})
.then(function success(data) {
const resourceControl = data.Portainer.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl, generatedVolumeIds);
})
.then(function success() {
Notifications.success('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 createComposeStackFromTemplate(template, userId, accessControlData) {
var stackName = $scope.formValues.name;
for (var i = 0; i < template.Env.length; i++) {
var envvar = template.Env[i];
if (envvar.preset) {
envvar.value = envvar.default;
}
}
var repositoryOptions = {
RepositoryURL: template.Repository.url,
ComposeFilePathInRepository: template.Repository.stackfile,
FromAppTemplate: true,
};
const endpointId = +$state.params.endpointId;
StackService.createComposeStackFromGitRepository(stackName, repositoryOptions, template.Env, endpointId)
.then(function success(data) {
const resourceControl = data.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Success', 'Stack successfully deployed');
$state.go('docker.stacks');
})
.catch(function error(err) {
Notifications.error('Deployment error', err);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function createStackFromTemplate(template, userId, accessControlData) {
var stackName = $scope.formValues.name;
var env = _.filter(
_.map(template.Env, function transformEnvVar(envvar) {
return {
name: envvar.name,
value: envvar.preset || !envvar.value ? envvar.default : envvar.value,
};
}),
function removeUndefinedVars(envvar) {
return envvar.value && envvar.name;
}
);
var repositoryOptions = {
RepositoryURL: template.Repository.url,
ComposeFilePathInRepository: template.Repository.stackfile,
FromAppTemplate: true,
};
const endpointId = +$state.params.endpointId;
StackService.createSwarmStackFromGitRepository(stackName, repositoryOptions, env, endpointId)
.then(function success(data) {
const resourceControl = data.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Success', 'Stack successfully deployed');
$state.go('docker.stacks');
})
.catch(function error(err) {
Notifications.error('Deployment error', err);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
$scope.createTemplate = function () {
var userDetails = Authentication.getUserDetails();
var userId = userDetails.ID;
var accessControlData = $scope.formValues.AccessControlData;
if (!validateForm(accessControlData, $scope.isAdmin)) {
return;
}
var template = $scope.state.selectedTemplate;
$scope.state.actionInProgress = true;
if (template.Type === 2) {
createStackFromTemplate(template, userId, accessControlData);
} else if (template.Type === 3) {
createComposeStackFromTemplate(template, userId, accessControlData);
} else {
createContainerFromTemplate(template, userId, accessControlData);
}
};
$scope.unselectTemplate = function () {
return $async(async () => {
$scope.state.selectedTemplate = null;
});
};
$scope.selectTemplate = function (template) {
return $async(async () => {
if ($scope.state.selectedTemplate) {
await $scope.unselectTemplate($scope.state.selectedTemplate);
}
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';
});
}
$scope.formValues.name = template.Name ? template.Name : '';
$scope.state.selectedTemplate = template;
$scope.state.deployable = isDeployable($scope.applicationState.endpoint, template.Type);
$anchorScroll('view-top');
});
};
function isDeployable(endpoint, templateType) {
let deployable = false;
switch (templateType) {
case 1:
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE || endpoint.mode.provider === DOCKER_STANDALONE;
break;
case 2:
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE;
break;
case 3:
deployable = endpoint.mode.provider === DOCKER_SWARM_MODE || endpoint.mode.provider === DOCKER_STANDALONE;
break;
}
return deployable;
}
function createTemplateConfiguration(template) {
var network = $scope.formValues.network;
var name = $scope.formValues.name;
return TemplateService.createTemplateConfiguration(template, name, network);
}
function initView() {
$scope.isAdmin = Authentication.isAdmin();
var endpointMode = $scope.applicationState.endpoint.mode;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
const endpointId = +$state.params.endpointId;
const showSwarmStacks = endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25;
$scope.disabledTypes = !showSwarmStacks ? [TemplateType.SwarmStack] : [];
$q.all({
templates: TemplateService.templates(endpointId),
volumes: VolumeService.getVolumes(),
networks: NetworkService.networks(
endpointMode.provider === 'DOCKER_STANDALONE' || endpointMode.provider === 'DOCKER_SWARM_MODE',
false,
endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
),
})
.then(function success(data) {
var templates = data.templates;
$scope.templates = templates;
$scope.availableVolumes = _.orderBy(data.volumes.Volumes, [(volume) => volume.Name.toLowerCase()], ['asc']);
var networks = data.networks;
$scope.availableNetworks = networks;
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occurred during apps initialization.');
});
}
initView();
},
]);