mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 08:19:40 +02:00
refactor(edge/stacks): migrate create view to react [EE-2223] (#11575)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (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:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (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 version:]) (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:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (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
f22aed34b5
commit
8a81d95253
64 changed files with 1878 additions and 1005 deletions
|
@ -1,13 +1,17 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import { EnvVarType } from '@/react/portainer/templates/app-templates/view-model';
|
||||
import {
|
||||
EnvVarType,
|
||||
TemplateViewModel,
|
||||
} from '@/react/portainer/templates/app-templates/view-model';
|
||||
AppTemplate,
|
||||
TemplateType,
|
||||
} from '@/react/portainer/templates/app-templates/types';
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
|
||||
import { AppTemplateFieldset } from './AppTemplateFieldset';
|
||||
|
||||
test('renders AppTemplateFieldset component', () => {
|
||||
test('renders AppTemplateFieldset component', async () => {
|
||||
const testedEnv = {
|
||||
name: 'VAR2',
|
||||
label: 'Variable 2',
|
||||
|
@ -27,27 +31,44 @@ test('renders AppTemplateFieldset component', () => {
|
|||
testedEnv,
|
||||
];
|
||||
const template = {
|
||||
Note: 'This is a template note',
|
||||
Env: env,
|
||||
} as TemplateViewModel;
|
||||
id: 1,
|
||||
note: 'This is a template note',
|
||||
env,
|
||||
type: TemplateType.ComposeStack,
|
||||
categories: ['edge'],
|
||||
title: 'Template title',
|
||||
description: 'Template description',
|
||||
administrator_only: false,
|
||||
image: 'template-image',
|
||||
repository: {
|
||||
url: '',
|
||||
stackfile: '',
|
||||
},
|
||||
} satisfies AppTemplate;
|
||||
|
||||
const values: Record<string, string> = {
|
||||
VAR1: 'value1',
|
||||
VAR2: 'value2',
|
||||
};
|
||||
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<AppTemplateFieldset
|
||||
template={template}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
/>
|
||||
server.use(
|
||||
http.get('/api/templates', () =>
|
||||
HttpResponse.json({ version: '3', templates: [template] })
|
||||
),
|
||||
http.get('/api/registries', () => HttpResponse.json([]))
|
||||
);
|
||||
|
||||
const templateNoteElement = screen.getByText('This is a template note');
|
||||
expect(templateNoteElement).toBeInTheDocument();
|
||||
const onChange = vi.fn();
|
||||
const Wrapped = withTestQueryProvider(AppTemplateFieldset);
|
||||
render(
|
||||
<Wrapped templateId={template.id} values={values} onChange={onChange} />
|
||||
);
|
||||
|
||||
screen.debug();
|
||||
|
||||
await expect(
|
||||
screen.findByText('This is a template note')
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
const envVarsFieldsetElement = screen.getByLabelText(testedEnv.label, {
|
||||
exact: false,
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||
import { useAppTemplate } from '@/react/portainer/templates/app-templates/queries/useAppTemplates';
|
||||
import { TemplateNote } from '@/react/portainer/templates/components/TemplateNote';
|
||||
import {
|
||||
EnvVarsFieldset,
|
||||
EnvVarsValue,
|
||||
} from '@/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
|
||||
import { TemplateNote } from '@/react/portainer/templates/components/TemplateNote';
|
||||
|
||||
export function AppTemplateFieldset({
|
||||
template,
|
||||
templateId,
|
||||
values,
|
||||
onChange,
|
||||
errors,
|
||||
}: {
|
||||
template: TemplateViewModel;
|
||||
templateId: TemplateViewModel['Id'];
|
||||
values: EnvVarsValue;
|
||||
onChange: (value: EnvVarsValue) => void;
|
||||
errors?: FormikErrors<EnvVarsValue>;
|
||||
}) {
|
||||
const templateQuery = useAppTemplate(templateId);
|
||||
if (!templateQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const template = templateQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateNote note={template.Note} />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
import { TemplateNote } from '@/react/portainer/templates/components/TemplateNote';
|
||||
import { useCustomTemplate } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplate';
|
||||
|
||||
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||
|
||||
|
@ -10,13 +11,21 @@ export function CustomTemplateFieldset({
|
|||
errors,
|
||||
onChange,
|
||||
values,
|
||||
template,
|
||||
templateId,
|
||||
}: {
|
||||
values: Values['variables'];
|
||||
onChange: (values: Values['variables']) => void;
|
||||
errors: ArrayError<Values['variables']> | undefined;
|
||||
template: CustomTemplate;
|
||||
templateId: CustomTemplate['Id'];
|
||||
}) {
|
||||
const templateQuery = useCustomTemplate(templateId);
|
||||
|
||||
if (!templateQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const template = templateQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateNote note={template.Note} />
|
||||
|
|
|
@ -1,49 +1,38 @@
|
|||
import { SetStateAction, useEffect, useState } from 'react';
|
||||
import { SetStateAction } from 'react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
|
||||
import { getDefaultValues as getAppVariablesDefaultValues } from '../../../../portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
|
||||
|
||||
import { TemplateSelector } from './TemplateSelector';
|
||||
import { SelectedTemplateValue, Values } from './types';
|
||||
import { Values } from './types';
|
||||
import { CustomTemplateFieldset } from './CustomTemplateFieldset';
|
||||
import { AppTemplateFieldset } from './AppTemplateFieldset';
|
||||
|
||||
export function TemplateFieldset({
|
||||
values: initialValues,
|
||||
setValues: setInitialValues,
|
||||
values,
|
||||
setValues,
|
||||
errors,
|
||||
}: {
|
||||
errors?: FormikErrors<Values>;
|
||||
values: Values;
|
||||
setValues: (values: SetStateAction<Values>) => void;
|
||||
}) {
|
||||
const [values, setControlledValues] = useState(initialValues); // todo remove when all view is in react
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
initialValues.type !== values.type ||
|
||||
initialValues.template?.Id !== values.template?.Id
|
||||
) {
|
||||
setControlledValues(initialValues);
|
||||
}
|
||||
}, [initialValues, values]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateSelector
|
||||
error={
|
||||
typeof errors?.template === 'string' ? errors?.template : undefined
|
||||
}
|
||||
error={errors?.templateId}
|
||||
value={values}
|
||||
onChange={handleChangeTemplate}
|
||||
/>
|
||||
{values.template && (
|
||||
{values.templateId && (
|
||||
<>
|
||||
{values.type === 'custom' && (
|
||||
<CustomTemplateFieldset
|
||||
template={values.template}
|
||||
templateId={values.templateId}
|
||||
values={values.variables}
|
||||
onChange={(variables) =>
|
||||
setValues((values) => ({ ...values, variables }))
|
||||
|
@ -54,7 +43,7 @@ export function TemplateFieldset({
|
|||
|
||||
{values.type === 'app' && (
|
||||
<AppTemplateFieldset
|
||||
template={values.template}
|
||||
templateId={values.templateId}
|
||||
values={values.envVars}
|
||||
onChange={(envVars) =>
|
||||
setValues((values) => ({ ...values, envVars }))
|
||||
|
@ -67,36 +56,36 @@ export function TemplateFieldset({
|
|||
</>
|
||||
);
|
||||
|
||||
function setValues(values: SetStateAction<Values>) {
|
||||
setControlledValues(values);
|
||||
setInitialValues(values);
|
||||
}
|
||||
|
||||
function handleChangeTemplate(value?: SelectedTemplateValue) {
|
||||
function handleChangeTemplate(
|
||||
template: TemplateViewModel | CustomTemplate | undefined,
|
||||
type: 'app' | 'custom' | undefined
|
||||
): void {
|
||||
setValues(() => {
|
||||
if (!value || !value.type || !value.template) {
|
||||
if (!template || !type) {
|
||||
return {
|
||||
type: undefined,
|
||||
template: undefined,
|
||||
templateId: undefined,
|
||||
variables: [],
|
||||
envVars: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (value.type === 'app') {
|
||||
if (type === 'app') {
|
||||
return {
|
||||
template: value.template,
|
||||
type: value.type,
|
||||
templateId: template.Id,
|
||||
type,
|
||||
variables: [],
|
||||
envVars: getAppVariablesDefaultValues(value.template.Env || []),
|
||||
envVars: getAppVariablesDefaultValues(
|
||||
(template as TemplateViewModel).Env || []
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
template: value.template,
|
||||
type: value.type,
|
||||
templateId: template.Id,
|
||||
type,
|
||||
variables: getVariablesFieldDefaultValues(
|
||||
value.template.Variables || []
|
||||
(template as CustomTemplate).Variables || []
|
||||
),
|
||||
envVars: {},
|
||||
};
|
||||
|
@ -106,10 +95,9 @@ export function TemplateFieldset({
|
|||
|
||||
export function getInitialTemplateValues(): Values {
|
||||
return {
|
||||
template: undefined,
|
||||
templateId: undefined,
|
||||
type: undefined,
|
||||
variables: [],
|
||||
file: '',
|
||||
envVars: {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import { CustomTemplate } from '@/react/portainer/templates/custom-templates/typ
|
|||
import { server } from '@/setup-tests/server';
|
||||
import selectEvent from '@/react/test-utils/react-select';
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||
|
||||
import { SelectedTemplateValue } from './types';
|
||||
import { TemplateSelector } from './TemplateSelector';
|
||||
|
||||
test('renders TemplateSelector component', async () => {
|
||||
|
@ -109,7 +109,10 @@ function renderComponent({
|
|||
customTemplates = [],
|
||||
error,
|
||||
}: {
|
||||
onChange?: (value: SelectedTemplateValue) => void;
|
||||
onChange?: (
|
||||
template: TemplateViewModel | CustomTemplate | undefined,
|
||||
type: 'app' | 'custom' | undefined
|
||||
) => void;
|
||||
appTemplates?: Array<Partial<AppTemplate>>;
|
||||
customTemplates?: Array<Partial<CustomTemplate>>;
|
||||
error?: string;
|
||||
|
@ -128,7 +131,7 @@ function renderComponent({
|
|||
|
||||
render(
|
||||
<Wrapped
|
||||
value={{ template: undefined, type: undefined }}
|
||||
value={{ templateId: undefined, type: undefined }}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
/>
|
||||
|
|
|
@ -4,6 +4,8 @@ import { GroupBase } from 'react-select';
|
|||
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
|
||||
import { useAppTemplates } from '@/react/portainer/templates/app-templates/queries/useAppTemplates';
|
||||
import { TemplateType } from '@/react/portainer/templates/app-templates/types';
|
||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Select as ReactSelect } from '@@/form-components/ReactSelect';
|
||||
|
@ -16,10 +18,13 @@ export function TemplateSelector({
|
|||
error,
|
||||
}: {
|
||||
value: SelectedTemplateValue;
|
||||
onChange: (value: SelectedTemplateValue) => void;
|
||||
onChange: (
|
||||
template: TemplateViewModel | CustomTemplate | undefined,
|
||||
type: 'app' | 'custom' | undefined
|
||||
) => void;
|
||||
error?: string;
|
||||
}) {
|
||||
const { getTemplate, options } = useOptions();
|
||||
const { options, getTemplate } = useOptions();
|
||||
|
||||
return (
|
||||
<FormControl label="Template" inputId="template_selector" errors={error}>
|
||||
|
@ -28,26 +33,20 @@ export function TemplateSelector({
|
|||
formatGroupLabel={GroupLabel}
|
||||
placeholder="Select an Edge stack template"
|
||||
value={{
|
||||
label: value.template?.Title,
|
||||
id: value.template?.Id,
|
||||
templateId: value.templateId,
|
||||
type: value.type,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
onChange({
|
||||
template: undefined,
|
||||
type: undefined,
|
||||
});
|
||||
onChange(undefined, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, type } = value;
|
||||
if (!id || type === undefined) {
|
||||
const { templateId, type } = value;
|
||||
if (!templateId || type === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = getTemplate({ id, type });
|
||||
onChange({ template, type } as SelectedTemplateValue);
|
||||
onChange(getTemplate({ type, id: templateId }), type);
|
||||
}}
|
||||
options={options}
|
||||
data-cy="edge-stacks-create-template-selector"
|
||||
|
@ -80,7 +79,8 @@ function useOptions() {
|
|||
options:
|
||||
appTemplatesQuery.data?.map((template) => ({
|
||||
label: `${template.Title} - ${template.Description}`,
|
||||
id: template.Id,
|
||||
|
||||
templateId: template.Id,
|
||||
type: 'app' as 'app' | 'custom',
|
||||
})) || [],
|
||||
},
|
||||
|
@ -90,14 +90,16 @@ function useOptions() {
|
|||
customTemplatesQuery.data && customTemplatesQuery.data.length > 0
|
||||
? customTemplatesQuery.data.map((template) => ({
|
||||
label: `${template.Title} - ${template.Description}`,
|
||||
id: template.Id,
|
||||
|
||||
templateId: template.Id,
|
||||
type: 'custom' as 'app' | 'custom',
|
||||
}))
|
||||
: [
|
||||
{
|
||||
label: 'No edge custom templates available',
|
||||
id: 0,
|
||||
type: 'custom' as 'app' | 'custom',
|
||||
|
||||
templateId: undefined,
|
||||
type: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { VariablesFieldValue } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||
|
||||
export type SelectedTemplateValue =
|
||||
| { template: CustomTemplate; type: 'custom' }
|
||||
| { template: TemplateViewModel; type: 'app' }
|
||||
| { template: undefined; type: undefined };
|
||||
| { templateId: number; type: 'custom' }
|
||||
| { templateId: number; type: 'app' }
|
||||
| { templateId: undefined; type: undefined };
|
||||
|
||||
export type Values = {
|
||||
file?: string;
|
||||
variables: VariablesFieldValue;
|
||||
envVars: Record<string, string>;
|
||||
} & SelectedTemplateValue;
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
import { mixed, object, SchemaOf, string } from 'yup';
|
||||
import { mixed, number, object, SchemaOf } from 'yup';
|
||||
|
||||
import { variablesFieldValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { envVarsFieldsetValidation } from '@/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
|
||||
import { TemplateEnv } from '@/react/portainer/templates/app-templates/types';
|
||||
|
||||
function validation({
|
||||
import { Values } from './types';
|
||||
|
||||
export function templateFieldsetValidation({
|
||||
customVariablesDefinitions,
|
||||
envVarDefinitions,
|
||||
}: {
|
||||
customVariablesDefinitions: VariableDefinition[];
|
||||
envVarDefinitions: Array<TemplateEnv>;
|
||||
}) {
|
||||
}): SchemaOf<Values> {
|
||||
return object({
|
||||
type: string().oneOf(['custom', 'app']).required(),
|
||||
type: mixed<'app' | 'custom'>().oneOf(['custom', 'app']).optional(),
|
||||
envVars: envVarsFieldsetValidation(envVarDefinitions)
|
||||
.optional()
|
||||
.when('type', {
|
||||
is: 'app',
|
||||
then: (schema: SchemaOf<unknown, never>) => schema.required(),
|
||||
}),
|
||||
file: mixed().optional(),
|
||||
template: object().optional().default(null),
|
||||
templateId: mixed()
|
||||
.optional()
|
||||
.when('type', {
|
||||
is: true,
|
||||
then: () => number().required(),
|
||||
}),
|
||||
variables: variablesFieldValidation(customVariablesDefinitions)
|
||||
.optional()
|
||||
.when('type', {
|
||||
|
@ -30,5 +36,3 @@ function validation({
|
|||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export { validation as templateFieldsetValidation };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue