1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

feat(custom-templates): migrate create view to react [EE-6400] (#10715)

This commit is contained in:
Chaim Lev-Ari 2023-12-06 14:11:02 +01:00 committed by GitHub
parent bd5ba7b5d0
commit dabcf4f7db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 495 additions and 960 deletions

View file

@ -0,0 +1,51 @@
import { StackType } from '../types';
const dockerTexts = {
editor: {
placeholder: 'Define or paste the content of your docker compose file here',
description: (
<p>
You can get more information about Compose file format in the{' '}
<a
href="https://docs.docker.com/compose/compose-file/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
),
},
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: (
<>
<p>
Templates allow deploying any kind of Kubernetes resource
(Deployment, Secret, ConfigMap...)
</p>
<p>
You can get more information about Kubernetes file format in the
<a
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
</>
),
},
upload: 'You can upload a Manifest file from your computer.',
},
} as const;

View file

@ -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 (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
validateOnMount
>
<InnerForm isLoading={mutation.isLoading || isSaveCredentialsLoading} />
</Formik>
);
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;
}
}

View file

@ -1,28 +0,0 @@
import { PageHeader } from '@@/PageHeader';
import { Widget } from '@@/Widget';
import { CreateTemplateForm } from './CreateTemplateForm';
export function CreateView() {
return (
<div>
<PageHeader
title="Create Custom template"
breadcrumbs={[
{ label: 'Custom Templates', link: '^' },
'Create Custom template',
]}
/>
<div className="row">
<div className="col-sm-12">
<Widget>
<Widget.Body>
<CreateTemplateForm />
</Widget.Body>
</Widget>
</div>
</div>
</div>
);
}

View file

@ -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({

View file

@ -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'],

View file

@ -43,7 +43,7 @@ export function TeamsField({
/>
) : (
<span className="small text-muted">
You have not yet created any teams. Head over to the
You have not yet created any teams. Head over to the{' '}
<Link to="portainer.teams">Teams view</Link> to manage teams.
</span>
)}

View file

@ -34,7 +34,7 @@ export function UsersField({ name, users, value, onChange, errors }: Props) {
/>
) : (
<span className="small text-muted">
You have not yet created any users. Head over to the
You have not yet created any users. Head over to the{' '}
<Link to="portainer.users">Users view</Link> to manage users.
</span>
)}

View file

@ -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,
};
}

View file

@ -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<T = Environment | null>(
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'),
}
);
}

View file

@ -24,12 +24,12 @@ export function NewCredentialForm({
<Checkbox
id="repository-save-credential"
label="save credential"
checked={value.SaveCredential}
checked={value.SaveCredential || false}
className="[&+label]:mb-0"
onChange={(e) => onChange({ SaveCredential: e.target.checked })}
/>
<Input
value={value.NewCredentialName}
value={value.NewCredentialName || ''}
name="new_credential_name"
placeholder="credential name"
className="ml-4 w-48"

View file

@ -0,0 +1,91 @@
import { Formik } from 'formik';
import { 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 { EnvironmentId } from '@/react/portainer/environments/types';
import { useEnvironmentDeploymentOptions } from '@/react/portainer/environments/queries/useEnvironment';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { isKubernetesEnvironment } from '@/react/portainer/environments/utils';
import { useInitialValues } from './useInitialValues';
import { FormValues, initialBuildMethods } from './types';
import { useValidation } from './useValidation';
import { InnerForm } from './InnerForm';
export function CreateForm({
environmentId,
defaultType,
}: {
environmentId?: EnvironmentId;
defaultType: StackType;
}) {
const isEdge = !environmentId;
const router = useRouter();
const mutation = useCreateTemplateMutation();
const validation = useValidation(isEdge);
const buildMethods = useBuildMethods();
const initialValues = useInitialValues({
defaultType,
isEdge,
buildMethods: buildMethods.map((method) => method.value),
});
if (!initialValues) {
return null;
}
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
validateOnMount
>
<InnerForm
isLoading={mutation.isLoading}
environmentId={environmentId}
buildMethods={buildMethods}
/>
</Formik>
);
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;
}
});
}

