1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +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

@ -0,0 +1,243 @@
import { useRouter } from '@uirouter/react';
import { Formik, Form } from 'formik';
import { notifySuccess } from '@/portainer/services/notifications';
import {
CreateStackPayload,
useCreateStack,
} from '@/react/common/stacks/queries/useCreateStack/useCreateStack';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { AccessControlForm } from '@/react/portainer/access-control';
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { NameField } from '@/react/common/stacks/CreateView/NameField';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import {
isTemplateVariablesEnabled,
renderTemplate,
} from '@/react/portainer/custom-templates/components/utils';
import {
CustomTemplatesVariablesField,
getVariablesFieldDefaultValues,
} from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { StackType } from '@/react/common/stacks/types';
import { toGitFormModel } from '@/react/portainer/gitops/types';
import { AdvancedSettings } from '@/react/portainer/templates/app-templates/DeployFormWidget/AdvancedSettings';
import { Button } from '@@/buttons';
import { FormActions } from '@@/form-components/FormActions';
import { FormSection } from '@@/form-components/FormSection';
import { WebEditorForm } from '@@/WebEditorForm';
import { useSwarmId } from '../../proxy/queries/useSwarm';
import { FormValues } from './types';
import { useValidation } from './useValidation';
export function DeployForm({
template,
unselect,
templateFile,
isDeployable,
}: {
template: CustomTemplate;
templateFile: string;
unselect: () => void;
isDeployable: boolean;
}) {
const router = useRouter();
const { user } = useCurrentUser();
const isEdgeAdminQuery = useIsEdgeAdmin();
const environmentId = useEnvironmentId();
const swarmIdQuery = useSwarmId(environmentId);
const mutation = useCreateStack();
const validation = useValidation({
isDeployable,
variableDefs: template.Variables,
isAdmin: isEdgeAdminQuery.isAdmin,
environmentId,
});
if (isEdgeAdminQuery.isLoading) {
return null;
}
const isGit = !!template.GitConfig;
const initialValues: FormValues = {
name: template.Title || '',
variables: getVariablesFieldDefaultValues(template.Variables),
accessControl: parseAccessControlFormData(
isEdgeAdminQuery.isAdmin,
user.Id
),
fileContent: templateFile,
};
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
validateOnMount
>
{({ values, errors, setFieldValue, isValid }) => (
<Form className="form-horizontal">
<FormSection title="Configuration">
<NameField
value={values.name}
onChange={(v) => setFieldValue('name', v)}
errors={errors.name}
/>
</FormSection>
{isTemplateVariablesEnabled && (
<CustomTemplatesVariablesField
definitions={template.Variables}
onChange={(v) => {
setFieldValue('variables', v);
const newFile = renderTemplate(
templateFile,
v,
template.Variables
);
setFieldValue('fileContent', newFile);
}}
value={values.variables}
errors={errors.variables}
/>
)}
<AdvancedSettings
label={(isOpen) => advancedSettingsLabel(isOpen, isGit)}
>
<WebEditorForm
id="custom-template-creation-editor"
value={values.fileContent}
onChange={(value) => {
if (isGit) {
return;
}
setFieldValue('fileContent', value);
}}
yaml
error={errors.fileContent}
placeholder="Define or paste the content of your docker compose file here"
readonly={isGit}
data-cy="custom-template-creation-editor"
>
<p>
You can get more information about Compose file format in the{' '}
<a
href="https://docs.docker.com/compose/compose-file/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>
.
</p>
</WebEditorForm>
</AdvancedSettings>
<AccessControlForm
formNamespace="accessControl"
onChange={(values) => setFieldValue('accessControl', values)}
values={values.accessControl}
errors={errors.accessControl}
environmentId={environmentId}
/>
<FormActions
isLoading={mutation.isLoading}
isValid={isValid}
loadingText="Deployment in progress..."
submitLabel="Deploy the stack"
data-cy="deploy-stack-button"
>
<Button
type="reset"
onClick={() => unselect()}
color="default"
data-cy="cancel-stack-creation"
>
Hide
</Button>
</FormActions>
</Form>
)}
</Formik>
);
function handleSubmit(values: FormValues) {
const payload = getPayload(values);
return mutation.mutate(payload, {
onSuccess() {
notifySuccess('Success', 'Stack created');
router.stateService.go('docker.stacks');
},
});
}
function getPayload(values: FormValues): CreateStackPayload {
const type =
template.Type === StackType.DockerCompose ? 'standalone' : 'swarm';
const isGit = !!template.GitConfig;
if (isGit) {
return type === 'standalone'
? {
type,
method: 'git',
payload: {
name: values.name,
environmentId,
git: toGitFormModel(template.GitConfig),
accessControl: values.accessControl,
},
}
: {
type,
method: 'git',
payload: {
name: values.name,
environmentId,
swarmId: swarmIdQuery.data || '',
git: toGitFormModel(template.GitConfig),
accessControl: values.accessControl,
},
};
}
return type === 'standalone'
? {
type,
method: 'string',
payload: {
name: values.name,
environmentId,
fileContent: values.fileContent,
accessControl: values.accessControl,
},
}
: {
type,
method: 'string',
payload: {
name: values.name,
environmentId,
swarmId: swarmIdQuery.data || '',
fileContent: values.fileContent,
accessControl: values.accessControl,
},
};
}
}
function advancedSettingsLabel(isOpen: boolean, isGit: boolean) {
if (isGit) {
return isOpen ? 'Hide stack' : 'View stack';
}
return isOpen ? 'Hide custom stack' : 'Customize stack';
}

