Skip to content

Commit

Permalink
feat: Project scoped stickiness (#3289)
Browse files Browse the repository at this point in the history
Project scoped stickiness 
<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

Adds `projectScopedStickiness` flag to experimental.ts
Refactor Stickiness select for reusability
Modify FlexibleStrategy to respect the setting.
Modify EnvironmentVariantModal to respect the setting

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
  • Loading branch information
andreas-unleash committed Mar 10, 2023
1 parent 99a5b96 commit 3193423
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 45 deletions.
Expand Up @@ -17,8 +17,9 @@ import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from 'component/providers/AccessP
import { WeightType } from 'constants/variantTypes';
import { v4 as uuidv4 } from 'uuid';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { updateWeightEdit } from 'component/common/util';
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';

const StyledFormSubtitle = styled('div')(({ theme }) => ({
display: 'flex',
Expand Down Expand Up @@ -65,10 +66,10 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
marginTop: theme.spacing(4),
}));

const StyledVariantForms = styled('div')(({ theme }) => ({
const StyledVariantForms = styled('div')({
display: 'flex',
flexDirection: 'column-reverse',
}));
});

const StyledStickinessContainer = styled('div')(({ theme }) => ({
display: 'flex',
Expand All @@ -83,7 +84,7 @@ const StyledDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1.5),
}));

