1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00

feat: Project scoped stickiness ()

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>
This commit is contained in:
andreas-unleash 2023-03-10 12:28:02 +02:00 committed by GitHub
parent 99a5b96c20
commit 3193423d2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 210 additions and 45 deletions
frontend/src
component
feature
FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal
StrategyTypes/FlexibleStrategy
project/Project
hooks
interfaces
utils
src

View File

@ -17,8 +17,9 @@ import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from 'component/providers/AccessP
import { WeightType } from 'constants/variantTypes'; import { WeightType } from 'constants/variantTypes';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { updateWeightEdit } from 'component/common/util'; 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 }) => ({ const StyledFormSubtitle = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -65,10 +66,10 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
marginTop: theme.spacing(4), marginTop: theme.spacing(4),
})); }));
const StyledVariantForms = styled('div')(({ theme }) => ({ const StyledVariantForms = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'column-reverse', flexDirection: 'column-reverse',
})); });
const StyledStickinessContainer = styled('div')(({ theme }) => ({ const StyledStickinessContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -83,7 +84,7 @@ const StyledDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1.5), marginBottom: theme.spacing(1.5),
})); }));
const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({ const StyledStickinessSelect = styled(StickinessSelect)(({ theme }) => ({
minWidth: theme.spacing(20), minWidth: theme.spacing(20),
width: '100%', width: '100%',
})); }));
@ -134,6 +135,7 @@ export const EnvironmentVariantsModal = ({
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { context } = useUnleashContext(); const { context } = useUnleashContext();
const { defaultStickiness } = useDefaultProjectStickiness(projectId);
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { data } = usePendingChangeRequests(projectId); const { data } = usePendingChangeRequests(projectId);
@ -161,7 +163,7 @@ export const EnvironmentVariantsModal = ({
stickiness: stickiness:
variantsEdit?.length > 0 variantsEdit?.length > 0
? variantsEdit[0].stickiness ? variantsEdit[0].stickiness
: 'default', : defaultStickiness,
new: true, new: true,
isValid: false, isValid: false,
id: uuidv4(), id: uuidv4(),
@ -225,7 +227,7 @@ export const EnvironmentVariantsModal = ({
isChangeRequestConfigured(environment?.name || '') && isChangeRequestConfigured(environment?.name || '') &&
uiConfig.flags.crOnVariants; uiConfig.flags.crOnVariants;
const stickiness = variants[0]?.stickiness || 'default'; const stickiness = variants[0]?.stickiness || defaultStickiness;
const stickinessOptions = useMemo( const stickinessOptions = useMemo(
() => [ () => [
'default', 'default',
@ -258,7 +260,6 @@ export const EnvironmentVariantsModal = ({
setError(apiPayload.error); setError(apiPayload.error);
} }
}, [apiPayload.error]); }, [apiPayload.error]);
return ( return (
<SidebarModal <SidebarModal
open={open} open={open}
@ -378,10 +379,13 @@ export const EnvironmentVariantsModal = ({
</a> </a>
</StyledDescription> </StyledDescription>
<div> <div>
<StyledGeneralSelect <StyledStickinessSelect
options={options}
value={stickiness} value={stickiness}
onChange={onStickinessChange} label={''}
editable
onChange={e =>
onStickinessChange(e.target.value)
}
/> />
</div> </div>
</> </>

View File

@ -1,7 +1,6 @@
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
import { IFeatureStrategyParameters } from 'interfaces/strategy'; import { IFeatureStrategyParameters } from 'interfaces/strategy';
import RolloutSlider from '../RolloutSlider/RolloutSlider'; import RolloutSlider from '../RolloutSlider/RolloutSlider';
import Select from 'component/common/select';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { import {
FLEXIBLE_STRATEGY_GROUP_ID, FLEXIBLE_STRATEGY_GROUP_ID,
@ -12,13 +11,10 @@ import {
parseParameterNumber, parseParameterNumber,
parseParameterString, parseParameterString,
} from 'utils/parseParameter'; } from 'utils/parseParameter';
import { StickinessSelect } from './StickinessSelect/StickinessSelect';
const builtInStickinessOptions = [ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
{ key: 'default', label: 'default' }, import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
{ key: 'userId', label: 'userId' }, import { useDefaultProjectStickiness } from '../../../../hooks/useDefaultProjectStickiness';
{ key: 'sessionId', label: 'sessionId' },
{ key: 'random', label: 'random' },
];
interface IFlexibleStrategyProps { interface IFlexibleStrategyProps {
parameters: IFeatureStrategyParameters; parameters: IFeatureStrategyParameters;
@ -30,9 +26,11 @@ interface IFlexibleStrategyProps {
const FlexibleStrategy = ({ const FlexibleStrategy = ({
updateParameter, updateParameter,
parameters, parameters,
context,
editable = true, editable = true,
}: IFlexibleStrategyProps) => { }: IFlexibleStrategyProps) => {
const projectId = useOptionalPathParam('projectId');
const { defaultStickiness } = useDefaultProjectStickiness(projectId);
const onUpdate = (field: string) => (newValue: string) => { const onUpdate = (field: string) => (newValue: string) => {
updateParameter(field, newValue); updateParameter(field, newValue);
}; };
@ -41,26 +39,25 @@ const FlexibleStrategy = ({
updateParameter('rollout', value.toString()); 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 = const rollout =
parameters.rollout !== undefined parameters.rollout !== undefined
? parseParameterNumber(parameters.rollout) ? parseParameterNumber(parameters.rollout)
: 100; : 100;
const resolveStickiness = () => {
if (parameters.stickiness === '') {
return defaultStickiness;
}
return parseParameterString(parameters.stickiness);
};
const stickiness = resolveStickiness();
if (parameters.stickiness === '') {
onUpdate('stickiness')(stickiness);
}
return ( return (
<div> <div>
<RolloutSlider <RolloutSlider
@ -84,14 +81,11 @@ const FlexibleStrategy = ({
Stickiness 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." /> <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> </Typography>
<Select <StickinessSelect
id="stickiness-select"
name="stickiness"
label="Stickiness" label="Stickiness"
options={stickinessOptions} value={stickiness}
value={parseParameterString(parameters.stickiness)} editable={editable}
disabled={!editable} dataTestId={FLEXIBLE_STRATEGY_STICKINESS_ID}
data-testid={FLEXIBLE_STRATEGY_STICKINESS_ID}
onChange={e => onUpdate('stickiness')(e.target.value)} onChange={e => onUpdate('stickiness')(e.target.value)}
/> />
&nbsp; &nbsp;

View File

@ -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%' }}
/>
);
};

View File

@ -10,6 +10,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { GO_BACK } from 'constants/navigate'; import { GO_BACK } from 'constants/navigate';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';
const CreateProject = () => { const CreateProject = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -27,11 +28,16 @@ const CreateProject = () => {
clearErrors, clearErrors,
validateProjectId, validateProjectId,
validateName, validateName,
setProjectStickiness,
projectStickiness,
errors, errors,
} = useProjectForm(); } = useProjectForm();
const { createProject, loading } = useProjectApi(); const { createProject, loading } = useProjectApi();
const { setDefaultProjectStickiness } =
useDefaultProjectStickiness(projectId);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
clearErrors(); clearErrors();
@ -42,6 +48,7 @@ const CreateProject = () => {
const payload = getProjectPayload(); const payload = getProjectPayload();
try { try {
await createProject(payload); await createProject(payload);
setDefaultProjectStickiness(payload.projectStickiness);
refetchUser(); refetchUser();
navigate(`/projects/${projectId}`); navigate(`/projects/${projectId}`);
setToastData({ setToastData({
@ -85,6 +92,8 @@ const CreateProject = () => {
projectId={projectId} projectId={projectId}
setProjectId={setProjectId} setProjectId={setProjectId}
projectName={projectName} projectName={projectName}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
setProjectName={setProjectName} setProjectName={setProjectName}
projectDesc={projectDesc} projectDesc={projectDesc}
setProjectDesc={setProjectDesc} setProjectDesc={setProjectDesc}

View File

@ -14,6 +14,7 @@ import { useContext } from 'react';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { GO_BACK } from 'constants/navigate'; import { GO_BACK } from 'constants/navigate';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';
const EditProject = () => { const EditProject = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -21,21 +22,30 @@ const EditProject = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const id = useRequiredPathParam('projectId'); const id = useRequiredPathParam('projectId');
const { project } = useProject(id); const { project } = useProject(id);
const { defaultStickiness, setDefaultProjectStickiness } =
useDefaultProjectStickiness(id);
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
projectId, projectId,
projectName, projectName,
projectDesc, projectDesc,
projectStickiness,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
setProjectStickiness,
getProjectPayload, getProjectPayload,
clearErrors, clearErrors,
validateProjectId, validateProjectId,
validateName, validateName,
errors, errors,
} = useProjectForm(id, project.name, project.description); } = useProjectForm(
id,
project.name,
project.description,
defaultStickiness
);
const formatApiCode = () => { const formatApiCode = () => {
return `curl --location --request PUT '${ return `curl --location --request PUT '${
@ -58,6 +68,7 @@ const EditProject = () => {
if (validName) { if (validName) {
try { try {
await editProject(id, payload); await editProject(id, payload);
setDefaultProjectStickiness(payload.projectStickiness);
refetch(); refetch();
navigate(`/projects/${id}`); navigate(`/projects/${id}`);
setToastData({ setToastData({
@ -98,6 +109,8 @@ const EditProject = () => {
setProjectId={setProjectId} setProjectId={setProjectId}
projectName={projectName} projectName={projectName}
setProjectName={setProjectName} setProjectName={setProjectName}
projectStickiness={projectStickiness}
setProjectStickiness={setProjectStickiness}
projectDesc={projectDesc} projectDesc={projectDesc}
setProjectDesc={setProjectDesc} setProjectDesc={setProjectDesc}
mode="Edit" mode="Edit"

View File

@ -9,10 +9,15 @@ import {
StyledButtonContainer, StyledButtonContainer,
StyledButton, StyledButton,
} from './ProjectForm.styles'; } from './ProjectForm.styles';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IProjectForm { interface IProjectForm {
projectId: string; projectId: string;
projectName: string; projectName: string;
projectDesc: string; projectDesc: string;
projectStickiness?: string;
setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>;
setProjectId: React.Dispatch<React.SetStateAction<string>>; setProjectId: React.Dispatch<React.SetStateAction<string>>;
setProjectName: React.Dispatch<React.SetStateAction<string>>; setProjectName: React.Dispatch<React.SetStateAction<string>>;
setProjectDesc: React.Dispatch<React.SetStateAction<string>>; setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
@ -31,14 +36,19 @@ const ProjectForm: React.FC<IProjectForm> = ({
projectId, projectId,
projectName, projectName,
projectDesc, projectDesc,
projectStickiness,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
setProjectStickiness,
errors, errors,
mode, mode,
validateProjectId, validateProjectId,
clearErrors, clearErrors,
}) => { }) => {
const { uiConfig } = useUiConfig();
const { projectScopedStickiness } = uiConfig.flags;
return ( return (
<StyledForm onSubmit={handleSubmit}> <StyledForm onSubmit={handleSubmit}>
<StyledContainer> <StyledContainer>
@ -80,6 +90,29 @@ const ProjectForm: React.FC<IProjectForm> = ({
value={projectDesc} value={projectDesc}
onChange={e => setProjectDesc(e.target.value)} onChange={e => setProjectDesc(e.target.value)}
/> />
<ConditionallyRender
condition={
Boolean(projectScopedStickiness) &&
setProjectStickiness != null
}
show={
<>
<StyledDescription>
What is the default stickiness for the project?
</StyledDescription>
<StickinessSelect
label="Stickiness"
value={projectStickiness}
onChange={e =>
setProjectStickiness &&
setProjectStickiness(e.target.value)
}
editable
/>
</>
}
/>
</StyledContainer> </StyledContainer>
<StyledButtonContainer> <StyledButtonContainer>

View File

@ -1,16 +1,24 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useDefaultProjectStickiness } from 'hooks/useDefaultProjectStickiness';
const useProjectForm = ( const useProjectForm = (
initialProjectId = '', initialProjectId = '',
initialProjectName = '', initialProjectName = '',
initialProjectDesc = '' initialProjectDesc = '',
initialProjectStickiness = 'default'
) => { ) => {
const [projectId, setProjectId] = useState(initialProjectId); const [projectId, setProjectId] = useState(initialProjectId);
const { defaultStickiness } = useDefaultProjectStickiness(projectId);
const [projectName, setProjectName] = useState(initialProjectName); const [projectName, setProjectName] = useState(initialProjectName);
const [projectDesc, setProjectDesc] = useState(initialProjectDesc); const [projectDesc, setProjectDesc] = useState(initialProjectDesc);
const [projectStickiness, setProjectStickiness] = useState(
defaultStickiness || initialProjectStickiness
);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const { validateId } = useProjectApi(); const { validateId } = useProjectApi();
useEffect(() => { useEffect(() => {
@ -30,6 +38,7 @@ const useProjectForm = (
id: projectId, id: projectId,
name: projectName, name: projectName,
description: projectDesc, description: projectDesc,
projectStickiness,
}; };
}; };
@ -64,9 +73,11 @@ const useProjectForm = (
projectId, projectId,
projectName, projectName,
projectDesc, projectDesc,
projectStickiness,
setProjectId, setProjectId,
setProjectName, setProjectName,
setProjectDesc, setProjectDesc,
setProjectStickiness,
getProjectPayload, getProjectPayload,
validateName, validateName,
validateProjectId, validateProjectId,

View File

@ -0,0 +1,35 @@
import useUiConfig from './api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from './usePlausibleTracker';
const DEFAULT_STICKINESS = 'default';
export const useDefaultProjectStickiness = (projectId?: string) => {
const { trackEvent } = usePlausibleTracker();
const { uiConfig } = useUiConfig();
const key = `defaultStickiness.${projectId}`;
const { projectScopedStickiness } = uiConfig.flags;
const projectStickiness = localStorage.getItem(key);
const defaultStickiness =
Boolean(projectScopedStickiness) &&
projectStickiness != null &&
projectId
? projectStickiness
: DEFAULT_STICKINESS;
const setDefaultProjectStickiness = (stickiness: string) => {
if (
Boolean(projectScopedStickiness) &&
projectId &&
stickiness !== ''
) {
localStorage.setItem(key, stickiness);
trackEvent('project_stickiness_set');
}
};
return {
defaultStickiness,
setDefaultProjectStickiness,
};
};

View File

@ -21,6 +21,7 @@ export type CustomEvents =
| 'unknown_ui_error' | 'unknown_ui_error'
| 'export_import' | 'export_import'
| 'project_api_tokens' | 'project_api_tokens'
| 'project_stickiness_set'
| 'notifications'; | 'notifications';
export const usePlausibleTracker = () => { export const usePlausibleTracker = () => {

View File

@ -50,6 +50,7 @@ export interface IFlags {
loginHistory?: boolean; loginHistory?: boolean;
bulkOperations?: boolean; bulkOperations?: boolean;
projectScopedSegments?: boolean; projectScopedSegments?: boolean;
projectScopedStickiness?: boolean;
} }
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -69,7 +69,7 @@ test('createFeatureStrategy with parameters', () => {
"groupId": "a", "groupId": "a",
"rollout": "50", "rollout": "50",
"s": "", "s": "",
"stickiness": "default", "stickiness": "",
}, },
} }
`); `);

View File

@ -40,7 +40,7 @@ const createFeatureStrategyParameterValue = (
} }
if (parameter.name === 'stickiness') { if (parameter.name === 'stickiness') {
return 'default'; return '';
} }
if (parameter.name === 'groupId') { if (parameter.name === 'groupId') {

View File

@ -83,6 +83,7 @@ exports[`should create default config 1`] = `
"notifications": false, "notifications": false,
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"projectScopedSegments": false, "projectScopedSegments": false,
"projectScopedStickiness": false,
"projectStatusApi": false, "projectStatusApi": false,
"proxyReturnAllToggles": false, "proxyReturnAllToggles": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,
@ -108,6 +109,7 @@ exports[`should create default config 1`] = `
"notifications": false, "notifications": false,
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"projectScopedSegments": false, "projectScopedSegments": false,
"projectScopedStickiness": false,
"projectStatusApi": false, "projectStatusApi": false,
"proxyReturnAllToggles": false, "proxyReturnAllToggles": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,

View File

@ -72,6 +72,10 @@ const flags = {
process.env.PROJECT_SCOPED_SEGMENTS, process.env.PROJECT_SCOPED_SEGMENTS,
false, false,
), ),
projectScopedStickiness: parseEnvVarBoolean(
process.env.PROJECT_SCOPED_STICKINESS,
false,
),
cleanClientApi: parseEnvVarBoolean(process.env.CLEAN_CLIENT_API, false), cleanClientApi: parseEnvVarBoolean(process.env.CLEAN_CLIENT_API, false),
}; };

View File

@ -43,6 +43,7 @@ process.nextTick(async () => {
projectStatusApi: true, projectStatusApi: true,
showProjectApiAccess: true, showProjectApiAccess: true,
projectScopedSegments: true, projectScopedSegments: true,
projectScopedStickiness: true,
}, },
}, },
authentication: { authentication: {