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
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:
parent
f22aed34b5
commit
8a81d95253
64 changed files with 1878 additions and 1005 deletions
|
@ -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">
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
309
app/react/edge/edge-stacks/components/StaggerFieldset.tsx
Normal file
309
app/react/edge/edge-stacks/components/StaggerFieldset.tsx
Normal 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 'Update Configuration' 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(),
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue