@@ -128,7 +107,66 @@ function useTemplate(
});
return {
- appTemplate: appTemplateQuery.data,
- customTemplate: customTemplateQuery.data,
+ appTemplate: type === 'app' ? appTemplateQuery.data : undefined,
+ customTemplate: type === 'custom' ? customTemplateQuery.data : undefined,
};
}
+
+function useInitialValues(
+ templateQuery: {
+ appTemplate: TemplateViewModel | undefined;
+ customTemplate: CustomTemplate | undefined;
+ },
+ templateParams: {
+ id: number | undefined;
+ type: 'app' | 'custom' | undefined;
+ }
+) {
+ const template = templateQuery.customTemplate || templateQuery.appTemplate;
+ const initialValues: FormValues = useMemo(
+ () => ({
+ name: '',
+ groupIds: [],
+ // if edge custom templates allow kube manifests/helm charts in future, add logic for setting this for the initial deploymentType
+ deploymentType: DeploymentType.Compose,
+ envVars: [],
+ privateRegistryId:
+ templateQuery.customTemplate?.EdgeSettings?.PrivateRegistryId ?? 0,
+ prePullImage:
+ templateQuery.customTemplate?.EdgeSettings?.PrePullImage ?? false,
+ retryDeploy:
+ templateQuery.customTemplate?.EdgeSettings?.RetryDeploy ?? false,
+ staggerConfig:
+ templateQuery.customTemplate?.EdgeSettings?.StaggerConfig ??
+ getDefaultStaggerConfig(),
+ method: templateParams.id ? 'template' : 'editor',
+ git: toGitFormModel(
+ templateQuery.customTemplate?.GitConfig,
+ parseAutoUpdateResponse()
+ ),
+ relativePath:
+ templateQuery.customTemplate?.EdgeSettings?.RelativePathSettings ??
+ getDefaultRelativePathModel(),
+ enableWebhook: false,
+ fileContent: '',
+ templateValues: getTemplateValues(templateParams.type, template),
+ useManifestNamespaces: false,
+ }),
+ [
+ templateQuery.customTemplate,
+ templateParams.id,
+ templateParams.type,
+ template,
+ ]
+ );
+
+ if (
+ templateParams.id &&
+ !templateQuery.customTemplate &&
+ !templateQuery.appTemplate
+ ) {
+ return null;
+ }
+
+ return initialValues;
+}
diff --git a/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts b/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts
index ae8b672ab..68fbc0d8d 100644
--- a/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts
+++ b/app/react/edge/edge-stacks/CreateView/CreateForm.validation.ts
@@ -117,7 +117,7 @@ export function useValidation({
}),
templateValues: templateFieldsetValidation({
customVariablesDefinitions: customTemplate?.Variables || [],
- envVarDefinitions: appTemplate?.Env || [],
+ appTemplateVariablesDefinitions: appTemplate?.Env || [],
}),
git: mixed().when('method', {
is: 'repository',
diff --git a/app/react/edge/edge-stacks/CreateView/DockerComposeForm.tsx b/app/react/edge/edge-stacks/CreateView/DockerComposeForm.tsx
index c8cc44d6b..334136dcb 100644
--- a/app/react/edge/edge-stacks/CreateView/DockerComposeForm.tsx
+++ b/app/react/edge/edge-stacks/CreateView/DockerComposeForm.tsx
@@ -1,4 +1,5 @@
-import { useFormikContext } from 'formik';
+import { FormikErrors, useFormikContext } from 'formik';
+import { SetStateAction } from 'react';
import { GitForm } from '@/react/portainer/gitops/GitForm';
import { baseEdgeStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
@@ -24,31 +25,18 @@ import { useRenderAppTemplate } from './useRenderAppTemplate';
const buildMethods = [editor, upload, git, edgeStackTemplate] as const;
-export function DockerComposeForm({
- webhookId,
- onChangeTemplate,
-}: {
+interface Props {
webhookId: string;
- onChangeTemplate: ({
- type,
- id,
- }: {
+ onChangeTemplate: (change: {
type: 'app' | 'custom' | undefined;
id: number | undefined;
}) => void;
-}) {
+}
+
+export function DockerComposeForm({ webhookId, onChangeTemplate }: Props) {
const { errors, values, setValues } = useFormikContext
();
const { method } = values;
- const { customTemplate, isInitialLoading: isCustomTemplateLoading } =
- useRenderCustomTemplate(values.templateValues, setValues);
- const { appTemplate, isInitialLoading: isAppTemplateLoading } =
- useRenderAppTemplate(values.templateValues, setValues);
-
- const isTemplate =
- method === edgeStackTemplate.value && (customTemplate || appTemplate);
- const isTemplateLoading = isCustomTemplateLoading || isAppTemplateLoading;
-
return (
<>
@@ -62,10 +50,10 @@ export function DockerComposeForm({
{method === edgeStackTemplate.value && (
-
- setValues((values) => {
+ <>
+ {
const templateValues = applySetStateAction(
templateAction,
values.templateValues
@@ -74,25 +62,36 @@ export function DockerComposeForm({
id: templateValues.templateId,
type: templateValues.type,
});
-
- return {
+ setValues((values) => ({
...values,
templateValues,
- };
- })
- }
- errors={errors?.templateValues}
- isLoadingValues={isTemplateLoading}
- />
+ }));
+ }}
+ errors={errors?.templateValues}
+ />
+ {values.templateValues.type === 'app' && (
+
+ )}
+ {values.templateValues.type === 'custom' && (
+
+ )}
+ >
)}
- {(method === editor.value || isTemplate) && !isTemplateLoading && (
+ {method === editor.value && (
handleChange({ fileContent: value })}
- readonly={
- method === edgeStackTemplate.value && !!customTemplate?.GitConfig
- }
error={errors?.fileContent}
/>
)}
@@ -154,3 +153,51 @@ export function DockerComposeForm({
}));
}
}
+
+type TemplateContentFieldProps = {
+ values: DockerFormValues;
+ handleChange: (newValues: Partial) => void;
+ errors?: FormikErrors;
+ setValues: (values: SetStateAction) => void;
+};
+
+function AppTemplateContentField({
+ values,
+ handleChange,
+ errors,
+ setValues,
+}: TemplateContentFieldProps) {
+ const { isInitialLoading } = useRenderAppTemplate(
+ values.templateValues,
+ setValues
+ );
+ return (
+ handleChange({ fileContent: value })}
+ error={errors?.fileContent}
+ isLoading={isInitialLoading}
+ />
+ );
+}
+
+function CustomTemplateContentField({
+ values,
+ handleChange,
+ errors,
+ setValues,
+}: TemplateContentFieldProps) {
+ const { customTemplate, isInitialLoading } = useRenderCustomTemplate(
+ values.templateValues,
+ setValues
+ );
+ return (
+ handleChange({ fileContent: value })}
+ error={errors?.fileContent}
+ readonly={!!customTemplate?.GitConfig}
+ isLoading={isInitialLoading}
+ />
+ );
+}
diff --git a/app/react/edge/edge-stacks/CreateView/DockerContentField.tsx b/app/react/edge/edge-stacks/CreateView/DockerContentField.tsx
index 812867904..015efcce3 100644
--- a/app/react/edge/edge-stacks/CreateView/DockerContentField.tsx
+++ b/app/react/edge/edge-stacks/CreateView/DockerContentField.tsx
@@ -1,3 +1,4 @@
+import { InlineLoader } from '@@/InlineLoader';
import { WebEditorForm } from '@@/WebEditorForm';
export function DockerContentField({
@@ -5,12 +6,18 @@ export function DockerContentField({
onChange,
readonly,
value,
+ isLoading,
}: {
value: string;
onChange: (value: string) => void;
error?: string;
readonly?: boolean;
+ isLoading?: boolean;
}) {
+ if (isLoading) {
+ return Loading stack content...;
+ }
+
return (
setFieldValue('name', value)}
value={values.name}
errors={errors.name}
+ placeholder="e.g. my-stack"
/>
- setValues((values) => ({
- ...values,
- staggerConfig: {
- ...values.staggerConfig,
- ...newStaggerValues,
- },
- }))
+ setFieldValue('staggerConfig', newStaggerValues)
}
/>
>
diff --git a/app/react/edge/edge-stacks/CreateView/NameField.tsx b/app/react/edge/edge-stacks/CreateView/NameField.tsx
index 988ece31c..903692092 100644
--- a/app/react/edge/edge-stacks/CreateView/NameField.tsx
+++ b/app/react/edge/edge-stacks/CreateView/NameField.tsx
@@ -17,16 +17,19 @@ export function NameField({
onChange,
value,
errors,
+ placeholder,
}: {
onChange(value: string): void;
value: string;
errors?: FormikErrors;
+ placeholder?: string;
}) {
return (
onChange(e.target.value)}
+ placeholder={placeholder}
value={value}
required
data-cy="edgeStackCreate-nameInput"
diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/types.ts b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/types.ts
index 57a1cdbe9..9903a641b 100644
--- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/types.ts
+++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/types.ts
@@ -1,4 +1,5 @@
import { VariablesFieldValue } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
+import { EnvVarsValue } from '@/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
export type SelectedTemplateValue =
| { templateId: number; type: 'custom' }
@@ -7,5 +8,5 @@ export type SelectedTemplateValue =
export type Values = {
variables: VariablesFieldValue;
- envVars: Record;
+ envVars: EnvVarsValue;
} & SelectedTemplateValue;
diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/validation.tsx b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/validation.tsx
index d11265763..e7c39d3a0 100644
--- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/validation.tsx
+++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/validation.tsx
@@ -9,14 +9,14 @@ import { Values } from './types';
export function templateFieldsetValidation({
customVariablesDefinitions,
- envVarDefinitions,
+ appTemplateVariablesDefinitions,
}: {
- customVariablesDefinitions: VariableDefinition[];
- envVarDefinitions: Array;
+ customVariablesDefinitions: Array;
+ appTemplateVariablesDefinitions: Array;
}): SchemaOf {
return object({
type: mixed<'app' | 'custom'>().oneOf(['custom', 'app']).optional(),
- envVars: envVarsFieldsetValidation(envVarDefinitions)
+ envVars: envVarsFieldsetValidation(appTemplateVariablesDefinitions)
.optional()
.when('type', {
is: 'app',
diff --git a/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx b/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx
new file mode 100644
index 000000000..ac597e01a
--- /dev/null
+++ b/app/react/edge/edge-stacks/CreateView/tests/app-templates.test.tsx
@@ -0,0 +1,101 @@
+import { DefaultBodyType, HttpResponse } from 'msw';
+import { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { http, server } from '@/setup-tests/server';
+import selectEvent from '@/react/test-utils/react-select';
+
+import { mockCodeMirror, renderCreateForm } from './utils.test';
+
+// keep mockTemplateId and mockTemplateType in module scope
+let mockTemplateId: number;
+let mockTemplateType: string;
+
+// browser address
+// /edge/stacks/new?templateId=54&templateType=app
+vi.mock('@uirouter/react', async (importOriginal: () => Promise