diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index fa8d0f3d4..8df53c616 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -146,6 +146,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) * if stack.CurrentDeploymentInfo == nil && stack.GitConfig != nil { stack.CurrentDeploymentInfo = &portainer.StackDeploymentInfo{ RepositoryURL: stack.GitConfig.URL, + ReferenceName: stack.GitConfig.ReferenceName, ConfigFilePath: stack.GitConfig.ConfigFilePath, AdditionalFiles: stack.AdditionalFiles, ConfigHash: stack.GitConfig.ConfigHash, diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index f745477bc..db4e4529b 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -1,6 +1,7 @@ package stacks import ( + "cmp" "context" "net/http" "time" @@ -26,7 +27,7 @@ type stackGitRedeployPayload struct { RepositoryUsername string RepositoryPassword string Env []portainer.Pair - Prune bool + Prune *bool // RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack RepullImageAndRedeploy bool @@ -128,16 +129,23 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) return httperror.BadRequest("Invalid request payload", err) } payload.RepullImageAndRedeploy = payload.RepullImageAndRedeploy || payload.PullImage - stack.GitConfig.ReferenceName = payload.RepositoryReferenceName - stack.Env = payload.Env - if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { - if stack.Option == nil { - stack.Option = &portainer.StackOption{} - } - stack.Option.Prune = payload.Prune + + stack.GitConfig.ReferenceName = cmp.Or(payload.RepositoryReferenceName, stack.GitConfig.ReferenceName) + + if payload.Env != nil { + stack.Env = payload.Env } - if stack.Type == portainer.KubernetesStack { + if payload.Prune != nil { + if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack { + if stack.Option == nil { + stack.Option = &portainer.StackOption{} + } + stack.Option.Prune = *payload.Prune + } + } + + if stack.Type == portainer.KubernetesStack && payload.StackName != "" { stack.Name = payload.StackName } @@ -186,6 +194,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) } stack.CurrentDeploymentInfo = &portainer.StackDeploymentInfo{ RepositoryURL: stack.GitConfig.URL, + ReferenceName: stack.GitConfig.ReferenceName, ConfigFilePath: stack.GitConfig.ConfigFilePath, AdditionalFiles: stack.AdditionalFiles, ConfigHash: stack.GitConfig.ConfigHash, diff --git a/api/portainer.go b/api/portainer.go index ca6484665..dd87cfce1 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -344,6 +344,8 @@ type ( RepositoryURL string `json:"RepositoryURL,omitempty"` // ConfigFilePath is the path to the config file in the git repository used for deploying the stack ConfigFilePath string `json:"ConfigFilePath,omitempty"` + // ReferenceName is the git reference (branch/tag) used for deploying the stack + ReferenceName string `json:"ReferenceName,omitempty"` // AdditionalFiles are the additional files used for deploying the stack AdditionalFiles []string `json:"AdditionalFiles,omitempty"` } diff --git a/api/stacks/deployments/deploy.go b/api/stacks/deployments/deploy.go index 79d3f079b..8909fc33a 100644 --- a/api/stacks/deployments/deploy.go +++ b/api/stacks/deployments/deploy.go @@ -146,6 +146,7 @@ func redeployWhenChangedSecondStage( stack.CurrentDeploymentInfo = &portainer.StackDeploymentInfo{ RepositoryURL: stack.GitConfig.URL, + ReferenceName: stack.GitConfig.ReferenceName, ConfigFilePath: stack.GitConfig.ConfigFilePath, AdditionalFiles: stack.AdditionalFiles, ConfigHash: stack.GitConfig.ConfigHash, diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 965b86321..83fc6c4fd 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -268,6 +268,9 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo const application = { name: 'kubernetes.applications.application', url: '/:namespace/:name?resource-type', + params: { + openGitSettings: { value: null, dynamic: true }, + }, views: { 'content@': { component: 'applicationDetailsView', diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 6b061ad1f..ba32499a3 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -103,7 +103,7 @@ > -
Actions
+
Actions
@@ -147,14 +147,6 @@
- - - - -
-
Actions
+
Actions
- - - diff --git a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.js b/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.js deleted file mode 100644 index 58b085d40..000000000 --- a/app/portainer/components/forms/kubernetes-redeploy-app-git-form/kubernetes-redeploy-app-git-form.js +++ /dev/null @@ -1,14 +0,0 @@ -import angular from 'angular'; -import controller from './kubernetes-redeploy-app-git-form.controller'; - -const kubernetesRedeployAppGitForm = { - templateUrl: './kubernetes-redeploy-app-git-form.html', - controller, - bindings: { - stack: '<', - namespace: '<', - stackName: '<', - }, -}; - -angular.module('portainer.app').component('kubernetesRedeployAppGitForm', kubernetesRedeployAppGitForm); diff --git a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx index 57f7dca7e..ddb15056a 100644 --- a/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx +++ b/app/react/edge/edge-stacks/ItemView/EditEdgeStackForm/GitForm/GitForm.tsx @@ -33,6 +33,7 @@ import { useRegistries } from '@/react/portainer/registries/queries/useRegistrie import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset'; import { parseRelativePathResponse } from '@/react/portainer/gitops/RelativePathFieldset/utils'; import { useSaveCredentialsIfRequired } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation'; +import { GitReferenceCard } from '@/react/portainer/gitops/GitReferenceCard'; import { LoadingButton } from '@@/buttons'; import { FormSection } from '@@/form-components/FormSection'; @@ -96,6 +97,7 @@ export function GitForm({ stack }: { stack: EdgeStack }) { updateStackMutation.isLoading || isSaveCredentialsLoading } isUpdateVersion={!!updateStackMutation.variables?.updateVersion} + stack={stack} /> ); @@ -156,12 +158,14 @@ function InnerForm({ isUpdateVersion, onUpdateSettingsClick, webhookId, + stack, }: { gitUrl: string; isLoading: boolean; isUpdateVersion: boolean; onUpdateSettingsClick(): void; webhookId: string; + stack: EdgeStack; }) { const registriesQuery = useRegistries(); const { values, setFieldValue, isValid, handleSubmit, errors, dirty } = @@ -205,6 +209,15 @@ function InnerForm({ }} /> + {!!stack.GitConfig && ( + + )} +
{!isSystemNamespace && ( -
- - {appStackKind === 'edge' ? ( - - ) : ( - - {externalApp - ? 'Edit external application' - : 'Edit this application'} - - )} - - {!applicationIsKind('Pod', app) && ( - <> - - + {!!stack?.GitConfig && ( +
+ - +
)} - {!externalApp && ( - + - )} - {appStackFileContent && ( - - Create template from application - - )} -
+
+ )} + + {!applicationIsKind('Pod', app) && ( + <> + + + + )} + {!externalApp && ( + + )} + {appStackFileContent && ( + + Create template from application + + )} +
+ ); +} diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/EditButtons.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/EditButtons.tsx new file mode 100644 index 000000000..ba0a63b37 --- /dev/null +++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/EditButtons.tsx @@ -0,0 +1,47 @@ +import { Stack } from '@/react/common/stacks/types'; +import { Authorized } from '@/react/hooks/useUser'; +import { EditGitSettingsButton } from '@/react/common/stacks/EditGitSettingsButton'; +import { GitPullButton } from '@/react/common/stacks/GitPullButton'; + +import { EdgeEditButton } from './EdgeEditButton'; +import { EditButton } from './EditButton'; + +type Props = { + isEdge?: boolean; + stackId?: number; + externalApp?: boolean; + stack?: Stack; +}; + +export function EditButtons({ + isEdge, + stackId, + externalApp, + + stack, +}: Props) { + if (isEdge) { + return ( + + + + ); + } + + if (stack?.GitConfig) { + return ( + <> + {!stack.FromAppTemplate && } + + + ); + } + + return ( + + + {externalApp ? 'Edit external application' : 'Edit this application'} + + + ); +} diff --git a/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx b/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx index 03d44b5fa..cc2204280 100644 --- a/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx +++ b/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx @@ -9,8 +9,11 @@ import { suppressConsoleLogs } from '@/setup-tests/suppress-console'; import { useAppStackFile } from './useAppStackFile'; -function renderQueryHook(id: number | undefined, kind: string) { - return renderHook(() => useAppStackFile(id, kind), { +function renderQueryHook( + id: number | undefined, + kind: 'edge' | 'compose' | 'kubernetes' +) { + return renderHook(() => useAppStackFile({ id, kind }), { wrapper: withTestQueryProvider(({ children }) => <>{children}), }); } diff --git a/app/react/kubernetes/applications/queries/useAppStackFile.ts b/app/react/kubernetes/applications/queries/useAppStackFile.ts index 8bb15524c..372b7ef92 100644 --- a/app/react/kubernetes/applications/queries/useAppStackFile.ts +++ b/app/react/kubernetes/applications/queries/useAppStackFile.ts @@ -6,8 +6,16 @@ import { getStackFile } from '@/react/common/stacks/queries/useStackFile'; import { queryKeys } from './query-keys'; -// Return the stack file content as a string for both edge and regular stacks. -export function useAppStackFile(id?: number, kind?: string) { +export function useAppStackFile( + { + id, + kind, + }: { + id: number | undefined; + kind?: 'edge' | 'compose' | 'kubernetes' | (string & NonNullable); + }, + { enabled }: { enabled?: boolean } = {} +) { return useQuery( queryKeys.appStackFile(id, kind), async ({ signal }) => { @@ -16,11 +24,9 @@ export function useAppStackFile(id?: number, kind?: string) { } if (kind === 'edge') { - // Fetch edge stack file return getEdgeStackFile(id); } - // Fetch regular stack file const stackFile = await getStackFile({ stackId: id, options: { signal }, @@ -28,7 +34,7 @@ export function useAppStackFile(id?: number, kind?: string) { return stackFile?.StackFileContent; }, { - enabled: !!id, + enabled: !!id && enabled, ...withGlobalError('Failed to load app stack file'), } );