View file

@ -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 (
<div>
<PageHeader
title="Create Custom template"
breadcrumbs={[
{ label: 'Custom Templates', link: '^' },
'Create Custom template',
]}
/>
<div className="row">
<div className="col-sm-12">
<Widget>
<Widget.Body>
<CreateForm
defaultType={defaultType}
environmentId={environmentId}
/>
</Widget.Body>
</Widget>
</div>
</div>
</div>
);
}
function useDefaultType() {
const {
state: { name },
} = useCurrentStateAndParams();
if (name?.includes('kubernetes')) {
return StackType.Kubernetes;
}
// edge or docker
return StackType.DockerCompose;
}

View file

@ -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<FormValues>();
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 (
<Form className="form-horizontal">
<CommonFields
@ -56,15 +69,19 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) {
errors={errors}
/>
<PlatformField
value={values.Platform}
onChange={(value) => setFieldValue('Platform', value)}
/>
{values.Type !== StackType.Kubernetes && (
<>
<PlatformField
value={values.Platform}
onChange={(value) => setFieldValue('Platform', value)}
/>
<TemplateTypeSelector
value={values.Type}
onChange={(value) => setFieldValue('Type', value)}
/>
<TemplateTypeSelector
value={values.Type}
onChange={(value) => setFieldValue('Type', value)}
/>
</>
)}
<FormSection title="Build method">
<BoxSelector
@ -76,32 +93,22 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) {
/>
</FormSection>
{values.Method === editor.value && (
{isEditor && (
<WebEditorForm
id="custom-template-creation-editor"
value={values.FileContent}
onChange={handleChangeFileContent}
yaml
placeholder="Define or paste the content of your docker compose file here"
placeholder={texts.editor.placeholder}
error={errors.FileContent}
>
<p>
You can get more information about Compose file format in the{' '}
<a
href="https://docs.docker.com/compose/compose-file/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
{texts.editor.description}
</WebEditorForm>
)}
{values.Method === upload.value && (
{values.Method === 'upload' && (
<FileUploadForm
description="You can upload a Compose file from your computer."
description={texts.upload}
value={values.File}
onChange={(value) => setFieldValue('File', value)}
required
@ -110,6 +117,9 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) {
{isGit && (
<GitForm
deployMethod={
values.Type === StackType.Kubernetes ? 'manifest' : 'compose'
}
value={values.Git}
onChange={(newValues) =>
setValues((values) => ({
@ -125,11 +135,21 @@ export function InnerForm({ isLoading }: { isLoading: boolean }) {
<CustomTemplatesVariablesDefinitionField
value={values.Variables}
onChange={(values) => setFieldValue('Variables', values)}
isVariablesNamesFromParent={values.Method === editor.value}
isVariablesNamesFromParent={isEditor}
errors={errors.Variables}
/>
)}
{!!values.AccessControl && (
<AccessControlForm
environmentId={environmentId || 0}
onChange={(values) => setFieldValue('AccessControl', values)}
values={values.AccessControl}
errors={errors.AccessControl as FormikErrors<AccessControlFormData>}
formNamespace="accessControl"
/>
)}
{values.EdgeSettings && (
<EdgeSettingsFieldset
setValues={(edgeSetValues) =>

View file

@ -3,7 +3,7 @@ import { type Values as CommonFieldsValues } from '@/react/portainer/custom-temp
import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
import { Platform } from '@/react/portainer/templates/types';
import { GitFormModel } from '@/react/portainer/gitops/types';
import { EdgeTemplateSettings } from '@/react/portainer/templates/custom-templates/types';
import { AccessControlFormData } from '@/react/portainer/access-control/types';
import {
editor,
@ -11,9 +11,11 @@ import {
git,
} from '@@/BoxSelector/common-options/build-methods';
export const buildMethods = [editor, upload, git] as const;
import { EdgeTemplateSettings } from '../types';
export type Method = (typeof buildMethods)[number]['value'];
export const initialBuildMethods = [editor, upload, git] as const;
export type Method = (typeof initialBuildMethods)[number]['value'];
export interface FormValues extends CommonFieldsValues {
Platform: Platform;
@ -23,5 +25,6 @@ export interface FormValues extends CommonFieldsValues {
File: File | undefined;
Git: GitFormModel;
Variables: DefinitionFieldValues;
AccessControl?: AccessControlFormData;
EdgeSettings?: EdgeTemplateSettings;
}

View file

@ -0,0 +1,94 @@
import { useCurrentStateAndParams } from '@uirouter/react';
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { useCurrentUser } from '@/react/hooks/useUser';
import { StackType } from '@/react/common/stacks/types';
import { Platform } from '../../types';
import { useFetchTemplateFile } from '../../app-templates/queries/useFetchTemplateFile';
import { getDefaultEdgeTemplateSettings } from '../types';
import { FormValues, Method } from './types';
export function useInitialValues({
defaultType,
isEdge = false,
buildMethods,
}: {
defaultType: StackType;
isEdge?: boolean;
buildMethods: Array<Method>;
}): FormValues | undefined {
const { user, isAdmin } = useCurrentUser();
const { appTemplateId, type = defaultType } = useAppTemplateParams();
const fileContentQuery = useFetchTemplateFile(appTemplateId);
if (fileContentQuery.isLoading) {
return undefined;
}
return {
Title: '',
FileContent: fileContentQuery.data ?? '',
Type: type,
Platform: Platform.LINUX,
File: undefined,
Method: buildMethods[0],
Description: '',
Note: '',
Logo: '',
Variables: [],
Git: {
RepositoryURL: '',
RepositoryReferenceName: '',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
ComposeFilePathInRepository: 'docker-compose.yml',
AdditionalFiles: [],
RepositoryURLValid: true,
TLSSkipVerify: false,
},
AccessControl: isEdge
? undefined
: parseAccessControlFormData(isAdmin, user.Id),
EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined,
};
}
function useAppTemplateParams() {
const {
params: { type, appTemplateId },
} = useCurrentStateAndParams();
return {
type: getStackType(type),
appTemplateId: getTemplateId(appTemplateId),
};
function getStackType(type: string): StackType | undefined {
if (!type) {
return undefined;
}
const typeNum = parseInt(type, 10);
if (
[
StackType.DockerSwarm,
StackType.DockerCompose,
StackType.Kubernetes,
].includes(typeNum)
) {
return typeNum;
}
return undefined;
}
function getTemplateId(appTemplateId: string): number | undefined {
const id = parseInt(appTemplateId, 10);
return Number.isNaN(id) ? undefined : id;
}
}

View file

@ -9,6 +9,7 @@ import { buildGitValidationSchema } from '@/react/portainer/gitops/GitForm';
import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation';
import { file } from '@@/form-components/yup-file-validation';
import {
@ -17,10 +18,9 @@ import {
upload,
} from '@@/BoxSelector/common-options/build-methods';
import { buildMethods } from './types';
import { edgeFieldsetValidation } from './EdgeSettingsFieldset.validation';
import { initialBuildMethods } from './types';
export function useValidation() {
export function useValidation(isEdge: boolean) {
const { user } = useCurrentUser();
const gitCredentialsQuery = useGitCredentials(user.Id);
const customTemplatesQuery = useCustomTemplates();
@ -38,7 +38,7 @@ export function useValidation() {
StackType.Kubernetes,
])
.default(StackType.DockerCompose),
Method: string().oneOf(buildMethods.map((m) => m.value)),
Method: string().oneOf(initialBuildMethods.map((m) => m.value)),
FileContent: string().when('Method', {
is: editor.value,
then: (schema) => schema.required('Template is required.'),
@ -52,10 +52,10 @@ export function useValidation() {
then: () => buildGitValidationSchema(gitCredentialsQuery.data || []),
}),
Variables: variablesValidation(),
EdgeSettings: edgeFieldsetValidation(),
EdgeSettings: isEdge ? edgeFieldsetValidation() : mixed(),
}).concat(
commonFieldsValidation({ templates: customTemplatesQuery.data })
),
[customTemplatesQuery.data, gitCredentialsQuery.data]
[customTemplatesQuery.data, gitCredentialsQuery.data, isEdge]
);
}

View file

@ -10,57 +10,96 @@ import {
withInvalidate,
} from '@/react-tools/react-query';
import { StackType } from '@/react/common/stacks/types';
import { FormValues } from '@/react/edge/templates/custom-templates/CreateView/types';
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
import {
CustomTemplate,
EdgeTemplateSettings,
} from '@/react/portainer/templates/custom-templates/types';
import { GitFormModel } from '@/react/portainer/gitops/types';
import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
import { AccessControlFormData } from '@/react/portainer/access-control/types';
import { applyResourceControl } from '@/react/portainer/access-control/access-control.service';
import { useCurrentUser } from '@/react/hooks/useUser';
import { UserId } from '@/portainer/users/types';
import { saveGitCredentialsIfNeeded } from '@/react/portainer/account/git-credentials/queries/useCreateGitCredentialsMutation';
import { Platform } from '../../types';
import { buildUrl } from './build-url';
import { queryKeys } from './query-keys';
interface CreateTemplatePayload {
EdgeTemplate?: boolean;
Platform: Platform;
Type: StackType;
Method: 'editor' | 'upload' | 'repository';
FileContent: string;
File: File | undefined;
Git: GitFormModel;
Variables: DefinitionFieldValues;
EdgeSettings?: EdgeTemplateSettings;
Title: string;
Description: string;
Note: string;
Logo: string;
AccessControl?: AccessControlFormData;
}
export function useCreateTemplateMutation() {
const { user } = useCurrentUser();
const queryClient = useQueryClient();
return useMutation(
createTemplate,
async (payload: CreateTemplatePayload) => {
const template = await createTemplate(user.Id, payload);
const resourceControl = template.ResourceControl;
if (resourceControl && payload.AccessControl) {
await applyResourceControl(payload.AccessControl, resourceControl.Id);
}
return template;
},
mutationOptions(
withInvalidate(queryClient, [['custom-templates']]),
withInvalidate(queryClient, [queryKeys.base()]),
withGlobalError('Failed to create template')
)
);
}
function createTemplate({
Method,
Git,
...values
}: FormValues & { EdgeTemplate?: boolean }) {
switch (Method) {
function createTemplate(userId: UserId, payload: CreateTemplatePayload) {
switch (payload.Method) {
case 'editor':
return createTemplateFromText(values);
return createTemplateFromText(payload);
case 'upload':
return createTemplateFromFile(values);
return createTemplateFromFile(payload);
case 'repository':
return createTemplateFromGit({
...values,
...Git,
...(values.EdgeSettings
? {
EdgeSettings: {
...values.EdgeSettings,
...values.EdgeSettings.RelativePathSettings,
},
}
: {}),
});
return createTemplateAndGitCredential(userId, payload);
default:
throw new Error('Unknown method');
}
}
async function createTemplateAndGitCredential(
userId: UserId,
{ Git: gitModel, ...values }: CreateTemplatePayload
) {
const newGitModel = await saveGitCredentialsIfNeeded(userId, gitModel);
return createTemplateFromGit({
...values,
...newGitModel,
...(values.EdgeSettings
? {
EdgeSettings: {
...values.EdgeSettings,
...values.EdgeSettings.RelativePathSettings,
},
}
: {}),
});
}
/**
* Payload for creating a custom template from file content.
*/
@ -179,6 +218,10 @@ interface CustomTemplateFromGitRepositoryPayload {
RepositoryUsername?: string;
/** Password used in basic authentication when RepositoryAuthentication is true. */
RepositoryPassword?: string;
/** GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication
* is true and RepositoryUsername/RepositoryPassword are not provided
*/
RepositoryGitCredentialID?: number;
/** Path to the Stack file inside the Git repository. */
ComposeFilePathInRepository: string;
/** Definitions of variables in the stack file. */