mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
cleanup in export and import (#2973)
This commit is contained in:
parent
6b22e2eb61
commit
80c444aa99
@ -11,7 +11,7 @@ const ImportExplanationHeader = styled(Typography)(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(0.5),
|
marginBottom: theme.spacing(0.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ImportExplanationDescription = styled(Typography)(({ theme }) => ({
|
const ImportExplanationDescription = styled(Box)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(3),
|
marginBottom: theme.spacing(3),
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
@ -132,11 +132,13 @@ export const ValidationStage: FC<{
|
|||||||
this configuration
|
this configuration
|
||||||
</ErrorHeader>
|
</ErrorHeader>
|
||||||
{validationResult.errors.map(error => (
|
{validationResult.errors.map(error => (
|
||||||
<Box sx={{ p: 2 }}>
|
<Box key={error.message} sx={{ p: 2 }}>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
<StyledItems>
|
<StyledItems>
|
||||||
{error.affectedItems.map(item => (
|
{error.affectedItems.map(item => (
|
||||||
<StyledItem>{item}</StyledItem>
|
<StyledItem key={item}>
|
||||||
|
{item}
|
||||||
|
</StyledItem>
|
||||||
))}
|
))}
|
||||||
</StyledItems>
|
</StyledItems>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -120,7 +120,6 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
|||||||
enabled: r.enabled,
|
enabled: r.enabled,
|
||||||
featureName: r.feature_name,
|
featureName: r.feature_name,
|
||||||
environment: r.environment,
|
environment: r.environment,
|
||||||
variants: r.variants,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,7 @@ import { IUnleashConfig } from '../../types/option';
|
|||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import ExportImportService, {
|
import ExportImportService from 'lib/services/export-import-service';
|
||||||
IImportDTO,
|
|
||||||
} from 'lib/services/export-import-service';
|
|
||||||
import { InvalidOperationError } from '../../error';
|
import { InvalidOperationError } from '../../error';
|
||||||
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
||||||
import { exportResultSchema } from '../../openapi/spec/export-result-schema';
|
import { exportResultSchema } from '../../openapi/spec/export-result-schema';
|
||||||
@ -51,12 +49,6 @@ class ExportImportController extends Controller {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.route({
|
|
||||||
method: 'post',
|
|
||||||
path: '/import',
|
|
||||||
permission: NONE,
|
|
||||||
handler: this.importData,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async export(
|
async export(
|
||||||
@ -94,16 +86,5 @@ class ExportImportController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async importData(
|
|
||||||
req: IAuthRequest<unknown, unknown, IImportDTO, unknown>,
|
|
||||||
res: Response,
|
|
||||||
): Promise<void> {
|
|
||||||
const dto = req.body;
|
|
||||||
const user = req.user;
|
|
||||||
await this.exportImportService.import(dto, user);
|
|
||||||
|
|
||||||
res.status(201).end();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export default ExportImportController;
|
export default ExportImportController;
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import {
|
import { IFeatureStrategy, IFeatureStrategySegment } from '../types/model';
|
||||||
FeatureToggle,
|
|
||||||
IFeatureEnvironment,
|
|
||||||
IFeatureStrategy,
|
|
||||||
IFeatureStrategySegment,
|
|
||||||
ISegment,
|
|
||||||
ITag,
|
|
||||||
} from '../types/model';
|
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IFeatureTagStore } from '../types/stores/feature-tag-store';
|
import { IFeatureTagStore } from '../types/stores/feature-tag-store';
|
||||||
import { IProjectStore } from '../types/stores/project-store';
|
import { IProjectStore } from '../types/stores/project-store';
|
||||||
@ -20,27 +13,17 @@ import { IEnvironmentStore } from '../types/stores/environment-store';
|
|||||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||||
import { IContextFieldStore, IUnleashStores } from '../types/stores';
|
import { IContextFieldStore, IUnleashStores } from '../types/stores';
|
||||||
import { ISegmentStore } from '../types/stores/segment-store';
|
import { ISegmentStore } from '../types/stores/segment-store';
|
||||||
import { IContextFieldDto } from '../types/stores/context-field-store';
|
|
||||||
import FeatureToggleService from './feature-toggle-service';
|
import FeatureToggleService from './feature-toggle-service';
|
||||||
import User from 'lib/types/user';
|
|
||||||
import { ExportQuerySchema } from '../openapi/spec/export-query-schema';
|
import { ExportQuerySchema } from '../openapi/spec/export-query-schema';
|
||||||
import { FEATURES_EXPORTED, IFlagResolver, IUnleashServices } from '../types';
|
import { FEATURES_EXPORTED, IFlagResolver, IUnleashServices } from '../types';
|
||||||
|
import { ExportResultSchema } from '../openapi';
|
||||||
|
|
||||||
export interface IImportDTO {
|
export interface IImportDTO {
|
||||||
data: IExportData;
|
data: ExportResultSchema;
|
||||||
project: string;
|
project: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExportData {
|
|
||||||
features: FeatureToggle[];
|
|
||||||
tags?: ITag[];
|
|
||||||
contextFields: IContextFieldDto[];
|
|
||||||
featureStrategies: IFeatureStrategy[];
|
|
||||||
featureEnvironments: IFeatureEnvironment[];
|
|
||||||
segments: ISegment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ExportImportService {
|
export default class ExportImportService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
@ -102,7 +85,7 @@ export default class ExportImportService {
|
|||||||
async export(
|
async export(
|
||||||
query: ExportQuerySchema,
|
query: ExportQuerySchema,
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<IExportData> {
|
): Promise<ExportResultSchema> {
|
||||||
const [
|
const [
|
||||||
features,
|
features,
|
||||||
featureEnvironments,
|
featureEnvironments,
|
||||||
@ -140,10 +123,22 @@ export default class ExportImportService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const result = {
|
const result = {
|
||||||
features,
|
features: features.map((item) => {
|
||||||
featureStrategies,
|
const { createdAt, archivedAt, lastSeenAt, ...rest } = item;
|
||||||
featureEnvironments,
|
return rest;
|
||||||
contextFields: filteredContextFields,
|
}),
|
||||||
|
featureStrategies: featureStrategies.map((item) => ({
|
||||||
|
name: item.strategyName,
|
||||||
|
...item,
|
||||||
|
})),
|
||||||
|
featureEnvironments: featureEnvironments.map((item) => ({
|
||||||
|
...item,
|
||||||
|
name: item.featureName,
|
||||||
|
})),
|
||||||
|
contextFields: filteredContextFields.map((item) => {
|
||||||
|
const { createdAt, ...rest } = item;
|
||||||
|
return rest;
|
||||||
|
}),
|
||||||
featureTags,
|
featureTags,
|
||||||
segments: filteredSegments,
|
segments: filteredSegments,
|
||||||
};
|
};
|
||||||
@ -169,48 +164,6 @@ export default class ExportImportService {
|
|||||||
.map((segment) => segment.segmentId);
|
.map((segment) => segment.segmentId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(dto: IImportDTO, user: User): Promise<void> {
|
|
||||||
await Promise.all(
|
|
||||||
dto.data.features.map((feature) =>
|
|
||||||
this.featureToggleService.createFeatureToggle(
|
|
||||||
dto.project,
|
|
||||||
feature,
|
|
||||||
user.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
dto.data.featureStrategies.map((featureStrategy) =>
|
|
||||||
this.featureToggleService.unprotectedCreateStrategy(
|
|
||||||
{
|
|
||||||
name: featureStrategy.strategyName,
|
|
||||||
constraints: featureStrategy.constraints,
|
|
||||||
parameters: featureStrategy.parameters,
|
|
||||||
segments: featureStrategy.segments,
|
|
||||||
sortOrder: featureStrategy.sortOrder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
featureName: featureStrategy.featureName,
|
|
||||||
environment: dto.environment,
|
|
||||||
projectId: dto.project,
|
|
||||||
},
|
|
||||||
user.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
dto.data.featureEnvironments.map((featureEnvironment) =>
|
|
||||||
this.featureToggleService.unprotectedUpdateEnabled(
|
|
||||||
dto.project,
|
|
||||||
featureEnvironment.featureName,
|
|
||||||
dto.environment,
|
|
||||||
featureEnvironment.enabled,
|
|
||||||
user.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ExportImportService;
|
module.exports = ExportImportService;
|
||||||
|
@ -6,17 +6,14 @@ import dbInit, { ITestDb } from '../../helpers/database-init';
|
|||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { IEventStore } from 'lib/types/stores/event-store';
|
import { IEventStore } from 'lib/types/stores/event-store';
|
||||||
import {
|
import {
|
||||||
FeatureToggle,
|
|
||||||
FeatureToggleDTO,
|
FeatureToggleDTO,
|
||||||
IEnvironmentStore,
|
IEnvironmentStore,
|
||||||
IFeatureStrategy,
|
|
||||||
IFeatureToggleStore,
|
IFeatureToggleStore,
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
ISegment,
|
ISegment,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
} from 'lib/types';
|
} from 'lib/types';
|
||||||
import { DEFAULT_ENV } from '../../../../lib/util';
|
import { DEFAULT_ENV } from '../../../../lib/util';
|
||||||
import { IImportDTO } from '../../../../lib/services/export-import-service';
|
|
||||||
import { ContextFieldSchema } from '../../../../lib/openapi';
|
import { ContextFieldSchema } from '../../../../lib/openapi';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
@ -185,7 +182,6 @@ test('exports features', async () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
environment: 'default',
|
environment: 'default',
|
||||||
featureName: 'first_feature',
|
featureName: 'first_feature',
|
||||||
variants: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
segments: [
|
segments: [
|
||||||
@ -248,7 +244,6 @@ test('should export custom context fields', async () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
environment: 'default',
|
environment: 'default',
|
||||||
featureName: 'first_feature',
|
featureName: 'first_feature',
|
||||||
variants: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
contextFields: [context],
|
contextFields: [context],
|
||||||
@ -289,7 +284,6 @@ test('should export tags', async () => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
environment: 'default',
|
environment: 'default',
|
||||||
featureName: 'first_feature',
|
featureName: 'first_feature',
|
||||||
variants: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
featureTags: [{ featureName, tagValue: 'tag1' }],
|
featureTags: [{ featureName, tagValue: 'tag1' }],
|
||||||
@ -317,103 +311,3 @@ test('returns all features, when no feature was defined', async () => {
|
|||||||
|
|
||||||
expect(body.features).toHaveLength(2);
|
expect(body.features).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('import features to existing project and environment', async () => {
|
|
||||||
const feature = 'first_feature';
|
|
||||||
const project = 'new_project';
|
|
||||||
const environment = 'staging';
|
|
||||||
const variants = [
|
|
||||||
{
|
|
||||||
name: 'variantA',
|
|
||||||
weight: 500,
|
|
||||||
payload: {
|
|
||||||
type: 'string',
|
|
||||||
value: 'payloadA',
|
|
||||||
},
|
|
||||||
overrides: [],
|
|
||||||
stickiness: 'default',
|
|
||||||
weightType: 'variable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'variantB',
|
|
||||||
weight: 500,
|
|
||||||
payload: {
|
|
||||||
type: 'string',
|
|
||||||
value: 'payloadB',
|
|
||||||
},
|
|
||||||
overrides: [],
|
|
||||||
stickiness: 'default',
|
|
||||||
weightType: 'variable',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const exportedFeature: FeatureToggle = {
|
|
||||||
project: 'old_project',
|
|
||||||
name: 'first_feature',
|
|
||||||
variants,
|
|
||||||
};
|
|
||||||
const exportedStrategy: IFeatureStrategy = {
|
|
||||||
id: '798cb25a-2abd-47bd-8a95-40ec13472309',
|
|
||||||
featureName: feature,
|
|
||||||
projectId: 'old_project',
|
|
||||||
environment: 'old_environment',
|
|
||||||
strategyName: 'default',
|
|
||||||
parameters: {},
|
|
||||||
constraints: [],
|
|
||||||
};
|
|
||||||
const importPayload: IImportDTO = {
|
|
||||||
data: {
|
|
||||||
features: [exportedFeature],
|
|
||||||
featureStrategies: [exportedStrategy],
|
|
||||||
featureEnvironments: [
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
featureName: 'first_feature',
|
|
||||||
environment: 'irrelevant',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
contextFields: [],
|
|
||||||
segments: [],
|
|
||||||
},
|
|
||||||
project: project,
|
|
||||||
environment: environment,
|
|
||||||
};
|
|
||||||
await createProject(project, environment);
|
|
||||||
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/features-batch/import')
|
|
||||||
.send(importPayload)
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(201);
|
|
||||||
|
|
||||||
const { body: importedFeature } = await app.request
|
|
||||||
.get(`/api/admin/features/${feature}`)
|
|
||||||
.expect(200);
|
|
||||||
expect(importedFeature).toMatchObject({
|
|
||||||
name: 'first_feature',
|
|
||||||
project: project,
|
|
||||||
variants,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { body: importedFeatureEnvironment } = await app.request
|
|
||||||
.get(
|
|
||||||
`/api/admin/projects/${project}/features/${feature}/environments/${environment}`,
|
|
||||||
)
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(importedFeatureEnvironment).toMatchObject({
|
|
||||||
name: feature,
|
|
||||||
environment,
|
|
||||||
enabled: true,
|
|
||||||
strategies: [
|
|
||||||
{
|
|
||||||
featureName: feature,
|
|
||||||
projectId: project,
|
|
||||||
environment: environment,
|
|
||||||
parameters: {},
|
|
||||||
constraints: [],
|
|
||||||
sortOrder: 9999,
|
|
||||||
name: 'default',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
Loading…
Reference in New Issue
Block a user