Skip to content

Commit

Permalink
feat: strategy types, constraints, segments
Browse files Browse the repository at this point in the history
  • Loading branch information
daveleek committed Nov 26, 2024
1 parent 14403d7 commit 11c6ccf
Show file tree
Hide file tree
Showing 6 changed files with 532 additions and 7 deletions.
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}
/>
);
};
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}
/>
</>
);
};
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 />}
/>
</>
);
};
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}
/>
);
}
};
Loading

0 comments on commit 11c6ccf

Please sign in to comment.