mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
chore: actions filter constraints (#6389)
https://linear.app/unleash/issue/2-1952/adapt-actions-form-ui-to-unleash-constraint-operators Implements the new action filters UI, now powered by constraint operators. This PR goes through some effort to not touch existing components too much, especially since they are critical for activation strategies. Instead, the new feature tries to adapt to the existing components and styling them appropriately, while still re-using them. We can refactor this at a later stage if needed. This UI will face some more drastic changes in the near future due to the feature rename, so I wanted to keep this PR mostly scoped to the constraint operators before proceeding with more changes.    As always, did some manual tests and it seems to be working great!
This commit is contained in:
parent
4f638a1c8d
commit
bddc508582
@ -87,7 +87,7 @@ export const FreeTextInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<ConstraintFormHeader style={{ marginBottom: 0 }}>
|
<ConstraintFormHeader style={{ marginBottom: 0 }}>
|
||||||
Set values (maximum 100 char length per value)
|
Set values (maximum 100 char length per value)
|
||||||
</ConstraintFormHeader>
|
</ConstraintFormHeader>
|
||||||
@ -125,7 +125,7 @@ export const FreeTextInput = ({
|
|||||||
removeValue={removeValue}
|
removeValue={removeValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ import {
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IResolveInputProps {
|
interface IResolveInputProps {
|
||||||
contextDefinition: IUnleashContextDefinition;
|
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
||||||
localConstraint: IConstraint;
|
localConstraint: Pick<IConstraint, 'value' | 'values'>;
|
||||||
constraintValues: string[];
|
constraintValues: string[];
|
||||||
constraintValue: string;
|
constraintValue: string;
|
||||||
setValue: (value: string) => void;
|
setValue: (value: string) => void;
|
||||||
|
@ -20,8 +20,8 @@ import {
|
|||||||
import { nonEmptyArray } from 'utils/nonEmptyArray';
|
import { nonEmptyArray } from 'utils/nonEmptyArray';
|
||||||
|
|
||||||
interface IUseConstraintInputProps {
|
interface IUseConstraintInputProps {
|
||||||
contextDefinition: IUnleashContextDefinition;
|
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
||||||
localConstraint: IConstraint;
|
localConstraint: Pick<IConstraint, 'operator' | 'value' | 'values'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUseConstraintOutput {
|
interface IUseConstraintOutput {
|
||||||
|
@ -9,7 +9,7 @@ import { ConditionallyRender } from '../../../../ConditionallyRender/Conditional
|
|||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
|
|
||||||
interface CaseSensitiveButtonProps {
|
interface CaseSensitiveButtonProps {
|
||||||
localConstraint: IConstraint;
|
localConstraint: Pick<IConstraint, 'caseInsensitive'>;
|
||||||
setCaseInsensitive: () => void;
|
setCaseInsensitive: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface InvertedOperatorButtonProps {
|
interface InvertedOperatorButtonProps {
|
||||||
localConstraint: IConstraint;
|
localConstraint: Pick<IConstraint, 'inverted'>;
|
||||||
setInvertedOperator: () => void;
|
setInvertedOperator: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,11 +26,29 @@ export const ProjectActionsFiltersCell = ({
|
|||||||
<TooltipLink
|
<TooltipLink
|
||||||
tooltip={
|
tooltip={
|
||||||
<>
|
<>
|
||||||
{filters.map(([parameter, value]) => (
|
{filters.map(
|
||||||
|
([
|
||||||
|
parameter,
|
||||||
|
{
|
||||||
|
inverted,
|
||||||
|
operator,
|
||||||
|
caseInsensitive,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
]) => (
|
||||||
<StyledItem key={parameter}>
|
<StyledItem key={parameter}>
|
||||||
<strong>{parameter}</strong>: {value}
|
<strong>{parameter}</strong>{' '}
|
||||||
|
{inverted ? 'NOT' : ''} {operator}{' '}
|
||||||
|
{caseInsensitive
|
||||||
|
? '(case insensitive)'
|
||||||
|
: ''}{' '}
|
||||||
|
<strong>
|
||||||
|
{values ? values.join(', ') : value}
|
||||||
|
</strong>
|
||||||
</StyledItem>
|
</StyledItem>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,10 +1,73 @@
|
|||||||
import { Badge, IconButton, Tooltip, styled } from '@mui/material';
|
import { IconButton, Tooltip, styled } from '@mui/material';
|
||||||
import { ActionsFilterState } from './useProjectActionsForm';
|
import { ActionsFilterState } from './useProjectActionsForm';
|
||||||
import { Delete } from '@mui/icons-material';
|
import { Delete } from '@mui/icons-material';
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { ProjectActionsFormItem } from './ProjectActionsFormItem';
|
import { ProjectActionsFormItem } from './ProjectActionsFormItem';
|
||||||
|
import { ConstraintOperatorSelect } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect';
|
||||||
|
import {
|
||||||
|
Operator,
|
||||||
|
allOperators,
|
||||||
|
dateOperators,
|
||||||
|
inOperators,
|
||||||
|
stringOperators,
|
||||||
|
} from 'constants/operators';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { oneOf } from 'utils/oneOf';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { CaseSensitiveButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton';
|
||||||
|
import { InvertedOperatorButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton';
|
||||||
|
import { ResolveInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput';
|
||||||
|
import { useConstraintInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
|
const StyledDeleteButton = styled(IconButton)({
|
||||||
|
marginRight: '-6px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledFilter = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledFilterHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'start',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledOperatorOptions = styled('div')(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
display: 'inline-flex',
|
||||||
|
flex: 1,
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledOperatorSelectWrapper = styled('div')(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
'&&& > div': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledOperatorButtonWrapper = styled('div')(({ theme }) => ({
|
||||||
|
display: 'inline-flex',
|
||||||
|
flex: 1,
|
||||||
|
'&&& button': {
|
||||||
|
marginRight: 0,
|
||||||
|
'&:not(.operator-is-active)': {
|
||||||
|
backgroundColor: theme.palette.background.elevation2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledInputContainer = styled('div')({
|
const StyledInputContainer = styled('div')({
|
||||||
|
width: '100%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -12,39 +75,141 @@ const StyledInput = styled(Input)({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
const StyledBadge = styled(Badge)(({ theme }) => ({
|
const StyledResolveInputWrapper = styled('div')(({ theme }) => ({
|
||||||
margin: theme.spacing(0, 1),
|
'& > h3': {
|
||||||
fontSize: theme.fontSizes.mainHeader,
|
margin: theme.spacing(1, 0, 0, 0),
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
'& > div': {
|
||||||
|
margin: 0,
|
||||||
|
maxWidth: 'unset',
|
||||||
|
'& > div': {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
'& .MuiFormControl-root': {
|
||||||
|
margin: theme.spacing(0.5, 0, 0, 0),
|
||||||
|
},
|
||||||
|
'&:not(:first-of-type)': {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
interface IProjectActionsFilterItemProps {
|
||||||
|
filter: ActionsFilterState;
|
||||||
|
index: number;
|
||||||
|
stateChanged: (updatedFilter: ActionsFilterState) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const ProjectActionsFilterItem = ({
|
export const ProjectActionsFilterItem = ({
|
||||||
filter,
|
filter,
|
||||||
index,
|
index,
|
||||||
stateChanged,
|
stateChanged,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: IProjectActionsFilterItemProps) => {
|
||||||
filter: ActionsFilterState;
|
const { parameter, inverted, operator, caseInsensitive, value, values } =
|
||||||
index: number;
|
filter;
|
||||||
stateChanged: (updatedFilter: ActionsFilterState) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}) => {
|
|
||||||
const { parameter, value } = filter;
|
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<>
|
<>
|
||||||
<span>Filter {index + 1}</span>
|
<span>Filter {index + 1}</span>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title='Delete filter' arrow>
|
<Tooltip title='Delete filter' arrow>
|
||||||
<IconButton onClick={onDelete}>
|
<StyledDeleteButton onClick={onDelete}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</StyledDeleteButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Adapted from `/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader.tsx`
|
||||||
|
const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const caseInsensitiveInOperators = useUiFlag('caseInsensitiveInOperators');
|
||||||
|
|
||||||
|
const validOperators = allOperators.filter(
|
||||||
|
(operator) => !oneOf(dateOperators, operator),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { input, validator, setError, error } = useConstraintInput({
|
||||||
|
contextDefinition: { legalValues: [] },
|
||||||
|
localConstraint: { operator, value, values },
|
||||||
|
});
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
const [typeValidatorResult, err] = validator();
|
||||||
|
if (!typeValidatorResult) {
|
||||||
|
setError(err);
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validate();
|
||||||
|
}, [value, error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
oneOf(stringOperators, operator) ||
|
||||||
|
(oneOf(inOperators, operator) && caseInsensitiveInOperators)
|
||||||
|
) {
|
||||||
|
setShowCaseSensitiveButton(true);
|
||||||
|
} else {
|
||||||
|
setShowCaseSensitiveButton(false);
|
||||||
|
}
|
||||||
|
}, [operator, caseInsensitiveInOperators]);
|
||||||
|
|
||||||
|
const onOperatorChange = (operator: Operator) => {
|
||||||
|
if (
|
||||||
|
oneOf(stringOperators, operator) ||
|
||||||
|
(oneOf(inOperators, operator) && caseInsensitiveInOperators)
|
||||||
|
) {
|
||||||
|
setShowCaseSensitiveButton(true);
|
||||||
|
} else {
|
||||||
|
setShowCaseSensitiveButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
operator,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (value: string) => {
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
values: undefined,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValues = (values: string[]) => {
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
value: undefined,
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeValue = (index: number) => {
|
||||||
|
const newValues = values?.filter((_, i) => i !== index) || [];
|
||||||
|
setValues(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectActionsFormItem index={index} header={header}>
|
<ProjectActionsFormItem index={index} header={header}>
|
||||||
|
<StyledFilter>
|
||||||
|
<StyledFilterHeader>
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
label='Parameter'
|
label='Parameter'
|
||||||
@ -57,19 +222,61 @@ export const ProjectActionsFilterItem = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
<StyledBadge>=</StyledBadge>
|
<StyledOperatorOptions>
|
||||||
<StyledInputContainer>
|
<StyledOperatorButtonWrapper>
|
||||||
<StyledInput
|
<InvertedOperatorButton
|
||||||
label='Value'
|
localConstraint={{ inverted }}
|
||||||
value={value}
|
setInvertedOperator={() =>
|
||||||
onChange={(e) =>
|
|
||||||
stateChanged({
|
stateChanged({
|
||||||
...filter,
|
...filter,
|
||||||
value: e.target.value,
|
inverted: !inverted || undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledInputContainer>
|
</StyledOperatorButtonWrapper>
|
||||||
|
<StyledOperatorSelectWrapper>
|
||||||
|
<ConstraintOperatorSelect
|
||||||
|
options={validOperators}
|
||||||
|
value={operator}
|
||||||
|
onChange={onOperatorChange}
|
||||||
|
/>
|
||||||
|
</StyledOperatorSelectWrapper>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showCaseSensitiveButton}
|
||||||
|
show={
|
||||||
|
<StyledOperatorButtonWrapper>
|
||||||
|
<CaseSensitiveButton
|
||||||
|
localConstraint={{ caseInsensitive }}
|
||||||
|
setCaseInsensitive={() =>
|
||||||
|
stateChanged({
|
||||||
|
...filter,
|
||||||
|
caseInsensitive:
|
||||||
|
!caseInsensitive ||
|
||||||
|
undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledOperatorButtonWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledOperatorOptions>
|
||||||
|
</StyledFilterHeader>
|
||||||
|
<StyledResolveInputWrapper>
|
||||||
|
<ResolveInput
|
||||||
|
setValues={setValues}
|
||||||
|
setValuesWithRecord={setValues}
|
||||||
|
setValue={setValue}
|
||||||
|
setError={setError}
|
||||||
|
localConstraint={{ value, values }}
|
||||||
|
constraintValues={values || []}
|
||||||
|
constraintValue={value || ''}
|
||||||
|
input={input}
|
||||||
|
error={error}
|
||||||
|
contextDefinition={{ legalValues: [] }}
|
||||||
|
removeValue={removeValue}
|
||||||
|
/>
|
||||||
|
</StyledResolveInputWrapper>
|
||||||
|
</StyledFilter>
|
||||||
</ProjectActionsFormItem>
|
</ProjectActionsFormItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,8 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { ProjectActionsActionItem } from './ProjectActionsActionItem';
|
import { ProjectActionsActionItem } from './ProjectActionsActionItem';
|
||||||
import { ProjectActionsFilterItem } from './ProjectActionsFilterItem';
|
import { ProjectActionsFilterItem } from './ProjectActionsFilterItem';
|
||||||
import { ProjectActionsFormStep } from './ProjectActionsFormStep';
|
import { ProjectActionsFormStep } from './ProjectActionsFormStep';
|
||||||
|
import { IN } from 'constants/operators';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
|
||||||
const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({
|
const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
@ -43,15 +45,10 @@ const StyledInput = styled(Input)(() => ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSecondaryDescription = styled('p')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
@ -59,6 +56,12 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledTooltip = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
interface IProjectActionsFormProps {
|
interface IProjectActionsFormProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@ -111,7 +114,7 @@ export const ProjectActionsForm = ({
|
|||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
parameter: '',
|
parameter: '',
|
||||||
value: '',
|
operator: IN,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@ -232,10 +235,6 @@ export const ProjectActionsForm = ({
|
|||||||
</ProjectActionsFormStep>
|
</ProjectActionsFormStep>
|
||||||
|
|
||||||
<ProjectActionsFormStep name='When this' verticalConnector>
|
<ProjectActionsFormStep name='When this' verticalConnector>
|
||||||
<StyledSecondaryDescription>
|
|
||||||
If no filters are defined then the action will be triggered
|
|
||||||
every time the incoming webhook is called.
|
|
||||||
</StyledSecondaryDescription>
|
|
||||||
{filters.map((filter, index) => (
|
{filters.map((filter, index) => (
|
||||||
<ProjectActionsFilterItem
|
<ProjectActionsFilterItem
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
@ -258,11 +257,28 @@ export const ProjectActionsForm = ({
|
|||||||
>
|
>
|
||||||
Add filter
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
|
<HelpIcon
|
||||||
|
htmlTooltip
|
||||||
|
tooltip={
|
||||||
|
<StyledTooltip>
|
||||||
|
<p>
|
||||||
|
Filters allow you to add conditions to the
|
||||||
|
execution of the actions based on the source
|
||||||
|
payload.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If no filters are defined then the action
|
||||||
|
will always be triggered from the selected
|
||||||
|
source, no matter the payload.
|
||||||
|
</p>
|
||||||
|
</StyledTooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</ProjectActionsFormStep>
|
</ProjectActionsFormStep>
|
||||||
|
|
||||||
<ProjectActionsFormStep
|
<ProjectActionsFormStep
|
||||||
name='Do these action(s)'
|
name='Do these actions'
|
||||||
verticalConnector
|
verticalConnector
|
||||||
resourceLink={
|
resourceLink={
|
||||||
<RouterLink to='/admin/service-accounts'>
|
<RouterLink to='/admin/service-accounts'>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useActions } from 'hooks/api/getters/useActions/useActions';
|
import { useActions } from 'hooks/api/getters/useActions/useActions';
|
||||||
import { IAction, IActionSet } from 'interfaces/action';
|
import { IAction, IActionSet, ParameterMatch } from 'interfaces/action';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
@ -7,14 +7,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
enum ErrorField {
|
enum ErrorField {
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
TRIGGER = 'trigger',
|
TRIGGER = 'trigger',
|
||||||
|
FILTERS = 'filters',
|
||||||
ACTOR = 'actor',
|
ACTOR = 'actor',
|
||||||
ACTIONS = 'actions',
|
ACTIONS = 'actions',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActionsFilterState = {
|
export type ActionsFilterState = ParameterMatch & {
|
||||||
id: string;
|
id: string;
|
||||||
parameter: string;
|
parameter: string;
|
||||||
value: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionsActionState = Omit<
|
export type ActionsActionState = Omit<
|
||||||
@ -27,6 +28,7 @@ export type ActionsActionState = Omit<
|
|||||||
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
|
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
|
||||||
[ErrorField.NAME]: undefined,
|
[ErrorField.NAME]: undefined,
|
||||||
[ErrorField.TRIGGER]: undefined,
|
[ErrorField.TRIGGER]: undefined,
|
||||||
|
[ErrorField.FILTERS]: undefined,
|
||||||
[ErrorField.ACTOR]: undefined,
|
[ErrorField.ACTOR]: undefined,
|
||||||
[ErrorField.ACTIONS]: undefined,
|
[ErrorField.ACTIONS]: undefined,
|
||||||
};
|
};
|
||||||
@ -50,10 +52,17 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
setSourceId(action?.match?.sourceId ?? 0);
|
setSourceId(action?.match?.sourceId ?? 0);
|
||||||
setFilters(
|
setFilters(
|
||||||
Object.entries(action?.match?.payload ?? {}).map(
|
Object.entries(action?.match?.payload ?? {}).map(
|
||||||
([parameter, value]) => ({
|
([
|
||||||
|
parameter,
|
||||||
|
{ inverted, operator, caseInsensitive, value, values },
|
||||||
|
]) => ({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
parameter,
|
parameter,
|
||||||
value: value as string,
|
inverted,
|
||||||
|
operator,
|
||||||
|
caseInsensitive,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -87,6 +96,13 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
setErrors((errors) => ({ ...errors, [field]: error }));
|
setErrors((errors) => ({ ...errors, [field]: error }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearError(ErrorField.FILTERS);
|
||||||
|
if (filters.some(({ error }) => error)) {
|
||||||
|
setError(ErrorField.FILTERS, 'One or more filters have errors.');
|
||||||
|
}
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
const isEmpty = (value: string) => !value.length;
|
const isEmpty = (value: string) => !value.length;
|
||||||
|
|
||||||
const isNameNotUnique = (value: string) =>
|
const isNameNotUnique = (value: string) =>
|
||||||
|
@ -100,9 +100,25 @@ export const ProjectActionsModal = ({
|
|||||||
payload: filters
|
payload: filters
|
||||||
.filter((f) => f.parameter.length > 0)
|
.filter((f) => f.parameter.length > 0)
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, filter) => ({
|
(
|
||||||
|
acc,
|
||||||
|
{
|
||||||
|
parameter,
|
||||||
|
inverted,
|
||||||
|
operator,
|
||||||
|
caseInsensitive,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[filter.parameter]: filter.value,
|
[parameter]: {
|
||||||
|
inverted,
|
||||||
|
operator,
|
||||||
|
caseInsensitive,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IConstraint } from './strategy';
|
||||||
|
|
||||||
export interface IActionSet {
|
export interface IActionSet {
|
||||||
id: number;
|
id: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -12,10 +14,15 @@ export interface IActionSet {
|
|||||||
|
|
||||||
type MatchSource = 'incoming-webhook';
|
type MatchSource = 'incoming-webhook';
|
||||||
|
|
||||||
|
export type ParameterMatch = Pick<
|
||||||
|
IConstraint,
|
||||||
|
'inverted' | 'operator' | 'caseInsensitive' | 'value' | 'values'
|
||||||
|
>;
|
||||||
|
|
||||||
export interface IMatch {
|
export interface IMatch {
|
||||||
source: MatchSource;
|
source: MatchSource;
|
||||||
sourceId: number;
|
sourceId: number;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, ParameterMatch>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAction {
|
export interface IAction {
|
||||||
|
Loading…
Reference in New Issue
Block a user