mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +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
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:
parent
e142939929
commit
236e669332
32 changed files with 443 additions and 1089 deletions
|
@ -1,13 +1,8 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
|
||||
import { kubeEditCustomTemplateView } from './kube-edit-custom-template-view';
|
||||
|
||||
export default angular
|
||||
.module('portainer.kubernetes.custom-templates', [])
|
||||
.config(config)
|
||||
.component('kubeCustomTemplatesView', kubeCustomTemplatesView)
|
||||
.component('kubeEditCustomTemplateView', kubeEditCustomTemplateView).name;
|
||||
export default angular.module('portainer.kubernetes.custom-templates', []).config(config).component('kubeCustomTemplatesView', kubeCustomTemplatesView).name;
|
||||
|
||||
function config($stateRegistryProvider) {
|
||||
const templates = {
|
||||
|
@ -50,7 +45,7 @@ function config($stateRegistryProvider) {
|
|||
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubeEditCustomTemplateView',
|
||||
component: 'editCustomTemplatesView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import controller from './kube-edit-custom-template-view.controller.js';
|
||||
|
||||
export const kubeEditCustomTemplateView = {
|
||||
templateUrl: './kube-edit-custom-template-view.html',
|
||||
controller,
|
||||
};
|
|
@ -1,285 +0,0 @@
|
|||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
|
||||
import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
|
||||
class KubeEditCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||
Object.assign(this, { $async, $state, 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.templates = [];
|
||||
|
||||
this.validationData = {
|
||||
title: {
|
||||
pattern: KUBE_TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
error:
|
||||
"This field must consist of lower-case alphanumeric characters, '.', '_' or '-', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').",
|
||||
},
|
||||
};
|
||||
|
||||
this.getTemplate = this.getTemplate.bind(this);
|
||||
this.submitAction = this.submitAction.bind(this);
|
||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||
this.onBeforeUnload = this.onBeforeUnload.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.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(async () => {
|
||||
try {
|
||||
const { id } = this.$state.params;
|
||||
|
||||
const template = await this.CustomTemplateService.customTemplate(id);
|
||||
|
||||
if (template.GitConfig !== null) {
|
||||
this.state.isEditorReadOnly = true;
|
||||
}
|
||||
|
||||
try {
|
||||
template.FileContent = await this.CustomTemplateService.customTemplateFile(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;
|
||||
|
||||
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(values) {
|
||||
this.handleChange({ Variables: values });
|
||||
}
|
||||
|
||||
handleChange(values) {
|
||||
return this.$async(async () => {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 = this.templates.some((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(async () => {
|
||||
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('kubernetes.templates.custom');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update custom template');
|
||||
} finally {
|
||||
this.actionInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeFileContent(value) {
|
||||
if (stripSpaces(this.formValues.FileContent) !== stripSpaces(value)) {
|
||||
this.formValues.FileContent = value;
|
||||
this.parseTemplate(value);
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
this.$async(async () => {
|
||||
this.getTemplate();
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||
});
|
||||
}
|
||||
|
||||
isEditorDirty() {
|
||||
return this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty;
|
||||
}
|
||||
|
||||
uiCanExit() {
|
||||
if (this.isEditorDirty()) {
|
||||
return confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnload(event) {
|
||||
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubeEditCustomTemplateViewController;
|
||||
|
||||
function stripSpaces(str = '') {
|
||||
return str.replace(/(\r\n|\n|\r)/gm, '');
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
<page-header title="'Edit Custom Template'" breadcrumbs="[{label:'Custom templates', link:'kubernetes.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="$ctrl.form">
|
||||
<custom-templates-common-fields values="$ctrl.formValues" on-change="($ctrl.handleChange)" validation-data="$ctrl.validationData"></custom-templates-common-fields>
|
||||
|
||||
<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-form
|
||||
identifier="template-editor"
|
||||
value="$ctrl.formValues.FileContent"
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
read-only="$ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
<p>
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
|
||||
<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 || $ctrl.form.$invalid || !$ctrl.formValues.Title || !$ctrl.formValues.FileContent"
|
||||
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" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue