1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +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:
andreas-unleash 2022-10-10 15:32:34 +03:00 committed by GitHub
parent 10eb500360
commit 64b8df7ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 21 deletions

View File

@ -12,6 +12,8 @@ import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmu
import { getFeatureStrategyIcon } from 'utils/strategyNames'; import { getFeatureStrategyIcon } from 'utils/strategyNames';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyButton } from './CopyButton/CopyButton'; import { CopyButton } from './CopyButton/CopyButton';
import { useSegments } from '../../../../hooks/api/getters/useSegments/useSegments';
import { IFeatureStrategyPayload } from '../../../../interfaces/strategy';
interface IFeatureStrategyEmptyProps { interface IFeatureStrategyEmptyProps {
projectId: string; projectId: string;
@ -65,6 +67,7 @@ export const FeatureStrategyEmpty = ({
const { id, ...strategyCopy } = { const { id, ...strategyCopy } = {
...strategy, ...strategy,
environment: environmentId, environment: environmentId,
copyOf: strategy.id,
}; };
return addStrategyToFeature( return addStrategyToFeature(

View File

@ -8,7 +8,7 @@ import {
Tooltip, Tooltip,
} from '@mui/material'; } from '@mui/material';
import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { IFeatureEnvironment } from 'interfaces/featureToggle';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
@ -19,6 +19,7 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable'; import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useSegments } from '../../../../../../../../../../hooks/api/getters/useSegments/useSegments';
interface ICopyStrategyIconMenuProps { interface ICopyStrategyIconMenuProps {
environments: IFeatureEnvironment['name'][]; environments: IFeatureEnvironment['name'][];
@ -31,6 +32,8 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
}) => { }) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { segments } = useSegments(strategy.id);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { addStrategyToFeature } = useFeatureStrategyApi(); const { addStrategyToFeature } = useFeatureStrategyApi();
@ -48,6 +51,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
const { id, ...strategyCopy } = { const { id, ...strategyCopy } = {
...strategy, ...strategy,
environment: environmentId, environment: environmentId,
copyOf: strategy.id,
}; };
try { try {

View File

@ -19,6 +19,7 @@ export interface IFeatureStrategyPayload {
name?: string; name?: string;
constraints: IConstraint[]; constraints: IConstraint[];
parameters: IFeatureStrategyParameters; parameters: IFeatureStrategyParameters;
copyOf?: string;
} }
export interface IStrategy { export interface IStrategy {

View File

@ -101,7 +101,10 @@ export default class EnvironmentStore implements IEnvironmentStore {
async getAll(query?: Object): Promise<IEnvironment[]> { async getAll(query?: Object): Promise<IEnvironment[]> {
let qB = this.db<IEnvironmentsTable>(TABLE) let qB = this.db<IEnvironmentsTable>(TABLE)
.select('*') .select('*')
.orderBy('sort_order', 'created_at'); .orderBy([
{ column: 'sort_order', order: 'asc' },
{ column: 'created_at', order: 'asc' },
]);
if (query) { if (query) {
qB = qB.where(query); qB = qB.where(query);
} }

View File

@ -193,7 +193,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
feature_name: featureName, feature_name: featureName,
environment, environment,
}) })
.orderBy('sort_order', 'created_at'); .orderBy([
{ column: 'sort_order', order: 'asc' },
{ column: 'created_at', order: 'asc' },
]);
stopTimer(); stopTimer();
return rows.map(mapRow); return rows.map(mapRow);
} }

View File

@ -19,6 +19,9 @@ export const createFeatureStrategySchema = {
$ref: '#/components/schemas/constraintSchema', $ref: '#/components/schemas/constraintSchema',
}, },
}, },
copyOf: {
type: 'string',
},
parameters: { parameters: {
$ref: '#/components/schemas/parametersSchema', $ref: '#/components/schemas/parametersSchema',
}, },

View File

@ -21,6 +21,8 @@ export const publicSignupTokenSchema = {
type: 'string', type: 'string',
}, },
url: { 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', type: 'string',
}, },
name: { name: {
@ -43,12 +45,15 @@ export const publicSignupTokenSchema = {
}, },
users: { users: {
type: 'array', type: 'array',
description: 'Array of users that have signed up using the token',
items: { items: {
$ref: '#/components/schemas/userSchema', $ref: '#/components/schemas/userSchema',
}, },
nullable: true, nullable: true,
}, },
role: { role: {
description:
'Users who sign up using this token will be given this role.',
$ref: '#/components/schemas/roleSchema', $ref: '#/components/schemas/roleSchema',
}, },
}, },

View File

@ -39,6 +39,7 @@ import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environm
import { SetStrategySortOrderSchema } from '../../../openapi/spec/set-strategy-sort-order-schema'; import { SetStrategySortOrderSchema } from '../../../openapi/spec/set-strategy-sort-order-schema';
import { emptyResponse } from '../../../openapi/util/standard-responses'; import { emptyResponse } from '../../../openapi/util/standard-responses';
import { SegmentService } from '../../../services/segment-service';
interface FeatureStrategyParams { interface FeatureStrategyParams {
projectId: string; projectId: string;
@ -68,7 +69,10 @@ const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
type ProjectFeaturesServices = Pick< type ProjectFeaturesServices = Pick<
IUnleashServices, IUnleashServices,
'featureToggleServiceV2' | 'projectHealthService' | 'openApiService' | 'featureToggleServiceV2'
| 'projectHealthService'
| 'openApiService'
| 'segmentService'
>; >;
export default class ProjectFeaturesController extends Controller { export default class ProjectFeaturesController extends Controller {
@ -76,15 +80,22 @@ export default class ProjectFeaturesController extends Controller {
private openApiService: OpenApiService; private openApiService: OpenApiService;
private segmentService: SegmentService;
private readonly logger: Logger; private readonly logger: Logger;
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ featureToggleServiceV2, openApiService }: ProjectFeaturesServices, {
featureToggleServiceV2,
openApiService,
segmentService,
}: ProjectFeaturesServices,
) { ) {
super(config); super(config);
this.featureService = featureToggleServiceV2; this.featureService = featureToggleServiceV2;
this.openApiService = openApiService; this.openApiService = openApiService;
this.segmentService = segmentService;
this.logger = config.getLogger('/admin-api/project/features.ts'); this.logger = config.getLogger('/admin-api/project/features.ts');
this.route({ this.route({
@ -557,13 +568,29 @@ export default class ProjectFeaturesController extends Controller {
res: Response<FeatureStrategySchema>, res: Response<FeatureStrategySchema>,
): Promise<void> { ): Promise<void> {
const { projectId, featureName, environment } = req.params; const { projectId, featureName, environment } = req.params;
const { copyOf, ...strategyConfig } = req.body;
const userName = extractUsername(req); const userName = extractUsername(req);
const strategy = await this.featureService.createStrategy( const strategy = await this.featureService.createStrategy(
req.body, strategyConfig,
{ environment, projectId, featureName }, { environment, projectId, featureName },
userName, 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( async getFeatureStrategies(

View File

@ -53,6 +53,7 @@ export class PublicInviteController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['Public signup tokens'], tags: ['Public signup tokens'],
operationId: 'validatePublicSignupToken', operationId: 'validatePublicSignupToken',
summary: `Validates a public signup token exists, has not expired and is enabled`,
responses: { responses: {
200: emptyResponse, 200: emptyResponse,
...getStandardResponses(400), ...getStandardResponses(400),
@ -70,6 +71,8 @@ export class PublicInviteController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['Public signup tokens'], tags: ['Public signup tokens'],
operationId: 'addPublicSignupTokenUser', operationId: 'addPublicSignupTokenUser',
summary:
'Create a user with the "viewer" root role and link them to a signup token',
requestBody: createRequestSchema('createInvitedUserSchema'), requestBody: createRequestSchema('createInvitedUserSchema'),
responses: { responses: {
200: createResponseSchema('userSchema'), 200: createResponseSchema('userSchema'),

View File

@ -45,6 +45,7 @@ import {
IFeatureOverview, IFeatureOverview,
IFeatureStrategy, IFeatureStrategy,
IFeatureToggleQuery, IFeatureToggleQuery,
ISegment,
IStrategyConfig, IStrategyConfig,
IVariant, IVariant,
WeightType, WeightType,
@ -283,12 +284,14 @@ class FeatureToggleService {
featureStrategyToPublic( featureStrategyToPublic(
featureStrategy: IFeatureStrategy, featureStrategy: IFeatureStrategy,
segments: ISegment[] = [],
): Saved<IStrategyConfig> { ): Saved<IStrategyConfig> {
return { return {
id: featureStrategy.id, id: featureStrategy.id,
name: featureStrategy.strategyName, name: featureStrategy.strategyName,
constraints: featureStrategy.constraints || [], constraints: featureStrategy.constraints || [],
parameters: featureStrategy.parameters, parameters: featureStrategy.parameters,
segments: segments.map((segment) => segment.id) ?? [],
}; };
} }
@ -330,7 +333,13 @@ class FeatureToggleService {
}); });
const tags = await this.tagStore.getAllTagsForFeature(featureName); 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( await this.eventStore.store(
new FeatureStrategyAddEvent({ new FeatureStrategyAddEvent({
project: projectId, project: projectId,
@ -385,10 +394,17 @@ class FeatureToggleService {
updates, updates,
); );
const segments = await this.segmentService.getByStrategy(
strategy.id,
);
// Store event! // Store event!
const tags = await this.tagStore.getAllTagsForFeature(featureName); const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy); const data = this.featureStrategyToPublic(strategy, segments);
const preData = this.featureStrategyToPublic(existingStrategy); const preData = this.featureStrategyToPublic(
existingStrategy,
segments,
);
await this.eventStore.store( await this.eventStore.store(
new FeatureStrategyUpdateEvent({ new FeatureStrategyUpdateEvent({
project: projectId, project: projectId,
@ -424,8 +440,14 @@ class FeatureToggleService {
existingStrategy, existingStrategy,
); );
const tags = await this.tagStore.getAllTagsForFeature(featureName); const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy); const segments = await this.segmentService.getByStrategy(
const preData = this.featureStrategyToPublic(existingStrategy); strategy.id,
);
const data = this.featureStrategyToPublic(strategy, segments);
const preData = this.featureStrategyToPublic(
existingStrategy,
segments,
);
await this.eventStore.store( await this.eventStore.store(
new FeatureStrategyUpdateEvent({ new FeatureStrategyUpdateEvent({
featureName, featureName,
@ -488,6 +510,7 @@ class FeatureToggleService {
featureName: string, featureName: string,
environment: string = DEFAULT_ENV, environment: string = DEFAULT_ENV,
): Promise<Saved<IStrategyConfig>[]> { ): Promise<Saved<IStrategyConfig>[]> {
this.logger.debug('getStrategiesForEnvironment');
const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment( const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment(
environment, environment,
featureName, featureName,
@ -499,13 +522,22 @@ class FeatureToggleService {
featureName, featureName,
environment, environment,
); );
return featureStrategies.map((strat) => ({ const result = [];
for (const strat of featureStrategies) {
const segments =
(await this.segmentService.getByStrategy(strat.id)).map(
(segment) => segment.id,
) ?? [];
result.push({
id: strat.id, id: strat.id,
name: strat.strategyName, name: strat.strategyName,
constraints: strat.constraints, constraints: strat.constraints,
parameters: strat.parameters, parameters: strat.parameters,
sortOrder: strat.sortOrder, sortOrder: strat.sortOrder,
})); segments,
});
}
return result;
} }
throw new NotFoundError( throw new NotFoundError(
`Feature ${featureName} does not have environment ${environment}`, `Feature ${featureName} does not have environment ${environment}`,
@ -727,12 +759,23 @@ class FeatureToggleService {
const strategy = await this.featureStrategiesStore.getStrategyById( const strategy = await this.featureStrategiesStore.getStrategyById(
strategyId, strategyId,
); );
return {
const segments = await this.segmentService.getByStrategy(strategyId);
let result: Saved<IStrategyConfig> = {
id: strategy.id, id: strategy.id,
name: strategy.strategyName, name: strategy.strategyName,
constraints: strategy.constraints || [], constraints: strategy.constraints || [],
parameters: strategy.parameters, parameters: strategy.parameters,
segments: [],
}; };
if (segments && segments.length > 0) {
result = {
...result,
segments: segments.map((segment) => segment.id),
};
}
return result;
} }
async getEnvironmentInfo( async getEnvironmentInfo(

View File

@ -124,7 +124,6 @@ export class SegmentService {
const sourceStrategySegments = await this.getByStrategy( const sourceStrategySegments = await this.getByStrategy(
sourceStrategyId, sourceStrategyId,
); );
await Promise.all( await Promise.all(
sourceStrategySegments.map((sourceStrategySegment) => { sourceStrategySegments.map((sourceStrategySegment) => {
return this.addToStrategy( return this.addToStrategy(

View File

@ -709,6 +709,9 @@ exports[`should serve the OpenAPI spec 1`] = `
}, },
"type": "array", "type": "array",
}, },
"copyOf": {
"type": "string",
},
"name": { "name": {
"type": "string", "type": "string",
}, },
@ -2483,14 +2486,17 @@ exports[`should serve the OpenAPI spec 1`] = `
}, },
"role": { "role": {
"$ref": "#/components/schemas/roleSchema", "$ref": "#/components/schemas/roleSchema",
"description": "Users who sign up using this token will be given this role.",
}, },
"secret": { "secret": {
"type": "string", "type": "string",
}, },
"url": { "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", "type": "string",
}, },
"users": { "users": {
"description": "Array of users that have signed up using the token",
"items": { "items": {
"$ref": "#/components/schemas/userSchema", "$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.", "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": [ "tags": [
"Public signup tokens", "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.", "description": "The request data does not match what we expect.",
}, },
}, },
"summary": "Validates a public signup token exists, has not expired and is enabled",
"tags": [ "tags": [
"Public signup tokens", "Public signup tokens",
], ],