mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
fixed segments not being copied (#2105)
* fixed segments not being copied * fix fmt * bug fix * return segmentId[] when getting a feature strategy * do not return segments if they are not there * Update src/lib/services/feature-toggle-service.ts Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> * fix lint * fix: more explicit column sorting and bug fix * update snapshot * rollback * add segment ids to feature strategies * bug fix Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
10eb500360
commit
64b8df7ee0
@ -12,6 +12,8 @@ import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmu
|
||||
import { getFeatureStrategyIcon } from 'utils/strategyNames';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { CopyButton } from './CopyButton/CopyButton';
|
||||
import { useSegments } from '../../../../hooks/api/getters/useSegments/useSegments';
|
||||
import { IFeatureStrategyPayload } from '../../../../interfaces/strategy';
|
||||
|
||||
interface IFeatureStrategyEmptyProps {
|
||||
projectId: string;
|
||||
@ -65,6 +67,7 @@ export const FeatureStrategyEmpty = ({
|
||||
const { id, ...strategyCopy } = {
|
||||
...strategy,
|
||||
environment: environmentId,
|
||||
copyOf: strategy.id,
|
||||
};
|
||||
|
||||
return addStrategyToFeature(
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
|
||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
@ -19,6 +19,7 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useSegments } from '../../../../../../../../../../hooks/api/getters/useSegments/useSegments';
|
||||
|
||||
interface ICopyStrategyIconMenuProps {
|
||||
environments: IFeatureEnvironment['name'][];
|
||||
@ -31,6 +32,8 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
||||
}) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { segments } = useSegments(strategy.id);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { addStrategyToFeature } = useFeatureStrategyApi();
|
||||
@ -48,6 +51,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
||||
const { id, ...strategyCopy } = {
|
||||
...strategy,
|
||||
environment: environmentId,
|
||||
copyOf: strategy.id,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -19,6 +19,7 @@ export interface IFeatureStrategyPayload {
|
||||
name?: string;
|
||||
constraints: IConstraint[];
|
||||
parameters: IFeatureStrategyParameters;
|
||||
copyOf?: string;
|
||||
}
|
||||
|
||||
export interface IStrategy {
|
||||
|
@ -101,7 +101,10 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
||||
async getAll(query?: Object): Promise<IEnvironment[]> {
|
||||
let qB = this.db<IEnvironmentsTable>(TABLE)
|
||||
.select('*')
|
||||
.orderBy('sort_order', 'created_at');
|
||||
.orderBy([
|
||||
{ column: 'sort_order', order: 'asc' },
|
||||
{ column: 'created_at', order: 'asc' },
|
||||
]);
|
||||
if (query) {
|
||||
qB = qB.where(query);
|
||||
}
|
||||
|
@ -193,7 +193,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
feature_name: featureName,
|
||||
environment,
|
||||
})
|
||||
.orderBy('sort_order', 'created_at');
|
||||
.orderBy([
|
||||
{ column: 'sort_order', order: 'asc' },
|
||||
{ column: 'created_at', order: 'asc' },
|
||||
]);
|
||||
stopTimer();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
@ -19,6 +19,9 @@ export const createFeatureStrategySchema = {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
copyOf: {
|
||||
type: 'string',
|
||||
},
|
||||
parameters: {
|
||||
$ref: '#/components/schemas/parametersSchema',
|
||||
},
|
||||
|
@ -21,6 +21,8 @@ export const publicSignupTokenSchema = {
|
||||
type: 'string',
|
||||
},
|
||||
url: {
|
||||
description:
|
||||
'The public signup link for the token. Users who follow this link will be taken to a signup page where they can create an Unleash user.',
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
@ -43,12 +45,15 @@ export const publicSignupTokenSchema = {
|
||||
},
|
||||
users: {
|
||||
type: 'array',
|
||||
description: 'Array of users that have signed up using the token',
|
||||
items: {
|
||||
$ref: '#/components/schemas/userSchema',
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
role: {
|
||||
description:
|
||||
'Users who sign up using this token will be given this role.',
|
||||
$ref: '#/components/schemas/roleSchema',
|
||||
},
|
||||
},
|
||||
|
@ -39,6 +39,7 @@ import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environm
|
||||
import { SetStrategySortOrderSchema } from '../../../openapi/spec/set-strategy-sort-order-schema';
|
||||
|
||||
import { emptyResponse } from '../../../openapi/util/standard-responses';
|
||||
import { SegmentService } from '../../../services/segment-service';
|
||||
|
||||
interface FeatureStrategyParams {
|
||||
projectId: string;
|
||||
@ -68,7 +69,10 @@ const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||
|
||||
type ProjectFeaturesServices = Pick<
|
||||
IUnleashServices,
|
||||
'featureToggleServiceV2' | 'projectHealthService' | 'openApiService'
|
||||
| 'featureToggleServiceV2'
|
||||
| 'projectHealthService'
|
||||
| 'openApiService'
|
||||
| 'segmentService'
|
||||
>;
|
||||
|
||||
export default class ProjectFeaturesController extends Controller {
|
||||
@ -76,15 +80,22 @@ export default class ProjectFeaturesController extends Controller {
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
private segmentService: SegmentService;
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{ featureToggleServiceV2, openApiService }: ProjectFeaturesServices,
|
||||
{
|
||||
featureToggleServiceV2,
|
||||
openApiService,
|
||||
segmentService,
|
||||
}: ProjectFeaturesServices,
|
||||
) {
|
||||
super(config);
|
||||
this.featureService = featureToggleServiceV2;
|
||||
this.openApiService = openApiService;
|
||||
this.segmentService = segmentService;
|
||||
this.logger = config.getLogger('/admin-api/project/features.ts');
|
||||
|
||||
this.route({
|
||||
@ -557,13 +568,29 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res: Response<FeatureStrategySchema>,
|
||||
): Promise<void> {
|
||||
const { projectId, featureName, environment } = req.params;
|
||||
const { copyOf, ...strategyConfig } = req.body;
|
||||
|
||||
const userName = extractUsername(req);
|
||||
const strategy = await this.featureService.createStrategy(
|
||||
req.body,
|
||||
strategyConfig,
|
||||
{ environment, projectId, featureName },
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(strategy);
|
||||
|
||||
if (copyOf) {
|
||||
this.logger.info(
|
||||
`Cloning segments from: strategyId=${copyOf} to: strategyId=${strategy.id} `,
|
||||
);
|
||||
await this.segmentService.cloneStrategySegments(
|
||||
copyOf,
|
||||
strategy.id,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedStrategy = await this.featureService.getStrategy(
|
||||
strategy.id,
|
||||
);
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async getFeatureStrategies(
|
||||
|
@ -53,6 +53,7 @@ export class PublicInviteController extends Controller {
|
||||
openApiService.validPath({
|
||||
tags: ['Public signup tokens'],
|
||||
operationId: 'validatePublicSignupToken',
|
||||
summary: `Validates a public signup token exists, has not expired and is enabled`,
|
||||
responses: {
|
||||
200: emptyResponse,
|
||||
...getStandardResponses(400),
|
||||
@ -70,6 +71,8 @@ export class PublicInviteController extends Controller {
|
||||
openApiService.validPath({
|
||||
tags: ['Public signup tokens'],
|
||||
operationId: 'addPublicSignupTokenUser',
|
||||
summary:
|
||||
'Create a user with the "viewer" root role and link them to a signup token',
|
||||
requestBody: createRequestSchema('createInvitedUserSchema'),
|
||||
responses: {
|
||||
200: createResponseSchema('userSchema'),
|
||||
|
@ -45,6 +45,7 @@ import {
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleQuery,
|
||||
ISegment,
|
||||
IStrategyConfig,
|
||||
IVariant,
|
||||
WeightType,
|
||||
@ -283,12 +284,14 @@ class FeatureToggleService {
|
||||
|
||||
featureStrategyToPublic(
|
||||
featureStrategy: IFeatureStrategy,
|
||||
segments: ISegment[] = [],
|
||||
): Saved<IStrategyConfig> {
|
||||
return {
|
||||
id: featureStrategy.id,
|
||||
name: featureStrategy.strategyName,
|
||||
constraints: featureStrategy.constraints || [],
|
||||
parameters: featureStrategy.parameters,
|
||||
segments: segments.map((segment) => segment.id) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -330,7 +333,13 @@ class FeatureToggleService {
|
||||
});
|
||||
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const strategy = this.featureStrategyToPublic(newFeatureStrategy);
|
||||
const segments = await this.segmentService.getByStrategy(
|
||||
newFeatureStrategy.id,
|
||||
);
|
||||
const strategy = this.featureStrategyToPublic(
|
||||
newFeatureStrategy,
|
||||
segments,
|
||||
);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyAddEvent({
|
||||
project: projectId,
|
||||
@ -385,10 +394,17 @@ class FeatureToggleService {
|
||||
updates,
|
||||
);
|
||||
|
||||
const segments = await this.segmentService.getByStrategy(
|
||||
strategy.id,
|
||||
);
|
||||
|
||||
// Store event!
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const data = this.featureStrategyToPublic(strategy);
|
||||
const preData = this.featureStrategyToPublic(existingStrategy);
|
||||
const data = this.featureStrategyToPublic(strategy, segments);
|
||||
const preData = this.featureStrategyToPublic(
|
||||
existingStrategy,
|
||||
segments,
|
||||
);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyUpdateEvent({
|
||||
project: projectId,
|
||||
@ -424,8 +440,14 @@ class FeatureToggleService {
|
||||
existingStrategy,
|
||||
);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const data = this.featureStrategyToPublic(strategy);
|
||||
const preData = this.featureStrategyToPublic(existingStrategy);
|
||||
const segments = await this.segmentService.getByStrategy(
|
||||
strategy.id,
|
||||
);
|
||||
const data = this.featureStrategyToPublic(strategy, segments);
|
||||
const preData = this.featureStrategyToPublic(
|
||||
existingStrategy,
|
||||
segments,
|
||||
);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyUpdateEvent({
|
||||
featureName,
|
||||
@ -488,6 +510,7 @@ class FeatureToggleService {
|
||||
featureName: string,
|
||||
environment: string = DEFAULT_ENV,
|
||||
): Promise<Saved<IStrategyConfig>[]> {
|
||||
this.logger.debug('getStrategiesForEnvironment');
|
||||
const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment(
|
||||
environment,
|
||||
featureName,
|
||||
@ -499,13 +522,22 @@ class FeatureToggleService {
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
return featureStrategies.map((strat) => ({
|
||||
id: strat.id,
|
||||
name: strat.strategyName,
|
||||
constraints: strat.constraints,
|
||||
parameters: strat.parameters,
|
||||
sortOrder: strat.sortOrder,
|
||||
}));
|
||||
const result = [];
|
||||
for (const strat of featureStrategies) {
|
||||
const segments =
|
||||
(await this.segmentService.getByStrategy(strat.id)).map(
|
||||
(segment) => segment.id,
|
||||
) ?? [];
|
||||
result.push({
|
||||
id: strat.id,
|
||||
name: strat.strategyName,
|
||||
constraints: strat.constraints,
|
||||
parameters: strat.parameters,
|
||||
sortOrder: strat.sortOrder,
|
||||
segments,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new NotFoundError(
|
||||
`Feature ${featureName} does not have environment ${environment}`,
|
||||
@ -727,12 +759,23 @@ class FeatureToggleService {
|
||||
const strategy = await this.featureStrategiesStore.getStrategyById(
|
||||
strategyId,
|
||||
);
|
||||
return {
|
||||
|
||||
const segments = await this.segmentService.getByStrategy(strategyId);
|
||||
let result: Saved<IStrategyConfig> = {
|
||||
id: strategy.id,
|
||||
name: strategy.strategyName,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters,
|
||||
segments: [],
|
||||
};
|
||||
|
||||
if (segments && segments.length > 0) {
|
||||
result = {
|
||||
...result,
|
||||
segments: segments.map((segment) => segment.id),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getEnvironmentInfo(
|
||||
|
@ -124,7 +124,6 @@ export class SegmentService {
|
||||
const sourceStrategySegments = await this.getByStrategy(
|
||||
sourceStrategyId,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
sourceStrategySegments.map((sourceStrategySegment) => {
|
||||
return this.addToStrategy(
|
||||
|
@ -709,6 +709,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"copyOf": {
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
@ -2483,14 +2486,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/components/schemas/roleSchema",
|
||||
"description": "Users who sign up using this token will be given this role.",
|
||||
},
|
||||
"secret": {
|
||||
"type": "string",
|
||||
},
|
||||
"url": {
|
||||
"description": "The public signup link for the token. Users who follow this link will be taken to a signup page where they can create an Unleash user.",
|
||||
"type": "string",
|
||||
},
|
||||
"users": {
|
||||
"description": "Array of users that have signed up using the token",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
@ -7472,6 +7478,7 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
"description": "The provided resource can not be created or updated because it would conflict with the current state of the resource or with an already existing resource, respectively.",
|
||||
},
|
||||
},
|
||||
"summary": "Create a user with the "viewer" root role and link them to a signup token",
|
||||
"tags": [
|
||||
"Public signup tokens",
|
||||
],
|
||||
@ -7498,6 +7505,7 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
"description": "The request data does not match what we expect.",
|
||||
},
|
||||
},
|
||||
"summary": "Validates a public signup token exists, has not expired and is enabled",
|
||||
"tags": [
|
||||
"Public signup tokens",
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user