diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js index 284019177..c806c8e4b 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js @@ -112,8 +112,7 @@ export default class CreateEdgeStackViewController { PrePullImage: template.EdgeSettings.PrePullImage || false, RetryDeploy: template.EdgeSettings.RetryDeploy || false, PrivateRegistryId: template.EdgeSettings.PrivateRegistryId || null, - SupportRelativePath: template.EdgeSettings.RelativePathSettings.SupportRelativePath || false, - FilesystemPath: template.EdgeSettings.RelativePathSettings.FilesystemPath || '', + ...template.EdgeSettings.RelativePathSettings, } : {}), }; @@ -195,11 +194,7 @@ export default class CreateEdgeStackViewController { createStack() { return this.$async(async () => { const name = this.formValues.Name; - let method = this.state.Method; - - if (method === 'template') { - method = 'editor'; - } + const method = getMethod(this.state.Method, this.state.templateValues.template); if (!this.validateForm(method)) { return; @@ -338,3 +333,20 @@ export default class CreateEdgeStackViewController { ); } } + +/** + * + * @param {'template'|'repository' | 'editor' | 'upload'} method + * @param {import('@/react/portainer/templates/custom-templates/types').CustomTemplate | undefined} template + * @returns 'repository' | 'editor' | 'upload' + */ +function getMethod(method, template) { + if (method !== 'template') { + return method; + } + + if (template && template.GitConfig) { + return 'repository'; + } + return 'editor'; +} diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js index 859bd0207..99b132f97 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js @@ -12,6 +12,11 @@ class DockerComposeFormController { this.onChangeFile = this.onChangeFile.bind(this); this.onChangeMethod = this.onChangeMethod.bind(this); this.onChangeFormValues = this.onChangeFormValues.bind(this); + this.isGitTemplate = this.isGitTemplate.bind(this); + } + + isGitTemplate() { + return this.state.Method === 'template' && !!this.templateValues.template && !!this.templateValues.template.GitConfig; } onChangeFormValues(newValues) { diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html index 09e86b028..58401cd41 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.html @@ -15,7 +15,8 @@ ng-required="true" yml="true" placeholder="Define or paste the content of your docker compose file here" - read-only="$ctrl.state.Method === 'template' && $ctrl.template.GitConfig" + versions="$ctrl.formValues.versions" + read-only="$ctrl.isGitTemplate()" > You can get more information about Compose file format in the @@ -28,7 +29,7 @@ You can upload a Compose file from your computer. -
+
); @@ -105,9 +106,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { return; } - const credentialId = await saveCredentialsIfRequired( - values.authentication - ); + const credentialId = await saveCredentials(values.authentication); updateStackMutation.mutate(getPayload(values, credentialId, false), { onSuccess() { @@ -121,7 +120,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { ); async function handleSubmit(values: FormValues) { - const credentialId = await saveCredentialsIfRequired(values.authentication); + const credentialId = await saveCredentials(values.authentication); updateStackMutation.mutate(getPayload(values, credentialId, true), { onSuccess() { @@ -151,29 +150,6 @@ export function GitForm({ stack }: { stack: EdgeStack }) { ...values, }; } - - async function saveCredentialsIfRequired(authentication: GitAuthModel) { - if ( - !authentication.SaveCredential || - !authentication.RepositoryPassword || - !authentication.NewCredentialName - ) { - return authentication.RepositoryGitCredentialID; - } - - try { - const credential = await saveCredentialsMutation.mutateAsync({ - userId: user.Id, - username: authentication.RepositoryUsername, - password: authentication.RepositoryPassword, - name: authentication.NewCredentialName, - }); - return credential.id; - } catch (err) { - notifyError('Error', err as Error, 'Unable to save credentials'); - return undefined; - } - } } function InnerForm({ diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper.tsx index a650aff70..5f5b515de 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper.tsx @@ -29,7 +29,7 @@ export function PrivateRegistryFieldsetWrapper({ }) { const dryRunMutation = useParseRegistries(); - const registriesQuery = useRegistries(); + const registriesQuery = useRegistries({ hideDefault: true }); if (!registriesQuery.data) { return null; diff --git a/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx b/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx index 20d09aab2..c7f4436b1 100644 --- a/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx +++ b/app/react/edge/templates/custom-templates/CreateView/CreateTemplateForm.tsx @@ -7,9 +7,12 @@ import { useCreateTemplateMutation } from '@/react/portainer/templates/custom-te 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'; @@ -19,6 +22,8 @@ export function CreateTemplateForm() { const mutation = useCreateTemplateMutation(); const validation = useValidation(); const { appTemplateId, type } = useParams(); + const { saveCredentials, isLoading: isSaveCredentialsLoading } = + useSaveCredentialsIfRequired(); const fileContentQuery = useFetchTemplateFile(appTemplateId); @@ -58,13 +63,19 @@ export function CreateTemplateForm() { validationSchema={validation} validateOnMount > - + ); - function handleSubmit(values: FormValues) { + async function handleSubmit(values: FormValues) { + const credentialId = await saveCredentials(values.Git); + mutation.mutate( - { ...values, EdgeTemplate: true }, + { + ...values, + EdgeTemplate: true, + Git: toGitRequest(values.Git, credentialId), + }, { onSuccess() { notifySuccess('Success', 'Template created'); diff --git a/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx b/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx index b09aecf6b..497bf6781 100644 --- a/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx +++ b/app/react/edge/templates/custom-templates/CreateView/InnerForm.tsx @@ -42,7 +42,7 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) { usePreventExit( initialValues.FileContent, values.FileContent, - values.Method === editor.value && !isSubmitting + values.Method === editor.value && !isSubmitting && !isLoading ); const isGit = values.Method === git.value; @@ -108,16 +108,7 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) { /> )} - {isTemplateVariablesEnabled && ( - setFieldValue('Variables', values)} - isVariablesNamesFromParent={values.Method === editor.value} - errors={errors.Variables} - /> - )} - - {values.Method === git.value && ( + {isGit && ( @@ -130,6 +121,15 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) { /> )} + {isTemplateVariablesEnabled && ( + setFieldValue('Variables', values)} + isVariablesNamesFromParent={values.Method === editor.value} + errors={errors.Variables} + /> + )} + {values.EdgeSettings && ( diff --git a/app/react/edge/templates/custom-templates/EditView/EditTemplateForm.tsx b/app/react/edge/templates/custom-templates/EditView/EditTemplateForm.tsx index b88b10f2a..2727bc0d3 100644 --- a/app/react/edge/templates/custom-templates/EditView/EditTemplateForm.tsx +++ b/app/react/edge/templates/custom-templates/EditView/EditTemplateForm.tsx @@ -11,6 +11,9 @@ import { 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'; @@ -22,6 +25,8 @@ export function EditTemplateForm({ template }: { template: CustomTemplate }) { 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; @@ -49,7 +54,7 @@ export function EditTemplateForm({ template }: { template: CustomTemplate }) { validateOnMount > ); - function handleSubmit(values: FormValues) { + async function handleSubmit(values: FormValues) { + const credentialId = await saveCredentials(values.Git); + mutation.mutate( { id: template.Id, @@ -74,7 +81,7 @@ export function EditTemplateForm({ template }: { template: CustomTemplate }) { Platform: values.Platform, Variables: values.Variables, EdgeSettings: values.EdgeSettings, - ...values.Git, + ...(values.Git ? toGitRequest(values.Git, credentialId) : {}), }, { onSuccess() { diff --git a/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx b/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx index 8836e0e67..ede5f4839 100644 --- a/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx +++ b/app/react/edge/templates/custom-templates/EditView/InnerForm.tsx @@ -51,7 +51,7 @@ export function InnerForm({ usePreventExit( initialValues.FileContent, values.FileContent, - !isEditorReadonly && !isSubmitting + !isEditorReadonly && !isSubmitting && !isLoading ); return (
diff --git a/app/react/edge/templates/custom-templates/common/git.ts b/app/react/edge/templates/custom-templates/common/git.ts new file mode 100644 index 000000000..0e8c52232 --- /dev/null +++ b/app/react/edge/templates/custom-templates/common/git.ts @@ -0,0 +1,35 @@ +import { transformGitAuthenticationViewModel } from '@/react/portainer/gitops/AuthFieldset/utils'; +import { GitFormModel } from '@/react/portainer/gitops/types'; + +export function toGitRequest( + gitConfig: GitFormModel, + credentialId: number | undefined +): GitFormModel { + return { + ...gitConfig, + ...getGitAuthValues(gitConfig, credentialId), + }; +} + +function getGitAuthValues( + gitConfig: GitFormModel | undefined, + credentialId: number | undefined +) { + if (!credentialId) { + return gitConfig; + } + + const authModel = transformGitAuthenticationViewModel({ + ...gitConfig, + RepositoryGitCredentialID: credentialId, + }); + + return authModel + ? { + RepositoryAuthentication: true, + RepositoryGitCredentialID: authModel.GitCredentialID, + RepositoryPassword: authModel.Password, + RepositoryUsername: authModel.Username, + } + : {}; +} diff --git a/app/react/portainer/account/git-credentials/git-credentials.service.ts b/app/react/portainer/account/git-credentials/git-credentials.service.ts index f49adc57b..8c606a6aa 100644 --- a/app/react/portainer/account/git-credentials/git-credentials.service.ts +++ b/app/react/portainer/account/git-credentials/git-credentials.service.ts @@ -6,25 +6,7 @@ import { UserId } from '@/portainer/users/types'; import { isBE } from '../../feature-flags/feature-flags.service'; -import { - CreateGitCredentialPayload, - GitCredential, - UpdateGitCredentialPayload, -} from './types'; - -export async function createGitCredential( - gitCredential: CreateGitCredentialPayload -) { - try { - const { data } = await axios.post<{ gitCredential: GitCredential }>( - buildGitUrl(gitCredential.userId), - gitCredential - ); - return data.gitCredential; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to create git credential'); - } -} +import { GitCredential, UpdateGitCredentialPayload } from './types'; export async function getGitCredentials(userId: number) { try { @@ -141,24 +123,7 @@ export function useGitCredential(userId: number, id: number) { }); } -export function useCreateGitCredentialMutation() { - const queryClient = useQueryClient(); - - return useMutation(createGitCredential, { - onSuccess: (_, payload) => { - notifySuccess('Credentials created successfully', payload.name); - return queryClient.invalidateQueries(['gitcredentials']); - }, - meta: { - error: { - title: 'Failure', - message: 'Unable to create credential', - }, - }, - }); -} - -function buildGitUrl(userId: number, credentialId?: number) { +export function buildGitUrl(userId: number, credentialId?: number) { return credentialId ? `/users/${userId}/gitcredentials/${credentialId}` : `/users/${userId}/gitcredentials`; diff --git a/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts b/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts new file mode 100644 index 000000000..5be9abdff --- /dev/null +++ b/app/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation.ts @@ -0,0 +1,82 @@ +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 { useCurrentUser } from '@/react/hooks/useUser'; + +import { GitCredential } from '../types'; +import { buildGitUrl } from '../git-credentials.service'; + +export interface CreateGitCredentialPayload { + userId: number; + name: string; + username?: string; + password: string; +} + +export function useCreateGitCredentialMutation() { + const queryClient = useQueryClient(); + + return useMutation(createGitCredential, { + onSuccess: (_, payload) => { + notifySuccess('Credentials created successfully', payload.name); + return queryClient.invalidateQueries(['gitcredentials']); + }, + meta: { + error: { + title: 'Failure', + message: 'Unable to create credential', + }, + }, + }); +} + +async function createGitCredential(gitCredential: CreateGitCredentialPayload) { + try { + const { data } = await axios.post<{ gitCredential: GitCredential }>( + buildGitUrl(gitCredential.userId), + gitCredential + ); + return data.gitCredential; + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to create git credential'); + } +} + +export function useSaveCredentialsIfRequired() { + const saveCredentialsMutation = useCreateGitCredentialMutation(); + const { user } = useCurrentUser(); + + return { + saveCredentials: saveCredentialsIfRequired, + isLoading: saveCredentialsMutation.isLoading, + }; + + async function saveCredentialsIfRequired(authentication?: GitAuthModel) { + if (!authentication) { + return undefined; + } + + if ( + !authentication.SaveCredential || + !authentication.RepositoryPassword || + !authentication.NewCredentialName + ) { + return authentication.RepositoryGitCredentialID; + } + + try { + const credential = await saveCredentialsMutation.mutateAsync({ + userId: user.Id, + username: authentication.RepositoryUsername, + password: authentication.RepositoryPassword, + name: authentication.NewCredentialName, + }); + return credential.id; + } catch (err) { + notifyError('Error', err as Error, 'Unable to save credentials'); + return undefined; + } + } +} diff --git a/app/react/portainer/account/git-credentials/types.ts b/app/react/portainer/account/git-credentials/types.ts index 1b1f72dcd..57b3d8ff4 100644 --- a/app/react/portainer/account/git-credentials/types.ts +++ b/app/react/portainer/account/git-credentials/types.ts @@ -13,13 +13,6 @@ export interface GitCredentialFormValues { password: string; } -export interface CreateGitCredentialPayload { - userId: number; - name: string; - username?: string; - password: string; -} - export interface UpdateGitCredentialPayload { name: string; username?: string; diff --git a/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx b/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx index d457cba16..1e1b3b566 100644 --- a/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx +++ b/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx @@ -35,7 +35,7 @@ export function RelativePathFieldset({ const { errors } = useValidation(value); - const { enableFsPath0, enableFsPath1, toggleFsPath } = useEnableFsPath(); + const { enableFsPath0, enableFsPath1, toggleFsPath } = useEnableFsPath(value); const pathTip0 = 'For relative path volumes use with Docker Swarm, you must have a network filesystem which all of your nodes can access.'; diff --git a/app/react/portainer/gitops/RelativePathFieldset/useEnableFsPath.ts b/app/react/portainer/gitops/RelativePathFieldset/useEnableFsPath.ts index f811cff7f..e2f5b2664 100644 --- a/app/react/portainer/gitops/RelativePathFieldset/useEnableFsPath.ts +++ b/app/react/portainer/gitops/RelativePathFieldset/useEnableFsPath.ts @@ -1,7 +1,11 @@ import { useState } from 'react'; -export function useEnableFsPath() { - const [state, setState] = useState([]); +import { RelativePathModel } from './types'; + +export function useEnableFsPath(initialValue: RelativePathModel) { + const [state, setState] = useState(() => + initialValue.SupportPerDeviceConfigs ? [1] : [] + ); const enableFsPath0 = state.length && state[0] === 0; const enableFsPath1 = state.length && state[0] === 1; diff --git a/app/react/portainer/registries/queries/useRegistries.ts b/app/react/portainer/registries/queries/useRegistries.ts index e72502003..06d8c937d 100644 --- a/app/react/portainer/registries/queries/useRegistries.ts +++ b/app/react/portainer/registries/queries/useRegistries.ts @@ -9,11 +9,7 @@ import { usePublicSettings } from '../../settings/queries'; import { queryKeys } from './query-keys'; export function useRegistries( - queryOptions: { - enabled?: boolean; - select?: (registries: Registry[]) => T; - onSuccess?: (data: T) => void; - } = {} + queryOptions: GenericRegistriesQueryOptions = {} ) { return useGenericRegistriesQuery( queryKeys.base(), @@ -22,13 +18,11 @@ export function useRegistries( ); } -/** - * @field hideDefault - is used to hide the default registry from the list of registries, regardless of the user's settings. Kubernetes views use this. - */ export type GenericRegistriesQueryOptions = { enabled?: boolean; select?: (registries: Registry[]) => T; onSuccess?: (data: T) => void; + /** is used to hide the default registry from the list of registries, regardless of the user's settings. Kubernetes views use this. */ hideDefault?: boolean; };