mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 07:45:22 +02:00
address review comments
Some checks failed
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
Some checks failed
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
This commit is contained in:
parent
218a32a49f
commit
17971a10cc
13 changed files with 326 additions and 354 deletions
|
@ -1,6 +1,6 @@
|
||||||
import { StackId } from '../types';
|
import { StackId } from '../types';
|
||||||
|
|
||||||
export const stacksQueryKeys = {
|
export const stacksQueryKeys = {
|
||||||
stackFile: (stackId: StackId) => ['stacks', stackId, 'file'],
|
stackFile: (stackId: StackId) => ['stacks', stackId, 'file'] as const,
|
||||||
stacks: ['stacks'],
|
stacks: ['stacks'] as const,
|
||||||
};
|
};
|
||||||
|
|
29
app/react/common/stacks/queries/useStack.ts
Normal file
29
app/react/common/stacks/queries/useStack.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { Stack, StackId } from '../types';
|
||||||
|
|
||||||
|
import { stacksQueryKeys } from './query-keys';
|
||||||
|
import { buildStackUrl } from './buildUrl';
|
||||||
|
|
||||||
|
export function useStack(stackId?: StackId) {
|
||||||
|
return useQuery(
|
||||||
|
stacksQueryKeys.stackFile(stackId || 0),
|
||||||
|
() => getStack(stackId!),
|
||||||
|
{
|
||||||
|
...withError('Unable to retrieve stack'),
|
||||||
|
enabled: !!stackId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStack(stackId: StackId) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<Stack>(buildStackUrl(stackId));
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Unable to retrieve stack');
|
||||||
|
}
|
||||||
|
}
|
29
app/react/common/stacks/queries/useStackFile.ts
Normal file
29
app/react/common/stacks/queries/useStackFile.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { StackFile, StackId } from '../types';
|
||||||
|
|
||||||
|
import { stacksQueryKeys } from './query-keys';
|
||||||
|
import { buildStackUrl } from './buildUrl';
|
||||||
|
|
||||||
|
export function useStackFile(stackId?: StackId) {
|
||||||
|
return useQuery(
|
||||||
|
stacksQueryKeys.stackFile(stackId || 0),
|
||||||
|
() => getStackFile(stackId!),
|
||||||
|
{
|
||||||
|
...withError('Unable to retrieve stack'),
|
||||||
|
enabled: !!stackId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStackFile(stackId: StackId) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<StackFile>(buildStackUrl(stackId, 'file'));
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Unable to retrieve stack file');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
import { useQuery } from 'react-query';
|
|
||||||
|
|
||||||
import axios from '@/portainer/services/axios';
|
|
||||||
import { withError } from '@/react-tools/react-query';
|
|
||||||
|
|
||||||
import { Stack, StackFile, StackId } from '../types';
|
|
||||||
|
|
||||||
import { stacksQueryKeys } from './query-keys';
|
|
||||||
import { buildStackUrl } from './buildUrl';
|
|
||||||
|
|
||||||
export function useStackQuery(stackId?: StackId) {
|
|
||||||
return useQuery(
|
|
||||||
stacksQueryKeys.stackFile(stackId || 0),
|
|
||||||
() => getStack(stackId),
|
|
||||||
{
|
|
||||||
...withError('Unable to retrieve stack'),
|
|
||||||
enabled: !!stackId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStack(stackId?: StackId) {
|
|
||||||
if (!stackId) {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
const { data } = await axios.get<Stack>(buildStackUrl(stackId));
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useStackFileQuery(stackId?: StackId) {
|
|
||||||
return useQuery(
|
|
||||||
stacksQueryKeys.stackFile(stackId || 0),
|
|
||||||
() => getStackFile(stackId),
|
|
||||||
{
|
|
||||||
...withError('Unable to retrieve stack'),
|
|
||||||
enabled: !!stackId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStackFile(stackId?: StackId) {
|
|
||||||
if (!stackId) {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
const { data } = await axios.get<StackFile>(buildStackUrl(stackId, 'file'));
|
|
||||||
return data;
|
|
||||||
}
|
|
|
@ -1,44 +1,65 @@
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import axios from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import {
|
import {
|
||||||
withError,
|
withError,
|
||||||
withInvalidate,
|
withInvalidate,
|
||||||
queryClient,
|
queryClient,
|
||||||
} from '@/react-tools/react-query';
|
} from '@/react-tools/react-query';
|
||||||
import { StackId } from '@/react/common/stacks/types';
|
import { Stack, StackId } from '@/react/common/stacks/types';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
|
import { stacksQueryKeys } from '@/react/common/stacks/queries/query-keys';
|
||||||
|
import { buildStackUrl } from '@/react/common/stacks/queries/buildUrl';
|
||||||
|
import {
|
||||||
|
AutoUpdateResponse,
|
||||||
|
GitAuthModel,
|
||||||
|
GitCredentialsModel,
|
||||||
|
} from '@/react/portainer/gitops/types';
|
||||||
|
import { saveGitCredentialsIfNeeded } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
|
||||||
|
|
||||||
import { stacksQueryKeys } from './query-keys';
|
export interface UpdateKubeGitStackPayload extends GitCredentialsModel {
|
||||||
import { buildStackUrl } from './buildUrl';
|
AutoUpdate: AutoUpdateResponse | null;
|
||||||
|
|
||||||
type UpdateKubeGitStackPayload = {
|
|
||||||
AutoUpdate: AutoUpdateModel;
|
|
||||||
RepositoryAuthentication: boolean;
|
|
||||||
RepositoryGitCredentialID: number;
|
|
||||||
RepositoryPassword: string;
|
|
||||||
RepositoryReferenceName: string;
|
RepositoryReferenceName: string;
|
||||||
RepositoryUsername: string;
|
|
||||||
TLSSkipVerify: boolean;
|
TLSSkipVerify: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
// update a stack from a git repository
|
// update a stack from a git repository
|
||||||
export function useUpdateKubeGitStackMutation(
|
export function useUpdateKubeGitStackMutation(
|
||||||
stackId: StackId,
|
stackId: StackId,
|
||||||
environmentId: EnvironmentId
|
environmentId: EnvironmentId,
|
||||||
|
userId: number
|
||||||
) {
|
) {
|
||||||
return useMutation(
|
return useMutation(
|
||||||
(stack: UpdateKubeGitStackPayload) =>
|
async ({
|
||||||
updateGitStack({ stack, stackId, environmentId }),
|
stack,
|
||||||
|
authentication,
|
||||||
|
}: {
|
||||||
|
stack: UpdateKubeGitStackPayload;
|
||||||
|
authentication: GitAuthModel;
|
||||||
|
}) => {
|
||||||
|
// save the new git credentials if the user has selected to save them
|
||||||
|
const newGitAuth = await saveGitCredentialsIfNeeded(
|
||||||
|
userId,
|
||||||
|
authentication
|
||||||
|
);
|
||||||
|
const stackWithUpdatedAuth: UpdateKubeGitStackPayload = {
|
||||||
|
...stack,
|
||||||
|
...newGitAuth,
|
||||||
|
};
|
||||||
|
return updateGitStack({
|
||||||
|
stack: stackWithUpdatedAuth,
|
||||||
|
stackId,
|
||||||
|
environmentId,
|
||||||
|
});
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...withError('Unable to update stack'),
|
...withError('Unable to update application'),
|
||||||
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
|
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGitStack({
|
async function updateGitStack({
|
||||||
stackId,
|
stackId,
|
||||||
stack,
|
stack,
|
||||||
environmentId,
|
environmentId,
|
||||||
|
@ -47,7 +68,11 @@ function updateGitStack({
|
||||||
stack: UpdateKubeGitStackPayload;
|
stack: UpdateKubeGitStackPayload;
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
}) {
|
}) {
|
||||||
return axios.put(buildStackUrl(stackId), stack, {
|
try {
|
||||||
params: { endpointId: environmentId },
|
return await axios.post<Stack>(buildStackUrl(stackId, 'git'), stack, {
|
||||||
});
|
params: { endpointId: environmentId },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e, 'Unable to update stack');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ interface Props extends AutomationTestingProps {
|
||||||
loadingText: string;
|
loadingText: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
|
submitButtonIcon?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormActions({
|
export function FormActions({
|
||||||
|
@ -19,6 +20,7 @@ export function FormActions({
|
||||||
isLoading,
|
isLoading,
|
||||||
children,
|
children,
|
||||||
isValid,
|
isValid,
|
||||||
|
submitButtonIcon,
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
|
@ -30,6 +32,7 @@ export function FormActions({
|
||||||
loadingText={loadingText}
|
loadingText={loadingText}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
|
icon={submitButtonIcon}
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
import { Pod } from 'kubernetes-types/core/v1';
|
import { Pod } from 'kubernetes-types/core/v1';
|
||||||
|
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
import { useStackFileQuery } from '@/react/common/stacks/queries/useStackQuery';
|
import { useStackFile } from '@/react/common/stacks/queries/useStackFile';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '@@/Widget';
|
import { Widget, WidgetBody } from '@@/Widget';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
@ -48,7 +48,7 @@ export function ApplicationDetailsWidget() {
|
||||||
);
|
);
|
||||||
const externalApp = app && isExternalApplication(app);
|
const externalApp = app && isExternalApplication(app);
|
||||||
const appStackId = Number(app?.metadata?.labels?.[appStackIdLabel]);
|
const appStackId = Number(app?.metadata?.labels?.[appStackIdLabel]);
|
||||||
const appStackFileQuery = useStackFileQuery(appStackId);
|
const appStackFileQuery = useStackFile(appStackId);
|
||||||
const { data: appServices } = useApplicationServices(
|
const { data: appServices } = useApplicationServices(
|
||||||
environmentId,
|
environmentId,
|
||||||
namespace,
|
namespace,
|
||||||
|
|
|
@ -1,53 +1,33 @@
|
||||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { RefreshCw } from 'lucide-react';
|
|
||||||
import { useRouter } from '@uirouter/react';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdateKubeGitStackPayload,
|
||||||
|
useUpdateKubeGitStackMutation,
|
||||||
|
} from '@/react/common/stacks/queries/useUpdateKubeGitStackMutation';
|
||||||
import { Stack, StackId } from '@/react/common/stacks/types';
|
import { Stack, StackId } from '@/react/common/stacks/types';
|
||||||
import {
|
import {
|
||||||
parseAutoUpdateResponse,
|
parseAutoUpdateResponse,
|
||||||
transformAutoUpdateViewModel,
|
transformAutoUpdateViewModel,
|
||||||
} from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
} from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
||||||
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
|
import { AutoUpdateMechanism } from '@/react/portainer/gitops/types';
|
||||||
import {
|
import { createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
AutoUpdateMechanism,
|
|
||||||
AutoUpdateModel,
|
|
||||||
} from '@/react/portainer/gitops/types';
|
|
||||||
import {
|
|
||||||
baseStackWebhookUrl,
|
|
||||||
createWebhookId,
|
|
||||||
} from '@/portainer/helpers/webhookHelper';
|
|
||||||
import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
|
|
||||||
import { RefField } from '@/react/portainer/gitops/RefField';
|
|
||||||
import { parseAuthResponse } from '@/react/portainer/gitops/AuthFieldset/utils';
|
import { parseAuthResponse } from '@/react/portainer/gitops/AuthFieldset/utils';
|
||||||
import { confirmEnableTLSVerify } from '@/react/portainer/gitops/utils';
|
|
||||||
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
|
|
||||||
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 { useAnalytics } from '@/react/hooks/useAnalytics';
|
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||||
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
|
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
|
||||||
import { useCreateGitCredentialMutation } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useStackQuery } from '@/react/common/stacks/queries/useStackQuery';
|
import { useStack } from '@/react/common/stacks/queries/useStack';
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
|
||||||
import { LoadingButton } from '@@/buttons';
|
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
|
||||||
import { confirm } from '@@/modals/confirm';
|
|
||||||
import { ModalType } from '@@/modals';
|
|
||||||
import { InlineLoader } from '@@/InlineLoader';
|
import { InlineLoader } from '@@/InlineLoader';
|
||||||
import { Alert } from '@@/Alert';
|
import { Alert } from '@@/Alert';
|
||||||
|
|
||||||
import { useUpdateKubeGitStackMutation } from './queries/useUpdateKubeGitStackMutation';
|
import { KubeAppGitFormValues } from './types';
|
||||||
import {
|
|
||||||
KubeAppGitFormValues,
|
|
||||||
RedeployGitStackPayload,
|
|
||||||
UpdateKubeGitStackPayload,
|
|
||||||
} from './types';
|
|
||||||
import { redeployGitAppFormValidationSchema } from './redeployGitAppFormValidationSchema';
|
import { redeployGitAppFormValidationSchema } from './redeployGitAppFormValidationSchema';
|
||||||
import { useRedeployKubeGitStackMutation } from './queries/useRedeployKubeGitStackMutation';
|
import { RedeployGitAppInnerForm } from './RedeployGitAppInnerForm';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
stackId: StackId;
|
stackId: StackId;
|
||||||
|
@ -55,7 +35,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
|
export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
|
||||||
const { data: stack, ...stackQuery } = useStackQuery(stackId);
|
const { data: stack, ...stackQuery } = useStack(stackId);
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
const initialValues = getInitialValues(stack);
|
const initialValues = getInitialValues(stack);
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
@ -63,10 +43,10 @@ export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
|
||||||
user.Id
|
user.Id
|
||||||
);
|
);
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const createGitCredentialMutation = useCreateGitCredentialMutation();
|
|
||||||
const updateKubeGitStackMutation = useUpdateKubeGitStackMutation(
|
const updateKubeGitStackMutation = useUpdateKubeGitStackMutation(
|
||||||
stack?.Id || 0,
|
stack?.Id || 0,
|
||||||
environmentId
|
environmentId,
|
||||||
|
user.Id
|
||||||
);
|
);
|
||||||
// keep a single generated webhook id, so that it doesn't change on every render
|
// keep a single generated webhook id, so that it doesn't change on every render
|
||||||
const generatedWebhookId = useRef(createWebhookId());
|
const generatedWebhookId = useRef(createWebhookId());
|
||||||
|
@ -122,40 +102,24 @@ export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
|
||||||
) {
|
) {
|
||||||
trackSubmit(values);
|
trackSubmit(values);
|
||||||
|
|
||||||
// save the new git credentials if the user has selected to save them
|
|
||||||
if (
|
|
||||||
values.authentication.SaveCredential &&
|
|
||||||
values.authentication.NewCredentialName &&
|
|
||||||
values.authentication.RepositoryPassword &&
|
|
||||||
values.authentication.RepositoryUsername
|
|
||||||
) {
|
|
||||||
createGitCredentialMutation.mutate({
|
|
||||||
name: values.authentication.NewCredentialName,
|
|
||||||
username: values.authentication.RepositoryUsername,
|
|
||||||
password: values.authentication.RepositoryPassword,
|
|
||||||
userId: user.Id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the kubernetes git stack
|
// update the kubernetes git stack
|
||||||
const { authentication } = values;
|
const { authentication } = values;
|
||||||
const updateKubeGitStack: UpdateKubeGitStackPayload = {
|
const updateKubeGitStack: UpdateKubeGitStackPayload = {
|
||||||
RepositoryAuthentication:
|
|
||||||
!!values.authentication.RepositoryAuthentication,
|
|
||||||
RepositoryReferenceName: values.repositoryReferenceName,
|
RepositoryReferenceName: values.repositoryReferenceName,
|
||||||
AutoUpdate: transformAutoUpdateViewModel(values.autoUpdate, webhookId),
|
AutoUpdate: transformAutoUpdateViewModel(values.autoUpdate, webhookId),
|
||||||
TLSSkipVerify: values.tlsSkipVerify,
|
TLSSkipVerify: values.tlsSkipVerify,
|
||||||
RepositoryGitCredentialID: authentication.RepositoryGitCredentialID,
|
...authentication,
|
||||||
RepositoryPassword: authentication.RepositoryPassword,
|
|
||||||
RepositoryUsername: authentication.RepositoryUsername,
|
|
||||||
};
|
};
|
||||||
await updateKubeGitStackMutation.mutateAsync(updateKubeGitStack, {
|
await updateKubeGitStackMutation.mutateAsync(
|
||||||
onSuccess: ({ data: newStack }) => {
|
{ stack: updateKubeGitStack, authentication },
|
||||||
const newInitialValues = getInitialValues(newStack);
|
{
|
||||||
notifySuccess('Success', 'Application saved successfully.');
|
onSuccess: ({ data: newStack }) => {
|
||||||
resetForm({ values: newInitialValues });
|
const newInitialValues = getInitialValues(newStack);
|
||||||
},
|
notifySuccess('Success', 'Application saved successfully.');
|
||||||
});
|
resetForm({ values: newInitialValues });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackSubmit(values: KubeAppGitFormValues) {
|
function trackSubmit(values: KubeAppGitFormValues) {
|
||||||
|
@ -184,168 +148,6 @@ export function RedeployGitAppForm({ stackId, namespaceName }: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function RedeployGitAppInnerForm({
|
|
||||||
stack,
|
|
||||||
namespaceName,
|
|
||||||
webhookId,
|
|
||||||
}: {
|
|
||||||
stack: Stack;
|
|
||||||
namespaceName: string;
|
|
||||||
webhookId: string;
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const environmentId = useEnvironmentId();
|
|
||||||
const redeployKubeGitStackMutation = useRedeployKubeGitStackMutation(
|
|
||||||
stack.Id,
|
|
||||||
environmentId
|
|
||||||
);
|
|
||||||
const [isRedeployLoading, setIsRedeployLoading] = useState(false);
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const {
|
|
||||||
values,
|
|
||||||
errors,
|
|
||||||
setFieldValue,
|
|
||||||
handleSubmit,
|
|
||||||
dirty,
|
|
||||||
isValid,
|
|
||||||
isSubmitting,
|
|
||||||
setFieldTouched,
|
|
||||||
} = useFormikContext<KubeAppGitFormValues>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form className="form-horizontal" onSubmit={handleSubmit}>
|
|
||||||
<AutoUpdateFieldset
|
|
||||||
value={values.autoUpdate}
|
|
||||||
onChange={(value: AutoUpdateModel) =>
|
|
||||||
setFieldValue('autoUpdate', value)
|
|
||||||
}
|
|
||||||
baseWebhookUrl={baseStackWebhookUrl()}
|
|
||||||
webhookId={webhookId}
|
|
||||||
errors={errors.autoUpdate}
|
|
||||||
environmentType="KUBERNETES"
|
|
||||||
isForcePullVisible={false}
|
|
||||||
webhooksDocs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
|
|
||||||
/>
|
|
||||||
<TimeWindowDisplay />
|
|
||||||
<FormSection title="Advanced configuration" isFoldable>
|
|
||||||
<RefField
|
|
||||||
value={values.repositoryReferenceName}
|
|
||||||
onChange={(refName) =>
|
|
||||||
setFieldValue('repositoryReferenceName', refName)
|
|
||||||
}
|
|
||||||
model={{
|
|
||||||
...values.authentication,
|
|
||||||
RepositoryURL: values.repositoryURL,
|
|
||||||
}}
|
|
||||||
error={errors.repositoryReferenceName}
|
|
||||||
stackId={stack.Id}
|
|
||||||
isUrlValid
|
|
||||||
/>
|
|
||||||
<AuthFieldset
|
|
||||||
value={values.authentication}
|
|
||||||
isAuthExplanationVisible
|
|
||||||
onChange={(value) =>
|
|
||||||
Object.entries(value).forEach(([key, value]) => {
|
|
||||||
setFieldValue(key, value);
|
|
||||||
// set touched after a delay to revalidate debounced username and access token fields
|
|
||||||
setTimeout(() => setFieldTouched(key, true), 400);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
errors={errors.authentication}
|
|
||||||
/>
|
|
||||||
<SwitchField
|
|
||||||
name="TLSSkipVerify"
|
|
||||||
label="Skip TLS verification"
|
|
||||||
labelClass="col-sm-3 col-lg-2"
|
|
||||||
tooltip="Enabling this will allow skipping TLS validation for any self-signed certificate."
|
|
||||||
checked={values.tlsSkipVerify}
|
|
||||||
onChange={onChangeTLSSkipVerify}
|
|
||||||
/>
|
|
||||||
</FormSection>
|
|
||||||
<FormSection title="Actions">
|
|
||||||
<div className="form-group">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<LoadingButton
|
|
||||||
type="button"
|
|
||||||
onClick={handleRedeploy}
|
|
||||||
className="!ml-0"
|
|
||||||
loadingText="In progress..."
|
|
||||||
isLoading={isRedeployLoading}
|
|
||||||
disabled={dirty}
|
|
||||||
icon={RefreshCw}
|
|
||||||
data-cy="application-redeploy-button"
|
|
||||||
>
|
|
||||||
Pull and update application
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton
|
|
||||||
type="submit"
|
|
||||||
loadingText="In progress..."
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
disabled={!dirty || !isValid}
|
|
||||||
data-cy="application-redeploy-button"
|
|
||||||
>
|
|
||||||
Save settings
|
|
||||||
</LoadingButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormSection>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleRedeploy() {
|
|
||||||
const confirmed = await confirm({
|
|
||||||
title: 'Are you sure?',
|
|
||||||
message:
|
|
||||||
'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
|
|
||||||
confirmButton: buildConfirmButton('Update', 'warning'),
|
|
||||||
modalType: ModalType.Warn,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsRedeployLoading(true);
|
|
||||||
|
|
||||||
// track the redeploy event
|
|
||||||
trackEvent('kubernetes-application-edit-git-pull', {
|
|
||||||
category: 'kubernetes',
|
|
||||||
});
|
|
||||||
|
|
||||||
// redeploy the application
|
|
||||||
const redeployPayload: RedeployGitStackPayload = {
|
|
||||||
RepositoryReferenceName: values.repositoryReferenceName,
|
|
||||||
RepositoryAuthentication:
|
|
||||||
!!values.authentication.RepositoryAuthentication,
|
|
||||||
RepositoryGitCredentialID:
|
|
||||||
values.authentication.RepositoryGitCredentialID,
|
|
||||||
RepositoryPassword: values.authentication.RepositoryPassword,
|
|
||||||
RepositoryUsername: values.authentication.RepositoryUsername,
|
|
||||||
Namespace: namespaceName,
|
|
||||||
};
|
|
||||||
await redeployKubeGitStackMutation.mutateAsync(redeployPayload, {
|
|
||||||
onSuccess: () => {
|
|
||||||
notifySuccess('Success', 'Application redeployed successfully.');
|
|
||||||
router.stateService.go('kubernetes.applications.application', {
|
|
||||||
id: stack.Id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsRedeployLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onChangeTLSSkipVerify(value: boolean) {
|
|
||||||
// If the user is disabling TLS verification, ask for confirmation
|
|
||||||
if (stack.GitConfig?.TLSSkipVerify && !value) {
|
|
||||||
const confirmed = await confirmEnableTLSVerify();
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFieldValue('tlsSkipVerify', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialValues(stack?: Stack): KubeAppGitFormValues | undefined {
|
function getInitialValues(stack?: Stack): KubeAppGitFormValues | undefined {
|
||||||
if (!stack || !stack.GitConfig) {
|
if (!stack || !stack.GitConfig) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { Form, useFormikContext } from 'formik';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { RefreshCw } from 'lucide-react';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { Stack } from '@/react/common/stacks/types';
|
||||||
|
import { AutoUpdateFieldset } from '@/react/portainer/gitops/AutoUpdateFieldset';
|
||||||
|
import { AutoUpdateModel } from '@/react/portainer/gitops/types';
|
||||||
|
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
|
||||||
|
import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
|
||||||
|
import { RefField } from '@/react/portainer/gitops/RefField';
|
||||||
|
import { confirmEnableTLSVerify } from '@/react/portainer/gitops/utils';
|
||||||
|
import { AuthFieldset } from '@/react/portainer/gitops/AuthFieldset';
|
||||||
|
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
import { LoadingButton } from '@@/buttons';
|
||||||
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
|
import { confirm } from '@@/modals/confirm';
|
||||||
|
import { ModalType } from '@@/modals';
|
||||||
|
import { FormActions } from '@@/form-components/FormActions';
|
||||||
|
|
||||||
|
import { KubeAppGitFormValues, RedeployGitStackPayload } from './types';
|
||||||
|
import { useRedeployKubeGitStackMutation } from './queries/useRedeployKubeGitStackMutation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
stack: Stack;
|
||||||
|
namespaceName: string;
|
||||||
|
webhookId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RedeployGitAppInnerForm({
|
||||||
|
stack,
|
||||||
|
namespaceName,
|
||||||
|
webhookId,
|
||||||
|
}: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const redeployKubeGitStackMutation = useRedeployKubeGitStackMutation(
|
||||||
|
stack.Id,
|
||||||
|
environmentId
|
||||||
|
);
|
||||||
|
const [isRedeployLoading, setIsRedeployLoading] = useState(false);
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
|
const {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
setFieldValue,
|
||||||
|
handleSubmit,
|
||||||
|
dirty,
|
||||||
|
isValid,
|
||||||
|
isSubmitting,
|
||||||
|
setFieldTouched,
|
||||||
|
} = useFormikContext<KubeAppGitFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="form-horizontal" onSubmit={handleSubmit}>
|
||||||
|
<AutoUpdateFieldset
|
||||||
|
value={values.autoUpdate}
|
||||||
|
onChange={(value: AutoUpdateModel) =>
|
||||||
|
setFieldValue('autoUpdate', value)
|
||||||
|
}
|
||||||
|
baseWebhookUrl={baseStackWebhookUrl()}
|
||||||
|
webhookId={webhookId}
|
||||||
|
errors={errors.autoUpdate}
|
||||||
|
environmentType="KUBERNETES"
|
||||||
|
isForcePullVisible={false}
|
||||||
|
webhooksDocs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
|
||||||
|
/>
|
||||||
|
<TimeWindowDisplay />
|
||||||
|
<FormSection title="Advanced configuration" isFoldable>
|
||||||
|
<RefField
|
||||||
|
value={values.repositoryReferenceName}
|
||||||
|
onChange={(refName) =>
|
||||||
|
setFieldValue('repositoryReferenceName', refName)
|
||||||
|
}
|
||||||
|
model={{
|
||||||
|
...values.authentication,
|
||||||
|
RepositoryURL: values.repositoryURL,
|
||||||
|
}}
|
||||||
|
error={errors.repositoryReferenceName}
|
||||||
|
stackId={stack.Id}
|
||||||
|
isUrlValid
|
||||||
|
/>
|
||||||
|
<AuthFieldset
|
||||||
|
value={values.authentication}
|
||||||
|
isAuthExplanationVisible
|
||||||
|
onChange={(value) =>
|
||||||
|
Object.entries(value).forEach(([key, value]) => {
|
||||||
|
setFieldValue(key, value);
|
||||||
|
// set touched after a delay to revalidate debounced username and access token fields
|
||||||
|
setTimeout(() => setFieldTouched(key, true), 400);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
errors={errors.authentication}
|
||||||
|
/>
|
||||||
|
<SwitchField
|
||||||
|
name="TLSSkipVerify"
|
||||||
|
label="Skip TLS verification"
|
||||||
|
labelClass="col-sm-3 col-lg-2"
|
||||||
|
tooltip="Enabling this will allow skipping TLS validation for any self-signed certificate."
|
||||||
|
checked={values.tlsSkipVerify}
|
||||||
|
onChange={onChangeTLSSkipVerify}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
<FormActions
|
||||||
|
submitLabel="Save settings"
|
||||||
|
loadingText="In progress..."
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isValid={dirty && isValid}
|
||||||
|
data-cy="application-git-save-button"
|
||||||
|
>
|
||||||
|
<LoadingButton
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleRedeploy}
|
||||||
|
loadingText="In progress..."
|
||||||
|
isLoading={isRedeployLoading}
|
||||||
|
disabled={dirty}
|
||||||
|
icon={RefreshCw}
|
||||||
|
data-cy="application-redeploy-button"
|
||||||
|
>
|
||||||
|
Pull and update application
|
||||||
|
</LoadingButton>
|
||||||
|
</FormActions>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleRedeploy() {
|
||||||
|
const confirmed = await confirm({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message:
|
||||||
|
'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
|
||||||
|
confirmButton: buildConfirmButton('Update', 'warning'),
|
||||||
|
modalType: ModalType.Warn,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsRedeployLoading(true);
|
||||||
|
|
||||||
|
// track the redeploy event
|
||||||
|
trackEvent('kubernetes-application-edit-git-pull', {
|
||||||
|
category: 'kubernetes',
|
||||||
|
});
|
||||||
|
|
||||||
|
// redeploy the application
|
||||||
|
const redeployPayload: RedeployGitStackPayload = {
|
||||||
|
RepositoryReferenceName: values.repositoryReferenceName,
|
||||||
|
RepositoryAuthentication:
|
||||||
|
!!values.authentication.RepositoryAuthentication,
|
||||||
|
RepositoryGitCredentialID:
|
||||||
|
values.authentication.RepositoryGitCredentialID,
|
||||||
|
RepositoryPassword: values.authentication.RepositoryPassword,
|
||||||
|
RepositoryUsername: values.authentication.RepositoryUsername,
|
||||||
|
Namespace: namespaceName,
|
||||||
|
};
|
||||||
|
await redeployKubeGitStackMutation.mutateAsync(redeployPayload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess('Success', 'Application redeployed successfully.');
|
||||||
|
router.stateService.go('kubernetes.applications.application', {
|
||||||
|
id: stack.Id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsRedeployLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onChangeTLSSkipVerify(value: boolean) {
|
||||||
|
// If the user is disabling TLS verification, ask for confirmation
|
||||||
|
if (stack.GitConfig?.TLSSkipVerify && !value) {
|
||||||
|
const confirmed = await confirmEnableTLSVerify();
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFieldValue('tlsSkipVerify', value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
import { useMutation } from 'react-query';
|
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|
||||||
import {
|
|
||||||
withError,
|
|
||||||
withInvalidate,
|
|
||||||
queryClient,
|
|
||||||
} from '@/react-tools/react-query';
|
|
||||||
import { Stack, StackId } from '@/react/common/stacks/types';
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
||||||
import { stacksQueryKeys } from '@/react/common/stacks/queries/query-keys';
|
|
||||||
import { buildStackUrl } from '@/react/common/stacks/queries/buildUrl';
|
|
||||||
|
|
||||||
import { UpdateKubeGitStackPayload } from '../types';
|
|
||||||
|
|
||||||
// update a stack from a git repository
|
|
||||||
export function useUpdateKubeGitStackMutation(
|
|
||||||
stackId: StackId,
|
|
||||||
environmentId: EnvironmentId
|
|
||||||
) {
|
|
||||||
return useMutation(
|
|
||||||
(stack: UpdateKubeGitStackPayload) =>
|
|
||||||
updateGitStack({ stack, stackId, environmentId }),
|
|
||||||
{
|
|
||||||
...withError('Unable to update application.'),
|
|
||||||
...withInvalidate(queryClient, [stacksQueryKeys.stackFile(stackId)]),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateGitStack({
|
|
||||||
stackId,
|
|
||||||
stack,
|
|
||||||
environmentId,
|
|
||||||
}: {
|
|
||||||
stackId: StackId;
|
|
||||||
stack: UpdateKubeGitStackPayload;
|
|
||||||
environmentId: EnvironmentId;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
return await axios.post<Stack>(buildStackUrl(stackId, 'git'), stack, {
|
|
||||||
params: { endpointId: environmentId },
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw parseAxiosError(e);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
AutoUpdateModel,
|
AutoUpdateModel,
|
||||||
AutoUpdateResponse,
|
|
||||||
GitAuthModel,
|
GitAuthModel,
|
||||||
GitCredentialsModel,
|
GitCredentialsModel,
|
||||||
} from '@/react/portainer/gitops/types';
|
} from '@/react/portainer/gitops/types';
|
||||||
|
@ -13,12 +12,6 @@ export interface KubeAppGitFormValues {
|
||||||
autoUpdate: AutoUpdateModel;
|
autoUpdate: AutoUpdateModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateKubeGitStackPayload extends GitCredentialsModel {
|
|
||||||
AutoUpdate: AutoUpdateResponse | null;
|
|
||||||
RepositoryReferenceName: string;
|
|
||||||
TLSSkipVerify: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RedeployGitStackPayload extends GitCredentialsModel {
|
export interface RedeployGitStackPayload extends GitCredentialsModel {
|
||||||
RepositoryReferenceName: string;
|
RepositoryReferenceName: string;
|
||||||
Namespace: string;
|
Namespace: string;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useQueryClient, useMutation } from 'react-query';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { GitAuthModel, GitFormModel } from '@/react/portainer/gitops/types';
|
import { GitAuthModel } from '@/react/portainer/gitops/types';
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { UserId } from '@/portainer/users/types';
|
import { UserId } from '@/portainer/users/types';
|
||||||
|
|
||||||
|
@ -84,8 +84,8 @@ export function useSaveCredentialsIfRequired() {
|
||||||
|
|
||||||
export async function saveGitCredentialsIfNeeded(
|
export async function saveGitCredentialsIfNeeded(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
gitModel: GitFormModel
|
gitModel: GitAuthModel
|
||||||
) {
|
): Promise<GitAuthModel> {
|
||||||
let credentialsId = gitModel.RepositoryGitCredentialID;
|
let credentialsId = gitModel.RepositoryGitCredentialID;
|
||||||
let username = gitModel.RepositoryUsername;
|
let username = gitModel.RepositoryUsername;
|
||||||
let password = gitModel.RepositoryPassword;
|
let password = gitModel.RepositoryPassword;
|
||||||
|
|
|
@ -84,7 +84,8 @@ async function createTemplateAndGitCredential(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
{ Git: gitModel, ...values }: CreateTemplatePayload
|
{ Git: gitModel, ...values }: CreateTemplatePayload
|
||||||
) {
|
) {
|
||||||
const newGitModel = await saveGitCredentialsIfNeeded(userId, gitModel);
|
const newGitAuthModel = await saveGitCredentialsIfNeeded(userId, gitModel);
|
||||||
|
const newGitModel = { ...gitModel, ...newGitAuthModel };
|
||||||
|
|
||||||
return createTemplateFromGit({
|
return createTemplateFromGit({
|
||||||
...values,
|
...values,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue