1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-06 06:15:22 +02:00

refactor(custom-templates): render template variables [EE-2602] (#6937)

This commit is contained in:
Chaim Lev-Ari 2022-05-31 13:00:47 +03:00 committed by GitHub
parent 71c0e8e661
commit 1ccdb64938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 829 additions and 47 deletions

View file

@ -0,0 +1,36 @@
import { ComponentMeta } from '@storybook/react';
import { useState } from 'react';
import {
CustomTemplatesVariablesDefinitionField,
VariableDefinition,
} from './CustomTemplatesVariablesDefinitionField';
export default {
title: 'Custom Templates/Variables Definition Field',
component: CustomTemplatesVariablesDefinitionField,
args: {},
} as ComponentMeta<typeof CustomTemplatesVariablesDefinitionField>;
function Template() {
const [value, setValue] = useState<VariableDefinition[]>([
{ label: '', name: '', defaultValue: '', description: '' },
]);
return (
<CustomTemplatesVariablesDefinitionField
value={value}
onChange={setValue}
errors={[
{
name: 'required',
defaultValue: 'non empty',
description: '',
label: 'invalid',
},
]}
/>
);
}
export const Story = Template.bind({});

View file

@ -0,0 +1,105 @@
import { FormError } from '@/portainer/components/form-components/FormError';
import { Input } from '@/portainer/components/form-components/Input';
import { InputList } from '@/portainer/components/form-components/InputList';
import {
InputListError,
ItemProps,
} from '@/portainer/components/form-components/InputList/InputList';
export interface VariableDefinition {
name: string;
label: string;
defaultValue: string;
description: string;
}
interface Props {
value: VariableDefinition[];
onChange: (value: VariableDefinition[]) => void;
errors?: InputListError<VariableDefinition>[] | string;
isVariablesNamesFromParent?: boolean;
}
export function CustomTemplatesVariablesDefinitionField({
onChange,
value,
errors,
isVariablesNamesFromParent,
}: Props) {
return (
<InputList
label="Variables Definition"
onChange={onChange}
value={value}
renderItem={(item, onChange, error) => (
<Item
item={item}
onChange={onChange}
error={error}
isNameReadonly={isVariablesNamesFromParent}
/>
)}
itemBuilder={() => ({
label: '',
name: '',
defaultValue: '',
description: '',
})}
errors={errors}
textTip="List should map the mustache variables in the template file, if default value is empty, the variable will be required."
isAddButtonHidden={isVariablesNamesFromParent}
/>
);
}
interface DefinitionItemProps extends ItemProps<VariableDefinition> {
isNameReadonly?: boolean;
}
function Item({ item, onChange, error, isNameReadonly }: DefinitionItemProps) {
return (
<div className="flex gap-2">
<div>
<Input
value={item.name}
name="name"
onChange={handleChange}
placeholder="Name (e.g var_name)"
readOnly={isNameReadonly}
/>
{error?.name && <FormError>{error.name}</FormError>}
</div>
<div>
<Input
value={item.label}
onChange={handleChange}
placeholder="Label"
name="label"
/>
{error?.label && <FormError>{error.label}</FormError>}
</div>
<div>
<Input
name="description"
value={item.description}
onChange={handleChange}
placeholder="Description"
/>
{error?.description && <FormError>{error.description}</FormError>}
</div>
<div>
<Input
value={item.defaultValue}
onChange={handleChange}
placeholder="Default Value"
name="defaultValue"
/>
{error?.defaultValue && <FormError>{error.defaultValue}</FormError>}
</div>
</div>
);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
onChange({ ...item, [e.target.name]: e.target.value });
}
}

View file

@ -0,0 +1 @@
export { CustomTemplatesVariablesDefinitionField } from './CustomTemplatesVariablesDefinitionField';

View file

@ -0,0 +1,52 @@
import { useState } from 'react';
import { VariableDefinition } from '../CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
import {
CustomTemplatesVariablesField,
Variables,
} from './CustomTemplatesVariablesField';
export default {
title: 'Custom Templates/Variables Field',
component: CustomTemplatesVariablesField,
};
const definitions: VariableDefinition[] = [
{
label: 'Image Name',
name: 'image_name',
defaultValue: 'nginx',
description: '',
},
{
label: 'Required field',
name: 'required_field',
defaultValue: '',
description: '',
},
{
label: 'Required field with tooltip',
name: 'required_field',
defaultValue: '',
description: 'tooltip',
},
];
function Template() {
const [value, setValue] = useState<Variables>(
Object.fromEntries(
definitions.map((def) => [def.name, def.defaultValue || ''])
)
);
return (
<CustomTemplatesVariablesField
value={value}
onChange={setValue}
definitions={definitions}
/>
);
}
export const Story = Template.bind({});

View file

@ -0,0 +1,54 @@
import { FormControl } from '@/portainer/components/form-components/FormControl';
import { FormSection } from '@/portainer/components/form-components/FormSection/FormSection';
import { Input } from '@/portainer/components/form-components/Input';
import { VariableDefinition } from '../CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
export type Variables = Record<string, string>;
interface Props {
value: Variables;
definitions?: VariableDefinition[];
onChange: (value: Variables) => void;
}
export function CustomTemplatesVariablesField({
value,
definitions,
onChange,
}: Props) {
if (!definitions || !definitions.length) {
return null;
}
return (
<FormSection title="Template Variables">
{definitions.map((def) => {
const inputId = `${def.name}-input`;
const variable = value[def.name] || '';
return (
<FormControl
required={!def.defaultValue}
label={def.label}
key={def.name}
inputId={inputId}
tooltip={def.description}
size="small"
>
<Input
name={`variables.${def.name}`}
value={variable}
id={inputId}
onChange={(e) =>
onChange({
...value,
[def.name]: e.target.value,
})
}
/>
</FormControl>
);
})}
</FormSection>
);
}

View file

@ -0,0 +1 @@
export { CustomTemplatesVariablesField } from './CustomTemplatesVariablesField';

View file

@ -0,0 +1,72 @@
import _ from 'lodash';
import Mustache from 'mustache';
import { VariableDefinition } from './CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
export function getTemplateVariables(templateStr: string) {
const template = validateAndParse(templateStr);
if (!template) {
return null;
}
return template
.filter(([type, value]) => type === 'name' && value)
.map(([, value]) => ({
name: value,
label: '',
defaultValue: '',
description: '',
}));
}
function validateAndParse(templateStr: string) {
if (!templateStr) {
return [];
}
try {
return Mustache.parse(templateStr);
} catch (e) {
return null;
}
}
export function intersectVariables(
oldVariables: VariableDefinition[] = [],
newVariables: VariableDefinition[] = []
) {
const oldVariablesWithLabel = oldVariables.filter((v) => !!v.label);
return [
...oldVariablesWithLabel,
...newVariables.filter(
(v) => !oldVariablesWithLabel.find(({ name }) => name === v.name)
),
];
}
export function renderTemplate(
template: string,
variables: Record<string, string>,
definitions: VariableDefinition[]
) {
const state = Object.fromEntries(
_.compact(
Object.entries(variables).map(([name, value]) => {
if (value) {
return [name, value];
}
const definition = definitions.find((def) => def.name === name);
if (!definition) {
return null;
}
return [name, definition.defaultValue || `{{ ${definition.name} }}`];
})
)
);
return Mustache.render(template, state);
}