-
-
Notifications
You must be signed in to change notification settings - Fork 730
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: strategy types, constraints, segments
- Loading branch information
Showing
6 changed files
with
532 additions
and
7 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyConstraints.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { FeatureStrategyConstraintAccordionList } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/FeatureStrategyConstraintAccordionList/FeatureStrategyConstraintAccordionList'; | ||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans'; | ||
import type { IConstraint } from 'interfaces/strategy'; | ||
import { useEffect } from 'react'; | ||
|
||
interface IMilestoneStrategyConstraintsProps { | ||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>; | ||
setStrategy: React.Dispatch< | ||
React.SetStateAction<Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>> | ||
>; | ||
} | ||
|
||
const filterConstraints = (constraint: any) => { | ||
if ( | ||
constraint.hasOwnProperty('values') && | ||
(!constraint.hasOwnProperty('value') || constraint.value === '') | ||
) { | ||
return constraint.values && constraint.values.length > 0; | ||
} | ||
|
||
if (constraint.hasOwnProperty('value')) { | ||
return constraint.value !== ''; | ||
} | ||
}; | ||
|
||
export const MilestoneStrategyConstraints = ({ | ||
strategy, | ||
setStrategy, | ||
}: IMilestoneStrategyConstraintsProps) => { | ||
useEffect(() => { | ||
return () => { | ||
if (!strategy.constraints) { | ||
return; | ||
} | ||
|
||
// If the component is unmounting we want to remove all constraints that do not have valid single value or | ||
// valid multivalues | ||
setStrategy((prev) => ({ | ||
...prev, | ||
constraints: prev.constraints?.filter(filterConstraints), | ||
})); | ||
}; | ||
}, []); | ||
|
||
const constraints = strategy.constraints || []; | ||
|
||
const setConstraints = (value: React.SetStateAction<IConstraint[]>) => { | ||
setStrategy((prev) => { | ||
return { | ||
...prev, | ||
constraints: | ||
value instanceof Function | ||
? value(prev.constraints || []) | ||
: value, | ||
}; | ||
}); | ||
}; | ||
|
||
return ( | ||
<FeatureStrategyConstraintAccordionList | ||
constraints={constraints} | ||
setConstraints={setConstraints} | ||
showCreateButton={true} | ||
/> | ||
); | ||
}; |
98 changes: 98 additions & 0 deletions
98
frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategySegment.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits'; | ||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; | ||
import type { ISegment } from 'interfaces/segment'; | ||
import { Box, styled, Typography } from '@mui/material'; | ||
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs'; | ||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; | ||
import { | ||
AutocompleteBox, | ||
type IAutocompleteBoxOption, | ||
} from 'component/common/AutocompleteBox/AutocompleteBox'; | ||
import { MilestoneStrategySegmentList } from './MilestoneStrategySegmentList'; | ||
|
||
const StyledHelpIconBox = styled(Box)(({ theme }) => ({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
marginTop: theme.spacing(1), | ||
marginBottom: theme.spacing(1), | ||
})); | ||
|
||
interface IMilestoneStrategySegmentProps { | ||
segments: ISegment[]; | ||
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>; | ||
} | ||
|
||
export const MilestoneStrategySegment = ({ | ||
segments: selectedSegments, | ||
setSegments: setSelectedSegments, | ||
}: IMilestoneStrategySegmentProps) => { | ||
const { segments: allSegments } = useSegments(); | ||
const { strategySegmentsLimit } = useSegmentLimits(); | ||
|
||
const atStrategySegmentsLimit: boolean = Boolean( | ||
strategySegmentsLimit && | ||
selectedSegments.length >= strategySegmentsLimit, | ||
); | ||
|
||
if (!allSegments || allSegments.length === 0) { | ||
return null; | ||
} | ||
|
||
const unusedSegments = allSegments.filter((segment) => { | ||
return !selectedSegments.find((selected) => selected.id === segment.id); | ||
}); | ||
|
||
const autocompleteOptions = unusedSegments.map((segment) => ({ | ||
value: String(segment.id), | ||
label: segment.name, | ||
})); | ||
|
||
const onChange = ([option]: IAutocompleteBoxOption[]) => { | ||
const selectedSegment = allSegments.find((segment) => { | ||
return String(segment.id) === option.value; | ||
}); | ||
if (selectedSegment) { | ||
setSelectedSegments((prev) => [...prev, selectedSegment]); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<StyledHelpIconBox> | ||
<Typography>Segments</Typography> | ||
<HelpIcon | ||
htmlTooltip | ||
tooltip={ | ||
<Box> | ||
<Typography variant='body2'> | ||
Segments are reusable sets of constraints that | ||
can be defined once and reused across feature | ||
toggle configurations. You can create a segment | ||
on the global or the project level. Read more | ||
about segments{' '} | ||
<a | ||
href='https://docs.getunleash.io/reference/segments' | ||
target='_blank' | ||
rel='noopener noreferrer' | ||
> | ||
here | ||
</a> | ||
</Typography> | ||
</Box> | ||
} | ||
/> | ||
</StyledHelpIconBox> | ||
{atStrategySegmentsLimit && <SegmentDocsStrategyWarning />} | ||
<AutocompleteBox | ||
label='Select segments' | ||
options={autocompleteOptions} | ||
onChange={onChange} | ||
disabled={atStrategySegmentsLimit} | ||
/> | ||
<MilestoneStrategySegmentList | ||
segments={selectedSegments} | ||
setSegments={setSelectedSegments} | ||
/> | ||
</> | ||
); | ||
}; |
80 changes: 80 additions & 0 deletions
80
frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategySegmentList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { Fragment, useState } from 'react'; | ||
import type { ISegment } from 'interfaces/segment'; | ||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip'; | ||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; | ||
import { styled } from '@mui/material'; | ||
|
||
const StyledList = styled('div')(({ theme }) => ({ | ||
display: 'flex', | ||
flexWrap: 'wrap', | ||
gap: theme.spacing(1), | ||
})); | ||
|
||
const StyledSelectedSegmentsLabel = styled('p')(({ theme }) => ({ | ||
color: theme.palette.text.secondary, | ||
})); | ||
|
||
const StyledAnd = styled('p')(({ theme }) => ({ | ||
fontSize: theme.fontSizes.smallerBody, | ||
padding: theme.spacing(0.75, 1), | ||
display: 'block', | ||
marginTop: 'auto', | ||
marginBottom: 'auto', | ||
alignItems: 'center', | ||
borderRadius: theme.shape.borderRadius, | ||
lineHeight: 1, | ||
color: theme.palette.text.primary, | ||
backgroundColor: theme.palette.background.elevation2, | ||
})); | ||
|
||
type IMilestoneStrategySegmentListProps = { | ||
segments: ISegment[]; | ||
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>; | ||
}; | ||
|
||
export const MilestoneStrategySegmentList = ({ | ||
segments, | ||
setSegments, | ||
}: IMilestoneStrategySegmentListProps) => { | ||
const [preview, setPreview] = useState<ISegment>(); | ||
const lastSegmentIndex = segments.length - 1; | ||
|
||
if (segments.length === 0) { | ||
console.log('segments.length === 0'); | ||
return null; | ||
} | ||
|
||
return ( | ||
<> | ||
<ConditionallyRender | ||
condition={segments && segments.length > 0} | ||
show={ | ||
<StyledSelectedSegmentsLabel> | ||
Selected Segments | ||
</StyledSelectedSegmentsLabel> | ||
} | ||
/> | ||
<StyledList> | ||
{segments.map((segment, i) => ( | ||
<Fragment key={segment.id}> | ||
<FeatureStrategySegmentChip | ||
segment={segment} | ||
setSegments={setSegments} | ||
preview={preview} | ||
setPreview={setPreview} | ||
/> | ||
<ConditionallyRender | ||
condition={i < lastSegmentIndex} | ||
show={<StyledAnd>AND</StyledAnd>} | ||
/> | ||
</Fragment> | ||
))} | ||
</StyledList> | ||
<ConditionallyRender | ||
condition={Boolean(preview)} | ||
show={() => <SegmentItem segment={preview!} isExpanded />} | ||
/> | ||
</> | ||
); | ||
}; |
59 changes: 59 additions & 0 deletions
59
frontend/src/component/releases/ReleasePlanTemplate/MilestoneStrategyType.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type { IFormErrors } from 'hooks/useFormErrors'; | ||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans'; | ||
import type { IStrategy } from 'interfaces/strategy'; | ||
import { MilestoneStrategyTypeFlexible } from './MilestoneStrategyTypeFlexible'; | ||
import GeneralStrategy from 'component/feature/StrategyTypes/GeneralStrategy/GeneralStrategy'; | ||
import UserWithIdStrategy from 'component/feature/StrategyTypes/UserWithIdStrategy/UserWithId'; | ||
import DefaultStrategy from 'component/feature/StrategyTypes/DefaultStrategy/DefaultStrategy'; | ||
|
||
interface IMilestoneStrategyTypeProps { | ||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>; | ||
strategyDefinition?: IStrategy; | ||
parameters: IReleasePlanMilestoneStrategy['parameters']; | ||
updateParameter: (field: string, value: string) => void; | ||
errors: IFormErrors; | ||
} | ||
export const MilestoneStrategyType = ({ | ||
strategy, | ||
strategyDefinition, | ||
parameters, | ||
updateParameter, | ||
errors, | ||
}: IMilestoneStrategyTypeProps) => { | ||
if (!strategyDefinition) { | ||
return null; | ||
} | ||
|
||
switch (strategy.name) { | ||
case 'default': | ||
return <DefaultStrategy strategyDefinition={strategyDefinition} />; | ||
case 'flexibleRollout': | ||
return ( | ||
<MilestoneStrategyTypeFlexible | ||
parameters={parameters} | ||
updateParameter={updateParameter} | ||
errors={errors} | ||
editable={true} | ||
/> | ||
); | ||
case 'userWithId': | ||
return ( | ||
<UserWithIdStrategy | ||
editable={true} | ||
parameters={strategy.parameters ?? {}} | ||
updateParameter={updateParameter} | ||
errors={errors} | ||
/> | ||
); | ||
default: | ||
return ( | ||
<GeneralStrategy | ||
strategyDefinition={strategyDefinition} | ||
parameters={strategy.parameters ?? {}} | ||
updateParameter={updateParameter} | ||
editable={true} | ||
errors={errors} | ||
/> | ||
); | ||
} | ||
}; |
Oops, something went wrong.