const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({
const StyledStickinessSelect = styled(StickinessSelect)(({ theme }) => ({
minWidth: theme.spacing(20),
width: '100%',
}));
Expand Down Expand Up @@ -134,6 +135,7 @@ export const EnvironmentVariantsModal = ({

const { uiConfig } = useUiConfig();
const { context } = useUnleashContext();
const { defaultStickiness } = useDefaultProjectStickiness(projectId);

const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { data } = usePendingChangeRequests(projectId);
Expand Down Expand Up @@ -161,7 +163,7 @@ export const EnvironmentVariantsModal = ({
stickiness:
variantsEdit?.length > 0
? variantsEdit[0].stickiness
: 'default',
: defaultStickiness,
new: true,
isValid: false,
id: uuidv4(),
Expand Down Expand Up @@ -225,7 +227,7 @@ export const EnvironmentVariantsModal = ({
isChangeRequestConfigured(environment?.name || '') &&
uiConfig.flags.crOnVariants;

const stickiness = variants[0]?.stickiness || 'default';
const stickiness = variants[0]?.stickiness || defaultStickiness;
const stickinessOptions = useMemo(
() => [
'default',
Expand Down Expand Up @@ -258,7 +260,6 @@ export const EnvironmentVariantsModal = ({
setError(apiPayload.error);
}
}, [apiPayload.error]);

return (
<SidebarModal
open={open}
Expand Down Expand Up @@ -378,10 +379,13 @@ export const EnvironmentVariantsModal = ({
</a>
</StyledDescription>
<div>
<StyledGeneralSelect
options={options}
<StyledStickinessSelect
value={stickiness}
onChange={onStickinessChange}
label={''}
editable
onChange={e =>
onStickinessChange(e.target.value)
}
/>
</div>
</>
Expand Down
@@ -1,7 +1,6 @@
import { Typography } from '@mui/material';
import { IFeatureStrategyParameters } from 'interfaces/strategy';
import RolloutSlider from '../RolloutSlider/RolloutSlider';
import Select from 'component/common/select';
import Input from 'component/common/Input/Input';
import {
FLEXIBLE_STRATEGY_GROUP_ID,
Expand All @@ -12,13 +11,10 @@ import {
parseParameterNumber,
parseParameterString,
} from 'utils/parseParameter';

const builtInStickinessOptions = [
{ key: 'default', label: 'default' },
{ key: 'userId', label: 'userId' },
{ key: 'sessionId', label: 'sessionId' },
{ key: 'random', label: 'random' },
];
import { StickinessSelect } from './StickinessSelect/StickinessSelect';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { useDefaultProjectStickiness } from '../../../../hooks/useDefaultProjectStickiness';

interface IFlexibleStrategyProps {
parameters: IFeatureStrategyParameters;
Expand All @@ -30,9 +26,11 @@ interface IFlexibleStrategyProps {
const FlexibleStrategy = ({
updateParameter,
parameters,
context,
editable = true,
}: IFlexibleStrategyProps) => {
const projectId = useOptionalPathParam('projectId');
const { defaultStickiness } = useDefaultProjectStickiness(projectId);

const onUpdate = (field: string) => (newValue: string) => {
updateParameter(field, newValue);
};
Expand All @@ -41,26 +39,25 @@ const FlexibleStrategy = ({
updateParameter('rollout', value.toString());
};

const resolveStickiness = () =>
builtInStickinessOptions.concat(
context
// @ts-expect-error
.filter(c => c.stickiness)
.filter(
// @ts-expect-error
c => !builtInStickinessOptions.find(s => s.key === c.name)
)
// @ts-expect-error
.map(c => ({ key: c.name, label: c.name }))
);

const stickinessOptions = resolveStickiness();

const rollout =
parameters.rollout !== undefined
? parseParameterNumber(parameters.rollout)
: 100;

const resolveStickiness = () => {
if (parameters.stickiness === '') {
return defaultStickiness;
}

return parseParameterString(parameters.stickiness);
};

const stickiness = resolveStickiness();

if (parameters.stickiness === '') {
onUpdate('stickiness')(stickiness);
}

return (
<div>
<RolloutSlider
Expand All @@ -84,14 +81,11 @@ const FlexibleStrategy = ({
Stickiness
<HelpIcon tooltip="Stickiness defines what parameter should be used to ensure that your users get consistency in features. By default unleash will use the first value present in the context in the order of userId, sessionId and random." />
</Typography>
<Select
id="stickiness-select"
name="stickiness"
<StickinessSelect
label="Stickiness"
options={stickinessOptions}
value={parseParameterString(parameters.stickiness)}
disabled={!editable}
data-testid={FLEXIBLE_STRATEGY_STICKINESS_ID}
value={stickiness}
editable={editable}
dataTestId={FLEXIBLE_STRATEGY_STICKINESS_ID}
onChange={e => onUpdate('stickiness')(e.target.value)}
/>
&nbsp;
Expand Down
@@ -0,0 +1,57 @@
import Select from 'component/common/select';
import { SelectChangeEvent } from '@mui/material';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
const builtInStickinessOptions = [
{ key: 'default', label: 'default' },
{ key: 'userId', label: 'userId' },
{ key: 'sessionId', label: 'sessionId' },
{ key: 'random', label: 'random' },
];

interface IStickinessSelectProps {
label: string;
value: string | undefined;
editable: boolean;
onChange: (event: SelectChangeEvent) => void;
dataTestId?: string;
}
export const StickinessSelect = ({
label,
editable,
value,
onChange,
dataTestId,
}: IStickinessSelectProps) => {
const { context } = useUnleashContext();

const resolveStickinessOptions = () =>
builtInStickinessOptions.concat(
context
.filter(contextDefinition => contextDefinition.stickiness)
.filter(
contextDefinition =>
!builtInStickinessOptions.find(
builtInStickinessOption =>
builtInStickinessOption.key ===
contextDefinition.name
)
)
.map(c => ({ key: c.name, label: c.name }))
);

const stickinessOptions = resolveStickinessOptions();

return (
<Select
id="stickiness-select"
name="stickiness"
label={label}
options={stickinessOptions}
value={value}
disabled={!editable}
data-testid={dataTestId}
onChange={onChange}
style={{ width: 'inherit', minWidth: '100%' }}
/>
);
};
Expand Up @@ -10,6 +10,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { GO_BACK } from 'constants/navigate';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';

const CreateProject = () => {
const { setToastData, setToastApiError } = useToast();
Expand All @@ -27,11 +28,16 @@ const CreateProject = () => {
clearErrors,
validateProjectId,
validateName,
setProjectStickiness,
projectStickiness,
errors,
} = useProjectForm();

const { createProject, loading } = useProjectApi();

const { setDefaultProjectStickiness } =
useDefaultProjectStickiness(projectId);

const handleSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
Expand All @@ -42,6 +48,7 @@ const CreateProject = () => {
const payload = getProjectPayload();
try {
await createProject(payload);
setDefaultProjectStickiness(payload.projectStickiness);
refetchUser();
navigate(`/projects/${projectId}`);
setToastData({
Expand Down Expand Up @@ -85,6 +92,8 @@ const CreateProject = () => {
projectId={projectId}
setProjectId={setProjectId}
projectName={projectName}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
setProjectName={setProjectName}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
Expand Down
Expand Up @@ -14,28 +14,38 @@ import { useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import { Alert } from '@mui/material';
import { GO_BACK } from 'constants/navigate';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';

const EditProject = () => {
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const { hasAccess } = useContext(AccessContext);
const id = useRequiredPathParam('projectId');
const { project } = useProject(id);
const { defaultStickiness, setDefaultProjectStickiness } =
useDefaultProjectStickiness(id);
const navigate = useNavigate();

const {
projectId,
projectName,
projectDesc,
projectStickiness,
setProjectId,
setProjectName,
setProjectDesc,
setProjectStickiness,
getProjectPayload,
clearErrors,
validateProjectId,
validateName,
errors,
} = useProjectForm(id, project.name, project.description);
} = useProjectForm(
id,
project.name,
project.description,
defaultStickiness
);

const formatApiCode = () => {
return `curl --location --request PUT '${
Expand All @@ -58,6 +68,7 @@ const EditProject = () => {
if (validName) {
try {
await editProject(id, payload);
setDefaultProjectStickiness(payload.projectStickiness);
refetch();
navigate(`/projects/${id}`);
setToastData({
Expand Down Expand Up @@ -98,6 +109,8 @@ const EditProject = () => {
setProjectId={setProjectId}
projectName={projectName}
setProjectName={setProjectName}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
mode="Edit"
Expand Down

0 comments on commit 3193423

Please sign in to comment.