diff --git a/app/docker/__module.js b/app/docker/__module.js
index ea2c5cb8e..062026ac9 100644
--- a/app/docker/__module.js
+++ b/app/docker/__module.js
@@ -126,7 +126,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
views: {
'content@': {
- component: 'createCustomTemplateView',
+ component: 'createCustomTemplatesView',
},
},
};
diff --git a/app/edge/__module.js b/app/edge/__module.js
index e0b31efdf..131a4f039 100644
--- a/app/edge/__module.js
+++ b/app/edge/__module.js
@@ -174,7 +174,7 @@ angular
views: {
'content@': {
- component: 'edgeCreateCustomTemplatesView',
+ component: 'createCustomTemplatesView',
},
},
});
diff --git a/app/edge/react/views/templates.ts b/app/edge/react/views/templates.ts
index c2b1caedc..3b8fe2044 100644
--- a/app/edge/react/views/templates.ts
+++ b/app/edge/react/views/templates.ts
@@ -4,9 +4,9 @@ import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { ListView } from '@/react/edge/templates/custom-templates/ListView';
-import { CreateView } from '@/react/edge/templates/custom-templates/CreateView';
-import { EditView } from '@/react/edge/templates/custom-templates/EditView';
+import { EditView as EdgeEditView } from '@/react/edge/templates/custom-templates/EditView';
import { AppTemplatesView } from '@/react/edge/templates/AppTemplatesView';
+import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView/CreateView';
export const templatesModule = angular
.module('portainer.app.react.components.templates', [])
@@ -19,10 +19,10 @@ export const templatesModule = angular
r2a(withCurrentUser(withUIRouter(ListView)), [])
)
.component(
- 'edgeCreateCustomTemplatesView',
+ 'createCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(CreateView)), [])
)
.component(
'edgeEditCustomTemplatesView',
- r2a(withCurrentUser(withUIRouter(EditView)), [])
+ r2a(withCurrentUser(withUIRouter(EdgeEditView)), [])
).name;
diff --git a/app/kubernetes/custom-templates/index.js b/app/kubernetes/custom-templates/index.js
index 522b94729..ef33784cb 100644
--- a/app/kubernetes/custom-templates/index.js
+++ b/app/kubernetes/custom-templates/index.js
@@ -2,14 +2,12 @@ import angular from 'angular';
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
import { kubeEditCustomTemplateView } from './kube-edit-custom-template-view';
-import { kubeCreateCustomTemplateView } from './kube-create-custom-template-view';
export default angular
.module('portainer.kubernetes.custom-templates', [])
.config(config)
.component('kubeCustomTemplatesView', kubeCustomTemplatesView)
- .component('kubeEditCustomTemplateView', kubeEditCustomTemplateView)
- .component('kubeCreateCustomTemplateView', kubeCreateCustomTemplateView).name;
+ .component('kubeEditCustomTemplateView', kubeEditCustomTemplateView).name;
function config($stateRegistryProvider) {
const templates = {
@@ -38,7 +36,7 @@ function config($stateRegistryProvider) {
views: {
'content@': {
- component: 'kubeCreateCustomTemplateView',
+ component: 'createCustomTemplatesView',
},
},
params: {
diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/index.js b/app/kubernetes/custom-templates/kube-create-custom-template-view/index.js
deleted file mode 100644
index 203af9545..000000000
--- a/app/kubernetes/custom-templates/kube-create-custom-template-view/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import controller from './kube-create-custom-template-view.controller.js';
-
-export const kubeCreateCustomTemplateView = {
- templateUrl: './kube-create-custom-template-view.html',
- controller,
-};
diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js
deleted file mode 100644
index 92d58f218..000000000
--- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
-import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
-import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
-import { confirmWebEditorDiscard } from '@@/modals/confirm';
-import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
-
-class KubeCreateCustomTemplateViewController {
- /* @ngInject */
- constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
- Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
-
- this.methodOptions = [editor, upload, git];
-
- this.templates = null;
- this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
-
- this.state = {
- method: 'editor',
- actionInProgress: false,
- formValidationError: '',
- isEditorDirty: false,
- isTemplateValid: true,
- };
-
- this.formValues = {
- FileContent: '',
- File: null,
- Title: '',
- Description: '',
- Note: '',
- Logo: '',
- AccessControlData: new AccessControlFormData(),
- Variables: [],
- RepositoryURL: '',
- RepositoryURLValid: false,
- RepositoryReferenceName: 'refs/heads/main',
- RepositoryAuthentication: false,
- RepositoryUsername: '',
- RepositoryPassword: '',
- ComposeFilePathInRepository: 'manifest.yml',
- };
-
- 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.onChangeFile = this.onChangeFile.bind(this);
- this.onChangeFileContent = this.onChangeFileContent.bind(this);
- this.onChangeMethod = this.onChangeMethod.bind(this);
- this.onBeforeOnload = this.onBeforeOnload.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.onVariablesChange = this.onVariablesChange.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 });
- }
-
- onChangeMethod(method) {
- this.state.method = method;
- this.formValues.Variables = [];
- }
-
- onChangeFileContent(content) {
- this.handleChange({ FileContent: content });
- this.parseTemplate(content);
- 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));
- }
- }
-
- onVariablesChange(value) {
- this.handleChange({ Variables: value });
- }
-
- onChangeFile(file) {
- this.handleChange({ File: file });
- }
-
- handleChange(values) {
- return this.$async(async () => {
- this.formValues = {
- ...this.formValues,
- ...values,
- };
- });
- }
-
- async createCustomTemplate() {
- return this.$async(async () => {
- const { method } = this.state;
-
- if (!this.validateForm(method)) {
- return;
- }
-
- this.state.actionInProgress = true;
- try {
- const customTemplate = await this.createCustomTemplateByMethod(method, this.formValues);
-
- const accessControlData = this.formValues.AccessControlData;
- const userDetails = this.Authentication.getUserDetails();
- const userId = userDetails.ID;
- await this.ResourceControlService.applyResourceControl(userId, accessControlData, customTemplate.ResourceControl);
-
- this.Notifications.success('Success', 'Custom template successfully created');
- this.state.isEditorDirty = false;
- this.$state.go('kubernetes.templates.custom');
- } catch (err) {
- this.Notifications.error('Failure', err, 'Failed creating custom template');
- } finally {
- this.state.actionInProgress = false;
- }
- });
- }
-
- createCustomTemplateByMethod(method, template) {
- template.Type = 3;
-
- switch (method) {
- case 'editor':
- return this.createCustomTemplateFromFileContent(template);
- case 'upload':
- return this.createCustomTemplateFromFileUpload(template);
- case 'repository':
- return this.createCustomTemplateFromGitRepository(template);
- }
- }
-
- createCustomTemplateFromFileContent(template) {
- return this.CustomTemplateService.createCustomTemplateFromFileContent(template);
- }
-
- createCustomTemplateFromFileUpload(template) {
- return this.CustomTemplateService.createCustomTemplateFromFileUpload(template);
- }
-
- createCustomTemplateFromGitRepository(template) {
- return this.CustomTemplateService.createCustomTemplateFromGitRepository(template);
- }
-
- validateForm(method) {
- this.state.formValidationError = '';
-
- if (method === 'editor' && this.formValues.FileContent === '') {
- this.state.formValidationError = 'Template file content must not be empty';
- return false;
- }
-
- const title = this.formValues.Title;
- const isNotUnique = this.templates.some((template) => template.Title === title);
- if (isNotUnique) {
- this.state.formValidationError = 'A template with the same name already exists';
- return false;
- }
-
- if (!this.state.isTemplateValid) {
- this.state.formValidationError = 'Template is not valid';
- 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;
- }
-
- async $onInit() {
- return this.$async(async () => {
- const { fileContent, type } = this.$state.params;
-
- this.formValues.FileContent = fileContent;
- this.parseTemplate(fileContent);
- if (type) {
- this.formValues.Type = +type;
- }
-
- try {
- this.templates = await this.CustomTemplateService.customTemplates(3);
- } catch (err) {
- this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
- }
-
- this.state.loading = false;
-
- window.addEventListener('beforeunload', this.onBeforeOnload);
- });
- }
-
- $onDestroy() {
- window.removeEventListener('beforeunload', this.onBeforeOnload);
- }
-
- isEditorDirty() {
- return this.state.method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty;
- }
-
- onBeforeOnload(event) {
- if (this.isEditorDirty()) {
- event.preventDefault();
- event.returnValue = '';
- }
- }
-
- uiCanExit() {
- if (this.isEditorDirty()) {
- return confirmWebEditorDiscard();
- }
- }
-}
-
-export default KubeCreateCustomTemplateViewController;
diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html
deleted file mode 100644
index f00ce4d06..000000000
--- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html
deleted file mode 100644
index ac36391b5..000000000
--- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js
deleted file mode 100644
index c1b170f5d..000000000
--- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js
+++ /dev/null
@@ -1,271 +0,0 @@
-import _ from 'lodash';
-import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
-import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
-import { getTemplateVariables, intersectVariables, isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
-import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
-import { confirmWebEditorDiscard } from '@@/modals/confirm';
-import { fetchFilePreview } from '@/react/portainer/templates/app-templates/queries/useFetchTemplateFile';
-
-class CreateCustomTemplateViewController {
- /* @ngInject */
- constructor($async, $state, $scope, $window, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService, StackService, StateManager) {
- Object.assign(this, {
- $async,
- $state,
- $window,
- $scope,
- Authentication,
- CustomTemplateService,
- FormValidator,
- Notifications,
- ResourceControlService,
- StackService,
- StateManager,
- });
-
- this.buildMethods = [editor, upload, git];
-
- this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
-
- this.formValues = {
- Title: '',
- FileContent: '',
- File: null,
- RepositoryURL: '',
- RepositoryReferenceName: '',
- RepositoryAuthentication: false,
- RepositoryUsername: '',
- RepositoryPassword: '',
- ComposeFilePathInRepository: 'docker-compose.yml',
- Description: '',
- Note: '',
- Logo: '',
- Platform: 1,
- Type: 1,
- AccessControlData: new AccessControlFormData(),
- Variables: [],
- TLSSkipVerify: false,
- };
-
- this.state = {
- Method: 'editor',
- formValidationError: '',
- actionInProgress: false,
- fromStack: false,
- loading: true,
- isEditorDirty: false,
- isTemplateValid: true,
- };
-
- this.validationData = {
- title: {
- pattern: TEMPLATE_NAME_VALIDATION_REGEX,
- error: "This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').",
- },
- };
-
- this.templates = [];
-
- this.createCustomTemplate = this.createCustomTemplate.bind(this);
- this.createCustomTemplateAsync = this.createCustomTemplateAsync.bind(this);
- this.validateForm = this.validateForm.bind(this);
- this.createCustomTemplateByMethod = this.createCustomTemplateByMethod.bind(this);
- this.createCustomTemplateFromFileContent = this.createCustomTemplateFromFileContent.bind(this);
- this.createCustomTemplateFromFileUpload = this.createCustomTemplateFromFileUpload.bind(this);
- this.createCustomTemplateFromGitRepository = this.createCustomTemplateFromGitRepository.bind(this);
- this.editorUpdate = this.editorUpdate.bind(this);
- this.onChangeMethod = this.onChangeMethod.bind(this);
- this.onVariablesChange = this.onVariablesChange.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.onChangePlatform = this.onChangePlatform.bind(this);
- this.onChangeType = this.onChangeType.bind(this);
- }
-
- onVariablesChange(value) {
- this.handleChange({ Variables: value });
- }
-
- onChangePlatform(value) {
- this.handleChange({ Platform: value });
- }
-
- onChangeType(value) {
- this.handleChange({ Type: value });
- }
-
- handleChange(values) {
- return this.$async(async () => {
- this.formValues = {
- ...this.formValues,
- ...values,
- };
- });
- }
-
- createCustomTemplate() {
- return this.$async(this.createCustomTemplateAsync);
- }
-
- onChangeMethod(method) {
- return this.$scope.$evalAsync(() => {
- this.formValues.FileContent = '';
- this.formValues.Variables = [];
- this.selectedTemplate = null;
- this.state.Method = method;
- });
- }
-
- async createCustomTemplateAsync() {
- let method = this.state.Method;
-
- if (method === 'template') {
- method = 'editor';
- }
-
- if (!this.validateForm(method)) {
- return;
- }
-
- this.state.actionInProgress = true;
- try {
- const customTemplate = await this.createCustomTemplateByMethod(method);
-
- const accessControlData = this.formValues.AccessControlData;
- const userDetails = this.Authentication.getUserDetails();
- const userId = userDetails.ID;
- await this.ResourceControlService.applyResourceControl(userId, accessControlData, customTemplate.ResourceControl);
-
- this.Notifications.success('Success', 'Custom template successfully created');
- this.state.isEditorDirty = false;
- this.$state.go('docker.templates.custom');
- } catch (err) {
- this.Notifications.error('Failure', err, 'A template with the same name already exists');
- } finally {
- this.state.actionInProgress = false;
- }
- }
-
- validateForm(method) {
- this.state.formValidationError = '';
-
- if (method === 'editor' && this.formValues.FileContent === '') {
- this.state.formValidationError = 'Template file content must not be empty';
- return false;
- }
-
- const title = this.formValues.Title;
- const isNotUnique = _.some(this.templates, (template) => template.Title === title);
- if (isNotUnique) {
- this.state.formValidationError = 'A template with the same name already exists';
- return false;
- }
-
- const isAdmin = this.Authentication.isAdmin();
- const accessControlData = this.formValues.AccessControlData;
- const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
-
- if (error) {
- this.state.formValidationError = error;
- return false;
- }
-
- return true;
- }
-
- createCustomTemplateByMethod(method) {
- switch (method) {
- case 'editor':
- return this.createCustomTemplateFromFileContent();
- case 'upload':
- return this.createCustomTemplateFromFileUpload();
- case 'repository':
- return this.createCustomTemplateFromGitRepository();
- }
- }
-
- createCustomTemplateFromFileContent() {
- return this.CustomTemplateService.createCustomTemplateFromFileContent(this.formValues);
- }
-
- createCustomTemplateFromFileUpload() {
- return this.CustomTemplateService.createCustomTemplateFromFileUpload(this.formValues);
- }
-
- createCustomTemplateFromGitRepository() {
- return this.CustomTemplateService.createCustomTemplateFromGitRepository(this.formValues);
- }
-
- editorUpdate(value) {
- this.formValues.FileContent = value;
- this.state.isEditorDirty = true;
- this.parseTemplate(value);
- }
-
- parseTemplate(templateStr) {
- if (!this.isTemplateVariablesEnabled) {
- return;
- }
-
- const [variables] = getTemplateVariables(templateStr);
-
- const isValid = !!variables;
-
- this.state.isTemplateValid = isValid;
-
- if (isValid) {
- this.onVariablesChange(intersectVariables(this.formValues.Variables, variables));
- }
- }
-
- async $onInit() {
- return this.$async(async () => {
- const applicationState = this.StateManager.getState();
-
- this.state.endpointMode = applicationState.endpoint.mode;
- let stackType = 0;
- if (this.state.endpointMode.provider === 'DOCKER_STANDALONE') {
- this.isDockerStandalone = true;
- stackType = 2;
- } else if (this.state.endpointMode.provider === 'DOCKER_SWARM_MODE') {
- stackType = 1;
- }
- this.formValues.Type = stackType;
-
- const { appTemplateId, type } = this.$state.params;
-
- if (type) {
- this.formValues.Type = +type;
- }
-
- if (appTemplateId) {
- this.formValues.FileContent = await fetchFilePreview(appTemplateId);
- }
-
- try {
- this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
- } catch (err) {
- this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
- }
-
- this.state.loading = false;
-
- this.$window.onbeforeunload = () => {
- if (this.state.Method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty) {
- return '';
- }
- };
- });
- }
-
- $onDestroy() {
- this.state.isEditorDirty = false;
- }
-
- async uiCanExit() {
- if (this.state.Method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty) {
- return confirmWebEditorDiscard();
- }
- }
-}
-
-export default CreateCustomTemplateViewController;
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/index.js b/app/portainer/views/custom-templates/create-custom-template-view/index.js
deleted file mode 100644
index 794d87f27..000000000
--- a/app/portainer/views/custom-templates/create-custom-template-view/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import CreateCustomTemplateViewController from './createCustomTemplateViewController.js';
-
-angular.module('portainer.app').component('createCustomTemplateView', {
- templateUrl: './createCustomTemplateView.html',
- controller: CreateCustomTemplateViewController,
-});
diff --git a/app/react/common/stacks/common/form-texts.tsx b/app/react/common/stacks/common/form-texts.tsx
new file mode 100644
index 000000000..6423092c7
--- /dev/null
+++ b/app/react/common/stacks/common/form-texts.tsx
@@ -0,0 +1,51 @@
+import { StackType } from '../types';
+
+const dockerTexts = {
+ editor: {
+ placeholder: 'Define or paste the content of your docker compose file here',
+ description: (
+
+ You can get more information about Compose file format in the{' '}
+
+ official documentation
+
+ .
+
+ ),
+ },
+ upload: 'You can upload a Compose file from your computer.',
+} as const;
+
+export const textByType = {
+ [StackType.DockerCompose]: dockerTexts,
+ [StackType.DockerSwarm]: dockerTexts,
+ [StackType.Kubernetes]: {
+ editor: {
+ placeholder: 'Define or paste the content of your manifest file here',
+ description: (
+ <>
+
+ Templates allow deploying any kind of Kubernetes resource
+ (Deployment, Secret, ConfigMap...)
+
+
+ You can get more information about Kubernetes file format in the
+
+ official documentation
+
+ .
+
+ >
+ ),
+ },
+ upload: 'You can upload a Manifest file from your computer.',
+ },
+} as const;
diff --git a/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx b/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx
deleted file mode 100644
index c7f4436b1..000000000
--- a/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Formik } from 'formik';
-import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
-
-import { StackType } from '@/react/common/stacks/types';
-import { notifySuccess } from '@/portainer/services/notifications';
-import { useCreateTemplateMutation } from '@/react/portainer/templates/custom-templates/queries/useCreateTemplateMutation';
-import { Platform } from '@/react/portainer/templates/types';
-import { useFetchTemplateFile } from '@/react/portainer/templates/app-templates/queries/useFetchTemplateFile';
-import { getDefaultEdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
-import { useSaveCredentialsIfRequired } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
-
-import { editor } from '@@/BoxSelector/common-options/build-methods';
-
-import { toGitRequest } from '../common/git';
-
-import { InnerForm } from './InnerForm';
-import { FormValues } from './types';
-import { useValidation } from './useValidation';
-
-export function CreateTemplateForm() {
- const router = useRouter();
- const mutation = useCreateTemplateMutation();
- const validation = useValidation();
- const { appTemplateId, type } = useParams();
- const { saveCredentials, isLoading: isSaveCredentialsLoading } =
- useSaveCredentialsIfRequired();
-
- const fileContentQuery = useFetchTemplateFile(appTemplateId);
-
- if (fileContentQuery.isLoading) {
- return null;
- }
-
- const initialValues: FormValues = {
- Title: '',
- FileContent: fileContentQuery.data ?? '',
- Type: type,
- File: undefined,
- Method: editor.value,
- Description: '',
- Note: '',
- Logo: '',
- Platform: Platform.LINUX,
- Variables: [],
- Git: {
- RepositoryURL: '',
- RepositoryReferenceName: '',
- RepositoryAuthentication: false,
- RepositoryUsername: '',
- RepositoryPassword: '',
- ComposeFilePathInRepository: 'docker-compose.yml',
- AdditionalFiles: [],
- RepositoryURLValid: true,
- TLSSkipVerify: false,
- },
- EdgeSettings: getDefaultEdgeTemplateSettings(),
- };
-
- return (
-
-
-
- );
-
- async function handleSubmit(values: FormValues) {
- const credentialId = await saveCredentials(values.Git);
-
- mutation.mutate(
- {
- ...values,
- EdgeTemplate: true,
- Git: toGitRequest(values.Git, credentialId),
- },
- {
- onSuccess() {
- notifySuccess('Success', 'Template created');
- router.stateService.go('^');
- },
- }
- );
- }
-}
-
-function useParams() {
- const {
- params: { type = StackType.DockerCompose, appTemplateId },
- } = useCurrentStateAndParams();
-
- return {
- type: getStackType(type),
- appTemplateId: getTemplateId(appTemplateId),
- };
-
- function getStackType(type: string): StackType {
- const typeNum = parseInt(type, 10);
-
- if (
- [
- StackType.DockerSwarm,
- StackType.DockerCompose,
- StackType.Kubernetes,
- ].includes(typeNum)
- ) {
- return typeNum;
- }
-
- return StackType.DockerCompose;
- }
-
- function getTemplateId(appTemplateId: string): number | undefined {
- const id = parseInt(appTemplateId, 10);
-
- return Number.isNaN(id) ? undefined : id;
- }
-}
diff --git a/app/react/edge/templates/custom-templates/CreateView/CreateView.tsx b/app/react/edge/templates/custom-templates/CreateView/CreateView.tsx
deleted file mode 100644
index 0e1f7804e..000000000
--- a/app/react/edge/templates/custom-templates/CreateView/CreateView.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PageHeader } from '@@/PageHeader';
-import { Widget } from '@@/Widget';
-
-import { CreateTemplateForm } from './CreateTemplateForm';
-
-export function CreateView() {
- return (
-
- );
-}
diff --git a/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx b/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx
index ede5f4839..6748c8a4e 100644
--- a/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx
+++ b/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx
@@ -13,14 +13,13 @@ import {
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
+import { EdgeSettingsFieldset } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset';
import { WebEditorForm, usePreventExit } from '@@/WebEditorForm';
import { FormActions } from '@@/form-components/FormActions';
import { Button } from '@@/buttons';
import { FormError } from '@@/form-components/FormError';
-import { EdgeSettingsFieldset } from '../CreateView/EdgeSettingsFieldset';
-
import { FormValues } from './types';
export function InnerForm({
diff --git a/app/react/edge/templates/custom-templates/EditView/useValidation.tsx b/app/react/edge/templates/custom-templates/EditView/useValidation.tsx
index 60dcc2b66..b7e871244 100644
--- a/app/react/edge/templates/custom-templates/EditView/useValidation.tsx
+++ b/app/react/edge/templates/custom-templates/EditView/useValidation.tsx
@@ -10,8 +10,7 @@ import { useGitCredentials } from '@/react/portainer/account/git-credentials/git
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
import { Platform } from '@/react/portainer/templates/types';
-
-import { edgeFieldsetValidation } from '../CreateView/EdgeSettingsFieldset.validation';
+import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation';
export function useValidation(
currentTemplateId: CustomTemplate['Id'],
diff --git a/app/react/portainer/access-control/EditDetails/TeamsField.tsx b/app/react/portainer/access-control/EditDetails/TeamsField.tsx
index 02f9f3aa3..73a6ec0a1 100644
--- a/app/react/portainer/access-control/EditDetails/TeamsField.tsx
+++ b/app/react/portainer/access-control/EditDetails/TeamsField.tsx
@@ -43,7 +43,7 @@ export function TeamsField({
/>
) : (
- You have not yet created any teams. Head over to the
+ You have not yet created any teams. Head over to the{' '}
Teams view to manage teams.
)}
diff --git a/app/react/portainer/access-control/EditDetails/UsersField.tsx b/app/react/portainer/access-control/EditDetails/UsersField.tsx
index 301f44eab..9d9191e9b 100644
--- a/app/react/portainer/access-control/EditDetails/UsersField.tsx
+++ b/app/react/portainer/access-control/EditDetails/UsersField.tsx
@@ -34,7 +34,7 @@ export function UsersField({ name, users, value, onChange, errors }: Props) {
/>
) : (
- You have not yet created any users. Head over to the
+ You have not yet created any users. Head over to the{' '}
Users view to manage users.
)}
diff --git a/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts b/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts
index 5be9abdff..27b40c6ec 100644
--- a/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts
+++ b/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts
@@ -2,8 +2,9 @@ import { useQueryClient, useMutation } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
-import { GitAuthModel } from '@/react/portainer/gitops/types';
+import { GitAuthModel, GitFormModel } from '@/react/portainer/gitops/types';
import { useCurrentUser } from '@/react/hooks/useUser';
+import { UserId } from '@/portainer/users/types';
import { GitCredential } from '../types';
import { buildGitUrl } from '../git-credentials.service';
@@ -80,3 +81,40 @@ export function useSaveCredentialsIfRequired() {
}
}
}
+
+export async function saveGitCredentialsIfNeeded(
+ userId: UserId,
+ gitModel: GitFormModel
+) {
+ let credentialsId = gitModel.RepositoryGitCredentialID;
+ let username = gitModel.RepositoryUsername;
+ let password = gitModel.RepositoryPassword;
+ if (
+ gitModel.SaveCredential &&
+ gitModel.RepositoryAuthentication &&
+ password &&
+ username &&
+ gitModel.NewCredentialName
+ ) {
+ const cred = await createGitCredential({
+ name: gitModel.NewCredentialName,
+ password,
+ username,
+ userId,
+ });
+ credentialsId = cred.id;
+ }
+
+ // clear username and password if credentials are provided
+ if (credentialsId && username) {
+ username = '';
+ password = '';
+ }
+
+ return {
+ ...gitModel,
+ RepositoryGitCredentialID: credentialsId,
+ RepositoryUsername: username,
+ RepositoryPassword: password,
+ };
+}
diff --git a/app/react/portainer/environments/queries/useEnvironment.ts b/app/react/portainer/environments/queries/useEnvironment.ts
index 21ac5a570..008be1c52 100644
--- a/app/react/portainer/environments/queries/useEnvironment.ts
+++ b/app/react/portainer/environments/queries/useEnvironment.ts
@@ -1,26 +1,39 @@
import { useQuery } from 'react-query';
-import { getEndpoint } from '@/react/portainer/environments/environment.service';
-import {
- Environment,
- EnvironmentId,
-} from '@/react/portainer/environments/types';
import { withError } from '@/react-tools/react-query';
+import { getDeploymentOptions, getEndpoint } from '../environment.service';
+import { Environment, EnvironmentId } from '../types';
+
import { environmentQueryKeys } from './query-keys';
export function useEnvironment(
- id?: EnvironmentId,
- select?: (environment: Environment | null) => T
+ environmentId?: EnvironmentId,
+ select?: (environment: Environment | null) => T,
+ options?: { autoRefreshRate?: number }
) {
return useQuery(
- id ? environmentQueryKeys.item(id) : [],
- () => (id ? getEndpoint(id) : null),
+ environmentId ? environmentQueryKeys.item(environmentId) : [],
+ () => (environmentId ? getEndpoint(environmentId) : null),
{
select,
...withError('Failed loading environment'),
staleTime: 50,
- enabled: !!id,
+ enabled: !!environmentId,
+ refetchInterval() {
+ return options?.autoRefreshRate ?? false;
+ },
+ }
+ );
+}
+
+export function useEnvironmentDeploymentOptions(id: EnvironmentId | undefined) {
+ return useQuery(
+ [...environmentQueryKeys.item(id!), 'deploymentOptions'],
+ () => getDeploymentOptions(id!),
+ {
+ enabled: !!id,
+ ...withError('Failed loading deployment options'),
}
);
}
diff --git a/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx b/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx
index b0c653bd7..983a764b1 100644
--- a/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx
+++ b/app/react/portainer/gitops/AuthFieldset/NewCredentialForm.tsx
@@ -24,12 +24,12 @@ export function NewCredentialForm({
onChange({ SaveCredential: e.target.checked })}
/>
method.value),
+ });
+
+ if (!initialValues) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+
+ function handleSubmit(values: FormValues) {
+ mutation.mutate(
+ {
+ ...values,
+ EdgeTemplate: isEdge,
+ },
+ {
+ onSuccess() {
+ notifySuccess('Success', 'Template created');
+ router.stateService.go('^');
+ },
+ }
+ );
+ }
+}
+
+function useBuildMethods() {
+ const environment = useCurrentEnvironment(false);
+
+ const deploymentOptionsQuery = useEnvironmentDeploymentOptions(
+ environment.data && isKubernetesEnvironment(environment.data.Type)
+ ? environment.data.Id
+ : undefined
+ );
+ return initialBuildMethods.filter((method) => {
+ switch (method.value) {
+ case 'editor':
+ return !deploymentOptionsQuery.data?.hideWebEditor;
+ case 'upload':
+ return !deploymentOptionsQuery.data?.hideFileUpload;
+ case 'repository':
+ return true;
+ default:
+ return true;
+ }
+ });
+}
diff --git a/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx b/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx
new file mode 100644
index 000000000..3b0aeb3cf
--- /dev/null
+++ b/app/react/portainer/templates/custom-templates/CreateView/CreateView.tsx
@@ -0,0 +1,51 @@
+import { useCurrentStateAndParams } from '@uirouter/react';
+
+import { StackType } from '@/react/common/stacks/types';
+import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
+
+import { PageHeader } from '@@/PageHeader';
+import { Widget } from '@@/Widget';
+
+import { CreateForm } from './CreateForm';
+
+export function CreateView() {
+ const defaultType = useDefaultType();
+ const environmentId = useEnvironmentId(false);
+
+ return (
+
+ );
+}
+
+function useDefaultType() {
+ const {
+ state: { name },
+ } = useCurrentStateAndParams();
+ if (name?.includes('kubernetes')) {
+ return StackType.Kubernetes;
+ }
+
+ // edge or docker
+ return StackType.DockerCompose;
+}
diff --git a/app/react/edge/templates/custom-templates/CreateView/EdgeSettingsFieldset.tsx b/app/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.tsx
similarity index 100%
rename from app/react/edge/templates/custom-templates/CreateView/EdgeSettingsFieldset.tsx
rename to app/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.tsx
diff --git a/app/react/edge/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation.ts b/app/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation.ts
similarity index 100%
rename from app/react/edge/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation.ts
rename to app/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation.ts
diff --git a/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx b/app/react/portainer/templates/custom-templates/CreateView/InnerForm.tsx
similarity index 70%
rename from app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx
rename to app/react/portainer/templates/custom-templates/CreateView/InnerForm.tsx
index 497bf6781..3c84f306f 100644
--- a/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx
+++ b/app/react/portainer/templates/custom-templates/CreateView/InnerForm.tsx
@@ -10,24 +10,33 @@ import {
isTemplateVariablesEnabled,
} from '@/react/portainer/custom-templates/components/utils';
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
-import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
+import { AccessControlForm } from '@/react/portainer/access-control';
+import { EnvironmentId } from '@/react/portainer/environments/types';
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
+import { AccessControlFormData } from '@/react/portainer/access-control/types';
+import { textByType } from '@/react/common/stacks/common/form-texts';
+import { StackType } from '@/react/common/stacks/types';
import { BoxSelector } from '@@/BoxSelector';
import { WebEditorForm, usePreventExit } from '@@/WebEditorForm';
import { FileUploadForm } from '@@/form-components/FileUpload';
import { FormActions } from '@@/form-components/FormActions';
import { FormSection } from '@@/form-components/FormSection';
-import {
- editor,
- upload,
- git,
-} from '@@/BoxSelector/common-options/build-methods';
-import { FormValues, Method, buildMethods } from './types';
+import { EdgeTemplateSettings } from '../types';
+
import { EdgeSettingsFieldset } from './EdgeSettingsFieldset';
+import { FormValues, Method, initialBuildMethods } from './types';
-export function InnerForm({ isLoading }: { isLoading: boolean }) {
+export function InnerForm({
+ isLoading,
+ environmentId,
+ buildMethods,
+}: {
+ isLoading: boolean;
+ environmentId?: EnvironmentId;
+ buildMethods: Array<(typeof initialBuildMethods)[number]>;
+}) {
const {
values,
initialValues,
@@ -39,13 +48,17 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) {
isSubmitting,
} = useFormikContext();
+ const isGit = values.Method === 'repository';
+ const isEditor = values.Method === 'editor';
+
usePreventExit(
initialValues.FileContent,
values.FileContent,
- values.Method === editor.value && !isSubmitting && !isLoading
+ isEditor && !isSubmitting && !isLoading
);
- const isGit = values.Method === git.value;
+ const texts = textByType[values.Type];
+
return (