View file

@ -0,0 +1,57 @@
import { DeployWidget } from '@/react/portainer/templates/components/DeployWidget';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { useCustomTemplateFile } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplateFile';
import { TextTip } from '@@/Tip/TextTip';
import { useIsDeployable } from './useIsDeployable';
import { DeployForm } from './DeployForm';
import { TemplateLoadError } from './TemplateLoadError';
export function StackFromCustomTemplateFormWidget({
template,
unselect,
}: {
template: CustomTemplate;
unselect: () => void;
}) {
const isDeployable = useIsDeployable(template.Type);
const fileQuery = useCustomTemplateFile(template.Id);
if (fileQuery.isLoading) {
return null;
}
return (
<DeployWidget
logo={template.Logo}
note={template.Note}
title={template.Title}
>
{fileQuery.isError && (
<TemplateLoadError
creatorId={template.CreatedByUserId}
templateId={template.Id}
/>
)}
{!isDeployable && (
<div className="form-group">
<div className="col-sm-12">
<TextTip>
This template type cannot be deployed on this environment.
</TextTip>
</div>
</div>
)}
{fileQuery.isSuccess && isDeployable && (
<DeployForm
template={template}
unselect={unselect}
templateFile={fileQuery.data}
isDeployable={isDeployable}
/>
)}
</DeployWidget>
);
}

View file

@ -0,0 +1,46 @@
import { UserId } from '@/portainer/users/types';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { Link } from '@@/Link';
import { FormError } from '@@/form-components/FormError';
export function TemplateLoadError({
templateId,
creatorId,
}: {
templateId: CustomTemplate['Id'];
creatorId: UserId;
}) {
const { user } = useCurrentUser();
const isEdgeAdminQuery = useIsEdgeAdmin();
if (isEdgeAdminQuery.isLoading) {
return null;
}
const isAdminOrWriter = isEdgeAdminQuery.isAdmin || user.Id === creatorId;
return (
<FormError>
{isAdminOrWriter ? (
<>
Custom template could not be loaded, please{' '}
<Link
to=".edit"
params={{ id: templateId }}
data-cy="edit-custom-template-link"
>
click here
</Link>{' '}
for configuration
</>
) : (
<>
Custom template could not be loaded, please contact your
administrator.
</>
)}
</FormError>
);
}

View file

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

View file

@ -0,0 +1,9 @@
import { AccessControlFormData } from '@/react/portainer/access-control/types';
import { VariablesFieldValue } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
export interface FormValues {
name: string;
variables: VariablesFieldValue;
accessControl: AccessControlFormData;
fileContent: string;
}

View file

@ -0,0 +1,20 @@
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { StackType } from '@/react/common/stacks/types';
import { useIsSwarm } from '../../proxy/queries/useInfo';
export function useIsDeployable(type: StackType) {
const environmentId = useEnvironmentId();
const isSwarm = useIsSwarm(environmentId);
switch (type) {
case StackType.DockerCompose:
return !isSwarm;
case StackType.DockerSwarm:
return isSwarm;
case StackType.Kubernetes:
default:
return false;
}
}

View file

@ -0,0 +1,37 @@
import { useMemo } from 'react';
import { object, string } from 'yup';
import { accessControlFormValidation } from '@/react/portainer/access-control/AccessControlForm';
import { useNameValidation } from '@/react/common/stacks/CreateView/NameField';
import { variablesFieldValidation } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
import { EnvironmentId } from '@/react/portainer/environments/types';
export function useValidation({
environmentId,
isAdmin,
variableDefs,
isDeployable,
}: {
variableDefs: Array<VariableDefinition>;
isAdmin: boolean;
environmentId: EnvironmentId;
isDeployable: boolean;
}) {
const name = useNameValidation(environmentId);
return useMemo(
() =>
object({
name: name.test({
name: 'is-deployable',
message: 'This template cannot be deployed on this environment',
test: () => isDeployable,
}),
accessControl: accessControlFormValidation(isAdmin),
fileContent: string().required('Required'),
variables: variablesFieldValidation(variableDefs),
}),
[isAdmin, isDeployable, name, variableDefs]
);
}