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

refactor(edge/groups): migrate view to react [EE-2219] (#11758)
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:s390x platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled

This commit is contained in:
Chaim Lev-Ari 2024-06-02 15:43:37 +03:00 committed by GitHub
parent b7cde35c3d
commit 9c70a43ac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 579 additions and 386 deletions

View file

@ -0,0 +1,45 @@
import { useFormikContext } from 'formik';
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
import { EdgeTypes } from '@/react/portainer/environments/types';
import { BoxSelector } from '@@/BoxSelector';
import { TagSelector } from '@@/TagSelector';
import { FormSection } from '@@/form-components/FormSection';
import { tagOptions } from './tag-options';
import { FormValues } from './types';
export function DynamicGroupFieldset() {
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
return (
<>
<FormSection title="Tags">
<BoxSelector
slim
value={values.partialMatch}
onChange={(partialMatch) =>
setFieldValue('partialMatch', partialMatch)
}
options={tagOptions}
radioName="partialMatch"
/>
<TagSelector
value={values.tagIds}
onChange={(tagIds) => setFieldValue('tagIds', tagIds)}
errors={errors.tagIds}
/>
</FormSection>
<EdgeGroupAssociationTable
data-cy="edgeGroupCreate-associatedEnvironmentsTable"
title="Associated environments by tags"
query={{
types: EdgeTypes,
tagIds: values.tagIds,
tagsPartialMatch: values.partialMatch,
}}
/>
</>
);
}

View file

@ -0,0 +1,90 @@
import { Form, Formik, useFormikContext } from 'formik';
import { FormSection } from '@@/form-components/FormSection';
import { BoxSelector } from '@@/BoxSelector';
import { FormActions } from '@@/form-components/FormActions';
import { EdgeGroup } from '../../types';
import { groupTypeOptions } from './group-type-options';
import { FormValues } from './types';
import { useValidation } from './useValidation';
import { DynamicGroupFieldset } from './DynamicGroupFieldset';
import { StaticGroupFieldset } from './StaticGroupFieldset';
import { NameField } from './NameField';
export function EdgeGroupForm({
onSubmit,
isLoading,
group,
}: {
onSubmit: (values: FormValues) => void;
isLoading: boolean;
group?: EdgeGroup;
}) {
const validation = useValidation({ id: group?.Id });
return (
<Formik
validationSchema={validation}
validateOnMount
initialValues={
group
? {
dynamic: group.Dynamic,
environmentIds: group.Endpoints,
name: group.Name,
partialMatch: group.PartialMatch,
tagIds: group.TagIds,
}
: {
name: '',
dynamic: false,
environmentIds: [],
partialMatch: false,
tagIds: [],
}
}
onSubmit={onSubmit}
>
<InnerForm isLoading={isLoading} isCreate={!group} />
</Formik>
);
}
function InnerForm({
isLoading,
isCreate,
}: {
isLoading: boolean;
isCreate: boolean;
}) {
const { values, setFieldValue, isValid, errors } =
useFormikContext<FormValues>();
return (
<Form className="form-horizontal">
<NameField errors={errors.name} />
<FormSection title="Group type">
<BoxSelector
slim
value={values.dynamic}
onChange={(dynamic) => setFieldValue('dynamic', dynamic)}
options={groupTypeOptions}
radioName="groupTypeDynamic"
/>
</FormSection>
{values.dynamic ? <DynamicGroupFieldset /> : <StaticGroupFieldset />}
<FormActions
submitLabel={isCreate ? 'Add edge group' : 'Save edge group'}
isLoading={isLoading}
isValid={isValid}
data-cy="edgeGroupCreate-addGroupButton"
loadingText="In progress..."
errors={errors}
/>
</Form>
);
}

View file

@ -0,0 +1,42 @@
import { Field, FormikErrors } from 'formik';
import { string } from 'yup';
import { useMemo } from 'react';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { useEdgeGroups } from '../../queries/useEdgeGroups';
import { EdgeGroup } from '../../types';
export function NameField({ errors }: { errors?: FormikErrors<string> }) {
return (
<FormControl label="Name" required errors={errors} inputId="group_name">
<Field
as={Input}
name="name"
placeholder="e.g. mygroup"
data-cy="edgeGroupCreate-groupNameInput"
id="group_name"
/>
</FormControl>
);
}
export function useNameValidation(id?: EdgeGroup['Id']) {
const edgeGroupsQuery = useEdgeGroups();
return useMemo(
() =>
string()
.required('Name is required')
.test({
name: 'is-unique',
test: (value) =>
!edgeGroupsQuery.data?.find(
(group) => group.Name === value && group.Id !== id
),
message: 'Name must be unique',
}),
[edgeGroupsQuery.data, id]
);
}

View file

@ -0,0 +1,40 @@
import { useFormikContext } from 'formik';
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
import { FormSection } from '@@/form-components/FormSection';
import { confirmDestructive } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { FormValues } from './types';
export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
return (
<FormSection title="Associated environments">
<div className="form-group">
<AssociatedEdgeEnvironmentsSelector
value={values.environmentIds}
error={errors.environmentIds}
onChange={async (environmentIds, meta) => {
if (meta.type === 'remove' && isEdit) {
const confirmed = await confirmDestructive({
title: 'Confirm action',
message:
'Removing the environment from this group will remove its corresponding edge stacks',
confirmButton: buildConfirmButton('Confirm'),
});
if (!confirmed) {
return;
}
}
setFieldValue('environmentIds', environmentIds);
}}
/>
</div>
</FormSection>
);
}

View file

@ -0,0 +1,22 @@
import { List, Tag } from 'lucide-react';
import { BoxSelectorOption } from '@@/BoxSelector';
export const groupTypeOptions: ReadonlyArray<BoxSelectorOption<boolean>> = [
{
id: 'static-group',
value: false,
label: 'Static',
description: 'Manually select Edge environments',
icon: List,
iconType: 'badge',
},
{
id: 'dynamic-group',
value: true,
label: 'Dynamic',
description: 'Automatically associate environments via tags',
icon: Tag,
iconType: 'badge',
},
] as const;

View file

@ -0,0 +1,23 @@
import { Tag } from 'lucide-react';
import { BoxSelectorOption } from '@@/BoxSelector';
export const tagOptions: ReadonlyArray<BoxSelectorOption<boolean>> = [
{
id: 'or-selector',
value: true,
label: 'Partial Match',
description:
'Associate any environment matching at least one of the selected tags',
icon: Tag,
iconType: 'badge',
},
{
id: 'and-selector',
value: false,
label: 'Full Match',
description: 'Associate any environment matching all of the selected tags',
icon: Tag,
iconType: 'badge',
},
];

View file

@ -0,0 +1,10 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
import { TagId } from '@/portainer/tags/types';
export interface FormValues {
name: string;
dynamic: boolean;
environmentIds: EnvironmentId[];
partialMatch: boolean;
tagIds: TagId[];
}

View file

@ -0,0 +1,27 @@
import { SchemaOf, array, boolean, number, object } from 'yup';
import { useMemo } from 'react';
import { EdgeGroup } from '../../types';
import { FormValues } from './types';
import { useNameValidation } from './NameField';
export function useValidation({
id,
}: { id?: EdgeGroup['Id'] } = {}): SchemaOf<FormValues> {
const nameValidation = useNameValidation(id);
return useMemo(
() =>
object({
name: nameValidation,
dynamic: boolean().default(false),
environmentIds: array(number().required()),
partialMatch: boolean().default(false),
tagIds: array(number().required()).when('dynamic', {
is: true,
then: (schema) => schema.min(1, 'Tags are required'),
}),
}),
[nameValidation]
);
}