1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

refactor(templates): migrate list view to react [EE-2296] (#10999)

This commit is contained in:
Chaim Lev-Ari 2024-04-11 09:29:30 +03:00 committed by GitHub
parent d38085a560
commit 6ff4fd3db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 2628 additions and 1315 deletions

View file

@ -1,9 +1,11 @@
import { FormikErrors } from 'formik';
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
import { EnvVarsFieldset } from './EnvVarsFieldset';
import { TemplateNote } from './TemplateNote';
import {
EnvVarsFieldset,
EnvVarsValue,
} from '@/react/portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
import { TemplateNote } from '@/react/portainer/templates/components/TemplateNote';
export function AppTemplateFieldset({
template,
@ -12,16 +14,16 @@ export function AppTemplateFieldset({
errors,
}: {
template: TemplateViewModel;
values: Record<string, string>;
onChange: (value: Record<string, string>) => void;
errors?: FormikErrors<Record<string, string>>;
values: EnvVarsValue;
onChange: (value: EnvVarsValue) => void;
errors?: FormikErrors<EnvVarsValue>;
}) {
return (
<>
<TemplateNote note={template.Note} />
<EnvVarsFieldset
options={template.Env || []}
value={values}
values={values}
onChange={onChange}
errors={errors}
/>

View file

@ -1,10 +1,10 @@
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 { ArrayError } from '@@/form-components/InputList/InputList';
import { Values } from './types';
import { TemplateNote } from './TemplateNote';
export function CustomTemplateFieldset({
errors,

View file

@ -1,117 +0,0 @@
import { vi } from 'vitest';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import {
EnvVarsFieldset,
getDefaultValues,
envVarsFieldsetValidation,
} from './EnvVarsFieldset';
test('renders EnvVarsFieldset component', () => {
const onChange = vi.fn();
const options = [
{ name: 'VAR1', label: 'Variable 1', preset: false },
{ name: 'VAR2', label: 'Variable 2', preset: false },
] as const;
const value = { VAR1: 'Value 1', VAR2: 'Value 2' };
const errors = {};
render(
<EnvVarsFieldset
onChange={onChange}
options={[...options]}
value={value}
errors={errors}
/>
);
options.forEach((option) => {
const labelElement = screen.getByLabelText(option.label, { exact: false });
expect(labelElement).toBeInTheDocument();
const inputElement = screen.getByDisplayValue(value[option.name]);
expect(inputElement).toBeInTheDocument();
});
});
test('calls onChange when input value changes', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const options = [{ name: 'VAR1', label: 'Variable 1', preset: false }];
const value = { VAR1: 'Value 1' };
const errors = {};
render(
<EnvVarsFieldset
onChange={onChange}
options={options}
value={value}
errors={errors}
/>
);
const inputElement = screen.getByDisplayValue(value.VAR1);
await user.clear(inputElement);
expect(onChange).toHaveBeenCalledWith({ VAR1: '' });
const newValue = 'New Value';
await user.type(inputElement, newValue);
expect(onChange).toHaveBeenCalled();
});
test('renders error message when there are errors', () => {
const onChange = vi.fn();
const options = [{ name: 'VAR1', label: 'Variable 1', preset: false }];
const value = { VAR1: 'Value 1' };
const errors = { VAR1: 'Required' };
render(
<EnvVarsFieldset
onChange={onChange}
options={options}
value={value}
errors={errors}
/>
);
const errorElement = screen.getByText('Required');
expect(errorElement).toBeInTheDocument();
});
test('returns default values', () => {
const definitions = [
{
name: 'VAR1',
label: 'Variable 1',
preset: false,
default: 'Default Value 1',
},
{
name: 'VAR2',
label: 'Variable 2',
preset: false,
default: 'Default Value 2',
},
];
const defaultValues = getDefaultValues(definitions);
expect(defaultValues).toEqual({
VAR1: 'Default Value 1',
VAR2: 'Default Value 2',
});
});
test('validates env vars fieldset', () => {
const schema = envVarsFieldsetValidation();
const validData = { VAR1: 'Value 1', VAR2: 'Value 2' };
const invalidData = { VAR1: '', VAR2: 'Value 2' };
const validResult = schema.isValidSync(validData);
const invalidResult = schema.isValidSync(invalidData);
expect(validResult).toBe(true);
expect(invalidResult).toBe(false);
});

View file

@ -1,104 +0,0 @@
import { FormikErrors } from 'formik';
import { SchemaOf, array, string } from 'yup';
import { TemplateEnv } from '@/react/portainer/templates/app-templates/types';
import { FormControl } from '@@/form-components/FormControl';
import { Input, Select } from '@@/form-components/Input';
type Value = Record<string, string>;
export function EnvVarsFieldset({
onChange,
options,
value,
errors,
}: {
options: Array<TemplateEnv>;
onChange: (value: Value) => void;
value: Value;
errors?: FormikErrors<Value>;
}) {
return (
<>
{options.map((env) => (
<Item
key={env.name}
option={env}
value={value[env.name]}
onChange={(value) => handleChange(env.name, value)}
errors={errors?.[env.name]}
/>
))}
</>
);
function handleChange(name: string, envValue: string) {
onChange({ ...value, [name]: envValue });
}
}
function Item({
onChange,
option,
value,
errors,
}: {
option: TemplateEnv;
value: string;
onChange: (value: string) => void;
errors?: FormikErrors<string>;
}) {
const inputId = `env_var_${option.name}`;
return (
<FormControl
label={option.label || option.name}
required={!option.preset}
errors={errors}
inputId={inputId}
>
{option.select ? (
<Select
value={value}
data-cy={`env-var-select-${option.name}`}
onChange={(e) => onChange(e.target.value)}
options={option.select.map((o) => ({
label: o.text,
value: o.value,
}))}
disabled={option.preset}
id={inputId}
/>
) : (
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={option.preset}
id={inputId}
data-cy="env-var-input"
/>
)}
</FormControl>
);
}
export function getDefaultValues(definitions: Array<TemplateEnv>): Value {
return Object.fromEntries(
definitions.map((v) => {
if (v.select) {
return [v.name, v.select.find((v) => v.default)?.value || ''];
}
return [v.name, v.default || ''];
})
);
}
export function envVarsFieldsetValidation(): SchemaOf<Value> {
return (
array()
.transform((_, orig) => Object.values(orig))
// casting to return the correct type - validation works as expected
.of(string().required('Required')) as unknown as SchemaOf<Value>
);
}

View file

@ -3,7 +3,8 @@ import { FormikErrors } from 'formik';
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { getDefaultValues as getAppVariablesDefaultValues } from './EnvVarsFieldset';
import { getDefaultValues as getAppVariablesDefaultValues } from '../../../../portainer/templates/app-templates/DeployFormWidget/EnvVarsFieldset';
import { TemplateSelector } from './TemplateSelector';
import { SelectedTemplateValue, Values } from './types';
import { CustomTemplateFieldset } from './CustomTemplateFieldset';

View file

@ -1,25 +0,0 @@
import { vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { TemplateNote } from './TemplateNote';
vi.mock('sanitize-html', () => ({
default: (note: string) => note, // Mock the sanitize-html library to return the input as is
}));
test('renders template note', async () => {
render(<TemplateNote note="Test note" />);
const templateNoteElement = screen.getByText(/Information/);
expect(templateNoteElement).toBeInTheDocument();
const noteElement = screen.getByText(/Test note/);
expect(noteElement).toBeInTheDocument();
});
test('does not render template note when note is undefined', async () => {
render(<TemplateNote note={undefined} />);
const templateNoteElement = screen.queryByText(/Information/);
expect(templateNoteElement).not.toBeInTheDocument();
});

View file

@ -1,23 +0,0 @@
import sanitize from 'sanitize-html';
export function TemplateNote({ note }: { note: string | undefined }) {
if (!note) {
return null;
}
return (
<div>
<div className="col-sm-12 form-section-title"> Information </div>
<div className="form-group">
<div className="col-sm-12">
<div
className="template-note"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitize(note),
}}
/>
</div>
</div>
</div>
);
}

View file

@ -2,17 +2,19 @@ import { mixed, object, SchemaOf, string } 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';
import { envVarsFieldsetValidation } from './EnvVarsFieldset';
export function validation({
definitions,
function validation({
customVariablesDefinitions,
envVarDefinitions,
}: {
definitions: VariableDefinition[];
customVariablesDefinitions: VariableDefinition[];
envVarDefinitions: Array<TemplateEnv>;
}) {
return object({
type: string().oneOf(['custom', 'app']).required(),
envVars: envVarsFieldsetValidation()
envVars: envVarsFieldsetValidation(envVarDefinitions)
.optional()
.when('type', {
is: 'app',
@ -20,7 +22,7 @@ export function validation({
}),
file: mixed().optional(),
template: object().optional().default(null),
variables: variablesFieldValidation(definitions)
variables: variablesFieldValidation(customVariablesDefinitions)
.optional()
.when('type', {
is: 'custom',