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:
parent
d38085a560
commit
6ff4fd3db2
103 changed files with 2628 additions and 1315 deletions
|
@ -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';
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { StackFromCustomTemplateFormWidget } from './StackFromCustomTemplateFormWidget';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue