1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00
This commit is contained in:
sjaanus 2023-10-24 09:56:55 +03:00
commit f27c4350b2
No known key found for this signature in database
GPG Key ID: 20E007C0248BA7FF
45 changed files with 295 additions and 325 deletions

View File

@ -23,6 +23,7 @@ interface IConstraintAccordionViewProps {
onEdit?: () => void; onEdit?: () => void;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
compact?: boolean; compact?: boolean;
disabled?: boolean;
renderAfter?: JSX.Element; renderAfter?: JSX.Element;
} }
@ -68,6 +69,7 @@ export const ConstraintAccordionView = ({
onDelete, onDelete,
sx = undefined, sx = undefined,
compact = false, compact = false,
disabled = false,
renderAfter, renderAfter,
}: IConstraintAccordionViewProps) => { }: IConstraintAccordionViewProps) => {
const [expandable, setExpandable] = useState(true); const [expandable, setExpandable] = useState(true);
@ -102,6 +104,7 @@ export const ConstraintAccordionView = ({
onDelete={onDelete} onDelete={onDelete}
singleValue={singleValue} singleValue={singleValue}
allowExpand={setExpandable} allowExpand={setExpandable}
disabled={disabled}
expanded={expanded} expanded={expanded}
compact={compact} compact={compact}
/> />

View File

@ -13,6 +13,7 @@ interface IConstraintAccordionViewHeaderProps {
expanded: boolean; expanded: boolean;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
compact?: boolean; compact?: boolean;
disabled?: boolean;
} }
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -34,6 +35,7 @@ export const ConstraintAccordionViewHeader = ({
allowExpand, allowExpand,
expanded, expanded,
compact, compact,
disabled,
}: IConstraintAccordionViewHeaderProps) => { }: IConstraintAccordionViewHeaderProps) => {
const { context } = useUnleashContext(); const { context } = useUnleashContext();
const { contextName } = constraint; const { contextName } = constraint;
@ -44,12 +46,13 @@ export const ConstraintAccordionViewHeader = ({
return ( return (
<StyledContainer> <StyledContainer>
<ConstraintIcon compact={compact} /> <ConstraintIcon compact={compact} disabled={disabled} />
<ConstraintAccordionViewHeaderInfo <ConstraintAccordionViewHeaderInfo
constraint={constraint} constraint={constraint}
singleValue={singleValue} singleValue={singleValue}
allowExpand={allowExpand} allowExpand={allowExpand}
expanded={expanded} expanded={expanded}
disabled={disabled}
/> />
<ConstraintAccordionHeaderActions <ConstraintAccordionHeaderActions
onEdit={onEdit} onEdit={onEdit}

View File

@ -50,6 +50,7 @@ interface ConstraintAccordionViewHeaderMetaInfoProps {
singleValue: boolean; singleValue: boolean;
expanded: boolean; expanded: boolean;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
disabled?: boolean;
maxLength?: number; maxLength?: number;
} }
@ -58,23 +59,34 @@ export const ConstraintAccordionViewHeaderInfo = ({
singleValue, singleValue,
allowExpand, allowExpand,
expanded, expanded,
disabled = false,
maxLength = 112, //The max number of characters in the values text for NOT allowing expansion maxLength = 112, //The max number of characters in the values text for NOT allowing expansion
}: ConstraintAccordionViewHeaderMetaInfoProps) => { }: ConstraintAccordionViewHeaderMetaInfoProps) => {
return ( return (
<StyledHeaderWrapper> <StyledHeaderWrapper>
<StyledHeaderMetaInfo> <StyledHeaderMetaInfo>
<Tooltip title={constraint.contextName} arrow> <Tooltip title={constraint.contextName} arrow>
<StyledHeaderText> <StyledHeaderText
sx={(theme) => ({
color: disabled
? theme.palette.text.secondary
: 'inherit',
})}
>
{constraint.contextName} {constraint.contextName}
</StyledHeaderText> </StyledHeaderText>
</Tooltip> </Tooltip>
<ConstraintViewHeaderOperator constraint={constraint} /> <ConstraintViewHeaderOperator
constraint={constraint}
disabled={disabled}
/>
<ConditionallyRender <ConditionallyRender
condition={singleValue} condition={singleValue}
show={ show={
<ConstraintAccordionViewHeaderSingleValue <ConstraintAccordionViewHeaderSingleValue
constraint={constraint} constraint={constraint}
allowExpand={allowExpand} allowExpand={allowExpand}
disabled={disabled}
/> />
} }
elseShow={ elseShow={
@ -83,6 +95,7 @@ export const ConstraintAccordionViewHeaderInfo = ({
expanded={expanded} expanded={expanded}
allowExpand={allowExpand} allowExpand={allowExpand}
maxLength={maxLength} maxLength={maxLength}
disabled={disabled}
/> />
} }
/> />

View File

@ -22,6 +22,7 @@ interface ConstraintSingleValueProps {
expanded: boolean; expanded: boolean;
maxLength: number; maxLength: number;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
disabled?: boolean;
} }
const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({
@ -55,6 +56,7 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({
expanded, expanded,
allowExpand, allowExpand,
maxLength, maxLength,
disabled = false,
}: ConstraintSingleValueProps) => { }: ConstraintSingleValueProps) => {
const [expandable, setExpandable] = useState(false); const [expandable, setExpandable] = useState(false);
@ -72,7 +74,15 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({
return ( return (
<StyledHeaderValuesContainerWrapper> <StyledHeaderValuesContainerWrapper>
<StyledHeaderValuesContainer> <StyledHeaderValuesContainer>
<StyledValuesSpan>{text}</StyledValuesSpan> <StyledValuesSpan
sx={(theme) => ({
color: disabled
? theme.palette.text.secondary
: 'inherit',
})}
>
{text}
</StyledValuesSpan>
<ConditionallyRender <ConditionallyRender
condition={expandable} condition={expandable}
show={ show={

View File

@ -15,6 +15,7 @@ const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
interface ConstraintSingleValueProps { interface ConstraintSingleValueProps {
constraint: IConstraint; constraint: IConstraint;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
disabled?: boolean;
} }
const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({
@ -26,6 +27,7 @@ const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({
export const ConstraintAccordionViewHeaderSingleValue = ({ export const ConstraintAccordionViewHeaderSingleValue = ({
constraint, constraint,
allowExpand, allowExpand,
disabled = false,
}: ConstraintSingleValueProps) => { }: ConstraintSingleValueProps) => {
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
@ -36,6 +38,9 @@ export const ConstraintAccordionViewHeaderSingleValue = ({
return ( return (
<StyledHeaderValuesContainerWrapper> <StyledHeaderValuesContainerWrapper>
<StyledSingleValueChip <StyledSingleValueChip
sx={(theme) => ({
color: disabled ? theme.palette.text.secondary : 'inherit',
})}
label={formatConstraintValue(constraint, locationSettings)} label={formatConstraintValue(constraint, locationSettings)}
/> />
</StyledHeaderValuesContainerWrapper> </StyledHeaderValuesContainerWrapper>

View File

@ -10,6 +10,7 @@ import { oneOf } from 'utils/oneOf';
interface ConstraintViewHeaderOperatorProps { interface ConstraintViewHeaderOperatorProps {
constraint: IConstraint; constraint: IConstraint;
disabled?: boolean;
} }
const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({
@ -28,6 +29,7 @@ const StyledHeaderConstraintContainer = styled('div')(({ theme }) => ({
export const ConstraintViewHeaderOperator = ({ export const ConstraintViewHeaderOperator = ({
constraint, constraint,
disabled = false,
}: ConstraintViewHeaderOperatorProps) => { }: ConstraintViewHeaderOperatorProps) => {
return ( return (
<StyledHeaderValuesContainerWrapper> <StyledHeaderValuesContainerWrapper>
@ -47,6 +49,7 @@ export const ConstraintViewHeaderOperator = ({
<ConstraintOperator <ConstraintOperator
constraint={constraint} constraint={constraint}
hasPrefix={Boolean(constraint.inverted)} hasPrefix={Boolean(constraint.inverted)}
disabled={disabled}
/> />
</StyledHeaderConstraintContainer> </StyledHeaderConstraintContainer>
<ConditionallyRender <ConditionallyRender

View File

@ -4,18 +4,24 @@ import { TrackChanges } from '@mui/icons-material';
interface IConstraintIconProps { interface IConstraintIconProps {
compact?: boolean; compact?: boolean;
disabled?: boolean;
} }
export const ConstraintIcon: VFC<IConstraintIconProps> = ({ compact }) => ( export const ConstraintIcon: VFC<IConstraintIconProps> = ({
compact,
disabled,
}) => (
<Box <Box
sx={{ sx={(theme) => ({
backgroundColor: 'primary.light', backgroundColor: disabled
? theme.palette.neutral.border
: 'primary.light',
p: compact ? '1px' : '2px', p: compact ? '1px' : '2px',
borderRadius: '50%', borderRadius: '50%',
width: compact ? '18px' : '24px', width: compact ? '18px' : '24px',
height: compact ? '18px' : '24px', height: compact ? '18px' : '24px',
marginRight: '13px', marginRight: '13px',
}} })}
> >
<TrackChanges <TrackChanges
sx={(theme) => ({ sx={(theme) => ({

View File

@ -6,6 +6,7 @@ import { styled } from '@mui/material';
interface IConstraintOperatorProps { interface IConstraintOperatorProps {
constraint: IConstraint; constraint: IConstraint;
hasPrefix?: boolean; hasPrefix?: boolean;
disabled?: boolean;
} }
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -15,19 +16,25 @@ const StyledContainer = styled('div')(({ theme }) => ({
lineHeight: 1.25, lineHeight: 1.25,
})); }));
const StyledName = styled('div')(({ theme }) => ({ const StyledName = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
lineHeight: 17 / 14, lineHeight: 17 / 14,
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
})); }));
const StyledText = styled('div')(({ theme }) => ({ const StyledText = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
color: theme.palette.neutral.main, color: disabled ? theme.palette.text.secondary : theme.palette.neutral.main,
})); }));
export const ConstraintOperator = ({ export const ConstraintOperator = ({
constraint, constraint,
hasPrefix, hasPrefix,
disabled = false,
}: IConstraintOperatorProps) => { }: IConstraintOperatorProps) => {
const operatorName = constraint.operator; const operatorName = constraint.operator;
const operatorText = formatOperatorDescription(constraint.operator); const operatorText = formatOperatorDescription(constraint.operator);
@ -40,8 +47,8 @@ export const ConstraintOperator = ({
paddingLeft: hasPrefix ? 0 : undefined, paddingLeft: hasPrefix ? 0 : undefined,
}} }}
> >
<StyledName>{operatorName}</StyledName> <StyledName disabled={disabled}>{operatorName}</StyledName>
<StyledText>{operatorText}</StyledText> <StyledText disabled={disabled}>{operatorText}</StyledText>
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -0,0 +1,46 @@
import { useTheme } from '@mui/material';
import { CSSProperties } from 'react';
interface IPercentageCircleProps {
percentage: number;
size?: `${number}rem`;
}
const PercentageCircle = ({
percentage,
size = '4rem',
}: IPercentageCircleProps) => {
const theme = useTheme();
const style: CSSProperties = {
display: 'block',
borderRadius: '100%',
transform: 'rotate(-90deg)',
height: size,
width: size,
background: theme.palette.background.elevation2,
};
// The percentage circle used to be drawn by CSS with a conic-gradient,
// but the result was either jagged or blurry. SVG seems to look better.
// See https://stackoverflow.com/a/70659532.
const radius = 100 / (2 * Math.PI);
const diameter = 2 * radius;
return (
<svg viewBox={`0 0 ${diameter} ${diameter}`} style={style} aria-hidden>
<title>A circle progress bar with {percentage}% completion.</title>
<circle
r={radius}
cx={radius}
cy={radius}
fill='none'
stroke={theme.palette.neutral.border}
strokeWidth={diameter}
strokeDasharray={`${percentage} 100`}
/>
</svg>
);
};
export default PercentageCircle;

View File

@ -16,6 +16,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
interface ISegmentItemProps { interface ISegmentItemProps {
segment: Partial<ISegment>; segment: Partial<ISegment>;
isExpanded?: boolean; isExpanded?: boolean;
disabled?: boolean;
constraintList?: JSX.Element; constraintList?: JSX.Element;
headerContent?: JSX.Element; headerContent?: JSX.Element;
} }
@ -49,20 +50,33 @@ const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'underline', textDecoration: 'underline',
}, },
})); }));
const StyledText = styled('span', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({
color: disabled ? theme.palette.text.secondary : 'inherit',
}));
export const SegmentItem: VFC<ISegmentItemProps> = ({ export const SegmentItem: VFC<ISegmentItemProps> = ({
segment, segment,
isExpanded, isExpanded,
headerContent, headerContent,
constraintList, constraintList,
disabled = false,
}) => { }) => {
const [isOpen, setIsOpen] = useState(isExpanded || false); const [isOpen, setIsOpen] = useState(isExpanded || false);
return ( return (
<StyledAccordion expanded={isOpen}> <StyledAccordion expanded={isOpen}>
<StyledAccordionSummary id={`segment-accordion-${segment.id}`}> <StyledAccordionSummary id={`segment-accordion-${segment.id}`}>
<DonutLarge color='secondary' sx={{ mr: 1 }} /> <DonutLarge
<span>Segment:</span> sx={(theme) => ({
mr: 1,
color: disabled
? theme.palette.neutral.border
: theme.palette.secondary.main,
})}
/>
<StyledText disabled={disabled}>Segment:</StyledText>
<StyledLink to={`/segments/edit/${segment.id}`}> <StyledLink to={`/segments/edit/${segment.id}`}>
{segment.name} {segment.name}
</StyledLink> </StyledLink>

View File

@ -9,8 +9,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintError } from './ConstraintError/ConstraintError';
import { ConstraintOk } from './ConstraintOk/ConstraintOk';
interface IConstraintExecutionWithoutResultsProps { interface IConstraintExecutionWithoutResultsProps {
constraints?: PlaygroundConstraintSchema[]; constraints?: PlaygroundConstraintSchema[];
@ -35,7 +33,11 @@ export const ConstraintExecutionWithoutResults: VFC<
condition={index > 0} condition={index > 0}
show={<StrategySeparator text='AND' />} show={<StrategySeparator text='AND' />}
/> />
<ConstraintAccordionView constraint={constraint} compact /> <ConstraintAccordionView
constraint={constraint}
compact
disabled
/>
</Fragment> </Fragment>
))} ))}
</ConstraintExecutionWrapper> </ConstraintExecutionWrapper>

View File

@ -52,6 +52,7 @@ export const DisabledStrategyExecution: VFC<IDisabledStrategyExecutionProps> =
parameters={parameters} parameters={parameters}
constraints={constraints} constraints={constraints}
input={input} input={input}
disabled
/> />
), ),
hasCustomStrategyParameters && ( hasCustomStrategyParameters && (
@ -61,9 +62,14 @@ export const DisabledStrategyExecution: VFC<IDisabledStrategyExecutionProps> =
/> />
), ),
name === 'default' && ( name === 'default' && (
<StyledBoxSummary sx={{ width: '100%' }}> <StyledBoxSummary
The standard strategy is <Badge color='success'>ON</Badge>{' '} sx={(theme) => ({
for all users. width: '100%',
color: theme.palette.text.secondary,
})}
>
The standard strategy is{' '}
<Badge color={'disabled'}>ON</Badge> for all users.
</StyledBoxSummary> </StyledBoxSummary>
), ),
].filter(Boolean); ].filter(Boolean);
@ -74,7 +80,12 @@ export const DisabledStrategyExecution: VFC<IDisabledStrategyExecutionProps> =
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation> // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<Fragment key={index}> <Fragment key={index}>
<ConditionallyRender <ConditionallyRender
condition={index > 0} condition={
index > 0 &&
(strategyResult.name === 'flexibleRollout'
? index < items.length
: index < items.length - 1)
}
show={<StrategySeparator text='AND' />} show={<StrategySeparator text='AND' />}
/> />
{item} {item}

View File

@ -8,6 +8,7 @@ interface IConstraintItemProps {
text: string; text: string;
input?: string | number | boolean | 'no value'; input?: string | number | boolean | 'no value';
showReason?: boolean; showReason?: boolean;
disabled?: boolean;
} }
const StyledDivContainer = styled('div', { const StyledDivContainer = styled('div', {
@ -34,12 +35,15 @@ const StyledChip = styled(Chip)(({ theme }) => ({
margin: theme.spacing(0.5), margin: theme.spacing(0.5),
})); }));
const StyledParagraph = styled('p')(({ theme }) => ({ const StyledParagraph = styled('p', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({
display: 'inline', display: 'inline',
margin: theme.spacing(0.5, 0), margin: theme.spacing(0.5, 0),
maxWidth: '95%', maxWidth: '95%',
textAlign: 'center', textAlign: 'center',
wordBreak: 'break-word', wordBreak: 'break-word',
color: disabled ? theme.palette.text.secondary : 'inherit',
})); }));
export const PlaygroundParameterItem = ({ export const PlaygroundParameterItem = ({
@ -47,10 +51,11 @@ export const PlaygroundParameterItem = ({
text, text,
input, input,
showReason = false, showReason = false,
disabled = false,
}: IConstraintItemProps) => { }: IConstraintItemProps) => {
const theme = useTheme(); const theme = useTheme();
const color = input === 'no value' ? 'error' : 'neutral'; const color = input === 'no value' && !disabled ? 'error' : 'neutral';
const reason = `value does not match any ${text}`; const reason = `value does not match any ${text}`;
return ( return (
@ -64,7 +69,11 @@ export const PlaygroundParameterItem = ({
show={ show={
<Typography <Typography
variant='subtitle1' variant='subtitle1'
color={theme.palette.error.main} color={
disabled
? theme.palette.text.secondary
: theme.palette.error.main
}
> >
{reason} {reason}
</Typography> </Typography>
@ -75,7 +84,7 @@ export const PlaygroundParameterItem = ({
show={<p>No {text}s added yet.</p>} show={<p>No {text}s added yet.</p>}
elseShow={ elseShow={
<div> <div>
<StyledParagraph> <StyledParagraph disabled={disabled}>
{value.length}{' '} {value.length}{' '}
{value.length > 1 ? `${text}s` : text} will get {value.length > 1 ? `${text}s` : text} will get
access. access.
@ -83,6 +92,7 @@ export const PlaygroundParameterItem = ({
{value.map((v: string | number) => ( {value.map((v: string | number) => (
<StyledChip <StyledChip
key={v} key={v}
disabled={disabled}
label={ label={
<StringTruncator <StringTruncator
maxWidth='300' maxWidth='300'
@ -98,7 +108,9 @@ export const PlaygroundParameterItem = ({
</StyledDivColumn> </StyledDivColumn>
<ConditionallyRender <ConditionallyRender
condition={Boolean(showReason)} condition={Boolean(showReason)}
show={<CancelOutlined color={'error'} />} show={
<CancelOutlined color={disabled ? 'disabled' : 'error'} />
}
elseShow={<div />} elseShow={<div />}
/> />
</StyledDivContainer> </StyledDivContainer>

View File

@ -29,6 +29,7 @@ export const SegmentExecutionWithoutResult: VFC<
/> />
} }
isExpanded isExpanded
disabled
/> />
<ConditionallyRender <ConditionallyRender
condition={ condition={

View File

@ -71,7 +71,12 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation> // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<Fragment key={index}> <Fragment key={index}>
<ConditionallyRender <ConditionallyRender
condition={index > 0} condition={
index > 0 &&
(strategyResult.name === 'flexibleRollout'
? index < items.length
: index < items.length - 1)
}
show={<StrategySeparator text='AND' />} show={<StrategySeparator text='AND' />}
/> />
{item} {item}

View File

@ -2,24 +2,34 @@ import {
parseParameterNumber, parseParameterNumber,
parseParameterStrings, parseParameterStrings,
} from 'utils/parseParameter'; } from 'utils/parseParameter';
import { Box } from '@mui/material'; import { Box, styled } from '@mui/material';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem'; import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem';
import { StyledBoxSummary } from '../StrategyExecution.styles'; import { StyledBoxSummary } from '../StrategyExecution.styles';
import { PlaygroundConstraintSchema, PlaygroundRequestSchema } from 'openapi'; import { PlaygroundConstraintSchema, PlaygroundRequestSchema } from 'openapi';
import { getMappedParam } from '../helpers'; import { getMappedParam } from '../helpers';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import DisabledPercentageCircle from 'component/common/PercentageCircle/DisabledPercentageCircle';
export interface PlaygroundResultStrategyExecutionParametersProps { export interface PlaygroundResultStrategyExecutionParametersProps {
parameters: { [key: string]: string }; parameters: { [key: string]: string };
constraints: PlaygroundConstraintSchema[]; constraints: PlaygroundConstraintSchema[];
input?: PlaygroundRequestSchema; input?: PlaygroundRequestSchema;
disabled?: boolean;
} }
const StyledText = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({
color: disabled ? theme.palette.text.secondary : theme.palette.neutral.main,
}));
export const PlaygroundResultStrategyExecutionParameters = ({ export const PlaygroundResultStrategyExecutionParameters = ({
parameters, parameters,
constraints, constraints,
input, input,
disabled = false,
}: PlaygroundResultStrategyExecutionParametersProps) => { }: PlaygroundResultStrategyExecutionParametersProps) => {
return ( return (
<> <>
@ -35,20 +45,44 @@ export const PlaygroundResultStrategyExecutionParameters = ({
key={key} key={key}
sx={{ display: 'flex', alignItems: 'center' }} sx={{ display: 'flex', alignItems: 'center' }}
> >
<Box sx={{ mr: '1rem' }}> <Box
sx={(theme) => ({
mr: '1rem',
color: disabled
? theme.palette.neutral.border
: theme.palette.text.secondary,
})}
>
<ConditionallyRender
condition={disabled}
show={
<DisabledPercentageCircle
percentage={percentage}
size='2rem'
/>
}
elseShow={
<PercentageCircle <PercentageCircle
percentage={percentage} percentage={percentage}
size='2rem' size='2rem'
/> />
}
/>
</Box> </Box>
<div> <StyledText disabled={disabled}>
<Badge color='success'>{percentage}%</Badge>{' '} <Badge
color={
disabled ? 'disabled' : 'success'
}
>
{percentage}%
</Badge>{' '}
of your base{' '} of your base{' '}
{constraints.length > 0 {constraints.length > 0
? 'who match constraints' ? 'who match constraints'
: ''}{' '} : ''}{' '}
is included. is included.
</div> </StyledText>
</StyledBoxSummary> </StyledBoxSummary>
); );
} }
@ -87,6 +121,7 @@ export const PlaygroundResultStrategyExecutionParameters = ({
text={'host'} text={'host'}
input={'no value'} input={'no value'}
showReason={undefined} showReason={undefined}
disabled={disabled}
/> />
); );
} }
@ -97,6 +132,7 @@ export const PlaygroundResultStrategyExecutionParameters = ({
key={key} key={key}
value={IPs} value={IPs}
text={'IP'} text={'IP'}
disabled={disabled}
input={ input={
input?.context?.[getMappedParam(key)] input?.context?.[getMappedParam(key)]
? input?.context?.[getMappedParam(key)] ? input?.context?.[getMappedParam(key)]

View File

@ -441,29 +441,12 @@ export const ProjectAccessAssign = ({
Select the role to assign for this project Select the role to assign for this project
</StyledInputDescription> </StyledInputDescription>
<StyledAutocompleteWrapper> <StyledAutocompleteWrapper>
<ConditionallyRender
condition={Boolean(
uiConfig.flags.multipleRoles,
)}
show={() => (
<MultipleRoleSelect <MultipleRoleSelect
data-testid={PA_ROLE_ID} data-testid={PA_ROLE_ID}
roles={roles} roles={roles}
value={selectedRoles} value={selectedRoles}
setValue={setRoles} setValue={setRoles}
/> />
)}
elseShow={() => (
<RoleSelect
data-testid={PA_ROLE_ID}
roles={roles}
value={selectedRoles[0]}
setValue={(role) =>
setRoles(role ? [role] : [])
}
/>
)}
/>
</StyledAutocompleteWrapper> </StyledAutocompleteWrapper>
</div> </div>

View File

@ -106,7 +106,6 @@ exports[`should create default config 1`] = `
}, },
}, },
"migrationLock": true, "migrationLock": true,
"multipleRoles": false,
"personalAccessTokensKillSwitch": false, "personalAccessTokensKillSwitch": false,
"playgroundImprovements": false, "playgroundImprovements": false,
"privateProjects": false, "privateProjects": false,
@ -151,7 +150,6 @@ exports[`should create default config 1`] = `
}, },
}, },
"migrationLock": true, "migrationLock": true,
"multipleRoles": false,
"personalAccessTokensKillSwitch": false, "personalAccessTokensKillSwitch": false,
"playgroundImprovements": false, "playgroundImprovements": false,
"privateProjects": false, "privateProjects": false,

View File

@ -51,8 +51,6 @@ export default class ClientInstanceStore implements IClientInstanceStore {
private metricTimer: Function; private metricTimer: Function;
private timer: Timeout;
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
this.db = db; this.db = db;
this.eventBus = eventBus; this.eventBus = eventBus;
@ -197,7 +195,5 @@ export default class ClientInstanceStore implements IClientInstanceStore {
return this.db(TABLE).where('app_name', appName).del(); return this.db(TABLE).where('app_name', appName).del();
} }
destroy(): void { destroy(): void {}
clearInterval(this.timer);
}
} }

View File

@ -24,9 +24,6 @@ async function getSetup() {
base, base,
clientFeatureToggleStore: stores.clientFeatureToggleStore, clientFeatureToggleStore: stores.clientFeatureToggleStore,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
@ -43,7 +40,6 @@ const callGetAll = async (controller: FeatureController) => {
let base; let base;
let request; let request;
let destroy;
let flagResolver; let flagResolver;
@ -51,7 +47,6 @@ beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
request = setup.request; request = setup.request;
destroy = setup.destroy;
flagResolver = { flagResolver = {
isEnabled: () => { isEnabled: () => {
return false; return false;
@ -59,10 +54,6 @@ beforeEach(async () => {
}; };
}); });
afterEach(() => {
destroy();
});
test('should get empty getFeatures via client', () => { test('should get empty getFeatures via client', () => {
expect.assertions(1); expect.assertions(1);
return request return request

View File

@ -4,7 +4,12 @@ import { createTestConfig } from '../../test/config/test-config';
import FakeEventStore from '../../test/fixtures/fake-event-store'; import FakeEventStore from '../../test/fixtures/fake-event-store';
import { randomId } from '../util/random-id'; import { randomId } from '../util/random-id';
import FakeProjectStore from '../../test/fixtures/fake-project-store'; import FakeProjectStore from '../../test/fixtures/fake-project-store';
import { EventService, ProxyService, SettingService } from '../../lib/services'; import {
EventService,
ProxyService,
SchedulerService,
SettingService,
} from '../../lib/services';
import { ISettingStore } from '../../lib/types'; import { ISettingStore } from '../../lib/types';
import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings'; import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings';
import { minutesToMilliseconds } from 'date-fns'; import { minutesToMilliseconds } from 'date-fns';
@ -55,7 +60,6 @@ test('corsOriginMiddleware origin validation', async () => {
userName, userName,
), ),
).rejects.toThrow('Invalid origin: a'); ).rejects.toThrow('Invalid origin: a');
proxyService.destroy();
}); });
test('corsOriginMiddleware without config', async () => { test('corsOriginMiddleware without config', async () => {
@ -82,7 +86,6 @@ test('corsOriginMiddleware without config', async () => {
expect(await proxyService.getFrontendSettings(false)).toEqual({ expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: [], frontendApiOrigins: [],
}); });
proxyService.destroy();
}); });
test('corsOriginMiddleware with config', async () => { test('corsOriginMiddleware with config', async () => {
@ -109,12 +112,9 @@ test('corsOriginMiddleware with config', async () => {
expect(await proxyService.getFrontendSettings(false)).toEqual({ expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: ['*'], frontendApiOrigins: ['*'],
}); });
proxyService.destroy();
}); });
test('corsOriginMiddleware with caching enabled', async () => { test('corsOriginMiddleware with caching enabled', async () => {
jest.useFakeTimers();
const { proxyService } = createSettingService([]); const { proxyService } = createSettingService([]);
const userName = randomId(); const userName = randomId();
@ -133,24 +133,11 @@ test('corsOriginMiddleware with caching enabled', async () => {
frontendApiOrigins: [], frontendApiOrigins: [],
}); });
jest.advanceTimersByTime(minutesToMilliseconds(2)); await proxyService.fetchFrontendSettings(); // called by the scheduler service
jest.useRealTimers();
/*
This is needed because it is not enough to fake time to test the
updated cache, we also need to make sure that all promises are
executed and completed, in the right order.
*/
await new Promise<void>((resolve) =>
process.nextTick(async () => {
const settings = await proxyService.getFrontendSettings(); const settings = await proxyService.getFrontendSettings();
expect(settings).toEqual({ expect(settings).toEqual({
frontendApiOrigins: ['*'], frontendApiOrigins: ['*'],
}); });
resolve();
}),
);
proxyService.destroy();
}); });

View File

@ -28,25 +28,16 @@ async function getSetup() {
return { return {
base, base,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let base; let base;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
base = setup.base; base = setup.base;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('should get ui config', async () => { test('should get ui config', async () => {

View File

@ -20,25 +20,16 @@ async function getSetup() {
return { return {
base, base,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let base; let base;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(async () => {
await destroy();
}); });
test('should get all context definitions', () => { test('should get all context definitions', () => {

View File

@ -19,25 +19,16 @@ async function getSetup() {
stores, stores,
perms, perms,
config, config,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('/api/admin/metrics/seen-toggles is deprecated', () => { test('/api/admin/metrics/seen-toggles is deprecated', () => {

View File

@ -33,16 +33,11 @@ describe('Public Signup API', () => {
request: supertest(app), request: supertest(app),
stores, stores,
perms, perms,
destroy: () => {
services.clientInstanceService.destroy();
services.publicSignupTokenService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
const user = { const user = {
username: 'some-username', username: 'some-username',
@ -55,12 +50,8 @@ describe('Public Signup API', () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => {
destroy();
});
const expireAt = (addDays: number = 7): Date => { const expireAt = (addDays: number = 7): Date => {
const now = new Date(); const now = new Date();
now.setDate(now.getDate() + addDays); now.setDate(now.getDate() + addDays);

View File

@ -5,8 +5,6 @@ import permissions from '../../../test/fixtures/permissions';
import getApp from '../../app'; import getApp from '../../app';
import { createServices } from '../../services'; import { createServices } from '../../services';
let destroy;
async function getSetup() { async function getSetup() {
const randomBase = `/random${Math.round(Math.random() * 1000)}`; const randomBase = `/random${Math.round(Math.random() * 1000)}`;
const perms = permissions(); const perms = permissions();
@ -18,10 +16,6 @@ async function getSetup() {
const services = createServices(stores, config); const services = createServices(stores, config);
const app = await getApp(config, stores, services); const app = await getApp(config, stores, services);
destroy = () => {
services.clientInstanceService.destroy();
};
return { return {
base: randomBase, base: randomBase,
strategyStore: stores.strategyStore, strategyStore: stores.strategyStore,
@ -30,10 +24,6 @@ async function getSetup() {
}; };
} }
afterEach(() => {
destroy();
});
test('add version numbers for /strategies', async () => { test('add version numbers for /strategies', async () => {
const { request, base } = await getSetup(); const { request, base } = await getSetup();
return request return request

View File

@ -21,26 +21,18 @@ async function getSetup() {
perms, perms,
tagStore: stores.tagStore, tagStore: stores.tagStore,
request: supertest(app), request: supertest(app),
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let base; let base;
let tagStore; let tagStore;
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
base = setup.base; base = setup.base;
tagStore = setup.tagStore; tagStore = setup.tagStore;
request = setup.request; request = setup.request;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
}); });
test('should get empty getTags via admin', () => { test('should get empty getTags via admin', () => {

View File

@ -19,5 +19,4 @@ test('should enable prometheus', async () => {
.get('/internal-backstage/prometheus') .get('/internal-backstage/prometheus')
.expect('Content-Type', /text/) .expect('Content-Type', /text/)
.expect(200); .expect(200);
services.clientInstanceService.destroy();
}); });

View File

@ -19,10 +19,7 @@ async function getSetup(opts?: IUnleashOptions) {
request: supertest(app), request: supertest(app),
stores: db.stores, stores: db.stores,
services, services,
destroy: async () => { destroy: db.destroy,
services.clientInstanceService.destroy();
await db.destroy();
},
}; };
} }
@ -31,7 +28,7 @@ let stores: IUnleashStores;
let services: IUnleashServices; let services: IUnleashServices;
let destroy; let destroy;
beforeEach(async () => { beforeAll(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
stores = setup.stores; stores = setup.stores;
@ -39,10 +36,14 @@ beforeEach(async () => {
services = setup.services; services = setup.services;
}); });
afterEach(() => { afterAll(() => {
destroy(); destroy();
}); });
afterEach(async () => {
await stores.featureToggleStore.deleteAll();
});
test('should validate client metrics', () => { test('should validate client metrics', () => {
return request return request
.post('/api/client/metrics') .post('/api/client/metrics')

View File

@ -14,20 +14,14 @@ async function getSetup() {
return { return {
request: supertest(app), request: supertest(app),
stores, stores,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => { afterEach(() => {
destroy();
getLogger.setMuteError(false); getLogger.setMuteError(false);
}); });

View File

@ -15,21 +15,15 @@ async function getSetup() {
return { return {
request: supertest(app), request: supertest(app),
stores, stores,
destroy: () => {
services.clientInstanceService.destroy();
},
}; };
} }
let request; let request;
let destroy;
beforeEach(async () => { beforeEach(async () => {
const setup = await getSetup(); const setup = await getSetup();
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => { afterEach(() => {
destroy();
getLogger.setMuteError(false); getLogger.setMuteError(false);
}); });

View File

@ -39,16 +39,11 @@ describe('Public Signup API', () => {
request: supertest(app), request: supertest(app),
stores, stores,
perms, perms,
destroy: () => {
services.clientInstanceService.destroy();
services.publicSignupTokenService.destroy();
},
}; };
} }
let stores; let stores;
let request; let request;
let destroy;
const user = { const user = {
username: 'some-username', username: 'some-username',
@ -61,12 +56,8 @@ describe('Public Signup API', () => {
const setup = await getSetup(); const setup = await getSetup();
stores = setup.stores; stores = setup.stores;
request = setup.request; request = setup.request;
destroy = setup.destroy;
}); });
afterEach(() => {
destroy();
});
const expireAt = (addDays: number = 7): Date => { const expireAt = (addDays: number = 7): Date => {
const now = new Date(); const now = new Date();
now.setDate(now.getDate() + addDays); now.setDate(now.getDate() + addDays);

View File

@ -58,9 +58,6 @@ async function createApp(
} }
services.schedulerService.stop(); services.schedulerService.stop();
metricsMonitor.stopMonitoring(); metricsMonitor.stopMonitoring();
stores.clientInstanceStore.destroy();
services.clientMetricsServiceV2.destroy();
services.proxyService.destroy();
services.addonService.destroy(); services.addonService.destroy();
await db.destroy(); await db.destroy();
}; };

View File

@ -5,45 +5,11 @@ import FakeEventStore from '../../../test/fixtures/fake-event-store';
import { createTestConfig } from '../../../test/config/test-config'; import { createTestConfig } from '../../../test/config/test-config';
import { FakePrivateProjectChecker } from '../../features/private-project/fakePrivateProjectChecker'; import { FakePrivateProjectChecker } from '../../features/private-project/fakePrivateProjectChecker';
/**
* A utility to wait for any pending promises in the test subject code.
* For instance, if the test needs to wait for a timeout/interval handler,
* and that handler does something async, advancing the timers is not enough:
* We have to explicitly wait for the second promise.
* For more info, see https://stackoverflow.com/a/51045733/2868829
*
* Usage in test code after advancing timers, but before making assertions:
*
* test('hello', async () => {
* jest.useFakeTimers('modern');
*
* // Schedule a timeout with a callback that does something async
* // before calling our spy
* const spy = jest.fn();
* setTimeout(async () => {
* await Promise.resolve();
* spy();
* }, 1000);
*
* expect(spy).not.toHaveBeenCalled();
*
* jest.advanceTimersByTime(1500);
* await flushPromises(); // this is required to make it work!
*
* expect(spy).toHaveBeenCalledTimes(1);
*
* jest.useRealTimers();
* });
*/
function flushPromises() {
return Promise.resolve(setImmediate);
}
let config; let config;
beforeAll(() => { beforeAll(() => {
config = createTestConfig({}); config = createTestConfig({});
}); });
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => { test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -75,8 +41,8 @@ test('Multiple registrations of same appname and instanceid within same time per
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(7000);
await flushPromises(); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
expect(appStoreSpy).toHaveBeenCalledTimes(1); expect(appStoreSpy).toHaveBeenCalledTimes(1);
expect(bulkSpy).toHaveBeenCalledTimes(1); expect(bulkSpy).toHaveBeenCalledTimes(1);
@ -93,7 +59,6 @@ test('Multiple registrations of same appname and instanceid within same time per
}); });
test('Multiple unique clients causes multiple registrations', async () => { test('Multiple unique clients causes multiple registrations', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -136,16 +101,14 @@ test('Multiple unique clients causes multiple registrations', async () => {
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
jest.advanceTimersByTime(7000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await flushPromises();
const registrations = appStoreSpy.mock.calls[0][0]; const registrations = appStoreSpy.mock.calls[0][0];
expect(registrations.length).toBe(2); expect(registrations.length).toBe(2);
jest.useRealTimers();
}); });
test('Same client registered outside of dedup interval will be registered twice', async () => { test('Same client registered outside of dedup interval will be registered twice', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -155,8 +118,6 @@ test('Same client registered outside of dedup interval will be registered twice'
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
const bulkInterval = secondsToMilliseconds(2);
const clientMetrics = new ClientInstanceService( const clientMetrics = new ClientInstanceService(
{ {
clientMetricsStoreV2: null, clientMetricsStoreV2: null,
@ -168,7 +129,6 @@ test('Same client registered outside of dedup interval will be registered twice'
}, },
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
bulkInterval,
); );
const client1 = { const client1 = {
appName: 'test_app', appName: 'test_app',
@ -181,14 +141,13 @@ test('Same client registered outside of dedup interval will be registered twice'
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3000); await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
await flushPromises();
expect(appStoreSpy).toHaveBeenCalledTimes(2); expect(appStoreSpy).toHaveBeenCalledTimes(2);
expect(bulkSpy).toHaveBeenCalledTimes(2); expect(bulkSpy).toHaveBeenCalledTimes(2);
@ -198,11 +157,9 @@ test('Same client registered outside of dedup interval will be registered twice'
expect(firstRegistrations.appName).toBe(secondRegistrations.appName); expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId); expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
jest.useRealTimers();
}); });
test('No registrations during a time period will not call stores', async () => { test('No registrations during a time period will not call stores', async () => {
jest.useFakeTimers();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore: any = { const clientApplicationsStore: any = {
@ -211,7 +168,7 @@ test('No registrations during a time period will not call stores', async () => {
const clientInstanceStore: any = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
new ClientInstanceService( const clientMetrics = new ClientInstanceService(
{ {
clientMetricsStoreV2: null, clientMetricsStoreV2: null,
strategyStore: null, strategyStore: null,
@ -223,8 +180,9 @@ test('No registrations during a time period will not call stores', async () => {
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
); );
jest.advanceTimersByTime(6000);
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
expect(appStoreSpy).toHaveBeenCalledTimes(0); expect(appStoreSpy).toHaveBeenCalledTimes(0);
expect(bulkSpy).toHaveBeenCalledTimes(0); expect(bulkSpy).toHaveBeenCalledTimes(0);
jest.useRealTimers();
}); });

View File

@ -29,8 +29,6 @@ export default class ClientInstanceService {
seenClients: Record<string, IClientApp> = {}; seenClients: Record<string, IClientApp> = {};
private timers: NodeJS.Timeout[] = [];
private clientMetricsStoreV2: IClientMetricsStoreV2; private clientMetricsStoreV2: IClientMetricsStoreV2;
private strategyStore: IStrategyStore; private strategyStore: IStrategyStore;
@ -47,10 +45,6 @@ export default class ClientInstanceService {
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
private bulkInterval: number;
private announcementInterval: number;
constructor( constructor(
{ {
clientMetricsStoreV2, clientMetricsStoreV2,
@ -73,8 +67,6 @@ export default class ClientInstanceService {
flagResolver, flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>, }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
privateProjectChecker: IPrivateProjectChecker, privateProjectChecker: IPrivateProjectChecker,
bulkInterval = secondsToMilliseconds(5),
announcementInterval = minutesToMilliseconds(5),
) { ) {
this.clientMetricsStoreV2 = clientMetricsStoreV2; this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.strategyStore = strategyStore; this.strategyStore = strategyStore;
@ -87,18 +79,6 @@ export default class ClientInstanceService {
this.logger = getLogger( this.logger = getLogger(
'/services/client-metrics/client-instance-service.ts', '/services/client-metrics/client-instance-service.ts',
); );
this.bulkInterval = bulkInterval;
this.announcementInterval = announcementInterval;
this.timers.push(
setInterval(() => this.bulkAdd(), this.bulkInterval).unref(),
);
this.timers.push(
setInterval(
() => this.announceUnannounced(),
this.announcementInterval,
).unref(),
);
} }
public async registerInstance( public async registerInstance(
@ -248,8 +228,4 @@ export default class ClientInstanceService {
async removeInstancesOlderThanTwoDays(): Promise<void> { async removeInstancesOlderThanTwoDays(): Promise<void> {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays(); return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
} }
destroy(): void {
this.timers.forEach(clearInterval);
}
} }

View File

@ -11,8 +11,6 @@ export type LastSeenInput = {
}; };
export class LastSeenService { export class LastSeenService {
private timers: NodeJS.Timeout[] = [];
private lastSeenToggles: Map<String, LastSeenInput> = new Map(); private lastSeenToggles: Map<String, LastSeenInput> = new Map();
private logger: Logger; private logger: Logger;
@ -79,8 +77,4 @@ export class LastSeenService {
async cleanLastSeen() { async cleanLastSeen() {
await this.lastSeenStore.cleanLastSeen(); await this.lastSeenStore.cleanLastSeen();
} }
destroy(): void {
this.timers.forEach(clearInterval);
}
} }

View File

@ -25,8 +25,6 @@ import { nameSchema } from '../../schema/feature-schema';
export default class ClientMetricsServiceV2 { export default class ClientMetricsServiceV2 {
private config: IUnleashConfig; private config: IUnleashConfig;
private timers: NodeJS.Timeout[] = [];
private unsavedMetrics: IClientMetricsEnv[] = []; private unsavedMetrics: IClientMetricsEnv[] = [];
private clientMetricsStoreV2: IClientMetricsStoreV2; private clientMetricsStoreV2: IClientMetricsStoreV2;
@ -41,7 +39,6 @@ export default class ClientMetricsServiceV2 {
{ clientMetricsStoreV2 }: Pick<IUnleashStores, 'clientMetricsStoreV2'>, { clientMetricsStoreV2 }: Pick<IUnleashStores, 'clientMetricsStoreV2'>,
config: IUnleashConfig, config: IUnleashConfig,
lastSeenService: LastSeenService, lastSeenService: LastSeenService,
bulkInterval = secondsToMilliseconds(5),
) { ) {
this.clientMetricsStoreV2 = clientMetricsStoreV2; this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.lastSeenService = lastSeenService; this.lastSeenService = lastSeenService;
@ -50,18 +47,10 @@ export default class ClientMetricsServiceV2 {
'/services/client-metrics/client-metrics-service-v2.ts', '/services/client-metrics/client-metrics-service-v2.ts',
); );
this.flagResolver = config.flagResolver; this.flagResolver = config.flagResolver;
}
this.timers.push( async clearMetrics(hoursAgo: number) {
setInterval(() => { return this.clientMetricsStoreV2.clearMetrics(hoursAgo);
this.bulkAdd().catch(console.error);
}, bulkInterval).unref(),
);
this.timers.push(
setInterval(() => {
this.clientMetricsStoreV2.clearMetrics(48).catch(console.error);
}, hoursToMilliseconds(12)).unref(),
);
} }
async filterValidToggleNames(toggleNames: string[]): Promise<string[]> { async filterValidToggleNames(toggleNames: string[]): Promise<string[]> {
@ -245,9 +234,4 @@ export default class ClientMetricsServiceV2 {
} }
return 'default'; return 'default';
} }
destroy(): void {
this.timers.forEach(clearInterval);
this.lastSeenService.destroy();
}
} }

View File

@ -118,6 +118,8 @@ export const scheduleServices = async (
featureToggleService, featureToggleService,
versionService, versionService,
lastSeenService, lastSeenService,
proxyService,
clientMetricsServiceV2,
} = services; } = services;
if (await maintenanceService.isMaintenanceMode()) { if (await maintenanceService.isMaintenanceMode()) {
@ -164,6 +166,18 @@ export const scheduleServices = async (
'removeInstancesOlderThanTwoDays', 'removeInstancesOlderThanTwoDays',
); );
schedulerService.schedule(
clientInstanceService.bulkAdd.bind(clientInstanceService),
secondsToMilliseconds(5),
'bulkAddInstances',
);
schedulerService.schedule(
clientInstanceService.announceUnannounced.bind(clientInstanceService),
minutesToMilliseconds(5),
'announceUnannounced',
);
schedulerService.schedule( schedulerService.schedule(
projectService.statusJob.bind(projectService), projectService.statusJob.bind(projectService),
hoursToMilliseconds(24), hoursToMilliseconds(24),
@ -205,6 +219,28 @@ export const scheduleServices = async (
hoursToMilliseconds(48), hoursToMilliseconds(48),
'checkLatestVersion', 'checkLatestVersion',
); );
schedulerService.schedule(
proxyService.fetchFrontendSettings.bind(proxyService),
minutesToMilliseconds(2),
'fetchFrontendSettings',
);
schedulerService.schedule(
() => {
clientMetricsServiceV2.bulkAdd().catch(console.error);
},
secondsToMilliseconds(5),
'bulkAddMetrics',
);
schedulerService.schedule(
() => {
clientMetricsServiceV2.clearMetrics(48).catch(console.error);
},
hoursToMilliseconds(12),
'clearMetrics',
);
}; };
export const createServices = ( export const createServices = (

View File

@ -53,18 +53,11 @@ export class ProxyService {
private cachedFrontendSettings?: FrontendSettings; private cachedFrontendSettings?: FrontendSettings;
private timer: NodeJS.Timeout | null;
constructor(config: Config, stores: Stores, services: Services) { constructor(config: Config, stores: Stores, services: Services) {
this.config = config; this.config = config;
this.logger = config.getLogger('services/proxy-service.ts'); this.logger = config.getLogger('services/proxy-service.ts');
this.stores = stores; this.stores = stores;
this.services = services; this.services = services;
this.timer = setInterval(
() => this.fetchFrontendSettings(),
minutesToMilliseconds(2),
).unref();
} }
async getProxyFeatures( async getProxyFeatures(
@ -181,7 +174,7 @@ export class ProxyService {
); );
} }
private async fetchFrontendSettings(): Promise<FrontendSettings> { async fetchFrontendSettings(): Promise<FrontendSettings> {
try { try {
this.cachedFrontendSettings = this.cachedFrontendSettings =
await this.services.settingService.get(frontendSettingsKey, { await this.services.settingService.get(frontendSettingsKey, {
@ -201,11 +194,4 @@ export class ProxyService {
} }
return this.fetchFrontendSettings(); return this.fetchFrontendSettings();
} }
destroy(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
} }

View File

@ -30,8 +30,6 @@ export class PublicSignupTokenService {
private logger: Logger; private logger: Logger;
private timer: NodeJS.Timeout;
private readonly unleashBase: string; private readonly unleashBase: string;
constructor( constructor(
@ -146,9 +144,4 @@ export class PublicSignupTokenService {
private getMinimumDate(date1: Date, date2: Date): Date { private getMinimumDate(date1: Date, date2: Date): Date {
return date1 < date2 ? date1 : date2; return date1 < date2 ? date1 : date2;
} }
destroy(): void {
clearInterval(this.timer);
this.timer = null;
}
} }

View File

@ -23,7 +23,6 @@ export type IFlagKey =
| 'filterInvalidClientMetrics' | 'filterInvalidClientMetrics'
| 'lastSeenByEnvironment' | 'lastSeenByEnvironment'
| 'customRootRolesKillSwitch' | 'customRootRolesKillSwitch'
| 'multipleRoles'
| 'featureNamingPattern' | 'featureNamingPattern'
| 'doraMetrics' | 'doraMetrics'
| 'variantTypeNumber' | 'variantTypeNumber'
@ -117,10 +116,6 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH, process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH,
false, false,
), ),
multipleRoles: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_MULTIPLE_ROLES,
false,
),
featureNamingPattern: parseEnvVarBoolean( featureNamingPattern: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_NAMING_PATTERN, process.env.UNLEASH_EXPERIMENTAL_FEATURE_NAMING_PATTERN,
false, false,

View File

@ -117,9 +117,7 @@ export default async function init(
await setupDatabase(stores); await setupDatabase(stores);
}, },
destroy: async () => { destroy: async () => {
const { clientInstanceStore } = stores;
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
clientInstanceStore.destroy();
testDb.destroy((error) => (error ? reject(error) : resolve())); testDb.destroy((error) => (error ? reject(error) : resolve()));
}); });
}, },

View File

@ -1,9 +1,12 @@
import { getDbConfig } from './helpers/database-config'; import { getDbConfig } from './helpers/database-config';
import { createTestConfig } from '../config/test-config'; import { createTestConfig } from '../config/test-config';
import { getInstance } from 'db-migrate'; import { getInstance } from 'db-migrate';
import { log } from 'db-migrate-shared';
import { Client } from 'pg'; import { Client } from 'pg';
import { IDBOption } from 'lib/types'; import { IDBOption } from 'lib/types';
log.setLogLevel('error');
async function initSchema(db: IDBOption): Promise<void> { async function initSchema(db: IDBOption): Promise<void> {
const client = new Client(db); const client = new Client(db);
await client.connect(); await client.connect();
@ -30,6 +33,8 @@ test('Up & down migrations work', async () => {
connectionTimeoutMillis: 2000, connectionTimeoutMillis: 2000,
}; };
// disable Intellij/WebStorm from setting verbose CLI argument to db-migrator
process.argv = process.argv.filter((it) => !it.includes('--verbose'));
const dbm = getInstance(true, { const dbm = getInstance(true, {
cwd: `${__dirname}/../../`, // relative to src/test/e2e cwd: `${__dirname}/../../`, // relative to src/test/e2e
config: { e2e }, config: { e2e },

View File

@ -12,7 +12,7 @@ const { APPLICATION_CREATED } = require('../../../lib/types/events');
let stores; let stores;
let db; let db;
let clientInstanceService; let clientInstanceService: ClientInstanceService;
let config: IUnleashConfig; let config: IUnleashConfig;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('client_metrics_service_serial', getLogger); db = await dbInit('client_metrics_service_serial', getLogger);
@ -25,13 +25,10 @@ beforeAll(async () => {
stores, stores,
config, config,
new FakePrivateProjectChecker(), new FakePrivateProjectChecker(),
bulkInterval,
announcementInterval,
); );
}); });
afterAll(async () => { afterAll(async () => {
await clientInstanceService.destroy();
await db.destroy(); await db.destroy();
}); });
test('Apps registered should be announced', async () => { test('Apps registered should be announced', async () => {
@ -58,11 +55,11 @@ test('Apps registered should be announced', async () => {
}; };
await clientInstanceService.registerClient(clientRegistration, '127.0.0.1'); await clientInstanceService.registerClient(clientRegistration, '127.0.0.1');
await clientInstanceService.registerClient(differentClient, '127.0.0.1'); await clientInstanceService.registerClient(differentClient, '127.0.0.1');
await new Promise((res) => setTimeout(res, 1200)); await clientInstanceService.bulkAdd(); // in prod called by a SchedulerService
const first = await stores.clientApplicationsStore.getUnannounced(); const first = await stores.clientApplicationsStore.getUnannounced();
expect(first.length).toBe(2); expect(first.length).toBe(2);
await clientInstanceService.registerClient(clientRegistration, '127.0.0.1'); await clientInstanceService.registerClient(clientRegistration, '127.0.0.1');
await new Promise((res) => setTimeout(res, secondsToMilliseconds(2))); await clientInstanceService.announceUnannounced(); // in prod called by a SchedulerService
const second = await stores.clientApplicationsStore.getUnannounced(); const second = await stores.clientApplicationsStore.getUnannounced();
expect(second.length).toBe(0); expect(second.length).toBe(0);
const events = await stores.eventStore.getEvents(); const events = await stores.eventStore.getEvents();

View File

@ -56,8 +56,6 @@ test('Should update last seen for known toggles', async () => {
const t1 = await stores.featureToggleStore.get('ta1'); const t1 = await stores.featureToggleStore.get('ta1');
expect(t1.lastSeenAt.getTime()).toBeGreaterThan(time); expect(t1.lastSeenAt.getTime()).toBeGreaterThan(time);
service.destroy();
}); });
test('Should not update last seen toggles with 0 metrics', async () => { test('Should not update last seen toggles with 0 metrics', async () => {
@ -102,8 +100,6 @@ test('Should not update last seen toggles with 0 metrics', async () => {
expect(t2.lastSeenAt).toBeNull(); expect(t2.lastSeenAt).toBeNull();
expect(t1.lastSeenAt.getTime()).toBeGreaterThanOrEqual(time); expect(t1.lastSeenAt.getTime()).toBeGreaterThanOrEqual(time);
service.destroy();
}); });
test('Should not update anything for 0 toggles', async () => { test('Should not update anything for 0 toggles', async () => {
@ -144,6 +140,4 @@ test('Should not update anything for 0 toggles', async () => {
const count = await service.store(); const count = await service.store();
expect(count).toBe(0); expect(count).toBe(0);
service.destroy();
}); });