mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-10 17:53:36 +02:00
Merge branch 'main' of https://github.com/Unleash/unleash
This commit is contained in:
commit
f27c4350b2
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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={
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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) => ({
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -29,6 +29,7 @@ export const SegmentExecutionWithoutResult: VFC<
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
isExpanded
|
isExpanded
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
|
@ -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}
|
||||||
|
@ -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)]
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
|
@ -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')
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
};
|
};
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 = (
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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 },
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user