app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js
import _ from 'lodash-es';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { confirmDelete } from '@@/modals/confirm';
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/react/portainer/custom-templates/components/CommonFields';
class CustomTemplatesViewController {
/* @ngInject */
constructor(
$anchorScroll,
$async,
$rootScope,
$state,
Authentication,
CustomTemplateService,
FormValidator,
NetworkService,
Notifications,
ResourceControlService,
StackService,
StateManager
) {
this.$anchorScroll = $anchorScroll;
this.$async = $async;
this.$rootScope = $rootScope;
this.$state = $state;
this.Authentication = Authentication;
this.CustomTemplateService = CustomTemplateService;
this.FormValidator = FormValidator;
this.NetworkService = NetworkService;
this.Notifications = Notifications;
this.ResourceControlService = ResourceControlService;
this.StateManager = StateManager;
this.StackService = StackService;
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
this.state = {
selectedTemplate: null,
showAdvancedOptions: false,
formValidationError: '',
actionInProgress: false,
deployable: false,
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
templateContent: '',
templateLoadFailed: false,
};
this.currentUser = {
isAdmin: false,
id: null,
};
this.formValues = {
network: '',
name: '',
fileContent: '',
AccessControlData: new AccessControlFormData(),
variables: [],
};
this.getTemplates = this.getTemplates.bind(this);
this.getTemplatesAsync = this.getTemplatesAsync.bind(this);
this.removeTemplates = this.removeTemplates.bind(this);
this.removeTemplatesAsync = this.removeTemplatesAsync.bind(this);
this.validateForm = this.validateForm.bind(this);
this.createStack = this.createStack.bind(this);
this.createStackAsync = this.createStackAsync.bind(this);
this.selectTemplate = this.selectTemplate.bind(this);
this.selectTemplateAsync = this.selectTemplateAsync.bind(this);
this.unselectTemplate = this.unselectTemplate.bind(this);
this.unselectTemplateAsync = this.unselectTemplateAsync.bind(this);
this.getNetworks = this.getNetworks.bind(this);
this.getNetworksAsync = this.getNetworksAsync.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.confirmDeleteAsync = this.confirmDeleteAsync.bind(this);
this.editorUpdate = this.editorUpdate.bind(this);
this.isEditAllowed = this.isEditAllowed.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
}
isEditAllowed(template) {
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
}
getTemplates() {
return this.$async(this.getTemplatesAsync);
}
async getTemplatesAsync() {
try {
const templates = await this.CustomTemplateService.customTemplates([1, 2]);
this.templates = templates.filter((t) => !t.EdgeTemplate);
} catch (err) {
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
}
}
removeTemplates(templates) {
return this.$async(this.removeTemplatesAsync, templates);
}
async removeTemplatesAsync(templates) {
for (let template of templates) {
try {
await this.CustomTemplateService.remove(template.id);
this.Notifications.success('Success', 'Removed template successfully');
_.remove(this.templates, template);
} catch (err) {
this.Notifications.error('Failed removing template', err, 'Unable to remove custom template');
}
}
}
onChangeTemplateVariables(variables) {
this.onChangeFormValues({ variables });
this.renderTemplate();
}
renderTemplate() {
if (!this.isTemplateVariablesEnabled) {
return;
}
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
this.onChangeFormValues({ fileContent });
}
onChangeFormValues(values) {
this.formValues = {
...this.formValues,
...values,
};
}
validateForm(accessControlData, isAdmin) {
this.state.formValidationError = '';
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
this.state.formValidationError = error;
return false;
}
return true;
}
createStack() {
return this.$async(this.createStackAsync);
}
async createStackAsync() {
const userId = this.currentUser.id;
const accessControlData = this.formValues.AccessControlData;
if (!this.validateForm(accessControlData, this.currentUser.isAdmin)) {
return;
}
const stackName = this.formValues.name;
const endpointId = this.endpoint.Id;
this.state.actionInProgress = true;
try {
const file = this.formValues.fileContent;
const createAction = this.state.selectedTemplate.Type === 1 ? this.StackService.createSwarmStackFromFileContent : this.StackService.createComposeStackFromFileContent;
const { ResourceControl: resourceControl } = await createAction(stackName, file, [], endpointId);
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
this.Notifications.success('Success', 'Stack successfully deployed');
this.$state.go('docker.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Failed to deploy stack');
} finally {
this.state.actionInProgress = false;
}
}
unselectTemplate() {
// wrapping unselect with async to make a digest cycle run between unselect to select
return this.$async(this.unselectTemplateAsync);
}
async unselectTemplateAsync() {
this.state.selectedTemplate = null;
this.formValues = {
network: '',
name: '',
fileContent: '',
AccessControlData: new AccessControlFormData(),
variables: [],
};
}
selectTemplate(templateId) {
return this.$async(this.selectTemplateAsync, templateId);
}
async selectTemplateAsync(templateId) {
if (this.state.selectedTemplate) {
await this.unselectTemplate(this.state.selectedTemplate);
}
const template = _.find(this.templates, { Id: templateId });
const isGit = template.GitConfig !== null;
this.state.isEditorReadOnly = isGit;
try {
this.state.templateContent = this.formValues.fileContent = await this.CustomTemplateService.customTemplateFile(template.Id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
}
this.formValues.network = _.find(this.availableNetworks, function (o) {
return o.Name === 'bridge';
});
this.formValues.name = template.Title ? template.Title : '';
this.state.selectedTemplate = template;
this.$anchorScroll('view-top');
const applicationState = this.StateManager.getState();
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
if (template.Variables && template.Variables.length > 0) {
const variables = getVariablesFieldDefaultValues(template.Variables);
this.onChangeTemplateVariables(variables);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
}
getNetworks(provider, apiVersion) {
return this.$async(this.getNetworksAsync, provider, apiVersion);
}
async getNetworksAsync(provider, apiVersion) {
try {
const networks = await this.NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
false,
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
);
this.availableNetworks = networks;
} catch (err) {
this.Notifications.error('Failure', err, 'Failed to load networks.');
}
}
confirmDelete(templateId) {
return this.$async(this.confirmDeleteAsync, templateId);
}
async confirmDeleteAsync(templateId) {
const confirmed = await confirmDelete('Are you sure that you want to delete this template?');
if (!confirmed) {
return;
}
try {
var template = _.find(this.templates, { Id: templateId });
await this.CustomTemplateService.remove(templateId);
this.Notifications.success('Template successfully deleted', template && template.Title);
this.templates = this.templates.filter((template) => template.Id !== templateId);
} catch (err) {
this.Notifications.error('Failure', err, 'Failed to delete template');
}
}
editorUpdate(value) {
this.formValues.fileContent = value;
}
isDeployable(endpoint, templateType) {
let deployable = false;
switch (templateType) {
case 1:
deployable = endpoint.mode.provider === this.DOCKER_SWARM_MODE;
break;
case 2:
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
break;
}
return deployable;
}
$onInit() {
const applicationState = this.StateManager.getState();
const {
endpoint: { mode: endpointMode },
apiVersion,
} = applicationState;
this.getTemplates();
this.getNetworks(endpointMode.provider, apiVersion);
this.currentUser.isAdmin = this.Authentication.isAdmin();
const user = this.Authentication.getUserDetails();
this.currentUser.id = user.ID;
}
}
export default CustomTemplatesViewController;