refactor(stacks): move custom templates selector to component (#5418)

* feat(app): introduce web-editor form component

* refactor(stacks): move custom templates selector to component

* fix(stacks): validate form for template
pull/5417/head
Chaim Lev-Ari 2021-08-18 14:40:38 +03:00 committed by GitHub
parent 6b37235eb4
commit 91653f9c36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 190 additions and 109 deletions

View File

@ -1,12 +1,18 @@
angular.module('portainer.app').controller('CodeEditorController', function CodeEditorController($document, CodeMirrorService, $scope) { angular.module('portainer.app').controller('CodeEditorController', function CodeEditorController($document, CodeMirrorService, $scope) {
var ctrl = this; var ctrl = this;
this.$onChanges = function $onChanges({ value }) {
if (value && value.currentValue && ctrl.editor) {
ctrl.editor.setValue(value.currentValue);
}
};
this.$onInit = function () { this.$onInit = function () {
$document.ready(function () { $document.ready(function () {
var editorElement = $document[0].getElementById(ctrl.identifier); var editorElement = $document[0].getElementById(ctrl.identifier);
ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly); ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly);
if (ctrl.onChange) { if (ctrl.onChange) {
ctrl.editor.on('change', (...args) => $scope.$apply(() => ctrl.onChange(...args))); ctrl.editor.on('change', (...args) => $scope.$evalAsync(() => ctrl.onChange(...args)));
} }
if (ctrl.value) { if (ctrl.value) {
ctrl.editor.setValue(ctrl.value); ctrl.editor.setValue(ctrl.value);

View File

@ -0,0 +1,36 @@
class CustomTemplateSelectorController {
/* @ngInject */
constructor($async, CustomTemplateService, Notifications) {
Object.assign(this, { $async, CustomTemplateService, Notifications });
this.selectedTemplate = null;
this.templates = null;
}
async handleChangeTemplate(templateId) {
this.selectedTemplate = this.templates.find((t) => t.id === templateId);
this.onChange(templateId);
}
$onChanges({ value }) {
if (value && value.currentValue && this.templates) {
this.handleChangeTemplate(value.currentValue);
}
}
$onInit() {
return this.$async(async () => {
try {
const templates = await this.CustomTemplateService.customTemplates(this.stackType);
this.templates = templates.map((template) => ({ ...template, label: `${template.Title} - ${template.Description}` }));
if (this.value) {
this.handleChangeTemplate(this.value);
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Custom Templates');
}
});
}
}
export default CustomTemplateSelectorController;

View File

@ -0,0 +1,32 @@
<div>
<div class="form-group">
<label for="stack_template" class="col-sm-1 control-label text-left">
Template
</label>
<div class="col-sm-11">
<select
ng-if="$ctrl.templates.length"
class="form-control"
ng-model="$ctrl.value"
ng-options="template.Id as template.label for template in $ctrl.templates"
ng-change="$ctrl.handleChangeTemplate($ctrl.value)"
>
<option value="" label="Select a Custom template" disabled selected="selected"> </option>
</select>
<span ng-if="!$ctrl.templates.length"> No custom templates are available. Head over to the <a ui-state="$ctrl.newTemplatePath">custom template view</a> to create one. </span>
</div>
</div>
<!-- description -->
<div ng-if="$ctrl.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-bind-html="$ctrl.selectedTemplate.note"></div>
</div>
</div>
</div>
<!-- !description -->
</div>

View File

@ -0,0 +1,16 @@
import angular from 'angular';
import controller from './custom-template-selector.controller.js';
export const customTemplateSelector = {
templateUrl: './custom-template-selector.html',
controller,
bindings: {
newTemplatePath: '@',
stackType: '<',
value: '<',
onChange: '<',
},
};
angular.module('portainer.app').component('customTemplateSelector', customTemplateSelector);

View File

@ -0,0 +1,5 @@
import angular from 'angular';
import { webEditorForm } from './web-editor-form';
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).name;

View File

@ -0,0 +1,19 @@
import controller from './web-editor-form.controller.js';
export const webEditorForm = {
templateUrl: './web-editor-form.html',
controller,
bindings: {
identifier: '@',
placeholder: '@',
yml: '<',
value: '<',
onChange: '<',
},
transclude: {
description: '?editorDescription',
},
};

View File

@ -0,0 +1,12 @@
class WebEditorFormController {
/* @ngInject */
constructor() {
this.editorUpdate = this.editorUpdate.bind(this);
}
editorUpdate(cm) {
this.onChange(cm.getValue());
}
}
export default WebEditorFormController;

View File

@ -0,0 +1,13 @@
<ng-form name="$ctrl.webEditorForm">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small" ng-transclude="description"> </span>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor identifier="{{ $ctrl.identifier }}" placeholder="{{ $ctrl.placeholder }}" yml="$ctrl.yml" value="$ctrl.value" on-change="($ctrl.editorUpdate)"></code-editor>
</div>
</div>
</ng-form>

View File

@ -1,7 +1,4 @@
class GitFormAdditionalFileItemController { class GitFormAdditionalFileItemController {
/* @ngInject */
constructor() {}
onChangePath(value) { onChangePath(value) {
const fieldIsInvalid = typeof value === 'undefined'; const fieldIsInvalid = typeof value === 'undefined';
if (fieldIsInvalid) { if (fieldIsInvalid) {

View File

@ -2,5 +2,6 @@ import angular from 'angular';
import gitFormModule from './forms/git-form'; import gitFormModule from './forms/git-form';
import porAccessManagementModule from './accessManagement'; import porAccessManagementModule from './accessManagement';
import formComponentsModule from './form-components';
export default angular.module('portainer.app.components', [gitFormModule, porAccessManagementModule]).name; export default angular.module('portainer.app.components', [gitFormModule, porAccessManagementModule, formComponentsModule]).name;

View File

@ -1,6 +1,6 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash-es';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel'; import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
angular angular
@ -25,6 +25,8 @@ angular
WebhookHelper, WebhookHelper,
clipboard clipboard
) { ) {
$scope.onChangeTemplateId = onChangeTemplateId;
$scope.formValues = { $scope.formValues = {
Name: '', Name: '',
StackFileContent: '', StackFileContent: '',
@ -60,9 +62,9 @@ angular
} }
}; };
$scope.$on('$destroy', function() { $scope.$on('$destroy', function () {
$scope.state.isEditorDirty = false; $scope.state.isEditorDirty = false;
}) });
$scope.onChangeFormValues = onChangeFormValues; $scope.onChangeFormValues = onChangeFormValues;
@ -219,8 +221,8 @@ angular
}); });
}; };
$scope.editorUpdate = function (cm) { $scope.onChangeFileContent = function onChangeFileContent(value) {
$scope.formValues.StackFileContent = cm.getValue(); $scope.formValues.StackFileContent = value;
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames); $scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
$scope.state.isEditorDirty = true; $scope.state.isEditorDirty = true;
}; };
@ -244,15 +246,18 @@ angular
} }
}; };
$scope.onChangeTemplate = async function onChangeTemplate(template) { function onChangeTemplateId(templateId) {
try { return $async(async () => {
$scope.formValues.StackFileContent = undefined; try {
$scope.selectedTemplate = template; $scope.state.templateId = templateId;
$scope.formValues.StackFileContent = await CustomTemplateService.customTemplateFile(template.Id);
} catch (err) { const fileContent = await CustomTemplateService.customTemplateFile(templateId);
Notifications.error('Failure', err, 'Unable to retrieve Custom Template file'); $scope.onChangeFileContent(fileContent);
} } catch (err) {
}; this.Notifications.error('Failure', err, 'Unable to retrieve Custom Template file');
}
});
}
async function initView() { async function initView() {
var endpointMode = $scope.applicationState.endpoint.mode; var endpointMode = $scope.applicationState.endpoint.mode;
@ -261,13 +266,6 @@ angular
$scope.state.StackType = 1; $scope.state.StackType = 1;
} }
try {
const templates = await CustomTemplateService.customTemplates($scope.state.StackType);
$scope.templates = _.map(templates, (template) => ({ ...template, label: `${template.Title} - ${template.Description}` }));
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve Custom Templates');
}
try { try {
const endpoint = EndpointProvider.currentEndpoint(); const endpoint = EndpointProvider.currentEndpoint();
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion; $scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;

View File

@ -81,31 +81,7 @@
</div> </div>
</div> </div>
<!-- !build-method --> <!-- !build-method -->
<!-- web-editor -->
<div ng-show="state.Method === 'editor'">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
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>.
</span>
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="stack-creation-editor"
placeholder="# Define or paste the content of your docker-compose file here"
yml="true"
on-change="(editorUpdate)"
></code-editor>
</div>
</div>
</div>
<!-- !web-editor -->
<!-- upload --> <!-- upload -->
<div ng-show="state.Method === 'upload'"> <div ng-show="state.Method === 'upload'">
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
@ -138,63 +114,34 @@
auto-update="true" auto-update="true"
show-auth-explanation="true" show-auth-explanation="true"
></git-form> ></git-form>
<!-- custom-template -->
<div ng-show="state.Method === 'template'"> <custom-template-selector
<div class="form-group"> ng-show="state.Method === 'template'"
<label for="stack_template" class="col-sm-1 control-label text-left"> new-template-path="docker.templates.custom.new"
Template stack-type="state.StackType"
</label> on-change="(onChangeTemplateId)"
<div class="col-sm-11"> value="state.templateId"
<select ></custom-template-selector>
ng-if="templates.length"
class="form-control" <web-editor-form
ng-model="selectedTemplate" ng-if="state.Method === 'editor' || (state.Method === 'template' && state.templateId)"
ng-options="template as template.label for template in templates" identifier="stack-creation-editor"
ng-change="onChangeTemplate(selectedTemplate)" value="formValues.StackFileContent"
> on-change="(onChangeFileContent)"
<option value="" label="Select a Custom template" disabled selected="selected"> </option> ng-required="true"
</select> yml="true"
<span ng-if="!templates.length"> placeholder="# Define or paste the content of your docker-compose file here"
No custom templates are available. Head over to the <a ui-sref="docker.templates.custom.new">custom template view</a> to create one. >
</span> <editor-description>
<span class="col-sm-12 text-muted small">
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>.
</span>
<div class="col-sm-12" ng-if="state.editorYamlValidationError">
<span class="text-danger small">{{ state.editorYamlValidationError }}</span>
</div> </div>
</div> </editor-description>
<!-- description --> </web-editor-form>
<div ng-if="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-bind-html="selectedTemplate.note"></div>
</div>
</div>
</div>
<!-- !description -->
<!-- editor -->
<div ng-if="selectedTemplate && formValues.StackFileContent">
<div class="col-sm-12 form-section-title">
Web editor
</div>
<div class="form-group">
<div class="col-sm-12" ng-if="state.editorYamlValidationError"
><span class="text-danger small">{{ state.editorYamlValidationError }}</span></div
>
</div>
<div class="form-group">
<div class="col-sm-12">
<code-editor
identifier="template-content-editor"
placeholder="# Define or paste the content of your docker-compose file here"
yml="true"
value="formValues.StackFileContent"
on-change="(editorUpdate)"
></code-editor>
</div>
</div>
</div>
</div>
<!-- !custom-template -->
<!-- environment-variables --> <!-- environment-variables -->
<environment-variables-panel ng-model="formValues.Env" explanation="These values will be used as substitutions in the stack file" on-change="(handleEnvVarChange)"> <environment-variables-panel ng-model="formValues.Env" explanation="These values will be used as substitutions in the stack file" on-change="(handleEnvVarChange)">
</environment-variables-panel> </environment-variables-panel>
@ -211,9 +158,8 @@
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress ng-disabled="state.actionInProgress
|| !createStackForm.$valid || !createStackForm.$valid
|| (state.Method === 'editor' && (!formValues.StackFileContent || state.editorYamlValidationError)) || ((state.Method === 'editor' || state.Method === 'template') && (!formValues.StackFileContent || state.editorYamlValidationError))
|| (state.Method === 'upload' && (!formValues.StackFile || state.uploadYamlValidationError)) || (state.Method === 'upload' && (!formValues.StackFile || state.uploadYamlValidationError))
|| (state.Method === 'template' && (!formValues.StackFileContent || !selectedTemplate || state.editorYamlValidationError))
|| (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && !formValues.RepositoryPassword))) || (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && !formValues.RepositoryPassword)))
|| !formValues.Name" || !formValues.Name"
ng-click="deployStack()" ng-click="deployStack()"