1
0
Fork 0
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

This commit is contained in:
Chaim Lev-Ari 2024-05-06 08:08:03 +03:00 committed by GitHub
parent f22aed34b5
commit 8a81d95253
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1878 additions and 1005 deletions

View file

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

View file

@ -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} />

View file

@ -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} />

View file

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

View file

@ -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}
/>

View file

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

View file

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

View file

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