mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
Merge 2916ab3c40
into 3bb317ad6d
This commit is contained in:
commit
60c65a5596
@ -9,6 +9,7 @@ import {
|
|||||||
getSegmentChangesThatWouldBeOverwritten,
|
getSegmentChangesThatWouldBeOverwritten,
|
||||||
getStrategyChangesThatWouldBeOverwritten,
|
getStrategyChangesThatWouldBeOverwritten,
|
||||||
} from './strategy-change-diff-calculation.js';
|
} from './strategy-change-diff-calculation.js';
|
||||||
|
import { constraintId } from 'constants/constraintId.js';
|
||||||
|
|
||||||
describe('Strategy change conflict detection', () => {
|
describe('Strategy change conflict detection', () => {
|
||||||
const existingStrategy: IFeatureStrategy = {
|
const existingStrategy: IFeatureStrategy = {
|
||||||
@ -175,6 +176,7 @@ describe('Strategy change conflict detection', () => {
|
|||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
caseInsensitive: false,
|
caseInsensitive: false,
|
||||||
|
[constraintId]: 'id1',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
variants: [
|
variants: [
|
||||||
@ -230,6 +232,7 @@ describe('Strategy change conflict detection', () => {
|
|||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
caseInsensitive: false,
|
caseInsensitive: false,
|
||||||
|
[constraintId]: 'id2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -249,6 +252,7 @@ describe('Strategy change conflict detection', () => {
|
|||||||
inverted: false,
|
inverted: false,
|
||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
values: ['blah'],
|
values: ['blah'],
|
||||||
|
[constraintId]: 'id2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -478,6 +482,7 @@ describe('Segment change conflict detection', () => {
|
|||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
caseInsensitive: false,
|
caseInsensitive: false,
|
||||||
|
[constraintId]: 'id3',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -494,6 +499,7 @@ describe('Segment change conflict detection', () => {
|
|||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
caseInsensitive: false,
|
caseInsensitive: false,
|
||||||
|
[constraintId]: 'id4',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,6 @@ import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsLis
|
|||||||
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint';
|
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint';
|
||||||
import { createEmptyConstraint } from '../../../../utils/createEmptyConstraint.ts';
|
import { createEmptyConstraint } from '../../../../utils/createEmptyConstraint.ts';
|
||||||
import { constraintId } from 'constants/constraintId.ts';
|
import { constraintId } from 'constants/constraintId.ts';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
export interface IEditableConstraintsListRef {
|
export interface IEditableConstraintsListRef {
|
||||||
addConstraint?: (contextName: string) => void;
|
addConstraint?: (contextName: string) => void;
|
||||||
}
|
}
|
||||||
@ -44,7 +43,6 @@ export const EditableConstraintsList = forwardRef<
|
|||||||
if (!constraints.every((constraint) => constraintId in constraint)) {
|
if (!constraints.every((constraint) => constraintId in constraint)) {
|
||||||
setConstraints(
|
setConstraints(
|
||||||
constraints.map((constraint) => ({
|
constraints.map((constraint) => ({
|
||||||
[constraintId]: uuidv4(),
|
|
||||||
...constraint,
|
...constraint,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { createEmptyConstraint } from 'utils/createEmptyConstraint';
|
import { createEmptyConstraint } from 'utils/createEmptyConstraint';
|
||||||
import { fromIConstraint, toIConstraint } from './editable-constraint-type.ts';
|
import { fromIConstraint, toIConstraint } from './editable-constraint-type.ts';
|
||||||
import { constraintId } from 'constants/constraintId';
|
import { constraintId } from 'constants/constraintId';
|
||||||
import type { IConstraint } from 'interfaces/strategy.ts';
|
|
||||||
|
|
||||||
test('mapping to and from retains the constraint id', () => {
|
test('mapping to and from retains the constraint id', () => {
|
||||||
const constraint = createEmptyConstraint('context');
|
const constraint = createEmptyConstraint('context');
|
||||||
@ -11,18 +10,6 @@ test('mapping to and from retains the constraint id', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mapping to an editable constraint adds a constraint id if there is none', () => {
|
|
||||||
const constraint: IConstraint = createEmptyConstraint('context');
|
|
||||||
delete constraint[constraintId];
|
|
||||||
|
|
||||||
const editableConstraint = fromIConstraint(constraint);
|
|
||||||
|
|
||||||
expect(editableConstraint[constraintId]).toBeDefined();
|
|
||||||
|
|
||||||
const iConstraint = toIConstraint(editableConstraint);
|
|
||||||
expect(iConstraint[constraintId]).toEqual(editableConstraint[constraintId]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mapping from an empty constraint removes redundant value / values', () => {
|
test('mapping from an empty constraint removes redundant value / values', () => {
|
||||||
const constraint = { ...createEmptyConstraint('context'), value: '' };
|
const constraint = { ...createEmptyConstraint('context'), value: '' };
|
||||||
expect(constraint).toHaveProperty('value');
|
expect(constraint).toHaveProperty('value');
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { constraintId } from 'constants/constraintId';
|
|
||||||
import {
|
import {
|
||||||
type DateOperator,
|
type DateOperator,
|
||||||
isDateOperator,
|
isDateOperator,
|
||||||
@ -11,7 +10,6 @@ import {
|
|||||||
isSemVerOperator,
|
isSemVerOperator,
|
||||||
} from 'constants/operators';
|
} from 'constants/operators';
|
||||||
import type { IConstraint } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
type EditableConstraintBase = Omit<
|
type EditableConstraintBase = Omit<
|
||||||
IConstraint,
|
IConstraint,
|
||||||
@ -74,14 +72,12 @@ export const fromIConstraint = (
|
|||||||
const { value, values, operator, ...rest } = constraint;
|
const { value, values, operator, ...rest } = constraint;
|
||||||
if (isSingleValueOperator(operator)) {
|
if (isSingleValueOperator(operator)) {
|
||||||
return {
|
return {
|
||||||
[constraintId]: uuidv4(),
|
|
||||||
...rest,
|
...rest,
|
||||||
operator,
|
operator,
|
||||||
value: value ?? '',
|
value: value ?? '',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
[constraintId]: uuidv4(),
|
|
||||||
...rest,
|
...rest,
|
||||||
operator,
|
operator,
|
||||||
values: new Set(values),
|
values: new Set(values),
|
||||||
|
@ -13,6 +13,7 @@ import { vi } from 'vitest';
|
|||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
import type { ContextFieldSchema } from 'openapi';
|
import type { ContextFieldSchema } from 'openapi';
|
||||||
import { NUM_EQ } from '@server/util/constants';
|
import { NUM_EQ } from '@server/util/constants';
|
||||||
|
import { constraintId } from 'constants/constraintId.js';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ test('calls onUpdate with new state', async () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: NOT_IN,
|
operator: NOT_IN,
|
||||||
values: ['A', 'B'],
|
values: ['A', 'B'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdate = vi.fn();
|
const onUpdate = vi.fn();
|
||||||
@ -71,6 +73,7 @@ describe('validators', () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: operator,
|
operator: operator,
|
||||||
value: '',
|
value: '',
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -94,6 +97,7 @@ describe('validators', () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: operator,
|
operator: operator,
|
||||||
value: '',
|
value: '',
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -117,6 +121,7 @@ describe('validators', () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: operator,
|
operator: operator,
|
||||||
value: '',
|
value: '',
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -140,6 +145,7 @@ describe('validators', () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: operator,
|
operator: operator,
|
||||||
values: [],
|
values: [],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -162,6 +168,7 @@ describe('validators', () => {
|
|||||||
contextName: 'context-field',
|
contextName: 'context-field',
|
||||||
operator: operator,
|
operator: operator,
|
||||||
values: ['a', 'b'],
|
values: ['a', 'b'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -189,6 +196,7 @@ describe('legal values', () => {
|
|||||||
contextName: definition.name,
|
contextName: definition.name,
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: [],
|
values: [],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -206,6 +214,7 @@ describe('legal values', () => {
|
|||||||
contextName: definition.name,
|
contextName: definition.name,
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: [],
|
values: [],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -231,6 +240,7 @@ describe('legal values', () => {
|
|||||||
contextName: 'field-with-no-legal-values',
|
contextName: 'field-with-no-legal-values',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: [],
|
values: [],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -244,6 +254,7 @@ describe('legal values', () => {
|
|||||||
contextName: definition.name,
|
contextName: definition.name,
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['A', 'B'],
|
values: ['A', 'B'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@ -260,6 +271,7 @@ describe('legal values', () => {
|
|||||||
contextName: definition.name,
|
contextName: definition.name,
|
||||||
operator: NUM_EQ,
|
operator: NUM_EQ,
|
||||||
values: [],
|
values: [],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
|
@ -6,6 +6,7 @@ import { renderHook, act } from '@testing-library/react';
|
|||||||
import type { IConstraint } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { IN, STR_CONTAINS } from 'constants/operators';
|
import { IN, STR_CONTAINS } from 'constants/operators';
|
||||||
import type { Operator } from 'constants/operators';
|
import type { Operator } from 'constants/operators';
|
||||||
|
import { constraintId } from 'constants/constraintId.ts';
|
||||||
|
|
||||||
const createTestConstraint = (
|
const createTestConstraint = (
|
||||||
contextName: string,
|
contextName: string,
|
||||||
@ -15,6 +16,7 @@ const createTestConstraint = (
|
|||||||
contextName,
|
contextName,
|
||||||
operator,
|
operator,
|
||||||
values,
|
values,
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('areConstraintsEqual', () => {
|
describe('areConstraintsEqual', () => {
|
||||||
@ -24,6 +26,7 @@ describe('areConstraintsEqual', () => {
|
|||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1', 'user2'],
|
values: ['user1', 'user2'],
|
||||||
inverted: false,
|
inverted: false,
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const constraint2: IConstraint = {
|
const constraint2: IConstraint = {
|
||||||
@ -31,6 +34,7 @@ describe('areConstraintsEqual', () => {
|
|||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1', 'user2'],
|
values: ['user1', 'user2'],
|
||||||
inverted: false,
|
inverted: false,
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(areConstraintsEqual(constraint1, constraint2)).toBe(true);
|
expect(areConstraintsEqual(constraint1, constraint2)).toBe(true);
|
||||||
@ -41,12 +45,14 @@ describe('areConstraintsEqual', () => {
|
|||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1', 'user2', 'user3'],
|
values: ['user1', 'user2', 'user3'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const constraint2: IConstraint = {
|
const constraint2: IConstraint = {
|
||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user2', 'user3', 'user1'],
|
values: ['user2', 'user3', 'user1'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(areConstraintsEqual(constraint1, constraint2)).toBe(true);
|
expect(areConstraintsEqual(constraint1, constraint2)).toBe(true);
|
||||||
@ -57,12 +63,14 @@ describe('areConstraintsEqual', () => {
|
|||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1', 'user2'],
|
values: ['user1', 'user2'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const constraint2: IConstraint = {
|
const constraint2: IConstraint = {
|
||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1', 'user3'],
|
values: ['user1', 'user3'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(areConstraintsEqual(constraint1, constraint2)).toBe(false);
|
expect(areConstraintsEqual(constraint1, constraint2)).toBe(false);
|
||||||
@ -73,12 +81,14 @@ describe('areConstraintsEqual', () => {
|
|||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: IN,
|
operator: IN,
|
||||||
values: ['user1'],
|
values: ['user1'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const constraint2: IConstraint = {
|
const constraint2: IConstraint = {
|
||||||
contextName: 'userId',
|
contextName: 'userId',
|
||||||
operator: STR_CONTAINS,
|
operator: STR_CONTAINS,
|
||||||
values: ['user1'],
|
values: ['user1'],
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(areConstraintsEqual(constraint1, constraint2)).toBe(false);
|
expect(areConstraintsEqual(constraint1, constraint2)).toBe(false);
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
} from '@server/types/permissions';
|
} from '@server/types/permissions';
|
||||||
import { StrategyItem } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem';
|
import { StrategyItem } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem';
|
||||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { constraintId } from 'constants/constraintId';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
interface ProjectEnvironmentDefaultStrategyProps {
|
interface ProjectEnvironmentDefaultStrategyProps {
|
||||||
environment: ProjectEnvironmentType;
|
environment: ProjectEnvironmentType;
|
||||||
@ -55,7 +57,11 @@ export const ProjectEnvironmentDefaultStrategy = ({
|
|||||||
return {
|
return {
|
||||||
...baseDefaultStrategy,
|
...baseDefaultStrategy,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
constraints: baseDefaultStrategy.constraints ?? [],
|
constraints:
|
||||||
|
baseDefaultStrategy.constraints?.map((constraint) => ({
|
||||||
|
...constraint,
|
||||||
|
[constraintId]: uuidv4(),
|
||||||
|
})) ?? [],
|
||||||
title: baseDefaultStrategy.title ?? '',
|
title: baseDefaultStrategy.title ?? '',
|
||||||
parameters: baseDefaultStrategy.parameters ?? {},
|
parameters: baseDefaultStrategy.parameters ?? {},
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { IConstraint, IConstraintWithId } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { SegmentFormStepOne } from './SegmentFormStepOne.tsx';
|
import { SegmentFormStepOne } from './SegmentFormStepOne.tsx';
|
||||||
import { SegmentFormStepTwo } from './SegmentFormStepTwo.tsx';
|
import { SegmentFormStepTwo } from './SegmentFormStepTwo.tsx';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
@ -14,7 +14,7 @@ interface ISegmentProps {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
project?: string;
|
project?: string;
|
||||||
constraints: IConstraintWithId[];
|
constraints: IConstraint[];
|
||||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setProject: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setProject: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
CREATE_SEGMENT,
|
CREATE_SEGMENT,
|
||||||
UPDATE_PROJECT_SEGMENT,
|
UPDATE_PROJECT_SEGMENT,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import type { IConstraintWithId } from 'interfaces/strategy.ts';
|
import type { IConstraint } from 'interfaces/strategy.ts';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ const setupRoutes = () => {
|
|||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
project: undefined,
|
project: undefined,
|
||||||
constraints: [] as IConstraintWithId[],
|
constraints: [] as IConstraint[],
|
||||||
setConstraints: vi.fn(),
|
setConstraints: vi.fn(),
|
||||||
setCurrentStep: vi.fn(),
|
setCurrentStep: vi.fn(),
|
||||||
mode: 'create' as const,
|
mode: 'create' as const,
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
UPDATE_SEGMENT,
|
UPDATE_SEGMENT,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
import type { IConstraint, IConstraintWithId } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
|
import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
|
||||||
import type { IEditableConstraintsListRef } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
|
import type { IEditableConstraintsListRef } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
|
||||||
@ -33,7 +33,7 @@ import { GO_BACK } from 'constants/navigate';
|
|||||||
|
|
||||||
interface ISegmentFormPartTwoProps {
|
interface ISegmentFormPartTwoProps {
|
||||||
project?: string;
|
project?: string;
|
||||||
constraints: IConstraintWithId[];
|
constraints: IConstraint[];
|
||||||
setConstraints: React.Dispatch<React.SetStateAction<IConstraint[]>>;
|
setConstraints: React.Dispatch<React.SetStateAction<IConstraint[]>>;
|
||||||
setCurrentStep: React.Dispatch<React.SetStateAction<SegmentFormStep>>;
|
setCurrentStep: React.Dispatch<React.SetStateAction<SegmentFormStep>>;
|
||||||
mode: SegmentFormMode;
|
mode: SegmentFormMode;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import type { IConstraint, IConstraintWithId } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSegmentValidation } from 'hooks/api/getters/useSegmentValidation/useSegmentValidation';
|
import { useSegmentValidation } from 'hooks/api/getters/useSegmentValidation/useSegmentValidation';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { constraintId } from 'constants/constraintId';
|
|
||||||
|
|
||||||
export const useSegmentForm = (
|
export const useSegmentForm = (
|
||||||
initialName = '',
|
initialName = '',
|
||||||
@ -14,10 +12,9 @@ export const useSegmentForm = (
|
|||||||
const [description, setDescription] = useState(initialDescription);
|
const [description, setDescription] = useState(initialDescription);
|
||||||
const [project, setProject] = useState<string | undefined>(initialProject);
|
const [project, setProject] = useState<string | undefined>(initialProject);
|
||||||
const initialConstraintsWithId = initialConstraints.map((constraint) => ({
|
const initialConstraintsWithId = initialConstraints.map((constraint) => ({
|
||||||
[constraintId]: uuidv4(),
|
|
||||||
...constraint,
|
...constraint,
|
||||||
}));
|
}));
|
||||||
const [constraints, setConstraints] = useState<IConstraintWithId[]>(
|
const [constraints, setConstraints] = useState<IConstraint[]>(
|
||||||
initialConstraintsWithId,
|
initialConstraintsWithId,
|
||||||
);
|
);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
||||||
import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
|
import type {
|
||||||
|
ChangeRequestType,
|
||||||
|
IChangeRequestFeature,
|
||||||
|
IFeatureChange,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { addConstraintIdsToFeatureChange } from 'utils/addConstraintIdsToFeatureChange.js';
|
||||||
|
|
||||||
export const useChangeRequest = (projectId: string, id: string) => {
|
export const useChangeRequest = (projectId: string, id: string) => {
|
||||||
const { data, error, mutate } = useSWR<ChangeRequestType>(
|
const { data, error, mutate } = useSWR<ChangeRequestType>(
|
||||||
@ -10,8 +16,32 @@ export const useChangeRequest = (projectId: string, id: string) => {
|
|||||||
{ refreshInterval: 15000 },
|
{ refreshInterval: 15000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { features, ...dataProps } = data || {};
|
||||||
|
const featuresWithConstraintIds: IChangeRequestFeature[] | undefined =
|
||||||
|
useMemo(() => {
|
||||||
|
return (
|
||||||
|
features?.map((feature) => {
|
||||||
|
const changes: IFeatureChange[] = feature.changes.map(
|
||||||
|
addConstraintIdsToFeatureChange,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
...feature,
|
||||||
|
changes,
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
}, [JSON.stringify(features)]);
|
||||||
|
|
||||||
|
const mappedData = data
|
||||||
|
? {
|
||||||
|
...dataProps,
|
||||||
|
features: featuresWithConstraintIds,
|
||||||
|
}
|
||||||
|
: data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: mappedData,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
refetchChangeRequest: () => mutate(),
|
refetchChangeRequest: () => mutate(),
|
||||||
error,
|
error,
|
||||||
|
@ -3,6 +3,7 @@ import type { IConstraint } from 'interfaces/strategy'; // Assuming you have you
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useConstraintsValidation } from './useConstraintsValidation.ts';
|
import { useConstraintsValidation } from './useConstraintsValidation.ts';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
import { constraintId } from 'constants/constraintId.ts';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
@ -26,12 +27,14 @@ test('should display Valid when constraints are valid', async () => {
|
|||||||
values: ['test'],
|
values: ['test'],
|
||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
contextName: 'irrelevant',
|
contextName: 'irrelevant',
|
||||||
|
[constraintId]: 'constraint id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'test',
|
value: 'test',
|
||||||
values: ['test'],
|
values: ['test'],
|
||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
contextName: 'irrelevant',
|
contextName: 'irrelevant',
|
||||||
|
[constraintId]: 'constraint id 2',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -42,12 +45,19 @@ test('should display Valid when constraints are valid', async () => {
|
|||||||
|
|
||||||
test('should display Invalid when constraints are invalid', async () => {
|
test('should display Invalid when constraints are invalid', async () => {
|
||||||
const emptyValueAndValues: IConstraint[] = [
|
const emptyValueAndValues: IConstraint[] = [
|
||||||
{ value: '', values: [], operator: 'IN', contextName: 'irrelevant' },
|
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
values: [],
|
values: [],
|
||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
contextName: 'irrelevant',
|
contextName: 'irrelevant',
|
||||||
|
[constraintId]: 'constraint id 3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
values: [],
|
||||||
|
operator: 'IN',
|
||||||
|
contextName: 'irrelevant',
|
||||||
|
[constraintId]: 'constraint id 4',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import useSWR, { type SWRConfiguration } from 'swr';
|
import useSWR, { type SWRConfiguration } from 'swr';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { emptyFeature } from './emptyFeature.ts';
|
import { emptyFeature } from './emptyFeature.ts';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler.ts';
|
import handleErrorResponses from '../httpErrorResponseHandler.ts';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
import { constraintId } from 'constants/constraintId.ts';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import type { IFeatureStrategy } from 'interfaces/strategy.ts';
|
||||||
|
|
||||||
export interface IUseFeatureOutput {
|
export interface IUseFeatureOutput {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
@ -35,8 +38,12 @@ export const useFeature = (
|
|||||||
mutate().catch(console.warn);
|
mutate().catch(console.warn);
|
||||||
}, [mutate]);
|
}, [mutate]);
|
||||||
|
|
||||||
|
const feature = useMemo(enrichConstraintsWithIds(data), [
|
||||||
|
JSON.stringify(data?.body),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
feature: data?.body || emptyFeature,
|
feature,
|
||||||
refetchFeature,
|
refetchFeature,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
status: data?.status,
|
status: data?.status,
|
||||||
@ -63,6 +70,42 @@ export const featureFetcher = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const enrichConstraintsWithIds =
|
||||||
|
(data?: IFeatureResponse) => (): IFeatureToggle => {
|
||||||
|
if (!data?.body) {
|
||||||
|
return emptyFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { strategies, environments, ...rest } = data.body;
|
||||||
|
|
||||||
|
const addConstraintIds = (strategy: IFeatureStrategy) => {
|
||||||
|
const { constraints, ...strategyRest } = strategy;
|
||||||
|
return {
|
||||||
|
...strategyRest,
|
||||||
|
constraints: constraints?.map((constraint) => ({
|
||||||
|
...constraint,
|
||||||
|
[constraintId]: uuidv4(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const strategiesWithConstraintIds = strategies?.map(addConstraintIds);
|
||||||
|
|
||||||
|
const environmentsWithStrategyIds = environments?.map((environment) => {
|
||||||
|
const { strategies, ...environmentRest } = environment;
|
||||||
|
return {
|
||||||
|
...environmentRest,
|
||||||
|
strategies: strategies?.map(addConstraintIds),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
strategies: strategiesWithConstraintIds,
|
||||||
|
environments: environmentsWithStrategyIds,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const formatFeatureApiPath = (
|
export const formatFeatureApiPath = (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import useSWRImmutable from 'swr/immutable';
|
import useSWRImmutable from 'swr/immutable';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { emptyFeature } from './emptyFeature.js';
|
|
||||||
import {
|
import {
|
||||||
type IUseFeatureOutput,
|
type IUseFeatureOutput,
|
||||||
type IFeatureResponse,
|
type IFeatureResponse,
|
||||||
featureFetcher,
|
featureFetcher,
|
||||||
formatFeatureApiPath,
|
formatFeatureApiPath,
|
||||||
useFeature,
|
useFeature,
|
||||||
|
enrichConstraintsWithIds,
|
||||||
} from 'hooks/api/getters/useFeature/useFeature';
|
} from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
|
||||||
// useFeatureImmutable is like useFeature, except it won't refetch data on
|
// useFeatureImmutable is like useFeature, except it won't refetch data on
|
||||||
@ -29,8 +29,12 @@ export const useFeatureImmutable = (
|
|||||||
await refetchFeature();
|
await refetchFeature();
|
||||||
}, [mutate, refetchFeature]);
|
}, [mutate, refetchFeature]);
|
||||||
|
|
||||||
|
const feature = useMemo(enrichConstraintsWithIds(data), [
|
||||||
|
JSON.stringify(data?.body),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
feature: data?.body || emptyFeature,
|
feature,
|
||||||
refetchFeature: refetch,
|
refetchFeature: refetch,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
status: data?.status,
|
status: data?.status,
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
||||||
import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
|
import type {
|
||||||
|
ChangeRequestType,
|
||||||
|
IFeatureChange,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR.js';
|
import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR.js';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { addConstraintIdsToFeatureChange } from 'utils/addConstraintIdsToFeatureChange.js';
|
||||||
|
|
||||||
const fetcher = (path: string) => {
|
const fetcher = (path: string) => {
|
||||||
return fetch(path)
|
return fetch(path)
|
||||||
@ -16,8 +21,29 @@ export const usePendingChangeRequests = (project: string) => {
|
|||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mappedData: typeof data = useMemo(
|
||||||
|
() =>
|
||||||
|
data?.map((changeRequest) => {
|
||||||
|
const { features, ...rest } = changeRequest || {};
|
||||||
|
const featuresWithConstraintIds =
|
||||||
|
features?.map((feature) => {
|
||||||
|
const changes: IFeatureChange[] = feature.changes.map(
|
||||||
|
addConstraintIdsToFeatureChange,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
...feature,
|
||||||
|
changes,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
|
return { ...rest, features: featuresWithConstraintIds };
|
||||||
|
}),
|
||||||
|
[JSON.stringify(data)],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mappedData,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
refetch: mutate,
|
refetch: mutate,
|
||||||
error,
|
error,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import useSWR, { type SWRConfiguration } from 'swr';
|
import useSWR, { type SWRConfiguration } from 'swr';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { getProjectOverviewFetcher } from './getProjectOverviewFetcher.js';
|
import { getProjectOverviewFetcher } from './getProjectOverviewFetcher.js';
|
||||||
import type { ProjectOverviewSchema } from 'openapi';
|
import type { ProjectOverviewSchema } from 'openapi';
|
||||||
|
|
||||||
@ -41,8 +41,26 @@ const useProjectOverview = (id: string, options: SWRConfiguration = {}) => {
|
|||||||
mutate();
|
mutate();
|
||||||
}, [mutate]);
|
}, [mutate]);
|
||||||
|
|
||||||
|
const overriddenData = useMemo(() => {
|
||||||
|
if (!data) return undefined;
|
||||||
return {
|
return {
|
||||||
project: data || fallbackProject,
|
...data,
|
||||||
|
environments: data.environments?.map((env) => {
|
||||||
|
return env.defaultStrategy
|
||||||
|
? {
|
||||||
|
...env,
|
||||||
|
defaultStrategy: {
|
||||||
|
...env.defaultStrategy,
|
||||||
|
title: 'custom title override',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: env;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: overriddenData || fallbackProject,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
|
@ -66,10 +66,6 @@ export interface IConstraint {
|
|||||||
caseInsensitive?: boolean;
|
caseInsensitive?: boolean;
|
||||||
operator: Operator;
|
operator: Operator;
|
||||||
contextName: string;
|
contextName: string;
|
||||||
[constraintId]?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IConstraintWithId extends IConstraint {
|
|
||||||
[constraintId]: string;
|
[constraintId]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
frontend/src/utils/addConstraintIdsToFeatureChange.ts
Normal file
31
frontend/src/utils/addConstraintIdsToFeatureChange.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type {
|
||||||
|
IFeatureChange,
|
||||||
|
IChangeRequestAddStrategy,
|
||||||
|
IChangeRequestUpdateStrategy,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { constraintId } from 'constants/constraintId';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const isAddStrategyChange = (
|
||||||
|
change: IFeatureChange,
|
||||||
|
): change is IChangeRequestAddStrategy => change.action === 'addStrategy';
|
||||||
|
const isUpdateStrategyChange = (
|
||||||
|
change: IFeatureChange,
|
||||||
|
): change is IChangeRequestUpdateStrategy => change.action === 'updateStrategy';
|
||||||
|
|
||||||
|
export const addConstraintIdsToFeatureChange = (change: IFeatureChange) => {
|
||||||
|
if (isAddStrategyChange(change) || isUpdateStrategyChange(change)) {
|
||||||
|
const { constraints, ...rest } = change.payload;
|
||||||
|
return {
|
||||||
|
...change,
|
||||||
|
payload: {
|
||||||
|
...rest,
|
||||||
|
constraints: constraints.map((constraint) => ({
|
||||||
|
...constraint,
|
||||||
|
[constraintId]: uuidv4(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
} as IFeatureChange;
|
||||||
|
}
|
||||||
|
return change;
|
||||||
|
};
|
@ -9,6 +9,7 @@ test('keys are ordered in the expected order', () => {
|
|||||||
operator: 'STR_CONTAINS',
|
operator: 'STR_CONTAINS',
|
||||||
contextName: 'context',
|
contextName: 'context',
|
||||||
caseInsensitive: true,
|
caseInsensitive: true,
|
||||||
|
[constraintId]: 'constraint-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const output = serializeConstraint(input);
|
const output = serializeConstraint(input);
|
||||||
@ -30,6 +31,7 @@ test('only value OR values is present, not both', () => {
|
|||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
contextName: 'context',
|
contextName: 'context',
|
||||||
caseInsensitive: true,
|
caseInsensitive: true,
|
||||||
|
[constraintId]: 'constraint-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
const noValue = serializeConstraint(input);
|
const noValue = serializeConstraint(input);
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import type { constraintId } from 'constants/constraintId';
|
||||||
import { isSingleValueOperator } from 'constants/operators';
|
import { isSingleValueOperator } from 'constants/operators';
|
||||||
import type { IConstraint } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
type SerializedConstraint = Omit<IConstraint, typeof constraintId>;
|
||||||
|
|
||||||
export const serializeConstraint = ({
|
export const serializeConstraint = ({
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
@ -8,10 +11,10 @@ export const serializeConstraint = ({
|
|||||||
operator,
|
operator,
|
||||||
contextName,
|
contextName,
|
||||||
caseInsensitive,
|
caseInsensitive,
|
||||||
}: IConstraint): IConstraint => {
|
}: IConstraint): SerializedConstraint => {
|
||||||
const makeConstraint = (
|
const makeConstraint = (
|
||||||
valueProp: { value: string } | { values: string[] },
|
valueProp: { value: string } | { values: string[] },
|
||||||
): IConstraint => {
|
): SerializedConstraint => {
|
||||||
return {
|
return {
|
||||||
contextName,
|
contextName,
|
||||||
operator,
|
operator,
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { constraintId } from 'constants/constraintId';
|
import { constraintId } from 'constants/constraintId';
|
||||||
import { isDateOperator } from 'constants/operators';
|
import { isDateOperator } from 'constants/operators';
|
||||||
import type { IConstraintWithId } from 'interfaces/strategy';
|
import type { IConstraint } from 'interfaces/strategy';
|
||||||
import { operatorsForContext } from 'utils/operatorsForContext';
|
import { operatorsForContext } from 'utils/operatorsForContext';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export const createEmptyConstraint = (
|
export const createEmptyConstraint = (contextName: string): IConstraint => {
|
||||||
contextName: string,
|
|
||||||
): IConstraintWithId => {
|
|
||||||
const operator = operatorsForContext(contextName)[0];
|
const operator = operatorsForContext(contextName)[0];
|
||||||
|
|
||||||
const value = isDateOperator(operator) ? new Date().toISOString() : '';
|
const value = isDateOperator(operator) ? new Date().toISOString() : '';
|
||||||
|
Loading…
Reference in New Issue
Block a user