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

refactor(templates): migrate edit view to react [EE-6412] (#10774)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run

This commit is contained in:
Chaim Lev-Ari 2024-01-08 14:32:32 +07:00 committed by GitHub
parent e142939929
commit 236e669332
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 443 additions and 1089 deletions

View file

@ -1,9 +1,9 @@
import _ from 'lodash-es';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
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 */

View file

@ -1,98 +0,0 @@
<page-header title="'Edit Custom Template'" breadcrumbs="[{label:'Custom templates', link:'docker.templates.custom'}, $ctrl.formValues.Title]" reload="true"> </page-header>
<div class="row" ng-if="$ctrl.formValues">
<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>
<git-form value="$ctrl.formValues" on-change="($ctrl.handleChange)" ng-if="$ctrl.formValues.GitConfig"></git-form>
<div class="form-group">
<div class="col-sm-12"
><button type="button" class="btn btn-sm btn-light !ml-0" ng-if="$ctrl.formValues.GitConfig" ng-click="$ctrl.previewFileFromGitRepository()">
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Reload custom template</button
>
</div>
<div class="col-sm-12" ng-if="$ctrl.state.templatePreviewFailed">
<p class="small vertical-center text-danger mt-5">
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>
Custom template could not be loaded, {{ $ctrl.state.templatePreviewError }}.</p
>
</div>
</div>
<!-- web-editor -->
<web-editor-form
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 -->
<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="true"
></custom-templates-variables-definition-field>
<por-access-control-form
form-data="$ctrl.formValues.AccessControlData"
resource-control="$ctrl.formValues.ResourceControl"
ng-if="$ctrl.formValues.AccessControlData"
></por-access-control-form>
<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.actionInProgress || customTemplateForm.$invalid
|| !$ctrl.formValues.Title
|| !$ctrl.formValues.FileContent
|| !$ctrl.state.isTemplateValid
"
ng-click="$ctrl.submitAction()"
button-spinner="$ctrl.actionInProgress"
>
<span ng-hide="$ctrl.actionInProgress">Update the template</span>
<span ng-show="$ctrl.actionInProgress">Update in progress...</span>
</button>
<span class="text-danger space-left" ng-if="$ctrl.state.formValidationError">
{{ $ctrl.state.formValidationError }}
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,274 +0,0 @@
import _ from 'lodash';
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
class EditCustomTemplateViewController {
/* @ngInject */
constructor($async, $state, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
this.formValues = {
Variables: [],
TLSSkipVerify: false,
Title: '',
Description: '',
Note: '',
Logo: '',
};
this.state = {
formValidationError: '',
isEditorDirty: false,
isTemplateValid: true,
isEditorReadOnly: false,
templateLoadFailed: false,
templatePreviewFailed: false,
templatePreviewError: '',
};
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.getTemplate = this.getTemplate.bind(this);
this.getTemplateAsync = this.getTemplateAsync.bind(this);
this.submitAction = this.submitAction.bind(this);
this.submitActionAsync = this.submitActionAsync.bind(this);
this.editorUpdate = this.editorUpdate.bind(this);
this.onVariablesChange = this.onVariablesChange.bind(this);
this.handleChange = this.handleChange.bind(this);
this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this);
this.onChangePlatform = this.onChangePlatform.bind(this);
this.onChangeType = this.onChangeType.bind(this);
}
onChangePlatform(value) {
this.handleChange({ Platform: value });
}
onChangeType(value) {
this.handleChange({ Type: value });
}
getTemplate() {
return this.$async(this.getTemplateAsync);
}
async getTemplateAsync() {
try {
const template = await this.CustomTemplateService.customTemplate(this.$state.params.id);
if (template.GitConfig !== null) {
this.state.isEditorReadOnly = true;
}
try {
template.FileContent = await this.CustomTemplateService.customTemplateFile(this.$state.params.id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
throw err;
}
template.Variables = template.Variables || [];
this.formValues = { ...this.formValues, ...template };
this.parseTemplate(template.FileContent);
this.parseGitConfig(template.GitConfig);
this.oldFileContent = this.formValues.FileContent;
if (template.ResourceControl) {
this.formValues.ResourceControl = new ResourceControlViewModel(template.ResourceControl);
}
this.formValues.AccessControlData = new AccessControlFormData();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
}
}
onVariablesChange(value) {
this.handleChange({ Variables: value });
}
handleChange(values) {
return this.$async(async () => {
this.formValues = {
...this.formValues,
...values,
};
});
}
validateForm() {
this.state.formValidationError = '';
if (!this.formValues.FileContent) {
this.state.formValidationError = 'Template file content must not be empty';
return false;
}
const title = this.formValues.Title;
const id = this.$state.params.id;
const isNotUnique = _.some(this.templates, (template) => template.Title === title && template.Id != id);
if (isNotUnique) {
this.state.formValidationError = `A template with the name ${title} 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;
}
submitAction() {
return this.$async(this.submitActionAsync);
}
async submitActionAsync() {
if (!this.validateForm()) {
return;
}
this.actionInProgress = true;
try {
await this.CustomTemplateService.updateCustomTemplate(this.formValues.Id, this.formValues);
const userDetails = this.Authentication.getUserDetails();
const userId = userDetails.ID;
await this.ResourceControlService.applyResourceControl(userId, this.formValues.AccessControlData, this.formValues.ResourceControl);
this.Notifications.success('Success', 'Custom template successfully updated');
this.state.isEditorDirty = false;
this.$state.go('docker.templates.custom');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update custom template');
} finally {
this.actionInProgress = false;
}
}
editorUpdate(value) {
if (this.formValues.FileContent.replace(/(\r\n|\n|\r)/gm, '') !== value.replace(/(\r\n|\n|\r)/gm, '')) {
this.formValues.FileContent = value;
this.parseTemplate(value);
this.state.isEditorDirty = true;
}
}
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));
}
}
parseGitConfig(config) {
if (config === null) {
return;
}
let flatConfig = {
RepositoryURL: config.URL,
RepositoryReferenceName: config.ReferenceName,
ComposeFilePathInRepository: config.ConfigFilePath,
RepositoryAuthentication: config.Authentication !== null,
TLSSkipVerify: config.TLSSkipVerify,
};
if (config.Authentication) {
flatConfig = {
...flatConfig,
RepositoryUsername: config.Authentication.Username,
RepositoryPassword: config.Authentication.Password,
};
}
this.formValues = { ...this.formValues, ...flatConfig };
}
previewFileFromGitRepository() {
this.state.templatePreviewFailed = false;
this.state.templatePreviewError = '';
let creds = {};
if (this.formValues.RepositoryAuthentication) {
creds = {
username: this.formValues.RepositoryUsername,
password: this.formValues.RepositoryPassword,
};
}
const payload = {
repository: this.formValues.RepositoryURL,
targetFile: this.formValues.ComposeFilePathInRepository,
tlsSkipVerify: this.formValues.TLSSkipVerify,
...creds,
};
this.$async(async () => {
try {
this.formValues.FileContent = await getFilePreview(payload);
this.state.isEditorDirty = true;
// check if the template contains mustache template symbol
this.parseTemplate(this.formValues.FileContent);
} catch (err) {
this.state.templatePreviewError = err.message;
this.state.templatePreviewFailed = true;
}
});
}
async uiCanExit() {
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
return confirmWebEditorDiscard();
}
}
async $onInit() {
this.getTemplate();
try {
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
} catch (err) {
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
}
this.$window.onbeforeunload = () => {
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
return '';
}
};
}
$onDestroy() {
this.state.isEditorDirty = false;
}
}
export default EditCustomTemplateViewController;

View file

@ -1,6 +0,0 @@
import EditCustomTemplateViewController from './editCustomTemplateViewController.js';
angular.module('portainer.app').component('editCustomTemplateView', {
templateUrl: './editCustomTemplateView.html',
controller: EditCustomTemplateViewController,
});