mirror of https://github.com/portainer/portainer
refactor(templates): migrate edit view to react [EE-6412] (#10774)
parent
e142939929
commit
236e669332
|
@ -211,10 +211,12 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||||
customTemplate.GitConfig = gitConfig
|
customTemplate.GitConfig = gitConfig
|
||||||
} else {
|
} else {
|
||||||
templateFolder := strconv.Itoa(customTemplateID)
|
templateFolder := strconv.Itoa(customTemplateID)
|
||||||
_, err = handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
|
projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to persist updated custom template file on disk", err)
|
return httperror.InternalServerError("Unable to persist updated custom template file on disk", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customTemplate.ProjectPath = projectPath
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate)
|
err = handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate)
|
||||||
|
|
|
@ -27,6 +27,3 @@ export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.';
|
||||||
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||||
export const PORTAINER_FADEOUT = 1500;
|
export const PORTAINER_FADEOUT = 1500;
|
||||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
|
||||||
export const KUBE_TEMPLATE_NAME_VALIDATION_REGEX =
|
|
||||||
'^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters
|
|
||||||
|
|
|
@ -137,7 +137,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
|
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'editCustomTemplateView',
|
component: 'editCustomTemplatesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -185,7 +185,7 @@ angular
|
||||||
|
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'edgeEditCustomTemplatesView',
|
component: 'editCustomTemplatesView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { ListView } from '@/react/edge/templates/custom-templates/ListView';
|
import { ListView } from '@/react/edge/templates/custom-templates/ListView';
|
||||||
import { EditView as EdgeEditView } from '@/react/edge/templates/custom-templates/EditView';
|
|
||||||
import { AppTemplatesView } from '@/react/edge/templates/AppTemplatesView';
|
import { AppTemplatesView } from '@/react/edge/templates/AppTemplatesView';
|
||||||
import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView/CreateView';
|
import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView';
|
||||||
|
import { EditView } from '@/react/portainer/templates/custom-templates/EditView';
|
||||||
|
|
||||||
export const templatesModule = angular
|
export const templatesModule = angular
|
||||||
.module('portainer.app.react.components.templates', [])
|
.module('portainer.app.react.components.templates', [])
|
||||||
|
@ -23,6 +23,6 @@ export const templatesModule = angular
|
||||||
r2a(withCurrentUser(withUIRouter(CreateView)), [])
|
r2a(withCurrentUser(withUIRouter(CreateView)), [])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'edgeEditCustomTemplatesView',
|
'editCustomTemplatesView',
|
||||||
r2a(withCurrentUser(withUIRouter(EdgeEditView)), [])
|
r2a(withCurrentUser(withUIRouter(EditView)), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
|
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
|
||||||
import { kubeEditCustomTemplateView } from './kube-edit-custom-template-view';
|
|
||||||
|
|
||||||
export default angular
|
export default angular.module('portainer.kubernetes.custom-templates', []).config(config).component('kubeCustomTemplatesView', kubeCustomTemplatesView).name;
|
||||||
.module('portainer.kubernetes.custom-templates', [])
|
|
||||||
.config(config)
|
|
||||||
.component('kubeCustomTemplatesView', kubeCustomTemplatesView)
|
|
||||||
.component('kubeEditCustomTemplateView', kubeEditCustomTemplateView).name;
|
|
||||||
|
|
||||||
function config($stateRegistryProvider) {
|
function config($stateRegistryProvider) {
|
||||||
const templates = {
|
const templates = {
|
||||||
|
@ -50,7 +45,7 @@ function config($stateRegistryProvider) {
|
||||||
|
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'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>
|
|
|
@ -1,9 +1,9 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
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 { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
import { confirmDelete } from '@@/modals/confirm';
|
import { confirmDelete } from '@@/modals/confirm';
|
||||||
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||||
|
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||||
|
|
||||||
class CustomTemplatesViewController {
|
class CustomTemplatesViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
|
|
@ -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>
|
|
|
@ -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;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import EditCustomTemplateViewController from './editCustomTemplateViewController.js';
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('editCustomTemplateView', {
|
|
||||||
templateUrl: './editCustomTemplateView.html',
|
|
||||||
controller: EditCustomTemplateViewController,
|
|
||||||
});
|
|
|
@ -1,108 +0,0 @@
|
||||||
import { Formik } from 'formik';
|
|
||||||
import { useRouter } from '@uirouter/react';
|
|
||||||
|
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
|
||||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
|
||||||
import { useCustomTemplateFile } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplateFile';
|
|
||||||
import { useUpdateTemplateMutation } from '@/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation';
|
|
||||||
import {
|
|
||||||
getTemplateVariables,
|
|
||||||
intersectVariables,
|
|
||||||
isTemplateVariablesEnabled,
|
|
||||||
} from '@/react/portainer/custom-templates/components/utils';
|
|
||||||
import { toGitFormModel } from '@/react/portainer/gitops/types';
|
|
||||||
import { useSaveCredentialsIfRequired } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
|
|
||||||
|
|
||||||
import { toGitRequest } from '../common/git';
|
|
||||||
|
|
||||||
import { InnerForm } from './InnerForm';
|
|
||||||
import { FormValues } from './types';
|
|
||||||
import { useValidation } from './useValidation';
|
|
||||||
|
|
||||||
export function EditTemplateForm({ template }: { template: CustomTemplate }) {
|
|
||||||
const mutation = useUpdateTemplateMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const isGit = !!template.GitConfig;
|
|
||||||
const validation = useValidation(template.Id, isGit);
|
|
||||||
const fileQuery = useCustomTemplateFile(template.Id, isGit);
|
|
||||||
const { saveCredentials, isLoading: isSaveCredentialsLoading } =
|
|
||||||
useSaveCredentialsIfRequired();
|
|
||||||
|
|
||||||
if (fileQuery.isLoading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialValues: FormValues = {
|
|
||||||
Title: template.Title,
|
|
||||||
Type: template.Type,
|
|
||||||
Description: template.Description,
|
|
||||||
Note: template.Note,
|
|
||||||
Logo: template.Logo,
|
|
||||||
Platform: template.Platform,
|
|
||||||
Variables: parseTemplate(fileQuery.data || ''),
|
|
||||||
|
|
||||||
FileContent: fileQuery.data || '',
|
|
||||||
Git: template.GitConfig ? toGitFormModel(template.GitConfig) : undefined,
|
|
||||||
EdgeSettings: template.EdgeSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
validationSchema={validation}
|
|
||||||
validateOnMount
|
|
||||||
>
|
|
||||||
<InnerForm
|
|
||||||
isLoading={mutation.isLoading || isSaveCredentialsLoading}
|
|
||||||
isEditorReadonly={isGit}
|
|
||||||
gitFileContent={isGit ? fileQuery.data : ''}
|
|
||||||
refreshGitFile={fileQuery.refetch}
|
|
||||||
gitFileError={
|
|
||||||
fileQuery.error instanceof Error ? fileQuery.error.message : ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleSubmit(values: FormValues) {
|
|
||||||
const credentialId = await saveCredentials(values.Git);
|
|
||||||
|
|
||||||
mutation.mutate(
|
|
||||||
{
|
|
||||||
id: template.Id,
|
|
||||||
EdgeTemplate: template.EdgeTemplate,
|
|
||||||
Description: values.Description,
|
|
||||||
Title: values.Title,
|
|
||||||
Type: values.Type,
|
|
||||||
Logo: values.Logo,
|
|
||||||
FileContent: values.FileContent,
|
|
||||||
Note: values.Note,
|
|
||||||
Platform: values.Platform,
|
|
||||||
Variables: values.Variables,
|
|
||||||
EdgeSettings: values.EdgeSettings,
|
|
||||||
...(values.Git ? toGitRequest(values.Git, credentialId) : {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
notifySuccess('Success', 'Template updated successfully');
|
|
||||||
router.stateService.go('^');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTemplate(templateContent: string) {
|
|
||||||
if (!isTemplateVariablesEnabled) {
|
|
||||||
return template.Variables;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [variables] = getTemplateVariables(templateContent);
|
|
||||||
|
|
||||||
if (!variables) {
|
|
||||||
return template.Variables;
|
|
||||||
}
|
|
||||||
|
|
||||||
return intersectVariables(template.Variables, variables);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useCustomTemplate } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplate';
|
|
||||||
import { notifyError } from '@/portainer/services/notifications';
|
|
||||||
|
|
||||||
import { PageHeader } from '@@/PageHeader';
|
|
||||||
import { Widget } from '@@/Widget';
|
|
||||||
|
|
||||||
import { EditTemplateForm } from './EditTemplateForm';
|
|
||||||
|
|
||||||
export function EditView() {
|
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
params: { id: templateId },
|
|
||||||
} = useCurrentStateAndParams();
|
|
||||||
const customTemplateQuery = useCustomTemplate(templateId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (customTemplateQuery.data && !customTemplateQuery.data.EdgeTemplate) {
|
|
||||||
notifyError('Error', new Error('Trying to load non edge template'));
|
|
||||||
router.stateService.go('^');
|
|
||||||
}
|
|
||||||
}, [customTemplateQuery.data, router.stateService]);
|
|
||||||
|
|
||||||
if (!customTemplateQuery.data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = customTemplateQuery.data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageHeader
|
|
||||||
title="Edit Custom Template"
|
|
||||||
breadcrumbs={[{ label: 'Custom templates', link: '^' }, template.Title]}
|
|
||||||
/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
|
||||||
<Widget.Body>
|
|
||||||
<EditTemplateForm template={template} />
|
|
||||||
</Widget.Body>
|
|
||||||
</Widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -91,33 +91,52 @@ export function CommonFields({
|
||||||
export function validation({
|
export function validation({
|
||||||
currentTemplateId,
|
currentTemplateId,
|
||||||
templates = [],
|
templates = [],
|
||||||
title,
|
viewType = 'docker',
|
||||||
}: {
|
}: {
|
||||||
currentTemplateId?: CustomTemplate['Id'];
|
currentTemplateId?: CustomTemplate['Id'];
|
||||||
templates?: Array<CustomTemplate>;
|
templates?: Array<CustomTemplate>;
|
||||||
title?: { pattern: string; error: string };
|
viewType?: 'kube' | 'docker' | 'edge';
|
||||||
} = {}): SchemaOf<Values> {
|
} = {}): SchemaOf<Values> {
|
||||||
let titleSchema = string()
|
const titlePattern = titlePatternValidation(viewType);
|
||||||
.required('Title is required.')
|
|
||||||
.test(
|
|
||||||
'is-unique',
|
|
||||||
'Title must be unique',
|
|
||||||
(value) =>
|
|
||||||
!value ||
|
|
||||||
!templates.some(
|
|
||||||
(template) =>
|
|
||||||
template.Title === value && template.Id !== currentTemplateId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (title?.pattern) {
|
|
||||||
const pattern = new RegExp(title.pattern);
|
|
||||||
titleSchema = titleSchema.matches(pattern, title.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return object({
|
return object({
|
||||||
Title: titleSchema,
|
Title: string()
|
||||||
|
.required('Title is required.')
|
||||||
|
.test(
|
||||||
|
'is-unique',
|
||||||
|
'Title must be unique',
|
||||||
|
(value) =>
|
||||||
|
!value ||
|
||||||
|
!templates.some(
|
||||||
|
(template) =>
|
||||||
|
template.Title === value && template.Id !== currentTemplateId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.matches(titlePattern.pattern, titlePattern.error),
|
||||||
Description: string().required('Description is required.'),
|
Description: string().required('Description is required.'),
|
||||||
Note: string().default(''),
|
Note: string().default(''),
|
||||||
Logo: string().default(''),
|
Logo: string().default(''),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||||
|
|
||||||
|
const KUBE_TEMPLATE_NAME_VALIDATION_REGEX =
|
||||||
|
'^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters
|
||||||
|
|
||||||
|
function titlePatternValidation(type: 'kube' | 'docker' | 'edge') {
|
||||||
|
switch (type) {
|
||||||
|
case 'kube':
|
||||||
|
return {
|
||||||
|
pattern: new RegExp(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').",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
pattern: new RegExp(TEMPLATE_NAME_VALIDATION_REGEX),
|
||||||
|
error:
|
||||||
|
"This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,19 +16,20 @@ import { InnerForm } from './InnerForm';
|
||||||
|
|
||||||
export function CreateForm({
|
export function CreateForm({
|
||||||
environmentId,
|
environmentId,
|
||||||
defaultType,
|
viewType,
|
||||||
}: {
|
}: {
|
||||||
environmentId?: EnvironmentId;
|
environmentId?: EnvironmentId;
|
||||||
defaultType: StackType;
|
viewType: 'kube' | 'docker' | 'edge';
|
||||||
}) {
|
}) {
|
||||||
const isEdge = !environmentId;
|
const isEdge = !environmentId;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const mutation = useCreateTemplateMutation();
|
const mutation = useCreateTemplateMutation();
|
||||||
const validation = useValidation(isEdge);
|
const validation = useValidation({ viewType });
|
||||||
const buildMethods = useBuildMethods();
|
const buildMethods = useBuildMethods();
|
||||||
|
|
||||||
const initialValues = useInitialValues({
|
const initialValues = useInitialValues({
|
||||||
defaultType,
|
defaultType:
|
||||||
|
viewType === 'kube' ? StackType.Kubernetes : StackType.DockerCompose,
|
||||||
isEdge,
|
isEdge,
|
||||||
buildMethods: buildMethods.map((method) => method.value),
|
buildMethods: buildMethods.map((method) => method.value),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
|
||||||
|
|
||||||
import { StackType } from '@/react/common/stacks/types';
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
import { PageHeader } from '@@/PageHeader';
|
import { PageHeader } from '@@/PageHeader';
|
||||||
import { Widget } from '@@/Widget';
|
import { Widget } from '@@/Widget';
|
||||||
|
|
||||||
|
import { useViewType } from '../useViewType';
|
||||||
|
|
||||||
import { CreateForm } from './CreateForm';
|
import { CreateForm } from './CreateForm';
|
||||||
|
|
||||||
export function CreateView() {
|
export function CreateView() {
|
||||||
const defaultType = useDefaultType();
|
const viewType = useViewType();
|
||||||
const environmentId = useEnvironmentId(false);
|
const environmentId = useEnvironmentId(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,10 +25,7 @@ export function CreateView() {
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Body>
|
<Widget.Body>
|
||||||
<CreateForm
|
<CreateForm viewType={viewType} environmentId={environmentId} />
|
||||||
defaultType={defaultType}
|
|
||||||
environmentId={environmentId}
|
|
||||||
/>
|
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,15 +33,3 @@ export function CreateView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDefaultType() {
|
|
||||||
const {
|
|
||||||
state: { name },
|
|
||||||
} = useCurrentStateAndParams();
|
|
||||||
if (name?.includes('kubernetes')) {
|
|
||||||
return StackType.Kubernetes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// edge or docker
|
|
||||||
return StackType.DockerCompose;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,11 +4,7 @@ import { CommonFields } from '@/react/portainer/custom-templates/components/Comm
|
||||||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
||||||
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
||||||
import {
|
import { isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||||
getTemplateVariables,
|
|
||||||
intersectVariables,
|
|
||||||
isTemplateVariablesEnabled,
|
|
||||||
} from '@/react/portainer/custom-templates/components/utils';
|
|
||||||
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
||||||
import { AccessControlForm } from '@/react/portainer/access-control';
|
import { AccessControlForm } from '@/react/portainer/access-control';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
@ -24,6 +20,7 @@ import { FormActions } from '@@/form-components/FormActions';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
||||||
import { EdgeTemplateSettings } from '../types';
|
import { EdgeTemplateSettings } from '../types';
|
||||||
|
import { useParseTemplateOnFileChange } from '../useParseTemplateOnFileChange';
|
||||||
|
|
||||||
import { EdgeSettingsFieldset } from './EdgeSettingsFieldset';
|
import { EdgeSettingsFieldset } from './EdgeSettingsFieldset';
|
||||||
import { FormValues, Method, initialBuildMethods } from './types';
|
import { FormValues, Method, initialBuildMethods } from './types';
|
||||||
|
@ -57,6 +54,10 @@ export function InnerForm({
|
||||||
isEditor && !isSubmitting && !isLoading
|
isEditor && !isSubmitting && !isLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeFileContent = useParseTemplateOnFileChange(
|
||||||
|
values.Variables
|
||||||
|
);
|
||||||
|
|
||||||
const texts = textByType[values.Type];
|
const texts = textByType[values.Type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -181,36 +182,6 @@ export function InnerForm({
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleChangeFileContent(value: string) {
|
|
||||||
setFieldValue(
|
|
||||||
'FileContent',
|
|
||||||
value,
|
|
||||||
isTemplateVariablesEnabled ? !value : true
|
|
||||||
);
|
|
||||||
parseTemplate(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTemplate(value: string) {
|
|
||||||
if (!isTemplateVariablesEnabled || value === '') {
|
|
||||||
setFieldValue('Variables', []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [variables, validationError] = getTemplateVariables(value);
|
|
||||||
const isValid = !!variables;
|
|
||||||
|
|
||||||
setFieldError(
|
|
||||||
'FileContent',
|
|
||||||
validationError ? `Template invalid: ${validationError}` : undefined
|
|
||||||
);
|
|
||||||
if (isValid) {
|
|
||||||
setFieldValue(
|
|
||||||
'Variables',
|
|
||||||
intersectVariables(values.Variables, variables)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChangeMethod(method: Method) {
|
function handleChangeMethod(method: Method) {
|
||||||
setFieldValue('FileContent', '');
|
setFieldValue('FileContent', '');
|
||||||
setFieldValue('Variables', []);
|
setFieldValue('Variables', []);
|
||||||
|
|
|
@ -20,7 +20,11 @@ import {
|
||||||
|
|
||||||
import { initialBuildMethods } from './types';
|
import { initialBuildMethods } from './types';
|
||||||
|
|
||||||
export function useValidation(isEdge: boolean) {
|
export function useValidation({
|
||||||
|
viewType,
|
||||||
|
}: {
|
||||||
|
viewType: 'kube' | 'docker' | 'edge';
|
||||||
|
}) {
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||||
const customTemplatesQuery = useCustomTemplates();
|
const customTemplatesQuery = useCustomTemplates();
|
||||||
|
@ -52,10 +56,13 @@ export function useValidation(isEdge: boolean) {
|
||||||
then: () => buildGitValidationSchema(gitCredentialsQuery.data || []),
|
then: () => buildGitValidationSchema(gitCredentialsQuery.data || []),
|
||||||
}),
|
}),
|
||||||
Variables: variablesValidation(),
|
Variables: variablesValidation(),
|
||||||
EdgeSettings: isEdge ? edgeFieldsetValidation() : mixed(),
|
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
|
||||||
}).concat(
|
}).concat(
|
||||||
commonFieldsValidation({ templates: customTemplatesQuery.data })
|
commonFieldsValidation({
|
||||||
|
templates: customTemplatesQuery.data,
|
||||||
|
viewType,
|
||||||
|
})
|
||||||
),
|
),
|
||||||
[customTemplatesQuery.data, gitCredentialsQuery.data, isEdge]
|
[customTemplatesQuery.data, gitCredentialsQuery.data, viewType]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { useEnvironmentDeploymentOptions } from '@/react/portainer/environments/queries/useEnvironment';
|
||||||
|
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||||
|
import { isKubernetesEnvironment } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
|
import { CustomTemplate } from '../types';
|
||||||
|
import { useUpdateTemplateMutation } from '../queries/useUpdateTemplateMutation';
|
||||||
|
import { useCustomTemplateFile } from '../queries/useCustomTemplateFile';
|
||||||
|
import { TemplateViewType } from '../useViewType';
|
||||||
|
|
||||||
|
import { useInitialValues } from './useInitialValues';
|
||||||
|
import { FormValues } from './types';
|
||||||
|
import { useValidation } from './useValidation';
|
||||||
|
import { InnerForm } from './InnerForm';
|
||||||
|
|
||||||
|
export function EditForm({
|
||||||
|
template,
|
||||||
|
environmentId,
|
||||||
|
viewType,
|
||||||
|
}: {
|
||||||
|
template: CustomTemplate;
|
||||||
|
environmentId?: EnvironmentId;
|
||||||
|
viewType: TemplateViewType;
|
||||||
|
}) {
|
||||||
|
const isEdge = template.EdgeTemplate;
|
||||||
|
const isGit = !!template.GitConfig;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const disableEditor = useDisableEditor(isGit);
|
||||||
|
const mutation = useUpdateTemplateMutation();
|
||||||
|
const validation = useValidation({
|
||||||
|
viewType,
|
||||||
|
isGit,
|
||||||
|
templateId: template.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileContentQuery = useCustomTemplateFile(template.Id);
|
||||||
|
|
||||||
|
const initialValues = useInitialValues({
|
||||||
|
isEdge,
|
||||||
|
template,
|
||||||
|
templateFile: fileContentQuery.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileContentQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={validation}
|
||||||
|
validateOnMount
|
||||||
|
>
|
||||||
|
<InnerForm
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
environmentId={environmentId}
|
||||||
|
isEditorReadonly={disableEditor}
|
||||||
|
refreshGitFile={fileContentQuery.refetch}
|
||||||
|
gitFileContent={fileContentQuery.data}
|
||||||
|
gitFileError={
|
||||||
|
fileContentQuery.error instanceof Error
|
||||||
|
? fileContentQuery.error.message
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit(values: FormValues) {
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
id: template.Id,
|
||||||
|
EdgeTemplate: template.EdgeTemplate,
|
||||||
|
Description: values.Description,
|
||||||
|
Title: values.Title,
|
||||||
|
Type: values.Type,
|
||||||
|
Logo: values.Logo,
|
||||||
|
FileContent: values.FileContent,
|
||||||
|
Note: values.Note,
|
||||||
|
Platform: values.Platform,
|
||||||
|
Variables: values.Variables,
|
||||||
|
EdgeSettings: values.EdgeSettings,
|
||||||
|
AccessControl: values.AccessControl,
|
||||||
|
resourceControlId: template.ResourceControl?.Id,
|
||||||
|
...values.Git,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
notifySuccess('Success', 'Template updated successfully');
|
||||||
|
router.stateService.go('^');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDisableEditor(isGit: boolean) {
|
||||||
|
const environment = useCurrentEnvironment(false);
|
||||||
|
|
||||||
|
const deploymentOptionsQuery = useEnvironmentDeploymentOptions(
|
||||||
|
environment.data && isKubernetesEnvironment(environment.data.Type)
|
||||||
|
? environment.data.Id
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return isGit || !!deploymentOptionsQuery.data?.hideAddWithForm;
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget } from '@@/Widget';
|
||||||
|
|
||||||
|
import { useCustomTemplate } from '../queries/useCustomTemplate';
|
||||||
|
import { useViewType } from '../useViewType';
|
||||||
|
|
||||||
|
import { EditForm } from './EditForm';
|
||||||
|
|
||||||
|
export function EditView() {
|
||||||
|
const viewType = useViewType();
|
||||||
|
const {
|
||||||
|
params: { id },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const environmentId = useEnvironmentId(false);
|
||||||
|
const templateQuery = useCustomTemplate(id);
|
||||||
|
|
||||||
|
if (!templateQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = templateQuery.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Edit Custom template"
|
||||||
|
breadcrumbs={[{ label: 'Custom Templates', link: '^' }, template.Title]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Body>
|
||||||
|
<EditForm
|
||||||
|
environmentId={environmentId}
|
||||||
|
template={template}
|
||||||
|
viewType={viewType}
|
||||||
|
/>
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,31 +5,36 @@ import { CommonFields } from '@/react/portainer/custom-templates/components/Comm
|
||||||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
||||||
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
||||||
import {
|
import { isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||||
getTemplateVariables,
|
|
||||||
intersectVariables,
|
|
||||||
isTemplateVariablesEnabled,
|
|
||||||
} from '@/react/portainer/custom-templates/components/utils';
|
|
||||||
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
||||||
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
|
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
|
||||||
import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
|
import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
|
||||||
import { EdgeSettingsFieldset } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset';
|
import { EdgeSettingsFieldset } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset';
|
||||||
|
import { StackType } from '@/react/common/stacks/types';
|
||||||
|
import { textByType } from '@/react/common/stacks/common/form-texts';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { AccessControlForm } from '@/react/portainer/access-control';
|
||||||
|
import { AccessControlFormData } from '@/react/portainer/access-control/types';
|
||||||
|
|
||||||
import { WebEditorForm, usePreventExit } from '@@/WebEditorForm';
|
import { WebEditorForm, usePreventExit } from '@@/WebEditorForm';
|
||||||
import { FormActions } from '@@/form-components/FormActions';
|
import { FormActions } from '@@/form-components/FormActions';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { FormError } from '@@/form-components/FormError';
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
|
import { useParseTemplateOnFileChange } from '../useParseTemplateOnFileChange';
|
||||||
|
|
||||||
import { FormValues } from './types';
|
import { FormValues } from './types';
|
||||||
|
|
||||||
export function InnerForm({
|
export function InnerForm({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
environmentId,
|
||||||
isEditorReadonly,
|
isEditorReadonly,
|
||||||
gitFileContent,
|
gitFileContent,
|
||||||
gitFileError,
|
gitFileError,
|
||||||
refreshGitFile,
|
refreshGitFile,
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
environmentId?: EnvironmentId;
|
||||||
isEditorReadonly: boolean;
|
isEditorReadonly: boolean;
|
||||||
gitFileContent?: string;
|
gitFileContent?: string;
|
||||||
gitFileError?: string;
|
gitFileError?: string;
|
||||||
|
@ -42,9 +47,9 @@ export function InnerForm({
|
||||||
errors,
|
errors,
|
||||||
isValid,
|
isValid,
|
||||||
setFieldError,
|
setFieldError,
|
||||||
|
setValues,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
dirty,
|
dirty,
|
||||||
setValues,
|
|
||||||
} = useFormikContext<FormValues>();
|
} = useFormikContext<FormValues>();
|
||||||
|
|
||||||
usePreventExit(
|
usePreventExit(
|
||||||
|
@ -52,6 +57,13 @@ export function InnerForm({
|
||||||
values.FileContent,
|
values.FileContent,
|
||||||
!isEditorReadonly && !isSubmitting && !isLoading
|
!isEditorReadonly && !isSubmitting && !isLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeFileContent = useParseTemplateOnFileChange(
|
||||||
|
values.Variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const texts = textByType[values.Type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="form-horizontal">
|
<Form className="form-horizontal">
|
||||||
<CommonFields
|
<CommonFields
|
||||||
|
@ -62,15 +74,19 @@ export function InnerForm({
|
||||||
errors={errors}
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PlatformField
|
{values.Type !== StackType.Kubernetes && (
|
||||||
value={values.Platform}
|
<>
|
||||||
onChange={(value) => setFieldValue('Platform', value)}
|
<PlatformField
|
||||||
/>
|
value={values.Platform}
|
||||||
|
onChange={(value) => setFieldValue('Platform', value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<TemplateTypeSelector
|
<TemplateTypeSelector
|
||||||
value={values.Type}
|
value={values.Type}
|
||||||
onChange={(value) => setFieldValue('Type', value)}
|
onChange={(value) => setFieldValue('Type', value)}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<WebEditorForm
|
<WebEditorForm
|
||||||
id="edit-custom-template-editor"
|
id="edit-custom-template-editor"
|
||||||
|
@ -80,33 +96,14 @@ export function InnerForm({
|
||||||
placeholder={
|
placeholder={
|
||||||
gitFileContent
|
gitFileContent
|
||||||
? 'Preview of the file from git repository'
|
? 'Preview of the file from git repository'
|
||||||
: 'Define or paste the content of your docker compose file here'
|
: texts.editor.placeholder
|
||||||
}
|
}
|
||||||
error={errors.FileContent}
|
error={errors.FileContent}
|
||||||
readonly={isEditorReadonly}
|
readonly={isEditorReadonly}
|
||||||
>
|
>
|
||||||
<p>
|
{texts.editor.description}
|
||||||
You can get more information about Compose file format in the{' '}
|
|
||||||
<a
|
|
||||||
href="https://docs.docker.com/compose/compose-file/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
official documentation
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</WebEditorForm>
|
</WebEditorForm>
|
||||||
|
|
||||||
{isTemplateVariablesEnabled && (
|
|
||||||
<CustomTemplatesVariablesDefinitionField
|
|
||||||
value={values.Variables}
|
|
||||||
onChange={(values) => setFieldValue('Variables', values)}
|
|
||||||
isVariablesNamesFromParent={!isEditorReadonly}
|
|
||||||
errors={errors.Variables}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{values.Git && (
|
{values.Git && (
|
||||||
<>
|
<>
|
||||||
<GitForm
|
<GitForm
|
||||||
|
@ -118,6 +115,9 @@ export function InnerForm({
|
||||||
Git: { ...values.Git!, ...newValues },
|
Git: { ...values.Git!, ...newValues },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
deployMethod={
|
||||||
|
values.Type === StackType.Kubernetes ? 'manifest' : 'compose'
|
||||||
|
}
|
||||||
errors={typeof errors.Git === 'object' ? errors.Git : undefined}
|
errors={typeof errors.Git === 'object' ? errors.Git : undefined}
|
||||||
/>
|
/>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -137,6 +137,25 @@ export function InnerForm({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isTemplateVariablesEnabled && (
|
||||||
|
<CustomTemplatesVariablesDefinitionField
|
||||||
|
value={values.Variables}
|
||||||
|
onChange={(values) => setFieldValue('Variables', values)}
|
||||||
|
isVariablesNamesFromParent={!isEditorReadonly}
|
||||||
|
errors={errors.Variables}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!values.AccessControl && (
|
||||||
|
<AccessControlForm
|
||||||
|
environmentId={environmentId || 0}
|
||||||
|
onChange={(values) => setFieldValue('AccessControl', values)}
|
||||||
|
values={values.AccessControl}
|
||||||
|
errors={errors.AccessControl as FormikErrors<AccessControlFormData>}
|
||||||
|
formNamespace="accessControl"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{values.EdgeSettings && (
|
{values.EdgeSettings && (
|
||||||
<EdgeSettingsFieldset
|
<EdgeSettingsFieldset
|
||||||
setValues={(edgeValues) =>
|
setValues={(edgeValues) =>
|
||||||
|
@ -163,33 +182,4 @@ export function InnerForm({
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleChangeFileContent(value: string) {
|
|
||||||
setFieldValue(
|
|
||||||
'FileContent',
|
|
||||||
value,
|
|
||||||
isTemplateVariablesEnabled ? !value : true
|
|
||||||
);
|
|
||||||
parseTemplate(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTemplate(value: string) {
|
|
||||||
if (!isTemplateVariablesEnabled || value === '') {
|
|
||||||
setFieldValue('Variables', []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [variables, validationError] = getTemplateVariables(value);
|
|
||||||
|
|
||||||
setFieldError(
|
|
||||||
'FileContent',
|
|
||||||
validationError ? `Template invalid: ${validationError}` : undefined
|
|
||||||
);
|
|
||||||
if (variables) {
|
|
||||||
setFieldValue(
|
|
||||||
'Variables',
|
|
||||||
intersectVariables(values.Variables, variables)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { StackType } from '@/react/common/stacks/types';
|
import { StackType } from '@/react/common/stacks/types';
|
||||||
|
import { type Values as CommonFieldsValues } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||||
import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { Platform } from '@/react/portainer/templates/types';
|
import { Platform } from '@/react/portainer/templates/types';
|
||||||
import { type Values as CommonFieldsValues } from '@/react/portainer/custom-templates/components/CommonFields';
|
|
||||||
import { GitFormModel } from '@/react/portainer/gitops/types';
|
import { GitFormModel } from '@/react/portainer/gitops/types';
|
||||||
import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
|
import { AccessControlFormData } from '@/react/portainer/access-control/types';
|
||||||
|
|
||||||
|
import { EdgeTemplateSettings } from '../types';
|
||||||
|
|
||||||
export interface FormValues extends CommonFieldsValues {
|
export interface FormValues extends CommonFieldsValues {
|
||||||
Platform: Platform;
|
Platform: Platform;
|
||||||
|
@ -11,5 +13,6 @@ export interface FormValues extends CommonFieldsValues {
|
||||||
FileContent: string;
|
FileContent: string;
|
||||||
Git?: GitFormModel;
|
Git?: GitFormModel;
|
||||||
Variables: DefinitionFieldValues;
|
Variables: DefinitionFieldValues;
|
||||||
|
AccessControl?: AccessControlFormData;
|
||||||
EdgeSettings?: EdgeTemplateSettings;
|
EdgeSettings?: EdgeTemplateSettings;
|
||||||
}
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
|
||||||
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
import { toGitFormModel } from '@/react/portainer/gitops/types';
|
||||||
|
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
|
import { CustomTemplate } from '../types';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function useInitialValues({
|
||||||
|
template,
|
||||||
|
templateFile,
|
||||||
|
isEdge,
|
||||||
|
}: {
|
||||||
|
template: CustomTemplate;
|
||||||
|
templateFile: string | undefined;
|
||||||
|
isEdge: boolean;
|
||||||
|
}): FormValues {
|
||||||
|
const { user, isAdmin } = useCurrentUser();
|
||||||
|
|
||||||
|
return {
|
||||||
|
Title: template.Title,
|
||||||
|
FileContent: templateFile || '',
|
||||||
|
Type: template.Type,
|
||||||
|
Platform: template.Platform,
|
||||||
|
Description: template.Description,
|
||||||
|
Note: template.Note,
|
||||||
|
Logo: template.Logo,
|
||||||
|
Variables: template.Variables,
|
||||||
|
Git: template.GitConfig ? toGitFormModel(template.GitConfig) : undefined,
|
||||||
|
AccessControl:
|
||||||
|
!isEdge && template.ResourceControl
|
||||||
|
? parseAccessControlFormData(
|
||||||
|
isAdmin,
|
||||||
|
user.Id,
|
||||||
|
new ResourceControlViewModel(template.ResourceControl)
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
EdgeSettings: template.EdgeSettings,
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,19 +3,26 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import { StackType } from '@/react/common/stacks/types';
|
import { StackType } from '@/react/common/stacks/types';
|
||||||
import { validation as commonFieldsValidation } from '@/react/portainer/custom-templates/components/CommonFields';
|
import { validation as commonFieldsValidation } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
import { Platform } from '@/react/portainer/templates/types';
|
||||||
import { variablesValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
import { variablesValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm';
|
import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm';
|
||||||
import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
|
import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
|
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
|
||||||
import { Platform } from '@/react/portainer/templates/types';
|
|
||||||
import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation';
|
import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation';
|
||||||
|
|
||||||
export function useValidation(
|
import { CustomTemplate } from '../types';
|
||||||
currentTemplateId: CustomTemplate['Id'],
|
import { TemplateViewType } from '../useViewType';
|
||||||
isGit: boolean
|
|
||||||
) {
|
export function useValidation({
|
||||||
|
isGit,
|
||||||
|
templateId,
|
||||||
|
viewType,
|
||||||
|
}: {
|
||||||
|
isGit: boolean;
|
||||||
|
templateId: CustomTemplate['Id'];
|
||||||
|
viewType: TemplateViewType;
|
||||||
|
}) {
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||||
const customTemplatesQuery = useCustomTemplates();
|
const customTemplatesQuery = useCustomTemplates();
|
||||||
|
@ -33,26 +40,26 @@ export function useValidation(
|
||||||
StackType.Kubernetes,
|
StackType.Kubernetes,
|
||||||
])
|
])
|
||||||
.default(StackType.DockerCompose),
|
.default(StackType.DockerCompose),
|
||||||
FileContent: isGit
|
FileContent: string().required('Template is required.'),
|
||||||
? string().default('')
|
|
||||||
: string().required('Template is required.'),
|
|
||||||
|
|
||||||
Git: isGit
|
Git: isGit
|
||||||
? buildGitValidationSchema(gitCredentialsQuery.data || [])
|
? buildGitValidationSchema(gitCredentialsQuery.data || [])
|
||||||
: mixed(),
|
: mixed(),
|
||||||
Variables: variablesValidation(),
|
Variables: variablesValidation(),
|
||||||
EdgeSettings: edgeFieldsetValidation(),
|
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
|
||||||
}).concat(
|
}).concat(
|
||||||
commonFieldsValidation({
|
commonFieldsValidation({
|
||||||
templates: customTemplatesQuery.data,
|
templates: customTemplatesQuery.data,
|
||||||
currentTemplateId,
|
currentTemplateId: templateId,
|
||||||
|
viewType,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
currentTemplateId,
|
|
||||||
customTemplatesQuery.data,
|
customTemplatesQuery.data,
|
||||||
gitCredentialsQuery.data,
|
gitCredentialsQuery.data,
|
||||||
isGit,
|
isGit,
|
||||||
|
templateId,
|
||||||
|
viewType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -42,7 +42,6 @@ interface CreateTemplatePayload {
|
||||||
Description: string;
|
Description: string;
|
||||||
Note: string;
|
Note: string;
|
||||||
Logo: string;
|
Logo: string;
|
||||||
AccessControl?: AccessControlFormData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateTemplateMutation() {
|
export function useCreateTemplateMutation() {
|
||||||
|
@ -50,7 +49,9 @@ export function useCreateTemplateMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
async (payload: CreateTemplatePayload) => {
|
async (
|
||||||
|
payload: CreateTemplatePayload & { AccessControl?: AccessControlFormData }
|
||||||
|
) => {
|
||||||
const template = await createTemplate(user.Id, payload);
|
const template = await createTemplate(user.Id, payload);
|
||||||
const resourceControl = template.ResourceControl;
|
const resourceControl = template.ResourceControl;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ export function useCustomTemplateFile(id?: CustomTemplate['Id'], git = false) {
|
||||||
{
|
{
|
||||||
...withGlobalError('Failed to get custom template file'),
|
...withGlobalError('Failed to get custom template file'),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
|
// there's nothing to do with a new file content, so we're disabling refetch
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
} from '@/react-tools/react-query';
|
} from '@/react-tools/react-query';
|
||||||
import { StackType } from '@/react/common/stacks/types';
|
import { StackType } from '@/react/common/stacks/types';
|
||||||
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
|
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
|
||||||
|
import { AccessControlFormData } from '@/react/portainer/access-control/types';
|
||||||
|
import { applyResourceControl } from '@/react/portainer/access-control/access-control.service';
|
||||||
|
|
||||||
import { CustomTemplate, EdgeTemplateSettings } from '../types';
|
import { CustomTemplate, EdgeTemplateSettings } from '../types';
|
||||||
import { Platform } from '../../types';
|
import { Platform } from '../../types';
|
||||||
|
@ -18,7 +20,21 @@ export function useUpdateTemplateMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
updateTemplate,
|
async (
|
||||||
|
payload: CustomTemplateUpdatePayload & {
|
||||||
|
AccessControl?: AccessControlFormData;
|
||||||
|
resourceControlId?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
await updateTemplate(payload);
|
||||||
|
|
||||||
|
if (payload.resourceControlId && payload.AccessControl) {
|
||||||
|
await applyResourceControl(
|
||||||
|
payload.AccessControl,
|
||||||
|
payload.resourceControlId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
mutationOptions(
|
mutationOptions(
|
||||||
withInvalidate(queryClient, [['custom-templates']]),
|
withInvalidate(queryClient, [['custom-templates']]),
|
||||||
withGlobalError('Failed to update template')
|
withGlobalError('Failed to update template')
|
||||||
|
@ -30,6 +46,7 @@ export function useUpdateTemplateMutation() {
|
||||||
* Payload for updating a custom template
|
* Payload for updating a custom template
|
||||||
*/
|
*/
|
||||||
interface CustomTemplateUpdatePayload {
|
interface CustomTemplateUpdatePayload {
|
||||||
|
id: CustomTemplate['Id'];
|
||||||
/** URL of the template's logo */
|
/** URL of the template's logo */
|
||||||
Logo?: string;
|
Logo?: string;
|
||||||
/** Title of the template */
|
/** Title of the template */
|
||||||
|
@ -78,9 +95,7 @@ interface CustomTemplateUpdatePayload {
|
||||||
EdgeSettings?: EdgeTemplateSettings;
|
EdgeSettings?: EdgeTemplateSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTemplate(
|
async function updateTemplate(values: CustomTemplateUpdatePayload) {
|
||||||
values: CustomTemplateUpdatePayload & { id: CustomTemplate['Id'] }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.put<CustomTemplate>(
|
const { data } = await axios.put<CustomTemplate>(
|
||||||
buildUrl({ id: values.id }),
|
buildUrl({ id: values.id }),
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { VariableDefinition } from '../../custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
|
import {
|
||||||
|
getTemplateVariables,
|
||||||
|
intersectVariables,
|
||||||
|
isTemplateVariablesEnabled,
|
||||||
|
} from '../../custom-templates/components/utils';
|
||||||
|
|
||||||
|
export function useParseTemplateOnFileChange(
|
||||||
|
oldVariables: VariableDefinition[]
|
||||||
|
) {
|
||||||
|
const { setFieldValue, setFieldError } = useFormikContext();
|
||||||
|
|
||||||
|
return handleChangeFileContent;
|
||||||
|
|
||||||
|
function handleChangeFileContent(value: string) {
|
||||||
|
setFieldValue(
|
||||||
|
'FileContent',
|
||||||
|
value,
|
||||||
|
isTemplateVariablesEnabled ? !value : true
|
||||||
|
);
|
||||||
|
parseTemplate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTemplate(value: string) {
|
||||||
|
if (!isTemplateVariablesEnabled || value === '') {
|
||||||
|
setFieldValue('Variables', []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [variables, validationError] = getTemplateVariables(value);
|
||||||
|
const isValid = !!variables;
|
||||||
|
|
||||||
|
setFieldError(
|
||||||
|
'FileContent',
|
||||||
|
validationError ? `Template invalid: ${validationError}` : undefined
|
||||||
|
);
|
||||||
|
if (isValid) {
|
||||||
|
setFieldValue('Variables', intersectVariables(oldVariables, variables));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
export type TemplateViewType = 'kube' | 'docker' | 'edge';
|
||||||
|
|
||||||
|
export function useViewType(): TemplateViewType {
|
||||||
|
const {
|
||||||
|
state: { name },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
if (name?.includes('kubernetes')) {
|
||||||
|
return 'kube';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name?.includes('docker')) {
|
||||||
|
return 'docker';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'edge';
|
||||||
|
}
|
Loading…
Reference in New Issue