mirror of
https://github.com/portainer/portainer.git
synced 2025-08-07 14:55:27 +02:00
feat(custom-templates): migrate create view to react [EE-6400] (#10715)
This commit is contained in:
parent
bd5ba7b5d0
commit
dabcf4f7db
30 changed files with 495 additions and 960 deletions
|
@ -1,119 +0,0 @@
|
|||
<page-header title="'Create Custom template'" breadcrumbs="[{label:'Custom Templates', link:'docker.templates.custom'}, 'Create Custom template']" reload="true"> </page-header>
|
||||
|
||||
<div class="row" ng-if="!$ctrl.state.loading">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="customTemplateForm">
|
||||
<custom-templates-common-fields values="$ctrl.formValues" on-change="($ctrl.handleChange)" validation-data="$ctrl.validationData"></custom-templates-common-fields>
|
||||
|
||||
<custom-templates-platform-selector value="$ctrl.formValues.Platform" on-change="($ctrl.onChangePlatform)"></custom-templates-platform-selector>
|
||||
|
||||
<custom-templates-type-selector value="$ctrl.formValues.Type" on-change="($ctrl.onChangeType)"></custom-templates-type-selector>
|
||||
|
||||
<!-- build-method -->
|
||||
<div ng-if="!$ctrl.state.fromStack">
|
||||
<div class="col-sm-12 form-section-title"> Build method </div>
|
||||
|
||||
<box-selector
|
||||
slim="true"
|
||||
options="$ctrl.buildMethods"
|
||||
value="$ctrl.state.Method"
|
||||
on-change="($ctrl.onChangeMethod)"
|
||||
radio-name="'buildMethod'"
|
||||
slim="true"
|
||||
></box-selector>
|
||||
</div>
|
||||
<!-- !build-method -->
|
||||
<!-- web-editor -->
|
||||
<web-editor-form
|
||||
ng-if="$ctrl.state.Method === 'editor'"
|
||||
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"
|
||||
>
|
||||
<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 -->
|
||||
<!-- upload -->
|
||||
<div ng-show="$ctrl.state.Method === 'upload'">
|
||||
<div class="col-sm-12 form-section-title"> Upload </div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small"> You can upload a Compose file from your computer. </span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.File"> Select file </button>
|
||||
<span class="space-left">
|
||||
{{ $ctrl.formValues.File.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<!-- repository -->
|
||||
<git-form
|
||||
ng-if="$ctrl.state.Method === 'repository'"
|
||||
value="$ctrl.formValues"
|
||||
on-change="($ctrl.handleChange)"
|
||||
is-docker-standalone="$ctrl.isDockerStandalone"
|
||||
></git-form>
|
||||
|
||||
<div class="form-group" ng-if="!$ctrl.state.isTemplateValid">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-warning">
|
||||
<pr-icon icon="'alert-triangle'" class-name="'space-right'"></pr-icon>
|
||||
Template is invalid.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<custom-templates-variables-definition-field
|
||||
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||
value="$ctrl.formValues.Variables"
|
||||
on-change="($ctrl.onVariablesChange)"
|
||||
is-variables-names-from-parent="$ctrl.state.Method === 'editor'"
|
||||
></custom-templates-variables-definition-field>
|
||||
|
||||
<!-- !repository -->
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
|
||||
|
||||
<!-- 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 !ml-0"
|
||||
ng-disabled="$ctrl.state.actionInProgress || customTemplateForm.$invalid
|
||||
|| ($ctrl.state.Method === 'editor' && !$ctrl.formValues.FileContent)
|
||||
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.File)
|
||||
|| ($ctrl.state.Method === 'repository' && ((!$ctrl.formValues.RepositoryURL || !$ctrl.formValues.ComposeFilePathInRepository) || ($ctrl.formValues.RepositoryAuthentication && (!$ctrl.formValues.RepositoryUsername || !$ctrl.formValues.RepositoryPassword))))
|
||||
|| !$ctrl.formValues.Title
|
||||
|| !$ctrl.state.isTemplateValid"
|
||||
ng-click="$ctrl.createCustomTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Create custom template</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
<span class="text-danger space-left" ng-if="$ctrl.state.formValidationError">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,271 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { fetchFilePreview } from '@/react/portainer/templates/app-templates/queries/useFetchTemplateFile';
|
||||
|
||||
class CreateCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $scope, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService, StackService, StateManager) {
|
||||
Object.assign(this, {
|
||||
$async,
|
||||
$state,
|
||||
$window,
|
||||
$scope,
|
||||
Authentication,
|
||||
CustomTemplateService,
|
||||
FormValidator,
|
||||
Notifications,
|
||||
ResourceControlService,
|
||||
StackService,
|
||||
StateManager,
|
||||
});
|
||||
|
||||
this.buildMethods = [editor, upload, git];
|
||||
|
||||
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
|
||||
|
||||
this.formValues = {
|
||||
Title: '',
|
||||
FileContent: '',
|
||||
File: null,
|
||||
RepositoryURL: '',
|
||||
RepositoryReferenceName: '',
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
ComposeFilePathInRepository: 'docker-compose.yml',
|
||||
Description: '',
|
||||
Note: '',
|
||||
Logo: '',
|
||||
Platform: 1,
|
||||
Type: 1,
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
Variables: [],
|
||||
TLSSkipVerify: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
Method: 'editor',
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
fromStack: false,
|
||||
loading: true,
|
||||
isEditorDirty: false,
|
||||
isTemplateValid: true,
|
||||
};
|
||||
|
||||
this.validationData = {
|
||||
title: {
|
||||
pattern: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
error: "This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').",
|
||||
},
|
||||
};
|
||||
|
||||
this.templates = [];
|
||||
|
||||
this.createCustomTemplate = this.createCustomTemplate.bind(this);
|
||||
this.createCustomTemplateAsync = this.createCustomTemplateAsync.bind(this);
|
||||
this.validateForm = this.validateForm.bind(this);
|
||||
this.createCustomTemplateByMethod = this.createCustomTemplateByMethod.bind(this);
|
||||
this.createCustomTemplateFromFileContent = this.createCustomTemplateFromFileContent.bind(this);
|
||||
this.createCustomTemplateFromFileUpload = this.createCustomTemplateFromFileUpload.bind(this);
|
||||
this.createCustomTemplateFromGitRepository = this.createCustomTemplateFromGitRepository.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onChangePlatform = this.onChangePlatform.bind(this);
|
||||
this.onChangeType = this.onChangeType.bind(this);
|
||||
}
|
||||
|
||||
onVariablesChange(value) {
|
||||
this.handleChange({ Variables: value });
|
||||
}
|
||||
|
||||
onChangePlatform(value) {
|
||||
this.handleChange({ Platform: value });
|
||||
}
|
||||
|
||||
onChangeType(value) {
|
||||
this.handleChange({ Type: value });
|
||||
}
|
||||
|
||||
handleChange(values) {
|
||||
return this.$async(async () => {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createCustomTemplate() {
|
||||
return this.$async(this.createCustomTemplateAsync);
|
||||
}
|
||||
|
||||
onChangeMethod(method) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.FileContent = '';
|
||||
this.formValues.Variables = [];
|
||||
this.selectedTemplate = null;
|
||||
this.state.Method = method;
|
||||
});
|
||||
}
|
||||
|
||||
async createCustomTemplateAsync() {
|
||||
let method = this.state.Method;
|
||||
|
||||
if (method === 'template') {
|
||||
method = 'editor';
|
||||
}
|
||||
|
||||
if (!this.validateForm(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const customTemplate = await this.createCustomTemplateByMethod(method);
|
||||
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
const userDetails = this.Authentication.getUserDetails();
|
||||
const userId = userDetails.ID;
|
||||
await this.ResourceControlService.applyResourceControl(userId, accessControlData, customTemplate.ResourceControl);
|
||||
|
||||
this.Notifications.success('Success', 'Custom template successfully created');
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('docker.templates.custom');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'A template with the same name already exists');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
validateForm(method) {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
if (method === 'editor' && this.formValues.FileContent === '') {
|
||||
this.state.formValidationError = 'Template file content must not be empty';
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = this.formValues.Title;
|
||||
const isNotUnique = _.some(this.templates, (template) => template.Title === title);
|
||||
if (isNotUnique) {
|
||||
this.state.formValidationError = 'A template with the same name already exists';
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAdmin = this.Authentication.isAdmin();
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
this.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
createCustomTemplateByMethod(method) {
|
||||
switch (method) {
|
||||
case 'editor':
|
||||
return this.createCustomTemplateFromFileContent();
|
||||
case 'upload':
|
||||
return this.createCustomTemplateFromFileUpload();
|
||||
case 'repository':
|
||||
return this.createCustomTemplateFromGitRepository();
|
||||
}
|
||||
}
|
||||
|
||||
createCustomTemplateFromFileContent() {
|
||||
return this.CustomTemplateService.createCustomTemplateFromFileContent(this.formValues);
|
||||
}
|
||||
|
||||
createCustomTemplateFromFileUpload() {
|
||||
return this.CustomTemplateService.createCustomTemplateFromFileUpload(this.formValues);
|
||||
}
|
||||
|
||||
createCustomTemplateFromGitRepository() {
|
||||
return this.CustomTemplateService.createCustomTemplateFromGitRepository(this.formValues);
|
||||
}
|
||||
|
||||
editorUpdate(value) {
|
||||
this.formValues.FileContent = value;
|
||||
this.state.isEditorDirty = true;
|
||||
this.parseTemplate(value);
|
||||
}
|
||||
|
||||
parseTemplate(templateStr) {
|
||||
if (!this.isTemplateVariablesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [variables] = getTemplateVariables(templateStr);
|
||||
|
||||
const isValid = !!variables;
|
||||
|
||||
this.state.isTemplateValid = isValid;
|
||||
|
||||
if (isValid) {
|
||||
this.onVariablesChange(intersectVariables(this.formValues.Variables, variables));
|
||||
}
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
return this.$async(async () => {
|
||||
const applicationState = this.StateManager.getState();
|
||||
|
||||
this.state.endpointMode = applicationState.endpoint.mode;
|
||||
let stackType = 0;
|
||||
if (this.state.endpointMode.provider === 'DOCKER_STANDALONE') {
|
||||
this.isDockerStandalone = true;
|
||||
stackType = 2;
|
||||
} else if (this.state.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
stackType = 1;
|
||||
}
|
||||
this.formValues.Type = stackType;
|
||||
|
||||
const { appTemplateId, type } = this.$state.params;
|
||||
|
||||
if (type) {
|
||||
this.formValues.Type = +type;
|
||||
}
|
||||
|
||||
if (appTemplateId) {
|
||||
this.formValues.FileContent = await fetchFilePreview(appTemplateId);
|
||||
}
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
this.state.loading = false;
|
||||
|
||||
this.$window.onbeforeunload = () => {
|
||||
if (this.state.Method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.state.isEditorDirty = false;
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
if (this.state.Method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty) {
|
||||
return confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateCustomTemplateViewController;
|
|
@ -1,6 +0,0 @@
|
|||
import CreateCustomTemplateViewController from './createCustomTemplateViewController.js';
|
||||
|
||||
angular.module('portainer.app').component('createCustomTemplateView', {
|
||||
templateUrl: './createCustomTemplateView.html',
|
||||
controller: CreateCustomTemplateViewController,
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue