1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

Chore(1-3807)/remove flag add edit strategy take2 (#10108)

Removes all usages of flag addEditStrategy and refactors code where
necessary.

This is only the first step of the cleanup. After this, there's still
lots of code to be removed. I've got a different PR that removes ~5k
lines of code (https://github.com/Unleash/unleash/pull/10105) that I
want to reach in pieces to make sure that everythnig works on the way
there.
This commit is contained in:
Thomas Heartman 2025-06-11 08:03:34 +02:00 committed by GitHub
parent 2d228eea76
commit 7e61e0dd09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 47 additions and 343 deletions

View File

@ -8,9 +8,9 @@ import type {
} from 'component/changeRequest/changeRequest.types';
import { useSegment } from 'hooks/api/getters/useSegment/useSegment';
import { SegmentDiff, SegmentTooltipLink } from '../../SegmentTooltipLink.tsx';
import { ConstraintAccordionList } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { ViewableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/ViewableConstraintsList';
import { useUiFlag } from 'hooks/useUiFlag';
import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning.tsx';
const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
@ -67,7 +67,6 @@ export const SegmentChangeDetails: FC<{
: currentSegment?.name;
const referenceSegment =
changeRequestState === 'Applied' ? snapshotSegment : currentSegment;
const addEditStrategy = useUiFlag('addEditStrategy');
return (
<SegmentContainer conflict={change.conflict}>
@ -116,16 +115,9 @@ export const SegmentChangeDetails: FC<{
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditWrapper>
{addEditStrategy ? (
<ViewableConstraintsList
constraints={change.payload.constraints}
/>
) : (
<ConstraintAccordionList
constraints={change.payload.constraints}
showLabel={false}
/>
)}
</>
)}
</SegmentContainer>

View File

@ -33,7 +33,6 @@ export const ConstraintsList: FC<{ children: ReactNode }> = ({ children }) => {
result.push(
<StyledListItem key={index}>
{index > 0 ? (
// todo (addEditStrategy): change divider for edit screen (probably a new component or a prop)
<ConstraintSeparator key={`${index}-divider`} />
) : null}
{child}

View File

@ -3,8 +3,6 @@ import { Chip, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ConstraintValueSearch as NewConstraintValueSearch } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/ConstraintValueSearch';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConstraintValueSearch } from 'component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
interface IMultipleValuesProps {
values: string[] | undefined;
@ -25,7 +23,6 @@ const SearchWrapper = styled('div')(({ theme }) => ({
export const MultipleValues = ({ values }: IMultipleValuesProps) => {
const [filter, setFilter] = useState('');
const useNewSearchComponent = useUiFlag('addEditStrategy');
if (!values || values.length === 0) return null;
@ -34,19 +31,12 @@ export const MultipleValues = ({ values }: IMultipleValuesProps) => {
<ConditionallyRender
condition={values.length > 20}
show={
useNewSearchComponent ? (
<SearchWrapper>
<NewConstraintValueSearch
filter={filter}
setFilter={setFilter}
/>
</SearchWrapper>
) : (
<ConstraintValueSearch
filter={filter}
setFilter={setFilter}
/>
)
}
/>
{values

View File

@ -6,9 +6,7 @@ import produce from 'immer';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import type { IUseWeakMap } from 'hooks/useWeakMap';
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion';
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint';
@ -52,7 +50,6 @@ export const NewConstraintAccordionList = forwardRef<
IConstraintList
>(({ constraints, setConstraints, state }, ref) => {
const { context } = useUnleashContext();
const addEditStrategy = useUiFlag('addEditStrategy');
const onEdit =
setConstraints &&
@ -113,7 +110,6 @@ export const NewConstraintAccordionList = forwardRef<
<StyledContainer id={constraintAccordionListId}>
<ConstraintsList>
{constraints.map((constraint, index) =>
addEditStrategy ? (
state.get(constraint)?.editing &&
Boolean(setConstraints) ? (
<EditableConstraint
@ -129,19 +125,6 @@ export const NewConstraintAccordionList = forwardRef<
key={constraint[constraintId]}
constraint={constraint}
/>
)
) : (
<NewConstraintAccordion
key={constraint[constraintId]}
constraint={constraint}
onEdit={onEdit?.bind(null, constraint)}
onCancel={onCancel.bind(null, index)}
onDelete={onRemove?.bind(null, index)}
onSave={onSave?.bind(null, index)}
onAutoSave={onAutoSave?.(constraint[constraintId])}
editing={Boolean(state.get(constraint)?.editing)}
compact
/>
),
)}
</ConstraintsList>

View File

@ -10,11 +10,9 @@ import {
type IConstraintAccordionListRef,
useConstraintAccordionList,
} from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { NewConstraintAccordionList } from 'component/common/NewConstraintAccordion/NewConstraintAccordionList/NewConstraintAccordionList';
import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
import { Limit } from 'component/common/Limit/Limit';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useUiFlag } from 'hooks/useUiFlag';
import { RecentlyUsedConstraints } from '../RecentlyUsedConstraints/RecentlyUsedConstraints.tsx';
interface IConstraintAccordionListProps {
@ -58,7 +56,6 @@ export const FeatureStrategyConstraintAccordionList = forwardRef<
ref as RefObject<IConstraintAccordionListRef>,
);
const { limit, limitReached } = useConstraintLimit(constraints.length);
const addEditStrategy = useUiFlag('addEditStrategy');
if (context.length === 0) {
return null;
@ -93,22 +90,13 @@ export const FeatureStrategyConstraintAccordionList = forwardRef<
}
/>
</StyledHelpIconBox>
{addEditStrategy ? (
setConstraints ? (
{setConstraints ? (
<EditableConstraintsList
ref={ref}
setConstraints={setConstraints}
constraints={constraints}
/>
) : null
) : (
<NewConstraintAccordionList
ref={ref}
setConstraints={setConstraints}
constraints={constraints}
state={state}
/>
)}
) : null}
<Box
sx={(theme) => ({
marginTop: theme.spacing(2),
@ -134,15 +122,10 @@ export const FeatureStrategyConstraintAccordionList = forwardRef<
>
Add constraint
</Button>
<ConditionallyRender
condition={Boolean(addEditStrategy)}
show={
<RecentlyUsedConstraints
setConstraints={setConstraints}
constraints={constraints}
/>
}
/>
</div>
}
/>

View File

@ -87,7 +87,6 @@ export const setupUiConfigEndpoint = () => {
environment: 'enterprise',
flags: {
newStrategyConfiguration: true,
addEditStrategy: true,
},
resourceLimits: {
featureEnvironmentStrategies: 2,

View File

@ -45,10 +45,8 @@ import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
import { Badge } from 'component/common/Badge/Badge';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import { UpgradeChangeRequests } from '../../FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests/UpgradeChangeRequests.tsx';
import { ConstraintSeparator } from 'component/common/ConstraintsList/ConstraintSeparator/ConstraintSeparator';
import { useUiFlag } from 'hooks/useUiFlag.ts';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
@ -211,7 +209,6 @@ export const FeatureStrategyForm = ({
environmentId,
);
const { strategyDefinition } = useStrategy(strategy?.name);
const addEditStrategy = useUiFlag('addEditStrategy');
useEffect(() => {
trackEvent('new-strategy-form', {
@ -350,35 +347,7 @@ export const FeatureStrategyForm = ({
<StyledHeaderBox>
<StyledTitle>
{formatStrategyName(strategy.name || '')}
<ConditionallyRender
condition={
!addEditStrategy &&
strategy.name === 'flexibleRollout'
}
show={
<Badge color='success' sx={{ marginLeft: '1rem' }}>
{strategy.parameters?.rollout}%
</Badge>
}
/>
</StyledTitle>
{foundEnvironment && !addEditStrategy ? (
<StyledEnvironmentBox>
<EnvironmentTypographyHeader>
Environment:
</EnvironmentTypographyHeader>
<EnvironmentIconBox>
<EnvironmentIcon
enabled={foundEnvironment.enabled}
/>{' '}
<EnvironmentTypography
enabled={foundEnvironment.enabled}
>
{foundEnvironment.name}
</EnvironmentTypography>
</EnvironmentIconBox>
</StyledEnvironmentBox>
) : null}
</StyledHeaderBox>
<StyledAlertBox>

View File

@ -10,7 +10,7 @@ import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { Box, styled, Typography } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { useUiFlag } from 'hooks/useUiFlag';
import { RecentlyUsedSegments } from './RecentlyUsedSegments/RecentlyUsedSegments.tsx';
interface IFeatureStrategySegmentProps {
@ -31,7 +31,6 @@ export const FeatureStrategySegment = ({
setSegments: setSelectedSegments,
projectId,
}: IFeatureStrategySegmentProps) => {
const addEditStrategy = useUiFlag('addEditStrategy');
const { segments: allSegments } = useSegments();
const { strategySegmentsLimit } = useSegmentLimits();
@ -99,7 +98,7 @@ export const FeatureStrategySegment = ({
options={autocompleteOptions}
onChange={onChange}
disabled={atStrategySegmentsLimit}
icon={addEditStrategy ? null : undefined}
icon={null}
width={'175px'}
/>
<FeatureStrategySegmentList

View File

@ -3,7 +3,6 @@ import { useRecentlyUsedSegments } from './useRecentlyUsedSegments.ts';
import type { ISegment } from 'interfaces/segment';
import { RecentlyUsedSegmentChip } from './RecentlyUsedSegmentChip.tsx';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useUiFlag } from 'hooks/useUiFlag';
type RecentlyUsedSegmentsProps = {
setSegments?: React.Dispatch<React.SetStateAction<ISegment[]>>;
@ -32,14 +31,7 @@ export const RecentlyUsedSegments = ({
}: RecentlyUsedSegmentsProps) => {
const { items: recentlyUsedSegmentIds } = useRecentlyUsedSegments();
const { segments: allSegments } = useSegments();
const addEditStrategyEnabled = useUiFlag('addEditStrategy');
if (
!addEditStrategyEnabled ||
recentlyUsedSegmentIds.length === 0 ||
!setSegments ||
!allSegments
) {
if (recentlyUsedSegmentIds.length === 0 || !setSegments || !allSegments) {
return null;
}

View File

@ -1,6 +1,4 @@
import RolloutSlider from './RolloutSlider.tsx';
import LegacyRolloutSlider from './LegacyRolloutSlider.tsx';
import { useUiFlag } from 'hooks/useUiFlag';
interface IRolloutSliderProps {
name: string;
@ -12,13 +10,7 @@ interface IRolloutSliderProps {
}
const ConditionalRolloutSlider = (props: IRolloutSliderProps) => {
const addEditStrategy = useUiFlag('addEditStrategy');
if (addEditStrategy) {
return <RolloutSlider {...props} />;
}
return <LegacyRolloutSlider {...props} />;
};
export default ConditionalRolloutSlider;

View File

@ -1,162 +0,0 @@
import { makeStyles, withStyles } from 'tss-react/mui';
import { Slider, Typography, Box, styled } from '@mui/material';
import { ROLLOUT_SLIDER_ID } from 'utils/testIds';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
const StyledSlider = withStyles(Slider, (theme) => ({
root: {
height: 8,
},
thumb: {
height: 24,
width: 24,
backgroundColor: theme.palette.background.paper,
border: '2px solid currentColor',
},
active: {},
valueLabel: {},
track: {
height: 8,
borderRadius: theme.shape.borderRadius,
},
rail: {
height: 8,
borderRadius: theme.shape.borderRadius,
},
}));
const StyledHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1),
}));
const StyledSubheader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1),
marginTop: theme.spacing(1),
}));
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginBottom: theme.spacing(1),
}));
const useStyles = makeStyles()((theme) => ({
slider: {
width: '100%',
maxWidth: '100%',
},
margin: {
height: theme.spacing(3),
},
}));
const marks = [
{
value: 0,
label: '0%',
},
{
value: 25,
label: '25%',
},
{
value: 50,
label: '50%',
},
{
value: 75,
label: '75%',
},
{
value: 100,
label: '100%',
},
];
interface IRolloutSliderProps {
name: string;
minLabel?: string;
maxLabel?: string;
value: number;
onChange: (e: Event, newValue: number | number[]) => void;
disabled?: boolean;
}
const LegacyRolloutSlider = ({
name,
value,
onChange,
disabled = false,
}: IRolloutSliderProps) => {
const { classes } = useStyles();
const valuetext = (value: number) => `${value}%`;
return (
<div className={classes.slider}>
<StyledBox>
<Typography id='discrete-slider-always'>{name}</Typography>
<HelpIcon
htmlTooltip
tooltip={
<Box>
<StyledHeader variant='h3'>
Rollout percentage
</StyledHeader>
<Typography variant='body2'>
The rollout percentage determines the proportion
of users exposed to a feature. It's based on the
MurmurHash of a user's unique identifier,
normalized to a number between 1 and 100. If the
normalized hash is less than or equal to the
rollout percentage, the user sees the feature.
This ensures a consistent, random distribution
of the feature among users.
</Typography>
<StyledSubheader variant='h3'>
Stickiness
</StyledSubheader>
<Typography variant='body2'>
Stickiness refers to the value used for hashing
to ensure a consistent user experience. It
determines the input for the MurmurHash,
ensuring that a user's feature exposure remains
consistent across sessions.
<br />
By default Unleash will use the first value
present in the context in the order of{' '}
<b>userId, sessionId and random</b>.
</Typography>
<StyledSubheader variant='h3'>
GroupId
</StyledSubheader>
<Typography variant='body2'>
The groupId is used as a seed for the hash
function, ensuring consistent feature exposure
across different feature flags for a uniform
user experience.
</Typography>
</Box>
}
/>
</StyledBox>
<StyledSlider
min={0}
max={100}
value={value}
getAriaValueText={valuetext}
aria-labelledby='discrete-slider-always'
step={1}
data-testid={ROLLOUT_SLIDER_ID}
marks={marks}
onChange={onChange}
valueLabelDisplay='on'
disabled={disabled}
/>
</div>
);
};
export default LegacyRolloutSlider;

View File

@ -49,7 +49,7 @@ const StyledActionsSmallScreen = styled('div')(({ theme }) => ({
export const InsightsHeader: VFC<DashboardHeaderProps> = ({ actions }) => {
const showInactiveUsers = useUiFlag('showInactiveUsers');
const pageName = useUiFlag('sideMenuCleanup') ? 'Analytics' : 'Insights';
const pageName = 'Analytics';
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));

View File

@ -9,7 +9,6 @@ import {
type IAutocompleteBoxOption,
} from 'component/common/AutocompleteBox/AutocompleteBox';
import { MilestoneStrategySegmentList } from './MilestoneStrategySegmentList.tsx';
import { useUiFlag } from 'hooks/useUiFlag';
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
@ -27,7 +26,6 @@ export const MilestoneStrategySegment = ({
segments: selectedSegments,
setSegments: setSelectedSegments,
}: IMilestoneStrategySegmentProps) => {
const addEditStrategy = useUiFlag('addEditStrategy');
const { segments: allSegments } = useSegments();
const { strategySegmentsLimit } = useSegmentLimits();
@ -90,7 +88,7 @@ export const MilestoneStrategySegment = ({
options={autocompleteOptions}
onChange={onChange}
disabled={atStrategySegmentsLimit}
icon={addEditStrategy ? null : undefined}
icon={null}
width={'175px'}
/>
<MilestoneStrategySegmentList

View File

@ -20,9 +20,7 @@ const setupRoutes = () => {
]);
testServerRoute(server, '/api/admin/ui-config', {
flags: {
addEditStrategy: true,
},
flags: {},
});
};

View File

@ -15,7 +15,6 @@ import {
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import type { IConstraint } from 'interfaces/strategy';
import { useNavigate } from 'react-router-dom';
import { ConstraintAccordionList } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
import type { IEditableConstraintsListRef } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
import type { SegmentFormStep, SegmentFormMode } from './SegmentForm.tsx';
@ -31,7 +30,6 @@ import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValues
import AccessContext from 'contexts/AccessContext';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { GO_BACK } from 'constants/navigate';
import { useUiFlag } from 'hooks/useUiFlag';
interface ISegmentFormPartTwoProps {
project?: string;
@ -122,7 +120,6 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
? [CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]
: [UPDATE_SEGMENT, UPDATE_PROJECT_SEGMENT];
const { segmentValuesLimit } = useSegmentLimits();
const addEditStrategy = useUiFlag('addEditStrategy');
const overSegmentValuesLimit: boolean = Boolean(
segmentValuesLimit && segmentValuesCount > segmentValuesLimit,
@ -202,25 +199,13 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
}
/>
<StyledConstraintContainer>
{addEditStrategy ? (
hasAccess(modePermission, project) && setConstraints ? (
{hasAccess(modePermission, project) && setConstraints ? (
<EditableConstraintsList
ref={constraintsAccordionListRef}
constraints={constraints}
setConstraints={setConstraints}
/>
) : null
) : (
<ConstraintAccordionList
ref={constraintsAccordionListRef}
constraints={constraints}
setConstraints={
hasAccess(modePermission, project)
? setConstraints
: undefined
}
/>
)}
) : null}
</StyledConstraintContainer>
</StyledForm>
<StyledButtonContainer>

View File

@ -6,7 +6,6 @@ import type {
import useAPI from '../useApi/useApi.js';
import { useRecentlyUsedConstraints } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/RecentlyUsedConstraints/useRecentlyUsedConstraints';
import { useRecentlyUsedSegments } from 'component/feature/FeatureStrategy/FeatureStrategySegment/RecentlyUsedSegments/useRecentlyUsedSegments';
import { useUiFlag } from 'hooks/useUiFlag';
const useFeatureStrategyApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
@ -16,7 +15,6 @@ const useFeatureStrategyApi = () => {
const { addItem: addToRecentlyUsedConstraints } =
useRecentlyUsedConstraints();
const { addItem: addToRecentlyUsedSegments } = useRecentlyUsedSegments();
const addEditStrategyEnabled = useUiFlag('addEditStrategy');
const addStrategyToFeature = async (
projectId: string,
@ -24,7 +22,6 @@ const useFeatureStrategyApi = () => {
environmentId: string,
payload: IFeatureStrategyPayload,
): Promise<IFeatureStrategy> => {
if (addEditStrategyEnabled) {
if (payload.constraints && payload.constraints.length > 0) {
addToRecentlyUsedConstraints(payload.constraints);
}
@ -32,7 +29,6 @@ const useFeatureStrategyApi = () => {
if (payload.segments && payload.segments.length > 0) {
addToRecentlyUsedSegments(payload.segments);
}
}
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies`;
const req = createRequest(
@ -67,7 +63,6 @@ const useFeatureStrategyApi = () => {
strategyId: string,
payload: IFeatureStrategyPayload,
): Promise<void> => {
if (addEditStrategyEnabled) {
if (payload.constraints && payload.constraints.length > 0) {
addToRecentlyUsedConstraints(payload.constraints);
}
@ -75,7 +70,6 @@ const useFeatureStrategyApi = () => {
if (payload.segments && payload.segments.length > 0) {
addToRecentlyUsedSegments(payload.segments);
}
}
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
const req = createRequest(

View File

@ -86,7 +86,6 @@ export type UiFlags = {
showUserDeviceCount?: boolean;
consumptionModel?: boolean;
edgeObservability?: boolean;
addEditStrategy?: boolean;
registerFrontendClient?: boolean;
customMetrics?: boolean;
lifecycleMetrics?: boolean;

View File

@ -56,7 +56,6 @@ export type IFlagKey =
| 'uniqueSdkTracking'
| 'consumptionModel'
| 'edgeObservability'
| 'addEditStrategy'
| 'registerFrontendClient'
| 'reportUnknownFlags'
| 'lastSeenBulkQuery'
@ -268,10 +267,6 @@ const flags: IFlags = {
process.env.EXPERIMENTAL_EDGE_OBSERVABILITY,
false,
),
addEditStrategy: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ADD_EDIT_STRATEGY,
false,
),
registerFrontendClient: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_REGISTER_FRONTEND_CLIENT,
false,

View File

@ -50,7 +50,6 @@ process.nextTick(async () => {
showUserDeviceCount: true,
deltaApi: true,
uniqueSdkTracking: true,
addEditStrategy: true,
strictSchemaValidation: true,
registerFrontendClient: true,
reportUnknownFlags: true,