mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(templates): migrate edit view to react [EE-6412] (#10774)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
This commit is contained in:
parent
e142939929
commit
236e669332
32 changed files with 443 additions and 1089 deletions
|
@ -1,108 +0,0 @@
|
|||
import { Formik } from 'formik';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
import { useCustomTemplateFile } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplateFile';
|
||||
import { useUpdateTemplateMutation } from '@/react/portainer/templates/custom-templates/queries/useUpdateTemplateMutation';
|
||||
import {
|
||||
getTemplateVariables,
|
||||
intersectVariables,
|
||||
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';
|
||||
import { useValidation } from './useValidation';
|
||||
|
||||
export function EditTemplateForm({ template }: { template: CustomTemplate }) {
|
||||
const mutation = useUpdateTemplateMutation();
|
||||
const router = useRouter();
|
||||
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;
|
||||
}
|
||||
|
||||
const initialValues: FormValues = {
|
||||
Title: template.Title,
|
||||
Type: template.Type,
|
||||
Description: template.Description,
|
||||
Note: template.Note,
|
||||
Logo: template.Logo,
|
||||
Platform: template.Platform,
|
||||
Variables: parseTemplate(fileQuery.data || ''),
|
||||
|
||||
FileContent: fileQuery.data || '',
|
||||
Git: template.GitConfig ? toGitFormModel(template.GitConfig) : undefined,
|
||||
EdgeSettings: template.EdgeSettings,
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm
|
||||
isLoading={mutation.isLoading || isSaveCredentialsLoading}
|
||||
isEditorReadonly={isGit}
|
||||
gitFileContent={isGit ? fileQuery.data : ''}
|
||||
refreshGitFile={fileQuery.refetch}
|
||||
gitFileError={
|
||||
fileQuery.error instanceof Error ? fileQuery.error.message : ''
|
||||
}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
|
||||
async function handleSubmit(values: FormValues) {
|
||||
const credentialId = await saveCredentials(values.Git);
|
||||
|
||||
mutation.mutate(
|
||||
{
|
||||
id: template.Id,
|
||||
EdgeTemplate: template.EdgeTemplate,
|
||||
Description: values.Description,
|
||||
Title: values.Title,
|
||||
Type: values.Type,
|
||||
Logo: values.Logo,
|
||||
FileContent: values.FileContent,
|
||||
Note: values.Note,
|
||||
Platform: values.Platform,
|
||||
Variables: values.Variables,
|
||||
EdgeSettings: values.EdgeSettings,
|
||||
...(values.Git ? toGitRequest(values.Git, credentialId) : {}),
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess('Success', 'Template updated successfully');
|
||||
router.stateService.go('^');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function parseTemplate(templateContent: string) {
|
||||
if (!isTemplateVariablesEnabled) {
|
||||
return template.Variables;
|
||||
}
|
||||
|
||||
const [variables] = getTemplateVariables(templateContent);
|
||||
|
||||
if (!variables) {
|
||||
return template.Variables;
|
||||
}
|
||||
|
||||
return intersectVariables(template.Variables, variables);
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCustomTemplate } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplate';
|
||||
import { notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Widget } from '@@/Widget';
|
||||
|
||||
import { EditTemplateForm } from './EditTemplateForm';
|
||||
|
||||
export function EditView() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
params: { id: templateId },
|
||||
} = useCurrentStateAndParams();
|
||||
const customTemplateQuery = useCustomTemplate(templateId);
|
||||
|
||||
useEffect(() => {
|
||||
if (customTemplateQuery.data && !customTemplateQuery.data.EdgeTemplate) {
|
||||
notifyError('Error', new Error('Trying to load non edge template'));
|
||||
router.stateService.go('^');
|
||||
}
|
||||
}, [customTemplateQuery.data, router.stateService]);
|
||||
|
||||
if (!customTemplateQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const template = customTemplateQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Edit Custom Template"
|
||||
breadcrumbs={[{ label: 'Custom templates', link: '^' }, template.Title]}
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<EditTemplateForm template={template} />
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -91,33 +91,52 @@ export function CommonFields({
|
|||
export function validation({
|
||||
currentTemplateId,
|
||||
templates = [],
|
||||
title,
|
||||
viewType = 'docker',
|
||||
}: {
|
||||
currentTemplateId?: CustomTemplate['Id'];
|
||||
templates?: Array<CustomTemplate>;
|
||||
title?: { pattern: string; error: string };
|
||||
viewType?: 'kube' | 'docker' | 'edge';
|
||||
} = {}): SchemaOf<Values> {
|
||||
let titleSchema = string()
|
||||
.required('Title is required.')
|
||||
.test(
|
||||
'is-unique',
|
||||
'Title must be unique',
|
||||
(value) =>
|
||||
!value ||
|
||||
!templates.some(
|
||||
(template) =>
|
||||
template.Title === value && template.Id !== currentTemplateId
|
||||
)
|
||||
);
|
||||
if (title?.pattern) {
|
||||
const pattern = new RegExp(title.pattern);
|
||||
titleSchema = titleSchema.matches(pattern, title.error);
|
||||
}
|
||||
const titlePattern = titlePatternValidation(viewType);
|
||||
|
||||
return object({
|
||||
Title: titleSchema,
|
||||
Title: string()
|
||||
.required('Title is required.')
|
||||
.test(
|
||||
'is-unique',
|
||||
'Title must be unique',
|
||||
(value) =>
|
||||
!value ||
|
||||
!templates.some(
|
||||
(template) =>
|
||||
template.Title === value && template.Id !== currentTemplateId
|
||||
)
|
||||
)
|
||||
.matches(titlePattern.pattern, titlePattern.error),
|
||||
Description: string().required('Description is required.'),
|
||||
Note: string().default(''),
|
||||
Logo: string().default(''),
|
||||
});
|
||||
}
|
||||
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
|
||||
const KUBE_TEMPLATE_NAME_VALIDATION_REGEX =
|
||||
'^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters
|
||||
|
||||
function titlePatternValidation(type: 'kube' | 'docker' | 'edge') {
|
||||
switch (type) {
|
||||
case 'kube':
|
||||
return {
|
||||
pattern: new RegExp(KUBE_TEMPLATE_NAME_VALIDATION_REGEX),
|
||||
error:
|
||||
"This field must consist of lower-case alphanumeric characters, '.', '_' or '-', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123').",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
pattern: new RegExp(TEMPLATE_NAME_VALIDATION_REGEX),
|
||||
error:
|
||||
"This field must consist of lower-case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,19 +16,20 @@ import { InnerForm } from './InnerForm';
|
|||
|
||||
export function CreateForm({
|
||||
environmentId,
|
||||
defaultType,
|
||||
viewType,
|
||||
}: {
|
||||
environmentId?: EnvironmentId;
|
||||
defaultType: StackType;
|
||||
viewType: 'kube' | 'docker' | 'edge';
|
||||
}) {
|
||||
const isEdge = !environmentId;
|
||||
const router = useRouter();
|
||||
const mutation = useCreateTemplateMutation();
|
||||
const validation = useValidation(isEdge);
|
||||
const validation = useValidation({ viewType });
|
||||
const buildMethods = useBuildMethods();
|
||||
|
||||
const initialValues = useInitialValues({
|
||||
defaultType,
|
||||
defaultType:
|
||||
viewType === 'kube' ? StackType.Kubernetes : StackType.DockerCompose,
|
||||
isEdge,
|
||||
buildMethods: buildMethods.map((method) => method.value),
|
||||
});
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
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 { useViewType } from '../useViewType';
|
||||
|
||||
import { CreateForm } from './CreateForm';
|
||||
|
||||
export function CreateView() {
|
||||
const defaultType = useDefaultType();
|
||||
const viewType = useViewType();
|
||||
const environmentId = useEnvironmentId(false);
|
||||
|
||||
return (
|
||||
|
@ -26,10 +25,7 @@ export function CreateView() {
|
|||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<CreateForm
|
||||
defaultType={defaultType}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<CreateForm viewType={viewType} environmentId={environmentId} />
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
|
@ -37,15 +33,3 @@ export function CreateView() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDefaultType() {
|
||||
const {
|
||||
state: { name },
|
||||
} = useCurrentStateAndParams();
|
||||
if (name?.includes('kubernetes')) {
|
||||
return StackType.Kubernetes;
|
||||
}
|
||||
|
||||
// edge or docker
|
||||
return StackType.DockerCompose;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,7 @@ import { CommonFields } from '@/react/portainer/custom-templates/components/Comm
|
|||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
||||
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
||||
import {
|
||||
getTemplateVariables,
|
||||
intersectVariables,
|
||||
isTemplateVariablesEnabled,
|
||||
} from '@/react/portainer/custom-templates/components/utils';
|
||||
import { isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
||||
import { AccessControlForm } from '@/react/portainer/access-control';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
@ -24,6 +20,7 @@ import { FormActions } from '@@/form-components/FormActions';
|
|||
import { FormSection } from '@@/form-components/FormSection';
|
||||
|
||||
import { EdgeTemplateSettings } from '../types';
|
||||
import { useParseTemplateOnFileChange } from '../useParseTemplateOnFileChange';
|
||||
|
||||
import { EdgeSettingsFieldset } from './EdgeSettingsFieldset';
|
||||
import { FormValues, Method, initialBuildMethods } from './types';
|
||||
|
@ -57,6 +54,10 @@ export function InnerForm({
|
|||
isEditor && !isSubmitting && !isLoading
|
||||
);
|
||||
|
||||
const handleChangeFileContent = useParseTemplateOnFileChange(
|
||||
values.Variables
|
||||
);
|
||||
|
||||
const texts = textByType[values.Type];
|
||||
|
||||
return (
|
||||
|
@ -181,36 +182,6 @@ export function InnerForm({
|
|||
</Form>
|
||||
);
|
||||
|
||||
function handleChangeFileContent(value: string) {
|
||||
setFieldValue(
|
||||
'FileContent',
|
||||
value,
|
||||
isTemplateVariablesEnabled ? !value : true
|
||||
);
|
||||
parseTemplate(value);
|
||||
}
|
||||
|
||||
function parseTemplate(value: string) {
|
||||
if (!isTemplateVariablesEnabled || value === '') {
|
||||
setFieldValue('Variables', []);
|
||||
return;
|
||||
}
|
||||
|
||||
const [variables, validationError] = getTemplateVariables(value);
|
||||
const isValid = !!variables;
|
||||
|
||||
setFieldError(
|
||||
'FileContent',
|
||||
validationError ? `Template invalid: ${validationError}` : undefined
|
||||
);
|
||||
if (isValid) {
|
||||
setFieldValue(
|
||||
'Variables',
|
||||
intersectVariables(values.Variables, variables)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeMethod(method: Method) {
|
||||
setFieldValue('FileContent', '');
|
||||
setFieldValue('Variables', []);
|
||||
|
|
|
@ -20,7 +20,11 @@ import {
|
|||
|
||||
import { initialBuildMethods } from './types';
|
||||
|
||||
export function useValidation(isEdge: boolean) {
|
||||
export function useValidation({
|
||||
viewType,
|
||||
}: {
|
||||
viewType: 'kube' | 'docker' | 'edge';
|
||||
}) {
|
||||
const { user } = useCurrentUser();
|
||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||
const customTemplatesQuery = useCustomTemplates();
|
||||
|
@ -52,10 +56,13 @@ export function useValidation(isEdge: boolean) {
|
|||
then: () => buildGitValidationSchema(gitCredentialsQuery.data || []),
|
||||
}),
|
||||
Variables: variablesValidation(),
|
||||
EdgeSettings: isEdge ? edgeFieldsetValidation() : mixed(),
|
||||
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
|
||||
}).concat(
|
||||
commonFieldsValidation({ templates: customTemplatesQuery.data })
|
||||
commonFieldsValidation({
|
||||
templates: customTemplatesQuery.data,
|
||||
viewType,
|
||||
})
|
||||
),
|
||||
[customTemplatesQuery.data, gitCredentialsQuery.data, isEdge]
|
||||
[customTemplatesQuery.data, gitCredentialsQuery.data, viewType]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import { Formik } from 'formik';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
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 { CustomTemplate } from '../types';
|
||||
import { useUpdateTemplateMutation } from '../queries/useUpdateTemplateMutation';
|
||||
import { useCustomTemplateFile } from '../queries/useCustomTemplateFile';
|
||||
import { TemplateViewType } from '../useViewType';
|
||||
|
||||
import { useInitialValues } from './useInitialValues';
|
||||
import { FormValues } from './types';
|
||||
import { useValidation } from './useValidation';
|
||||
import { InnerForm } from './InnerForm';
|
||||
|
||||
export function EditForm({
|
||||
template,
|
||||
environmentId,
|
||||
viewType,
|
||||
}: {
|
||||
template: CustomTemplate;
|
||||
environmentId?: EnvironmentId;
|
||||
viewType: TemplateViewType;
|
||||
}) {
|
||||
const isEdge = template.EdgeTemplate;
|
||||
const isGit = !!template.GitConfig;
|
||||
|
||||
const router = useRouter();
|
||||
const disableEditor = useDisableEditor(isGit);
|
||||
const mutation = useUpdateTemplateMutation();
|
||||
const validation = useValidation({
|
||||
viewType,
|
||||
isGit,
|
||||
templateId: template.Id,
|
||||
});
|
||||
|
||||
const fileContentQuery = useCustomTemplateFile(template.Id);
|
||||
|
||||
const initialValues = useInitialValues({
|
||||
isEdge,
|
||||
template,
|
||||
templateFile: fileContentQuery.data,
|
||||
});
|
||||
|
||||
if (fileContentQuery.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm
|
||||
isLoading={mutation.isLoading}
|
||||
environmentId={environmentId}
|
||||
isEditorReadonly={disableEditor}
|
||||
refreshGitFile={fileContentQuery.refetch}
|
||||
gitFileContent={fileContentQuery.data}
|
||||
gitFileError={
|
||||
fileContentQuery.error instanceof Error
|
||||
? fileContentQuery.error.message
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
mutation.mutate(
|
||||
{
|
||||
id: template.Id,
|
||||
EdgeTemplate: template.EdgeTemplate,
|
||||
Description: values.Description,
|
||||
Title: values.Title,
|
||||
Type: values.Type,
|
||||
Logo: values.Logo,
|
||||
FileContent: values.FileContent,
|
||||
Note: values.Note,
|
||||
Platform: values.Platform,
|
||||
Variables: values.Variables,
|
||||
EdgeSettings: values.EdgeSettings,
|
||||
AccessControl: values.AccessControl,
|
||||
resourceControlId: template.ResourceControl?.Id,
|
||||
...values.Git,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess('Success', 'Template updated successfully');
|
||||
router.stateService.go('^');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function useDisableEditor(isGit: boolean) {
|
||||
const environment = useCurrentEnvironment(false);
|
||||
|
||||
const deploymentOptionsQuery = useEnvironmentDeploymentOptions(
|
||||
environment.data && isKubernetesEnvironment(environment.data.Type)
|
||||
? environment.data.Id
|
||||
: undefined
|
||||
);
|
||||
|
||||
return isGit || !!deploymentOptionsQuery.data?.hideAddWithForm;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Widget } from '@@/Widget';
|
||||
|
||||
import { useCustomTemplate } from '../queries/useCustomTemplate';
|
||||
import { useViewType } from '../useViewType';
|
||||
|
||||
import { EditForm } from './EditForm';
|
||||
|
||||
export function EditView() {
|
||||
const viewType = useViewType();
|
||||
const {
|
||||
params: { id },
|
||||
} = useCurrentStateAndParams();
|
||||
const environmentId = useEnvironmentId(false);
|
||||
const templateQuery = useCustomTemplate(id);
|
||||
|
||||
if (!templateQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const template = templateQuery.data;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Edit Custom template"
|
||||
breadcrumbs={[{ label: 'Custom Templates', link: '^' }, template.Title]}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<EditForm
|
||||
environmentId={environmentId}
|
||||
template={template}
|
||||
viewType={viewType}
|
||||
/>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,31 +5,36 @@ import { CommonFields } from '@/react/portainer/custom-templates/components/Comm
|
|||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
|
||||
import { GitForm } from '@/react/portainer/gitops/GitForm';
|
||||
import {
|
||||
getTemplateVariables,
|
||||
intersectVariables,
|
||||
isTemplateVariablesEnabled,
|
||||
} from '@/react/portainer/custom-templates/components/utils';
|
||||
import { isTemplateVariablesEnabled } from '@/react/portainer/custom-templates/components/utils';
|
||||
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 { StackType } from '@/react/common/stacks/types';
|
||||
import { textByType } from '@/react/common/stacks/common/form-texts';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { AccessControlForm } from '@/react/portainer/access-control';
|
||||
import { AccessControlFormData } from '@/react/portainer/access-control/types';
|
||||
|
||||
import { WebEditorForm, usePreventExit } from '@@/WebEditorForm';
|
||||
import { FormActions } from '@@/form-components/FormActions';
|
||||
import { Button } from '@@/buttons';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
|
||||
import { useParseTemplateOnFileChange } from '../useParseTemplateOnFileChange';
|
||||
|
||||
import { FormValues } from './types';
|
||||
|
||||
export function InnerForm({
|
||||
isLoading,
|
||||
environmentId,
|
||||
isEditorReadonly,
|
||||
gitFileContent,
|
||||
gitFileError,
|
||||
refreshGitFile,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
environmentId?: EnvironmentId;
|
||||
isEditorReadonly: boolean;
|
||||
gitFileContent?: string;
|
||||
gitFileError?: string;
|
||||
|
@ -42,9 +47,9 @@ export function InnerForm({
|
|||
errors,
|
||||
isValid,
|
||||
setFieldError,
|
||||
setValues,
|
||||
isSubmitting,
|
||||
dirty,
|
||||
setValues,
|
||||
} = useFormikContext<FormValues>();
|
||||
|
||||
usePreventExit(
|
||||
|
@ -52,6 +57,13 @@ export function InnerForm({
|
|||
values.FileContent,
|
||||
!isEditorReadonly && !isSubmitting && !isLoading
|
||||
);
|
||||
|
||||
const handleChangeFileContent = useParseTemplateOnFileChange(
|
||||
values.Variables
|
||||
);
|
||||
|
||||
const texts = textByType[values.Type];
|
||||
|
||||
return (
|
||||
<Form className="form-horizontal">
|
||||
<CommonFields
|
||||
|
@ -62,15 +74,19 @@ export function InnerForm({
|
|||
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)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<WebEditorForm
|
||||
id="edit-custom-template-editor"
|
||||
|
@ -80,33 +96,14 @@ export function InnerForm({
|
|||
placeholder={
|
||||
gitFileContent
|
||||
? 'Preview of the file from git repository'
|
||||
: 'Define or paste the content of your docker compose file here'
|
||||
: texts.editor.placeholder
|
||||
}
|
||||
error={errors.FileContent}
|
||||
readonly={isEditorReadonly}
|
||||
>
|
||||
<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>
|
||||
|
||||
{isTemplateVariablesEnabled && (
|
||||
<CustomTemplatesVariablesDefinitionField
|
||||
value={values.Variables}
|
||||
onChange={(values) => setFieldValue('Variables', values)}
|
||||
isVariablesNamesFromParent={!isEditorReadonly}
|
||||
errors={errors.Variables}
|
||||
/>
|
||||
)}
|
||||
|
||||
{values.Git && (
|
||||
<>
|
||||
<GitForm
|
||||
|
@ -118,6 +115,9 @@ export function InnerForm({
|
|||
Git: { ...values.Git!, ...newValues },
|
||||
}))
|
||||
}
|
||||
deployMethod={
|
||||
values.Type === StackType.Kubernetes ? 'manifest' : 'compose'
|
||||
}
|
||||
errors={typeof errors.Git === 'object' ? errors.Git : undefined}
|
||||
/>
|
||||
<div className="form-group">
|
||||
|
@ -137,6 +137,25 @@ export function InnerForm({
|
|||
</>
|
||||
)}
|
||||
|
||||
{isTemplateVariablesEnabled && (
|
||||
<CustomTemplatesVariablesDefinitionField
|
||||
value={values.Variables}
|
||||
onChange={(values) => setFieldValue('Variables', values)}
|
||||
isVariablesNamesFromParent={!isEditorReadonly}
|
||||
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={(edgeValues) =>
|
||||
|
@ -163,33 +182,4 @@ export function InnerForm({
|
|||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
function handleChangeFileContent(value: string) {
|
||||
setFieldValue(
|
||||
'FileContent',
|
||||
value,
|
||||
isTemplateVariablesEnabled ? !value : true
|
||||
);
|
||||
parseTemplate(value);
|
||||
}
|
||||
|
||||
function parseTemplate(value: string) {
|
||||
if (!isTemplateVariablesEnabled || value === '') {
|
||||
setFieldValue('Variables', []);
|
||||
return;
|
||||
}
|
||||
|
||||
const [variables, validationError] = getTemplateVariables(value);
|
||||
|
||||
setFieldError(
|
||||
'FileContent',
|
||||
validationError ? `Template invalid: ${validationError}` : undefined
|
||||
);
|
||||
if (variables) {
|
||||
setFieldValue(
|
||||
'Variables',
|
||||
intersectVariables(values.Variables, variables)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { StackType } from '@/react/common/stacks/types';
|
||||
import { type Values as CommonFieldsValues } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
import { DefinitionFieldValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { Platform } from '@/react/portainer/templates/types';
|
||||
import { type Values as CommonFieldsValues } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
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 { EdgeTemplateSettings } from '../types';
|
||||
|
||||
export interface FormValues extends CommonFieldsValues {
|
||||
Platform: Platform;
|
||||
|
@ -11,5 +13,6 @@ export interface FormValues extends CommonFieldsValues {
|
|||
FileContent: string;
|
||||
Git?: GitFormModel;
|
||||
Variables: DefinitionFieldValues;
|
||||
AccessControl?: AccessControlFormData;
|
||||
EdgeSettings?: EdgeTemplateSettings;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { toGitFormModel } from '@/react/portainer/gitops/types';
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
import { CustomTemplate } from '../types';
|
||||
|
||||
import { FormValues } from './types';
|
||||
|
||||
export function useInitialValues({
|
||||
template,
|
||||
templateFile,
|
||||
isEdge,
|
||||
}: {
|
||||
template: CustomTemplate;
|
||||
templateFile: string | undefined;
|
||||
isEdge: boolean;
|
||||
}): FormValues {
|
||||
const { user, isAdmin } = useCurrentUser();
|
||||
|
||||
return {
|
||||
Title: template.Title,
|
||||
FileContent: templateFile || '',
|
||||
Type: template.Type,
|
||||
Platform: template.Platform,
|
||||
Description: template.Description,
|
||||
Note: template.Note,
|
||||
Logo: template.Logo,
|
||||
Variables: template.Variables,
|
||||
Git: template.GitConfig ? toGitFormModel(template.GitConfig) : undefined,
|
||||
AccessControl:
|
||||
!isEdge && template.ResourceControl
|
||||
? parseAccessControlFormData(
|
||||
isAdmin,
|
||||
user.Id,
|
||||
new ResourceControlViewModel(template.ResourceControl)
|
||||
)
|
||||
: undefined,
|
||||
EdgeSettings: template.EdgeSettings,
|
||||
};
|
||||
}
|
|
@ -3,19 +3,26 @@ import { useMemo } from 'react';
|
|||
|
||||
import { StackType } from '@/react/common/stacks/types';
|
||||
import { validation as commonFieldsValidation } from '@/react/portainer/custom-templates/components/CommonFields';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
import { Platform } from '@/react/portainer/templates/types';
|
||||
import { variablesValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
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 { Platform } from '@/react/portainer/templates/types';
|
||||
import { edgeFieldsetValidation } from '@/react/portainer/templates/custom-templates/CreateView/EdgeSettingsFieldset.validation';
|
||||
|
||||
export function useValidation(
|
||||
currentTemplateId: CustomTemplate['Id'],
|
||||
isGit: boolean
|
||||
) {
|
||||
import { CustomTemplate } from '../types';
|
||||
import { TemplateViewType } from '../useViewType';
|
||||
|
||||
export function useValidation({
|
||||
isGit,
|
||||
templateId,
|
||||
viewType,
|
||||
}: {
|
||||
isGit: boolean;
|
||||
templateId: CustomTemplate['Id'];
|
||||
viewType: TemplateViewType;
|
||||
}) {
|
||||
const { user } = useCurrentUser();
|
||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||
const customTemplatesQuery = useCustomTemplates();
|
||||
|
@ -33,26 +40,26 @@ export function useValidation(
|
|||
StackType.Kubernetes,
|
||||
])
|
||||
.default(StackType.DockerCompose),
|
||||
FileContent: isGit
|
||||
? string().default('')
|
||||
: string().required('Template is required.'),
|
||||
FileContent: string().required('Template is required.'),
|
||||
|
||||
Git: isGit
|
||||
? buildGitValidationSchema(gitCredentialsQuery.data || [])
|
||||
: mixed(),
|
||||
Variables: variablesValidation(),
|
||||
EdgeSettings: edgeFieldsetValidation(),
|
||||
EdgeSettings: viewType === 'edge' ? edgeFieldsetValidation() : mixed(),
|
||||
}).concat(
|
||||
commonFieldsValidation({
|
||||
templates: customTemplatesQuery.data,
|
||||
currentTemplateId,
|
||||
currentTemplateId: templateId,
|
||||
viewType,
|
||||
})
|
||||
),
|
||||
[
|
||||
currentTemplateId,
|
||||
customTemplatesQuery.data,
|
||||
gitCredentialsQuery.data,
|
||||
isGit,
|
||||
templateId,
|
||||
viewType,
|
||||
]
|
||||
);
|
||||
}
|
|
@ -42,7 +42,6 @@ interface CreateTemplatePayload {
|
|||
Description: string;
|
||||
Note: string;
|
||||
Logo: string;
|
||||
AccessControl?: AccessControlFormData;
|
||||
}
|
||||
|
||||
export function useCreateTemplateMutation() {
|
||||
|
@ -50,7 +49,9 @@ export function useCreateTemplateMutation() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
async (payload: CreateTemplatePayload) => {
|
||||
async (
|
||||
payload: CreateTemplatePayload & { AccessControl?: AccessControlFormData }
|
||||
) => {
|
||||
const template = await createTemplate(user.Id, payload);
|
||||
const resourceControl = template.ResourceControl;
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ export function useCustomTemplateFile(id?: CustomTemplate['Id'], git = false) {
|
|||
{
|
||||
...withGlobalError('Failed to get custom template file'),
|
||||
enabled: !!id,
|
||||
// there's nothing to do with a new file content, so we're disabling refetch
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from '@/react-tools/react-query';
|
||||
import { StackType } from '@/react/common/stacks/types';
|
||||
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
|
||||
import { AccessControlFormData } from '@/react/portainer/access-control/types';
|
||||
import { applyResourceControl } from '@/react/portainer/access-control/access-control.service';
|
||||
|
||||
import { CustomTemplate, EdgeTemplateSettings } from '../types';
|
||||
import { Platform } from '../../types';
|
||||
|
@ -18,7 +20,21 @@ export function useUpdateTemplateMutation() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
updateTemplate,
|
||||
async (
|
||||
payload: CustomTemplateUpdatePayload & {
|
||||
AccessControl?: AccessControlFormData;
|
||||
resourceControlId?: number;
|
||||
}
|
||||
) => {
|
||||
await updateTemplate(payload);
|
||||
|
||||
if (payload.resourceControlId && payload.AccessControl) {
|
||||
await applyResourceControl(
|
||||
payload.AccessControl,
|
||||
payload.resourceControlId
|
||||
);
|
||||
}
|
||||
},
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [['custom-templates']]),
|
||||
withGlobalError('Failed to update template')
|
||||
|
@ -30,6 +46,7 @@ export function useUpdateTemplateMutation() {
|
|||
* Payload for updating a custom template
|
||||
*/
|
||||
interface CustomTemplateUpdatePayload {
|
||||
id: CustomTemplate['Id'];
|
||||
/** URL of the template's logo */
|
||||
Logo?: string;
|
||||
/** Title of the template */
|
||||
|
@ -78,9 +95,7 @@ interface CustomTemplateUpdatePayload {
|
|||
EdgeSettings?: EdgeTemplateSettings;
|
||||
}
|
||||
|
||||
async function updateTemplate(
|
||||
values: CustomTemplateUpdatePayload & { id: CustomTemplate['Id'] }
|
||||
) {
|
||||
async function updateTemplate(values: CustomTemplateUpdatePayload) {
|
||||
try {
|
||||
const { data } = await axios.put<CustomTemplate>(
|
||||
buildUrl({ id: values.id }),
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { VariableDefinition } from '../../custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import {
|
||||
getTemplateVariables,
|
||||
intersectVariables,
|
||||
isTemplateVariablesEnabled,
|
||||
} from '../../custom-templates/components/utils';
|
||||
|
||||
export function useParseTemplateOnFileChange(
|
||||
oldVariables: VariableDefinition[]
|
||||
) {
|
||||
const { setFieldValue, setFieldError } = useFormikContext();
|
||||
|
||||
return handleChangeFileContent;
|
||||
|
||||
function handleChangeFileContent(value: string) {
|
||||
setFieldValue(
|
||||
'FileContent',
|
||||
value,
|
||||
isTemplateVariablesEnabled ? !value : true
|
||||
);
|
||||
parseTemplate(value);
|
||||
}
|
||||
|
||||
function parseTemplate(value: string) {
|
||||
if (!isTemplateVariablesEnabled || value === '') {
|
||||
setFieldValue('Variables', []);
|
||||
return;
|
||||
}
|
||||
|
||||
const [variables, validationError] = getTemplateVariables(value);
|
||||
const isValid = !!variables;
|
||||
|
||||
setFieldError(
|
||||
'FileContent',
|
||||
validationError ? `Template invalid: ${validationError}` : undefined
|
||||
);
|
||||
if (isValid) {
|
||||
setFieldValue('Variables', intersectVariables(oldVariables, variables));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
export type TemplateViewType = 'kube' | 'docker' | 'edge';
|
||||
|
||||
export function useViewType(): TemplateViewType {
|
||||
const {
|
||||
state: { name },
|
||||
} = useCurrentStateAndParams();
|
||||
if (name?.includes('kubernetes')) {
|
||||
return 'kube';
|
||||
}
|
||||
|
||||
if (name?.includes('docker')) {
|
||||
return 'docker';
|
||||
}
|
||||
|
||||
return 'edge';
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue