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

refactor(edge/stacks): migrate create view to react [EE-2223] (#11575)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run

This commit is contained in:
Chaim Lev-Ari 2024-05-06 08:08:03 +03:00 committed by GitHub
parent f22aed34b5
commit 8a81d95253
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1878 additions and 1005 deletions

View file

@ -1,12 +1,13 @@
import _ from 'lodash';
import { useState } from 'react';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { Select } from '@@/form-components/ReactSelect';
import { FormSection } from '@@/form-components/FormSection';
import { FormError } from '@@/form-components/FormError';
import { Link } from '@@/Link';
import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection';
import { useEdgeGroups } from '../../edge-groups/queries/useEdgeGroups';
@ -29,20 +30,28 @@ export function EdgeGroupsSelector({
isGroupVisible = () => true,
required,
}: Props) {
const [inputId] = useState(() => _.uniqueId('edge-groups-selector-'));
const selector = (
<InnerSelector
value={value}
onChange={onChange}
isGroupVisible={isGroupVisible}
inputId={inputId}
/>
);
return horizontal ? (
<FormControl errors={error} label="Edge Groups" required={required}>
<FormControl
errors={error}
label="Edge Groups"
required={required}
inputId={inputId}
>
{selector}
</FormControl>
) : (
<FormSection title={`Edge Groups${required ? ' *' : ''}`}>
<FormSection title={`Edge Groups${required ? ' *' : ''}`} htmlFor={inputId}>
<div className="form-group">
<div className="col-sm-12">{selector} </div>
{error && (
@ -59,10 +68,12 @@ function InnerSelector({
value,
onChange,
isGroupVisible,
inputId,
}: {
isGroupVisible(group: EdgeGroup): boolean;
value: SingleValue[];
onChange: (value: SingleValue[]) => void;
inputId: string;
}) {
const edgeGroupsQuery = useEdgeGroups();
@ -86,6 +97,7 @@ function InnerSelector({
placeholder="Select one or multiple group(s)"
closeMenuOnSelect={false}
data-cy="edge-stacks-groups-selector"
inputId={inputId}
/>
) : (
<div className="small text-muted">

View file

@ -1,6 +1,4 @@
import _ from 'lodash';
import { EditorType } from '@/react/edge/edge-stacks/types';
import { DeploymentType } from '@/react/edge/edge-stacks/types';
import { BoxSelector } from '@@/BoxSelector';
import { BoxSelectorOption } from '@@/BoxSelector/types';
@ -10,8 +8,8 @@ import {
} from '@@/BoxSelector/common-options/deployment-methods';
interface Props {
value: number;
onChange(value: number): void;
value: DeploymentType;
onChange(value: DeploymentType): void;
hasDockerEndpoint: boolean;
hasKubeEndpoint: boolean;
allowKubeToSelectCompose?: boolean;
@ -24,10 +22,10 @@ export function EdgeStackDeploymentTypeSelector({
hasKubeEndpoint,
allowKubeToSelectCompose,
}: Props) {
const deploymentOptions: BoxSelectorOption<number>[] = _.compact([
const deploymentOptions: BoxSelectorOption<DeploymentType>[] = [
{
...compose,
value: EditorType.Compose,
value: DeploymentType.Compose,
disabled: () => !allowKubeToSelectCompose && hasKubeEndpoint,
tooltip: () =>
hasKubeEndpoint
@ -36,7 +34,7 @@ export function EdgeStackDeploymentTypeSelector({
},
{
...kubernetes,
value: EditorType.Kubernetes,
value: DeploymentType.Kubernetes,
disabled: () => hasDockerEndpoint,
tooltip: () =>
hasDockerEndpoint
@ -44,7 +42,7 @@ export function EdgeStackDeploymentTypeSelector({
: '',
iconType: 'logo',
},
]);
];
return (
<>

View file

@ -0,0 +1,309 @@
import { number, string, object, SchemaOf } from 'yup';
import { FormikErrors } from 'formik';
import { useState, useEffect } from 'react';
import { FormSection } from '@@/form-components/FormSection';
import { RadioGroup } from '@@/RadioGroup/RadioGroup';
import { Input } from '@@/form-components/Input';
import { TextTip } from '@@/Tip/TextTip';
import { FormControl } from '@@/form-components/FormControl';
import { Button, ButtonGroup } from '@@/buttons';
import { StaggerParallelFieldset } from './StaggerParallelFieldset';
import {
StaggerConfig,
StaggerOption,
StaggerParallelOption,
UpdateFailureAction,
} from './StaggerFieldset.types';
interface Props {
values: StaggerConfig;
onChange: (value: Partial<StaggerConfig>) => void;
errors?: FormikErrors<StaggerConfig>;
isEdit?: boolean;
}
const staggerOptions = [
{
value: StaggerOption.AllAtOnce,
label: 'All edge devices at once',
},
{
value: StaggerOption.Parallel,
label: 'Parallel edge device(s)',
},
] as const;
export function StaggerFieldset({
values: initialValue,
onChange,
errors,
isEdit = true,
}: Props) {
const [values, setControlledValues] = useState(initialValue); // TODO: remove this state when form is not inside angularjs
useEffect(() => {
if (!!initialValue && initialValue.StaggerOption !== values.StaggerOption) {
setControlledValues(initialValue);
}
}, [initialValue, values]);
return (
<FormSection title="Update configurations">
{!isEdit && (
<div className="form-group">
<div className="col-sm-12">
<TextTip color="blue">
Please note that the &apos;Update Configuration&apos; setting
takes effect exclusively during edge stack updates, whether
triggered manually, through webhook events, or via GitOps updates
processes
</TextTip>
</div>
</div>
)}
<div className="form-group">
<div className="col-sm-12">
<RadioGroup
options={staggerOptions}
selectedOption={values.StaggerOption}
onOptionChange={(value) => {
handleChange({ StaggerOption: value });
}}
name="StaggerOption"
/>
</div>
</div>
{values.StaggerOption === StaggerOption.Parallel && (
<div className="mb-2">
<TextTip color="blue">
Specify the number of device(s) to be updated concurrently.
{values.StaggerParallelOption ===
StaggerParallelOption.Incremental && (
<div className="mb-2">
For example, if you start with 2 devices and multiply by 5, the
update will initially cover 2 edge devices, then 10 devices (2 x
5), followed by 50 devices (10 x 5), and so on.
</div>
)}
</TextTip>
<StaggerParallelFieldset
values={values}
onChange={handleChange}
errors={errors}
/>
<FormControl
label="Timeout"
inputId="timeout"
errors={errors?.Timeout}
>
<div>
<div style={{ display: 'inline-block', width: '150px' }}>
<Input
name="Timeout"
id="stagger-timeout"
placeholder="eg. 5 (optional)"
value={values.Timeout}
onChange={(e) =>
handleChange({
Timeout: e.currentTarget.value,
})
}
data-cy="edge-stacks-stagger-timeout-input"
/>
</div>
<span> {' minute(s) '} </span>
</div>
</FormControl>
<FormControl
label="Update delay"
inputId="update-delay"
errors={errors?.UpdateDelay}
>
<div>
<div style={{ display: 'inline-block', width: '150px' }}>
<Input
name="UpdateDelay"
data-cy="edge-stacks-stagger-update-delay-input"
id="stagger-update-delay"
placeholder="eg. 5 (optional)"
value={values.UpdateDelay}
onChange={(e) =>
handleChange({
UpdateDelay: e.currentTarget.value,
})
}
/>
</div>
<span> {' minute(s) '} </span>
</div>
</FormControl>
<FormControl
label="Update failure action"
inputId="update-failure-action"
errors={errors?.UpdateFailureAction}
>
<ButtonGroup>
<Button
className="btn-box-shadow"
data-cy="edge-stacks-stagger-update-failure-action-continue-button"
color={
values.UpdateFailureAction === UpdateFailureAction.Continue
? 'primary'
: 'light'
}
onClick={() =>
handleChange({
UpdateFailureAction: UpdateFailureAction.Continue,
})
}
>
Continue
</Button>
<Button
className="btn-box-shadow"
data-cy="edge-stacks-stagger-update-failure-action-pause-button"
color={
values.UpdateFailureAction === UpdateFailureAction.Pause
? 'primary'
: 'light'
}
onClick={() =>
handleChange({
UpdateFailureAction: UpdateFailureAction.Pause,
})
}
>
Pause
</Button>
<Button
className="btn-box-shadow"
data-cy="edge-stacks-stagger-update-failure-action-rollback-button"
color={
values.UpdateFailureAction === UpdateFailureAction.Rollback
? 'primary'
: 'light'
}
onClick={() =>
handleChange({
UpdateFailureAction: UpdateFailureAction.Rollback,
})
}
>
Rollback
</Button>
</ButtonGroup>
</FormControl>
</div>
)}
</FormSection>
);
function handleChange(partialValue: Partial<StaggerConfig>) {
onChange(partialValue);
setControlledValues((values) => ({ ...values, ...partialValue }));
}
}
export function staggerConfigValidation(): SchemaOf<StaggerConfig> {
return object({
StaggerOption: number()
.oneOf([StaggerOption.AllAtOnce, StaggerOption.Parallel])
.required('Stagger option is required'),
StaggerParallelOption: number()
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.oneOf([
StaggerParallelOption.Fixed,
StaggerParallelOption.Incremental,
]),
})
.optional(),
DeviceNumber: number()
.default(0)
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.when('StaggerParallelOption', {
is: StaggerParallelOption.Fixed,
then: (schema) =>
schema
.required('Devices number is at least 1')
.min(1, 'Devices number is at least 1'),
}),
})
.optional(),
DeviceNumberStartFrom: number()
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.when('StaggerParallelOption', {
is: StaggerParallelOption.Incremental,
then: (schema) =>
schema
.min(1, 'Devices number start from at least 1')
.required('Devices number is required'),
}),
})
.optional(),
DeviceNumberIncrementBy: number()
.default(2)
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.when('StaggerParallelOption', {
is: StaggerParallelOption.Incremental,
then: (schema) =>
schema
.min(2)
.max(10)
.required('Devices number increment by is required'),
}),
})
.optional(),
Timeout: string()
.default('')
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.test(
'is-number',
'Timeout must be a number',
(value) => !Number.isNaN(Number(value))
),
})
.optional(),
UpdateDelay: string()
.default('')
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.test(
'is-number',
'Timeout must be a number',
(value) => !Number.isNaN(Number(value))
),
})
.optional(),
UpdateFailureAction: number()
.default(UpdateFailureAction.Continue)
.when('StaggerOption', {
is: StaggerOption.Parallel,
then: (schema) =>
schema.oneOf([
UpdateFailureAction.Continue,
UpdateFailureAction.Pause,
UpdateFailureAction.Rollback,
]),
})
.optional(),
});
}

View file

@ -0,0 +1,130 @@
import { FormikErrors } from 'formik';
import { Select, Input } from '@@/form-components/Input';
import { FormError } from '@@/form-components/FormError';
import { StaggerConfig, StaggerParallelOption } from './StaggerFieldset.types';
interface Props {
values: StaggerConfig;
onChange: (value: Partial<StaggerConfig>) => void;
errors?: FormikErrors<StaggerConfig>;
}
export function StaggerParallelFieldset({ values, onChange, errors }: Props) {
const staggerParallelOptions = [
{
value: StaggerParallelOption.Fixed.toString(),
label: 'Number of device(s)',
},
{
value: StaggerParallelOption.Incremental.toString(),
label: 'Exponential rollout',
},
];
const deviceNumberIncrementBy = [
{
value: '2',
label: '2',
},
{
value: '5',
label: '5',
},
{
value: '10',
label: '10',
},
];
return (
<div
className='form-group mb-5 mt-2 after:clear-both after:table after:content-[""]' // to fix issues with float"
>
<div className="col-sm-3 col-lg-2">
<Select
id="stagger-parallel-option"
data-cy="edge-stack-stagger-parallel-option-select"
value={values.StaggerParallelOption?.toString()}
onChange={(e) =>
handleChange({
StaggerParallelOption: parseInt(e.currentTarget.value, 10),
})
}
options={staggerParallelOptions}
/>
</div>
{values.StaggerParallelOption === StaggerParallelOption.Fixed && (
<div className="col-sm-9 col-lg-10">
<Input
name="DeviceNumber"
data-cy="edge-stack-device-number-input"
id="device-number"
type="number"
placeholder="eg. 1 or 10"
min={1}
value={values.DeviceNumber || ''}
onChange={(e) => {
handleChange({
DeviceNumber: e.currentTarget.valueAsNumber || undefined,
});
}}
/>
{errors?.DeviceNumber && (
<FormError>{errors?.DeviceNumber}</FormError>
)}
</div>
)}
{values.StaggerParallelOption === StaggerParallelOption.Incremental && (
<div className="col-sm-9 col-lg-10">
<div>
<span> {' start with '} </span>
<div style={{ display: 'inline-block', width: '150px' }}>
<Input
name="DeviceNumberStartFrom"
data-cy="edge-stack-device-number-start-from-input"
type="number"
id="device-number-start-from"
min={1}
placeholder="eg. 1"
value={values.DeviceNumberStartFrom}
onChange={(e) =>
handleChange({
DeviceNumberStartFrom:
e.currentTarget.value !== ''
? e.currentTarget.valueAsNumber
: 0,
})
}
/>
</div>
<span> {' device(s) and multiply the group size by '} </span>
<Select
id="device-number-incremental"
data-cy="edge-stack-device-number-incremental-select"
value={values.DeviceNumberIncrementBy}
style={{ display: 'inline-block', width: '150px' }}
onChange={(e) =>
handleChange({
DeviceNumberIncrementBy: parseInt(e.currentTarget.value, 10),
})
}
options={deviceNumberIncrementBy}
/>
<span>{' for each rollout '} </span>
</div>
{errors?.DeviceNumberStartFrom && (
<FormError>{errors?.DeviceNumberStartFrom}</FormError>
)}
</div>
)}
</div>
);
function handleChange(partialValue: Partial<StaggerConfig>) {
onChange(partialValue);
}
}