mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
Merge branch 'main' into 1-3490
This commit is contained in:
commit
359cca7cf9
1
frontend/src/assets/icons/case-sensitive.svg
Normal file
1
frontend/src/assets/icons/case-sensitive.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="17" height="11" fill="#000"><path d="m2.602 7.45-.725 2.025a.696.696 0 0 1-.275.388.776.776 0 0 1-.45.137.788.788 0 0 1-.675-.35.76.76 0 0 1-.1-.75L3.527.55a.91.91 0 0 1 .3-.4.77.77 0 0 1 .475-.15h.65a.77.77 0 0 1 .475.15.91.91 0 0 1 .3.4l3.15 8.375c.1.267.07.512-.088.737a.765.765 0 0 1-.662.338.77.77 0 0 1-.475-.15.91.91 0 0 1-.3-.4l-.7-2h-4.05ZM3.127 6h3l-1.45-4.15h-.1L3.127 6Zm9.225 4.275c-.817 0-1.459-.213-1.925-.638-.467-.425-.7-1.004-.7-1.737 0-.7.27-1.27.812-1.713.542-.441 1.238-.662 2.088-.662.383 0 .737.03 1.062.087.325.059.604.155.838.288v-.35c0-.45-.155-.808-.463-1.075-.308-.267-.73-.4-1.262-.4a2.238 2.238 0 0 0-1.325.425.938.938 0 0 1-.5.2.687.687 0 0 1-.5-.15.658.658 0 0 1-.263-.438.521.521 0 0 1 .163-.462c.316-.283.675-.5 1.075-.65a3.9 3.9 0 0 1 1.375-.225c1.033 0 1.825.246 2.375.738.55.491.825 1.204.825 2.137v3.8c0 .2-.071.37-.213.513a.698.698 0 0 1-.512.212.72.72 0 0 1-.525-.225.72.72 0 0 1-.225-.525V9.2h-.075a2.285 2.285 0 0 1-.875.788 2.658 2.658 0 0 1-1.25.287Zm.25-1.25c.533 0 .987-.188 1.362-.563.375-.374.563-.829.563-1.362a2.849 2.849 0 0 0-.8-.3c-.3-.067-.575-.1-.825-.1-.534 0-.942.104-1.225.313-.284.208-.425.504-.425.887 0 .333.125.604.375.813.25.208.575.312.975.312Z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -1,6 +1,12 @@
|
|||||||
import type { Operator } from 'constants/operators';
|
import type { Operator } from 'constants/operators';
|
||||||
|
|
||||||
export const formatOperatorDescription = (operator: Operator): string => {
|
export const formatOperatorDescription = (
|
||||||
|
operator: Operator,
|
||||||
|
inverted?: boolean,
|
||||||
|
): string => {
|
||||||
|
if (inverted) {
|
||||||
|
return invertedConstraintOperatorDescriptions[operator];
|
||||||
|
}
|
||||||
return constraintOperatorDescriptions[operator];
|
return constraintOperatorDescriptions[operator];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -21,3 +27,21 @@ const constraintOperatorDescriptions = {
|
|||||||
SEMVER_GT: 'is a SemVer greater than',
|
SEMVER_GT: 'is a SemVer greater than',
|
||||||
SEMVER_LT: 'is a SemVer less than',
|
SEMVER_LT: 'is a SemVer less than',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const invertedConstraintOperatorDescriptions = {
|
||||||
|
IN: 'is not one of',
|
||||||
|
NOT_IN: 'is one of',
|
||||||
|
STR_CONTAINS: 'is a string that does not contain',
|
||||||
|
STR_STARTS_WITH: 'is a string that does not start with',
|
||||||
|
STR_ENDS_WITH: 'is a string that does not end with',
|
||||||
|
NUM_EQ: 'is a number not equal to',
|
||||||
|
NUM_GT: 'is a number not greater than',
|
||||||
|
NUM_GTE: 'is a number less than',
|
||||||
|
NUM_LT: 'is a number not less than',
|
||||||
|
NUM_LTE: 'is a number greater than',
|
||||||
|
DATE_BEFORE: 'is a date not before',
|
||||||
|
DATE_AFTER: 'is a date not after',
|
||||||
|
SEMVER_EQ: 'is a SemVer not equal to',
|
||||||
|
SEMVER_GT: 'is a SemVer not greater than',
|
||||||
|
SEMVER_LT: 'is a SemVer not less than',
|
||||||
|
};
|
||||||
|
@ -9,24 +9,44 @@ import { ValuesList } from '../ValuesList/ValuesList';
|
|||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||||
import { useConstraintTooltips } from './hooks/useConstraintTooltips';
|
import { useConstraintTooltips } from './hooks/useConstraintTooltips';
|
||||||
|
import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg';
|
||||||
|
import { isCaseSensitive } from './isCaseSensitive';
|
||||||
|
|
||||||
const Inverted: FC = () => (
|
const Operator: FC<{
|
||||||
<Tooltip title='NOT (operator is negated)' arrow>
|
label: ConstraintSchema['operator'];
|
||||||
<StrategyEvaluationChip label='≠' />
|
inverted?: boolean;
|
||||||
|
}> = ({ label, inverted }) => (
|
||||||
|
<Tooltip title={inverted ? `Not ${label}` : label} arrow>
|
||||||
|
<StrategyEvaluationChip
|
||||||
|
label={formatOperatorDescription(label, inverted)}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Operator: FC<{ label: ConstraintSchema['operator'] }> = ({ label }) => (
|
const StrategyEvalChipLessInlinePadding = styled(StrategyEvaluationChip)(
|
||||||
<Tooltip title={label} arrow>
|
({ theme }) => ({
|
||||||
<StrategyEvaluationChip label={formatOperatorDescription(label)} />
|
'> span': {
|
||||||
</Tooltip>
|
paddingInline: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const CaseInsensitive: FC = () => (
|
const CaseSensitive: FC = () => {
|
||||||
<Tooltip title='Case sensitive' arrow>
|
return (
|
||||||
<StrategyEvaluationChip label={<s>Aa</s>} />
|
<Tooltip title='The match is case sensitive' arrow>
|
||||||
|
<StrategyEvalChipLessInlinePadding
|
||||||
|
aria-label='The match is case sensitive'
|
||||||
|
label={
|
||||||
|
<CaseSensitiveIcon
|
||||||
|
style={{ verticalAlign: 'middle' }}
|
||||||
|
fill='currentColor'
|
||||||
|
aria-hidden={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const StyledOperatorGroup = styled('div')(({ theme }) => ({
|
const StyledOperatorGroup = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -63,9 +83,10 @@ export const ConstraintItemHeader: FC<
|
|||||||
</Truncator>
|
</Truncator>
|
||||||
</StyledConstraintName>
|
</StyledConstraintName>
|
||||||
<StyledOperatorGroup>
|
<StyledOperatorGroup>
|
||||||
{inverted ? <Inverted /> : null}
|
<Operator label={operator} inverted={inverted} />
|
||||||
<Operator label={operator} />
|
{isCaseSensitive(operator, caseInsensitive) ? (
|
||||||
{caseInsensitive ? <CaseInsensitive /> : null}
|
<CaseSensitive />
|
||||||
|
) : null}
|
||||||
</StyledOperatorGroup>
|
</StyledOperatorGroup>
|
||||||
<ValuesList
|
<ValuesList
|
||||||
values={items}
|
values={items}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
allOperators,
|
||||||
|
inOperators,
|
||||||
|
stringOperators,
|
||||||
|
} from 'constants/operators';
|
||||||
|
import { isCaseSensitive } from './isCaseSensitive';
|
||||||
|
|
||||||
|
test('`IN` and `NOT_IN` are always case sensitive', () => {
|
||||||
|
expect(
|
||||||
|
inOperators
|
||||||
|
.flatMap((operator) =>
|
||||||
|
[true, false, undefined].map((caseInsensitive) =>
|
||||||
|
isCaseSensitive(operator, caseInsensitive),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.every((result) => result === true),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('If `caseInsensitive` is true, all operators except for `IN` and `NOT_IN` are considered case insensitive', () => {
|
||||||
|
expect(
|
||||||
|
allOperators
|
||||||
|
.filter((operator) => !inOperators.includes(operator))
|
||||||
|
.map((operator) => isCaseSensitive(operator, true))
|
||||||
|
.every((result) => result === false),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([false, undefined])(
|
||||||
|
'If `caseInsensitive` is %s, only string (and in) operators are considered case sensitive',
|
||||||
|
(caseInsensitive) => {
|
||||||
|
const stringResults = stringOperators.map((operator) =>
|
||||||
|
isCaseSensitive(operator, caseInsensitive),
|
||||||
|
);
|
||||||
|
const nonStringResults = allOperators
|
||||||
|
.filter(
|
||||||
|
(operator) =>
|
||||||
|
![...stringOperators, ...inOperators].includes(operator),
|
||||||
|
)
|
||||||
|
.map((operator) => isCaseSensitive(operator, caseInsensitive));
|
||||||
|
|
||||||
|
expect(stringResults.every((result) => result === true)).toBe(true);
|
||||||
|
expect(nonStringResults.every((result) => result === false)).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
inOperators,
|
||||||
|
stringOperators,
|
||||||
|
type Operator,
|
||||||
|
} from 'constants/operators';
|
||||||
|
|
||||||
|
export const isCaseSensitive = (
|
||||||
|
operator: Operator,
|
||||||
|
caseInsensitive?: boolean,
|
||||||
|
) =>
|
||||||
|
inOperators.includes(operator) ||
|
||||||
|
(stringOperators.includes(operator) && !caseInsensitive);
|
@ -36,15 +36,17 @@ const StyledHeader = styled('header')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({
|
const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'auto 1fr',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
columnGap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({
|
const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
margin: 0,
|
gridColumn: '1/-1',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTruncator = styled(Truncator)(({ theme }) => ({
|
const StyledTruncator = styled(Truncator)(({ theme }) => ({
|
||||||
@ -52,14 +54,68 @@ const StyledTruncator = styled(Truncator)(({ theme }) => ({
|
|||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledStrategyCount = styled('p')(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
color: theme.palette.info.contrastText,
|
||||||
|
backgroundColor: theme.palette.info.light,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 'min-content',
|
||||||
|
borderRadius: theme.shape.borderRadiusExtraLarge,
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NeutralStrategyCount = styled(StyledStrategyCount)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type EnvironmentMetadata = {
|
||||||
|
strategyCount: number;
|
||||||
|
releasePlanCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type EnvironmentHeaderProps = {
|
type EnvironmentHeaderProps = {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
expandable?: boolean;
|
expandable?: boolean;
|
||||||
|
environmentMetadata?: EnvironmentMetadata;
|
||||||
} & AccordionSummaryProps;
|
} & AccordionSummaryProps;
|
||||||
|
|
||||||
|
const MetadataChip = ({
|
||||||
|
strategyCount,
|
||||||
|
releasePlanCount,
|
||||||
|
}: EnvironmentMetadata) => {
|
||||||
|
if (strategyCount === 0 && releasePlanCount === 0) {
|
||||||
|
return <NeutralStrategyCount>0 strategies added</NeutralStrategyCount>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releasePlanText = releasePlanCount > 0 ? 'Release plan' : undefined;
|
||||||
|
|
||||||
|
const strategyText = () => {
|
||||||
|
switch (strategyCount) {
|
||||||
|
case 0:
|
||||||
|
return undefined;
|
||||||
|
case 1:
|
||||||
|
return `1 strategy`;
|
||||||
|
default:
|
||||||
|
return `${strategyCount} strategies`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = `${[releasePlanText, strategyText()].filter(Boolean).join(', ')} added`;
|
||||||
|
|
||||||
|
return <StyledStrategyCount>{text}</StyledStrategyCount>;
|
||||||
|
};
|
||||||
|
|
||||||
export const EnvironmentHeader: FC<
|
export const EnvironmentHeader: FC<
|
||||||
PropsWithChildren<EnvironmentHeaderProps>
|
PropsWithChildren<EnvironmentHeaderProps>
|
||||||
> = ({ environmentId, children, expandable = true, ...props }) => {
|
> = ({
|
||||||
|
environmentId,
|
||||||
|
children,
|
||||||
|
expandable = true,
|
||||||
|
environmentMetadata,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<StyledAccordionSummary
|
<StyledAccordionSummary
|
||||||
@ -79,6 +135,9 @@ export const EnvironmentHeader: FC<
|
|||||||
<StyledTruncator component='h2'>
|
<StyledTruncator component='h2'>
|
||||||
{environmentId}
|
{environmentId}
|
||||||
</StyledTruncator>
|
</StyledTruncator>
|
||||||
|
{environmentMetadata ? (
|
||||||
|
<MetadataChip {...environmentMetadata} />
|
||||||
|
) : null}
|
||||||
</StyledHeaderTitle>
|
</StyledHeaderTitle>
|
||||||
{children}
|
{children}
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
|
@ -66,7 +66,7 @@ export const FeatureOverviewEnvironment = ({
|
|||||||
metrics = { yes: 0, no: 0 },
|
metrics = { yes: 0, no: 0 },
|
||||||
otherEnvironments = [],
|
otherEnvironments = [],
|
||||||
}: FeatureOverviewEnvironmentProps) => {
|
}: FeatureOverviewEnvironmentProps) => {
|
||||||
const [isOpen, setIsOopen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { isOss } = useUiConfig();
|
const { isOss } = useUiConfig();
|
||||||
@ -83,9 +83,13 @@ export const FeatureOverviewEnvironment = ({
|
|||||||
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${environment.name}`}
|
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${environment.name}`}
|
||||||
expanded={isOpen && hasActivations}
|
expanded={isOpen && hasActivations}
|
||||||
disabled={!hasActivations}
|
disabled={!hasActivations}
|
||||||
onChange={() => setIsOopen(isOpen ? !isOpen : hasActivations)}
|
onChange={() => setIsOpen(isOpen ? !isOpen : hasActivations)}
|
||||||
>
|
>
|
||||||
<EnvironmentHeader
|
<EnvironmentHeader
|
||||||
|
environmentMetadata={{
|
||||||
|
strategyCount: environment.strategies?.length ?? 0,
|
||||||
|
releasePlanCount: environment.releasePlans?.length ?? 0,
|
||||||
|
}}
|
||||||
environmentId={environment.name}
|
environmentId={environment.name}
|
||||||
expandable={hasActivations}
|
expandable={hasActivations}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user