diff --git a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts index 1398cfde4d..501a5b3411 100644 --- a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts +++ b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts @@ -24,12 +24,19 @@ import { import { FeatureFlagFilterModeUpdateValidator } from './validators/FeatureFlagFilterModeUpdateValidator'; import { AppRequest, PaginationResponse } from '../../types'; import { IImportError, FEATURE_FLAG_LIST_FILTER_MODE, SERVER_ERROR } from 'upgrade_types'; -import { FeatureFlagImportValidation, FeatureFlagValidation, IdValidator } from './validators/FeatureFlagValidator'; +import { + FeatureFlagImportValidation, + FeatureFlagListImportValidation, + FeatureFlagValidation, + IdValidator, +} from './validators/FeatureFlagValidator'; import { ExperimentUserService } from '../services/ExperimentUserService'; import { FeatureFlagListValidator } from '../controllers/validators/FeatureFlagListValidator'; import { Segment } from 'src/api/models/Segment'; import { Response } from 'express'; import { UserDTO } from '../DTO/UserDTO'; +import { ImportFeatureFlagListValidator } from './validators/FeatureFlagImportValidator'; +import { NotFoundException } from '@nestjs/common/exceptions'; interface FeatureFlagsPaginationInfo extends PaginationResponse { nodes: FeatureFlag[]; @@ -123,6 +130,19 @@ interface FeatureFlagsPaginationInfo extends PaginationResponse { * list: * type: object * $ref: '#/definitions/FeatureFlagInclusionExclusionList' + * FeatureFlagListImportObject: + * required: + * - files + * - listType + * - flagId + * properties: + * files: + * type: object + * listType: + * type: string + * enum: [featureFlagSegmentInclusion, featureFlagSegmentExclusion] + * flagId: + * type: string */ /** @@ -741,7 +761,7 @@ export class FeatureFlagsController { * @swagger * /flags/import: * post: - * description: Validating Feature Flag + * description: Importing Feature Flag * consumes: * - application/json * parameters: @@ -833,4 +853,199 @@ export class FeatureFlagsController { } return response.status(404).send('Feature Flag not found'); } + + /** + * @swagger + * /flags/lists/import/validation: + * post: + * description: Validating Feature Flag List + * consumes: + * - application/json + * parameters: + * - in: body + * name: lists + * description: Import FeatureFlag List Files + * required: true + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagListImportObject' + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Validations are completed + * schema: + * type: array + * items: + * type: object + * properties: + * fileName: + * type: string + * compatibilityType: + * type: string + * enum: [compatible, warning, incompatible] + * '401': + * description: AuthorizationRequiredError + * '500': + * description: Internal Server Error + */ + @Post('/lists/import/validation') + public async validateImportFeatureFlagList( + @Body({ validate: true }) lists: FeatureFlagListImportValidation, + @Req() request: AppRequest + ): Promise { + return await this.featureFlagService.validateImportFeatureFlagLists(lists.files, lists.flagId, request.logger); + } + + /** + * @swagger + * /flags/lists/import: + * post: + * description: Importing Feature Flag List + * consumes: + * - application/json + * parameters: + * - in: body + * name: lists + * description: Import FeatureFlag List Files + * required: true + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagListImportObject' + * tags: + * - Feature Flag Lists + * produces: + * - application/json + * responses: + * '200': + * description: New Feature flag is imported + * '401': + * description: AuthorizationRequiredError + * '500': + * description: Internal Server Error + */ + @Post('/lists/import') + public async importFeatureFlagLists( + @Body({ validate: true }) lists: FeatureFlagListImportValidation, + @CurrentUser() currentUser: UserDTO, + @Req() request: AppRequest + ): Promise { + return await this.featureFlagService.importFeatureFlagLists( + lists.files, + lists.flagId, + lists.listType, + currentUser, + request.logger + ); + } + + /** + * @swagger + * /flags/export/includeLists/{id}: + * get: + * description: Export All Include lists of Feature Flag JSON + * tags: + * - Feature Flags + * produces: + * - application/json + * parameters: + * - in: path + * flagId: Id + * description: Feature Flag Id + * required: true + * schema: + * type: string + * responses: + * '200': + * description: Get Feature Flag''s All Include Lists JSON + * '401': + * description: Authorization Required Error + * '404': + * description: Feature Flag not found + * '400': + * description: id must be a UUID + * '500': + * description: Internal Server Error + */ + @Get('/export/includeLists/:id') + public async exportAllIncludeLists( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest, + @Res() response: Response + ): Promise { + const lists = await this.featureFlagService.exportAllLists( + id, + FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION, + request.logger + ); + if (lists?.length) { + // download JSON file with appropriate headers to response body; + if (lists.length === 1) { + response.setHeader('Content-Disposition', `attachment; filename="${lists[0].segment.name}.json"`); + } else { + response.setHeader('Content-Disposition', `attachment; filename="lists.zip"`); + } + response.setHeader('Content-Type', 'application/json'); + } else { + throw new NotFoundException('Include lists not found.'); + } + + return lists; + } + + /** + * @swagger + * /flags/export/excludeLists/{id}: + * get: + * description: Export All Exclude lists of Feature Flag JSON + * tags: + * - Feature Flags + * produces: + * - application/json + * parameters: + * - in: path + * flagId: Id + * description: Feature Flag Id + * required: true + * schema: + * type: string + * responses: + * '200': + * description: Get Feature Flag''s All Include Lists JSON + * '401': + * description: Authorization Required Error + * '404': + * description: Feature Flag not found + * '400': + * description: id must be a UUID + * '500': + * description: Internal Server Error + */ + @Get('/export/excludeLists/:id') + public async exportAllExcludeLists( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest, + @Res() response: Response + ): Promise { + const lists = await this.featureFlagService.exportAllLists( + id, + FEATURE_FLAG_LIST_FILTER_MODE.EXCLUSION, + request.logger + ); + if (lists?.length) { + // download JSON file with appropriate headers to response body; + if (lists.length === 1) { + response.setHeader('Content-Disposition', `attachment; filename="${lists[0].segment.name}.json"`); + } else { + response.setHeader('Content-Disposition', `attachment; filename="lists.zip"`); + } + response.setHeader('Content-Type', 'application/json'); + } else { + throw new NotFoundException('Exclude lists not found.'); + } + + return lists; + } } diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagImportValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagImportValidator.ts index ce5c382789..387be692f7 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagImportValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagImportValidator.ts @@ -12,6 +12,7 @@ import { import { SEGMENT_TYPE } from 'upgrade_types'; import { Type } from 'class-transformer'; import { FeatureFlagCoreValidation } from './FeatureFlagValidator'; +import { SegmentInputValidator } from './SegmentInputValidator'; class IndividualValidator { @IsNotEmpty() @@ -88,7 +89,7 @@ class SegmentImportValidator { public subSegments: SegmentValidator[]; } -class FeatureFlagListImportValidator { +export class FeatureFlagListImportValidator { @IsDefined() @IsBoolean() public enabled: boolean; @@ -101,6 +102,15 @@ class FeatureFlagListImportValidator { public segment: SegmentImportValidator; } +export class ImportFeatureFlagListValidator { + @IsNotEmpty() + public listType: string; + + @ValidateNested() + @Type(() => SegmentInputValidator) + public segment: SegmentInputValidator; +} + export class FeatureFlagImportDataValidation extends FeatureFlagCoreValidation { @IsOptional() @IsArray() diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts index ebe4ade568..273d663b1a 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts @@ -1,6 +1,5 @@ import { IsNotEmpty, IsDefined, IsString, IsArray, IsEnum, IsOptional, ValidateNested, IsUUID } from 'class-validator'; -import { FILTER_MODE } from 'upgrade_types'; -import { FEATURE_FLAG_STATUS } from 'upgrade_types'; +import { FILTER_MODE, FEATURE_FLAG_STATUS, FEATURE_FLAG_LIST_FILTER_MODE } from 'upgrade_types'; import { Type } from 'class-transformer'; import { FeatureFlagListValidator } from './FeatureFlagListValidator'; @@ -67,6 +66,22 @@ export class FeatureFlagImportValidation { public files: FeatureFlagFile[]; } +export class FeatureFlagListImportValidation { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FeatureFlagFile) + public files: FeatureFlagFile[]; + + @IsString() + @IsNotEmpty() + @IsEnum(FEATURE_FLAG_LIST_FILTER_MODE) + public listType: FEATURE_FLAG_LIST_FILTER_MODE; + + @IsUUID() + @IsNotEmpty() + public flagId: string; +} + class FeatureFlagFile { @IsString() @IsNotEmpty() diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index 561e81f075..361211397a 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -10,6 +10,7 @@ import { FeatureFlagSegmentExclusionRepository } from '../repositories/FeatureFl import { EntityManager, In, DataSource } from 'typeorm'; import { InjectDataSource, InjectRepository } from '../../typeorm-typedi-extensions'; import { v4 as uuid } from 'uuid'; +import { env } from '../../env'; import { IFeatureFlagSearchParams, IFeatureFlagSortParams, @@ -43,12 +44,16 @@ import { ErrorWithType } from '../errors/ErrorWithType'; import { RequestedExperimentUser } from '../controllers/validators/ExperimentUserValidator'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; -import { FeatureFlagImportDataValidation } from '../controllers/validators/FeatureFlagImportValidator'; +import { + FeatureFlagImportDataValidation, + ImportFeatureFlagListValidator, +} from '../controllers/validators/FeatureFlagImportValidator'; import { ExperimentAuditLogRepository } from '../repositories/ExperimentAuditLogRepository'; import { UserDTO } from '../DTO/UserDTO'; import { diffString } from 'json-diff'; import { SegmentRepository } from '../repositories/SegmentRepository'; import { ExperimentAuditLog } from '../models/ExperimentAuditLog'; +import { NotFoundException } from '@nestjs/common/exceptions'; @Service() export class FeatureFlagService { @@ -1037,7 +1042,7 @@ export class FeatureFlagService { return validationErrors .map((result) => { if (result.status === 'fulfilled') { - return result.value; + return result.value ? result.value : null; } else { const { fileName, compatibilityType } = result.reason; return { fileName: fileName, compatibilityType: compatibilityType }; @@ -1100,4 +1105,207 @@ export class FeatureFlagService { compatibilityType: compatibilityType, }; } + + public async importFeatureFlagLists( + featureFlagListFiles: IFeatureFlagFile[], + featureFlagId: string, + listType: FEATURE_FLAG_LIST_FILTER_MODE, + currentUser: UserDTO, + logger: UpgradeLogger + ): Promise { + logger.info({ message: 'Import feature flags' }); + const validatedFlags = await this.validateImportFeatureFlagLists(featureFlagListFiles, featureFlagId, logger); + + const fileStatusArray = featureFlagListFiles.map((file) => { + const validation = validatedFlags.find((error) => error.fileName === file.fileName); + const isCompatible = validation && validation.compatibilityType !== FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + + return { + fileName: file.fileName, + error: isCompatible ? validation.compatibilityType : FF_COMPATIBILITY_TYPE.INCOMPATIBLE, + }; + }); + + const validFiles: ImportFeatureFlagListValidator[] = fileStatusArray + .filter((fileStatus) => fileStatus.error !== FF_COMPATIBILITY_TYPE.INCOMPATIBLE) + .map((fileStatus) => { + const featureFlagListFile = featureFlagListFiles.find((file) => file.fileName === fileStatus.fileName); + return JSON.parse(featureFlagListFile.fileContent as string); + }); + + const createdLists: (FeatureFlagSegmentInclusion | FeatureFlagSegmentExclusion)[] = + await this.dataSource.transaction(async (transactionalEntityManager) => { + const listDocs: FeatureFlagListValidator[] = []; + for (const list of validFiles) { + const listDoc: FeatureFlagListValidator = { + ...list, + enabled: false, + flagId: featureFlagId, + segment: { ...list.segment, id: uuid() }, + }; + + listDocs.push(listDoc); + } + + return await this.addList(listDocs, listType, currentUser, logger, transactionalEntityManager); + }); + + logger.info({ message: 'Imported feature flags', details: createdLists }); + + fileStatusArray.forEach((fileStatus) => { + if (fileStatus.error !== FF_COMPATIBILITY_TYPE.INCOMPATIBLE) { + fileStatus.error = null; + } + }); + return fileStatusArray; + } + + public async validateImportFeatureFlagLists( + featureFlagFiles: IFeatureFlagFile[], + featureFlagId: string, + logger: UpgradeLogger + ): Promise { + logger.info({ message: 'Validate feature flag lists' }); + + const parsedFeatureFlagLists = featureFlagFiles.map((featureFlagFile) => { + try { + return { + fileName: featureFlagFile.fileName, + content: JSON.parse(featureFlagFile.fileContent as string), + }; + } catch (parseError) { + logger.error({ message: 'Error in parsing feature flag file', details: parseError }); + return { + fileName: featureFlagFile.fileName, + content: null, + }; + } + }); + + const featureFlag = await this.findOne(featureFlagId, logger); + + const validationErrors = await Promise.allSettled( + parsedFeatureFlagLists.map(async (parsedFile) => { + if (!featureFlag || !parsedFile.content) { + return { + fileName: parsedFile.fileName, + compatibilityType: FF_COMPATIBILITY_TYPE.INCOMPATIBLE, + }; + } + + return this.validateImportFeatureFlagList(parsedFile.fileName, featureFlag, parsedFile.content); + }) + ); + + // Filter out the files that have no promise rejection errors + return validationErrors + .map((result) => { + if (result.status === 'fulfilled') { + return result.value ? result.value : null; + } else { + const { fileName, compatibilityType } = result.reason; + return { fileName: fileName, compatibilityType: compatibilityType }; + } + }) + .filter((error) => error !== null); + } + + public async validateImportFeatureFlagList( + fileName: string, + flag: FeatureFlag, + list: ImportFeatureFlagListValidator + ) { + let compatibilityType = FF_COMPATIBILITY_TYPE.COMPATIBLE; + + list = plainToClass(ImportFeatureFlagListValidator, list); + await validate(list, { forbidUnknownValues: true, stopAtFirstError: true }).then((errors) => { + if (errors.length > 0) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + }); + + if (!(list instanceof ImportFeatureFlagListValidator)) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + + if (list?.segment?.context !== flag.context[0]) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + + if (compatibilityType === FF_COMPATIBILITY_TYPE.COMPATIBLE) { + if (list.listType === 'Segment') { + const segments = await this.segmentService.getSegmentByIds(list.segment.subSegmentIds); + + if (!segments.length) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + + segments?.forEach((segment) => { + if (!segment || segment.context !== flag.context[0]) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + }); + } else if (list.listType !== 'Individual' && list.segment.groups.length) { + const contextMetaData = env.initialization.contextMetadata; + const groupTypes = contextMetaData[flag.context[0]].GROUP_TYPES; + if (!groupTypes.includes(list.listType)) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + + list.segment.groups.forEach((group) => { + if (group.type !== list.listType) { + compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE; + } + }); + } + } + + return { + fileName: fileName, + compatibilityType: compatibilityType, + }; + } + + public async exportAllLists( + id: string, + listType: FEATURE_FLAG_LIST_FILTER_MODE, + logger: UpgradeLogger + ): Promise { + const featureFlag = await this.findOne(id, logger); + let listsArray: ImportFeatureFlagListValidator[] = []; + if (featureFlag) { + let lists: (FeatureFlagSegmentExclusion | FeatureFlagSegmentExclusion)[] = []; + if (listType === FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION) { + lists = featureFlag.featureFlagSegmentInclusion; + } else if (listType === FEATURE_FLAG_LIST_FILTER_MODE.EXCLUSION) { + lists = featureFlag.featureFlagSegmentExclusion; + } else { + return null; + } + + if (!lists.length) return []; + + listsArray = lists.map((list) => { + const { name, description, context, type } = list.segment; + + const userIds = list.segment.individualForSegment.map((individual) => individual.userId); + + const subSegmentIds = list.segment.subSegments.map((subSegment) => subSegment.id); + + const groups = list.segment.groupForSegment.map((group) => { + return { type: group.type, groupId: group.groupId }; + }); + + const listDoc: ImportFeatureFlagListValidator = { + listType: list.listType, + segment: { name, description, context, type, userIds, subSegmentIds, groups }, + }; + return listDoc; + }); + } else { + throw new NotFoundException('Experiment not found.'); + } + + return listsArray; + } } diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts index bf06dadc19..2cffcc4a08 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts @@ -13,7 +13,7 @@ import { } from './store/feature-flags.model'; import { Observable, delay, of } from 'rxjs'; import { AddPrivateSegmentListRequest, EditPrivateSegmentListRequest } from '../segments/store/segments.model'; -import { IFeatureFlagFile } from 'upgrade_types'; +import { FEATURE_FLAG_LIST_FILTER_MODE, IFeatureFlagFile } from 'upgrade_types'; @Injectable() export class FeatureFlagsDataService { @@ -45,9 +45,15 @@ export class FeatureFlagsDataService { return this.http.put(url, flag); } - validateFeatureFlag(featureFlag: { files: IFeatureFlagFile[] }) { + validateFeatureFlag(featureFlags: { files: IFeatureFlagFile[] }) { const url = this.environment.api.validateFeatureFlag; - return this.http.post(url, featureFlag); + return this.http.post(url, featureFlags); + } + + validateFeatureFlagList(files: IFeatureFlagFile[], flagId: string, listType: FEATURE_FLAG_LIST_FILTER_MODE) { + const lists = { files: files, listType: listType, flagId: flagId }; + const url = this.environment.api.validateFeatureFlagList; + return this.http.post(url, lists); } importFeatureFlag(featureFlag: { files: IFeatureFlagFile[] }) { @@ -55,6 +61,12 @@ export class FeatureFlagsDataService { return this.http.post(url, featureFlag); } + importFeatureFlagList(files: IFeatureFlagFile[], flagId: string, listType: FEATURE_FLAG_LIST_FILTER_MODE) { + const lists = { files: files, listType: listType, flagId: flagId }; + const url = this.environment.api.importFeatureFlagList; + return this.http.post(url, lists); + } + updateFilterMode(params: UpdateFilterModeRequest): Observable { const url = this.environment.api.updateFilterMode; return this.http.patch(url, params); @@ -77,6 +89,16 @@ export class FeatureFlagsDataService { return this.http.get(url); } + exportAllIncludeListsDesign(id: string) { + const url = `${this.environment.api.exportFFAllIncludeListsDesign}/${id}`; + return this.http.get(url); + } + + exportAllExcludeListsDesign(id: string) { + const url = `${this.environment.api.exportFFAllExcludeListsDesign}/${id}`; + return this.http.get(url); + } + deleteFeatureFlag(id: string) { const url = `${this.environment.api.featureFlag}/${id}`; return this.http.delete(url); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts index d721bd47d3..2d85183a4b 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts @@ -164,6 +164,14 @@ export class FeatureFlagsService { this.store$.dispatch(FeatureFlagsActions.actionExportFeatureFlagDesign({ featureFlagId })); } + exportAllIncludeListsData(featureFlagId: string) { + this.store$.dispatch(FeatureFlagsActions.actionExportAllIncludeListsDesign({ featureFlagId })); + } + + exportAllExcludeListsData(featureFlagId: string) { + this.store$.dispatch(FeatureFlagsActions.actionExportAllExcludeListsDesign({ featureFlagId })); + } + setSearchKey(searchKey: FLAG_SEARCH_KEY) { this.localStorageService.setItem(FeatureFlagLocalStorageKeys.FEATURE_FLAG_SEARCH_KEY, searchKey); this.store$.dispatch(FeatureFlagsActions.actionSetSearchKey({ searchKey })); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts index ab00140bbb..8eb5cc2bf2 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts @@ -94,6 +94,32 @@ export const actionExportFeatureFlagDesignSuccess = createAction('[Feature Flags export const actionExportFeatureFlagDesignFailure = createAction('[Feature Flags] Export Feature Flag Design Failure'); +export const actionExportAllIncludeListsDesign = createAction( + '[Feature Flags] Export All Include Lists Design', + props<{ featureFlagId: string }>() +); + +export const actionExportAllIncludeListsDesignSuccess = createAction( + '[Feature Flags] Export All Include Lists Design Success' +); + +export const actionExportAllIncludeListsDesignFailure = createAction( + '[Feature Flags] Export All Include Lists Design Failure' +); + +export const actionExportAllExcludeListsDesign = createAction( + '[Feature Flags] Export All Exclude Lists Design', + props<{ featureFlagId: string }>() +); + +export const actionExportAllExcludeListsDesignSuccess = createAction( + '[Feature Flags] Export All Exclude Lists Design Success' +); + +export const actionExportAllExcludeListsDesignFailure = createAction( + '[Feature Flags] Export All Exclude Lists Design Failure' +); + export const actionSetIsLoadingFeatureFlags = createAction( '[Feature Flags] Set Is Loading Flags', props<{ isLoadingFeatureFlags: boolean }>() diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts index a0894d9ee8..43aaf8b03c 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts @@ -336,5 +336,49 @@ export class FeatureFlagsEffects { ) ); + exportAllIncludeListsDesign$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionExportAllIncludeListsDesign), + map((action) => ({ featureFlagId: action.featureFlagId })), + switchMap(({ featureFlagId }) => + this.featureFlagsDataService.exportAllIncludeListsDesign(featureFlagId).pipe( + map((exportedAllListsDesign: any[]) => { + if (exportedAllListsDesign.length) { + this.commonExportHelpersService.convertDataToDownload(exportedAllListsDesign, 'Lists'); + this.notificationService.showSuccess('Feature Flag Design JSON downloaded!'); + } + return FeatureFlagsActions.actionExportAllIncludeListsDesignSuccess(); + }), + catchError((error) => { + this.notificationService.showError('Failed to export All include lists Design'); + return of(FeatureFlagsActions.actionExportAllIncludeListsDesignFailure()); + }) + ) + ) + ) + ); + + exportAllExcludeListsDesign$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionExportAllExcludeListsDesign), + map((action) => ({ featureFlagId: action.featureFlagId })), + switchMap(({ featureFlagId }) => + this.featureFlagsDataService.exportAllExcludeListsDesign(featureFlagId).pipe( + map((exportedAllListsDesign: any[]) => { + if (exportedAllListsDesign) { + this.commonExportHelpersService.convertDataToDownload(exportedAllListsDesign, 'Lists'); + this.notificationService.showSuccess('Feature Flag Design JSON downloaded!'); + } + return FeatureFlagsActions.actionExportAllExcludeListsDesignSuccess(); + }), + catchError((error) => { + this.notificationService.showError('Failed to export All exclude lists Design'); + return of(FeatureFlagsActions.actionExportAllExcludeListsDesignFailure()); + }) + ) + ) + ) + ); + private getSearchString$ = () => this.store$.pipe(select(selectSearchString)).pipe(first()); } diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts index 713fc93478..c04f82e479 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts @@ -159,11 +159,6 @@ export enum FEATURE_FLAG_DETAILS_PAGE_ACTIONS { EMAIL_DATA = 'Email Feature Flag Data', } -export enum FEATURE_FLAG_PARTICIPANT_LIST_KEY { - INCLUDE = 'featureFlagSegmentInclusion', - EXCLUDE = 'featureFlagSegmentExclusion', -} - export const FLAG_ROOT_COLUMN_NAMES = { NAME: 'name', STATUS: 'status', diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts index 65078e1dfc..8738e72137 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts @@ -1,6 +1,13 @@ import { AppState } from '../../core.state'; import { EntityState } from '@ngrx/entity'; -import { SEGMENT_TYPE, SEGMENT_STATUS, SEGMENT_SEARCH_KEY, SORT_AS_DIRECTION, SEGMENT_SORT_KEY } from 'upgrade_types'; +import { + SEGMENT_TYPE, + SEGMENT_STATUS, + SEGMENT_SEARCH_KEY, + SORT_AS_DIRECTION, + SEGMENT_SORT_KEY, + FEATURE_FLAG_LIST_FILTER_MODE, +} from 'upgrade_types'; import { ParticipantListTableRow } from '../../feature-flags/store/feature-flags.model'; export { SEGMENT_STATUS }; @@ -165,6 +172,11 @@ export interface UpsertPrivateSegmentListParams { flagId: string; } +export interface ImportListParams { + listType: FEATURE_FLAG_LIST_FILTER_MODE; + flagId: string; +} + export enum LIST_OPTION_TYPE { INDIVIDUAL = 'Individual', SEGMENT = 'Segment', diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html index 7f1581c123..d710100920 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html @@ -16,12 +16,24 @@ >

- {{ 'feature-flags.import-feature-flag.message.text' | translate }} - Learn more + + {{ 'feature-flags.import-feature-flag.message.text' | translate }} + + + {{ 'feature-flags.import-feature-flag-list.message.text' | translate }} + + Learn more

- +
@@ -32,22 +44,26 @@ [disabled]="element.compatibilityType === 'compatible'" (click)="toggleExpand()" > - {{ element.compatibilityType === 'compatible' || !isDescriptionExpanded ? 'chevron_right' : 'expand_more' }} + {{ + element.compatibilityType === 'compatible' || !isDescriptionExpanded ? 'chevron_right' : 'expand_more' + }} - - + + - + @@ -56,23 +72,33 @@ - +
File Name {{element.fileName}} File Name{{ element.fileName }} Compatibility Type Compatibility Type - +
- {{'feature-flags.import-flag-modal.compatibility-description.incompatible.text' | translate}} + + {{ 'feature-flags.import-flag-modal.compatibility-description.incompatible.text' | translate }} + + + {{ 'feature-flags.import-flag-list-modal.compatibility-description.incompatible.text' | translate }} +
- {{'feature-flags.import-flag-modal.compatibility-description.warning.text' | translate}} + + {{ 'feature-flags.import-flag-modal.compatibility-description.warning.text' | translate }} + + + {{ 'feature-flags.import-flag-list-modal.compatibility-description.warning.text' | translate }} +
- + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts index 51413cc912..7f7b832931 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts @@ -13,7 +13,7 @@ import { CommonModalConfig } from '../../../../../shared-standalone-component-li import { FeatureFlagsService } from '../../../../../core/feature-flags/feature-flags.service'; import { MatTableDataSource } from '@angular/material/table'; import { ValidateFeatureFlagError } from '../../../../../core/feature-flags/store/feature-flags.model'; -import { importError } from '../../../../../core/segments/store/segments.model'; +import { importError, ImportListParams } from '../../../../../core/segments/store/segments.model'; import { NotificationService } from '../../../../../core/notifications/notification.service'; import { IFeatureFlagFile } from 'upgrade_types'; @@ -47,7 +47,7 @@ export class ImportFeatureFlagModalComponent { ]).pipe(map(([uploadedCount, isLoading]) => isLoading || uploadedCount === 0)); constructor( - @Inject(MAT_DIALOG_DATA) public data: CommonModalConfig, + @Inject(MAT_DIALOG_DATA) public data: CommonModalConfig, public featureFlagsService: FeatureFlagsService, public featureFlagsDataService: FeatureFlagsDataService, public dialogRef: MatDialogRef, @@ -86,9 +86,22 @@ export class ImportFeatureFlagModalComponent { async checkValidation(files: IFeatureFlagFile[]) { try { - const validationErrors = (await firstValueFrom( - this.featureFlagsDataService.validateFeatureFlag({ files: files }) - )) as ValidateFeatureFlagError[]; + let validationErrors: ValidateFeatureFlagError[]; + + if (this.data.title === 'Import Feature Flag') { + validationErrors = (await firstValueFrom( + this.featureFlagsDataService.validateFeatureFlag({ files: files }) + )) as ValidateFeatureFlagError[]; + } else if (this.data.title === 'Import List') { + validationErrors = (await firstValueFrom( + this.featureFlagsDataService.validateFeatureFlagList( + files, + this.data.params.flagId, + this.data.params.listType + ) + )) as ValidateFeatureFlagError[]; + } + this.fileValidationErrors = validationErrors.filter((data) => data.compatibilityType != null) || []; this.fileValidationErrorDataSource.data = this.fileValidationErrors; this.featureFlagsService.setIsLoadingImportFeatureFlag(false); @@ -113,9 +126,21 @@ export class ImportFeatureFlagModalComponent { async importFiles() { try { this.isImportActionBtnDisabled.next(true); - const importResult = (await firstValueFrom( - this.featureFlagsDataService.importFeatureFlag({ files: this.fileData }) - )) as importError[]; + let importResult: importError[]; + + if (this.data.title === 'Import Feature Flag') { + importResult = (await firstValueFrom( + this.featureFlagsDataService.importFeatureFlag({ files: this.fileData }) + )) as importError[]; + } else if (this.data.title === 'Import List') { + importResult = (await firstValueFrom( + this.featureFlagsDataService.importFeatureFlagList( + this.fileData, + this.data.params.flagId, + this.data.params.listType + ) + )) as importError[]; + } this.showNotification(importResult); this.isImportActionBtnDisabled.next(false); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html index 048e42d373..cdaf8d328c 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html @@ -12,8 +12,11 @@ header-right [showPrimaryButton]="(permissions$ | async)?.featureFlags.update" [primaryButtonText]="'feature-flags.details.add-exclude-list.button.text' | translate" + [showMenuButton]="(permissions$ | async)?.featureFlags.update" + [menuButtonItems]="menuButtonItems" [isSectionCardExpanded]="isSectionCardExpanded" (primaryButtonClick)="onAddExcludeListClick(flag.context[0], flag.id)" + (menuButtonItemClick)="onMenuButtonItemClick($event, flag)" (sectionCardExpandChange)="onSectionCardExpandChange($event)" > diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts index 1d8226fe66..1aa2262f72 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts @@ -11,6 +11,7 @@ import { FeatureFlagExclusionsTableComponent } from './feature-flag-exclusions-t import { FeatureFlagsService } from '../../../../../../../core/feature-flags/feature-flags.service'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; import { + FeatureFlag, PARTICIPANT_LIST_ROW_ACTION, ParticipantListRowActionEvent, ParticipantListTableRow, @@ -45,15 +46,16 @@ export class FeatureFlagExclusionsSectionCardComponent { tableRowCount$ = this.featureFlagService.selectFeatureFlagExclusionsLength$; selectedFlag$ = this.featureFlagService.selectedFeatureFlag$; + menuButtonItems: IMenuButtonItem[] = [ + { name: 'Import Exclude List', disabled: false }, + { name: 'Export All Exclude Lists', disabled: false }, + ]; + constructor( private featureFlagService: FeatureFlagsService, private dialogService: DialogService, private authService: AuthService ) {} - menuButtonItems: IMenuButtonItem[] = [ - // { name: 'Import Exclude List', disabled: false }, - // { name: 'Export All Exclude Lists', disabled: false }, - ]; ngOnInit() { this.permissions$ = this.authService.userPermissions$; @@ -63,8 +65,30 @@ export class FeatureFlagExclusionsSectionCardComponent { this.dialogService.openAddExcludeListModal(appContext, flagId); } - onMenuButtonItemClick(event) { - console.log('Menu Button Item Clicked:', event); + onMenuButtonItemClick(event, flag: FeatureFlag) { + const confirmMessage = 'feature-flags.export-all-exclude-lists-design.confirmation-text.text'; + switch (event) { + case 'Import Exclude List': + this.dialogService + .openImportFeatureFlagExcludeListModal(flag.id) + .afterClosed() + .subscribe(() => this.featureFlagService.fetchFeatureFlagById(flag.id)); + break; + case 'Export All Exclude Lists': + if (flag.featureFlagSegmentExclusion.length) { + this.dialogService + .openExportDesignModal('Export All Exclude Lists', confirmMessage) + .afterClosed() + .subscribe((isExportClicked: boolean) => { + if (isExportClicked) { + this.featureFlagService.exportAllExcludeListsData(flag.id); + } + }); + } + break; + default: + console.log('Unknown action'); + } } onSectionCardExpandChange(isSectionCardExpanded: boolean) { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-table/feature-flag-exclusions-table.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-table/feature-flag-exclusions-table.component.ts index c7982e5b84..7e98727a0c 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-table/feature-flag-exclusions-table.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-table/feature-flag-exclusions-table.component.ts @@ -1,12 +1,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FeatureFlagsService } from '../../../../../../../../core/feature-flags/feature-flags.service'; -import { - FEATURE_FLAG_PARTICIPANT_LIST_KEY, - ParticipantListRowActionEvent, -} from '../../../../../../../../core/feature-flags/store/feature-flags.model'; +import { ParticipantListRowActionEvent } from '../../../../../../../../core/feature-flags/store/feature-flags.model'; import { CommonDetailsParticipantListTableComponent } from '../../../../../../../../shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; +import { FEATURE_FLAG_LIST_FILTER_MODE } from 'upgrade_types'; @Component({ selector: 'app-feature-flag-exclusions-table', @@ -18,7 +16,7 @@ import { TranslateModule } from '@ngx-translate/core'; }) export class FeatureFlagExclusionsTableComponent { @Input() actionsDisabled?: boolean = false; - tableType = FEATURE_FLAG_PARTICIPANT_LIST_KEY.EXCLUDE; + tableType = FEATURE_FLAG_LIST_FILTER_MODE.EXCLUSION; dataSource$ = this.featureFlagService.selectFeatureFlagExclusions$; isLoading$ = this.featureFlagService.isLoadingSelectedFeatureFlag$; @Output() rowAction = new EventEmitter(); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html index 9de8dcea24..9ca33e15bc 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html @@ -16,10 +16,13 @@ [slideToggleDisabled]="!(permissions$ | async)?.featureFlags.update" [showPrimaryButton]="(permissions$ | async)?.featureFlags.update" [primaryButtonText]="'feature-flags.details.add-include-list.button.text' | translate" + [showMenuButton]="(permissions$ | async)?.featureFlags.update" + [menuButtonItems]="menuButtonItems" [isSectionCardExpanded]="isSectionCardExpanded && flag.filterMode !== FILTER_MODE.INCLUDE_ALL" [primaryActionBtnDisabled]="flag.filterMode === FILTER_MODE.INCLUDE_ALL" [sectionCardExpandBtnDisabled]="flag.filterMode === FILTER_MODE.INCLUDE_ALL" (primaryButtonClick)="onAddIncludeListClick(flag.context[0], flag.id)" + (menuButtonItemClick)="onMenuButtonItemClick($event, flag)" (slideToggleChange)="onSlideToggleChange($event, flag.id)" (sectionCardExpandChange)="onSectionCardExpandChange($event)" > diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts index 32e0eb2059..6a3da78569 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts @@ -15,6 +15,7 @@ import { MatDialogRef } from '@angular/material/dialog'; import { CommonSimpleConfirmationModalComponent } from '../../../../../../../shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component'; import { Observable, Subscription, combineLatest, map } from 'rxjs'; import { + FeatureFlag, PARTICIPANT_LIST_ROW_ACTION, ParticipantListRowActionEvent, ParticipantListTableRow, @@ -48,6 +49,12 @@ export class FeatureFlagInclusionsSectionCardComponent { tableRowCount$ = this.featureFlagService.selectFeatureFlagInclusionsLength$; selectedFlag$ = this.featureFlagService.selectedFeatureFlag$; + subscriptions = new Subscription(); + menuButtonItems: IMenuButtonItem[] = [ + { name: 'Import Include List', disabled: false }, + { name: 'Export All Include Lists', disabled: false }, + ]; + rowCountWithInclude$: Observable = combineLatest([this.tableRowCount$, this.selectedFlag$]).pipe( map(([tableRowCount, selectedFeatureFlag]) => selectedFeatureFlag?.filterMode === FILTER_MODE.INCLUDE_ALL ? 0 : tableRowCount @@ -63,11 +70,6 @@ export class FeatureFlagInclusionsSectionCardComponent { private dialogService: DialogService, private authService: AuthService ) {} - subscriptions = new Subscription(); - menuButtonItems: IMenuButtonItem[] = [ - // { name: 'Import Include List', disabled: false }, - // { name: 'Export All Include Lists', disabled: false }, - ]; confirmIncludeAllChangeDialogRef: MatDialogRef; @@ -121,8 +123,30 @@ export class FeatureFlagInclusionsSectionCardComponent { this.isSectionCardExpanded = newFilterMode !== FILTER_MODE.INCLUDE_ALL; } - onMenuButtonItemClick(event) { - console.log('Menu Button Item Clicked:', event); + onMenuButtonItemClick(event, flag: FeatureFlag) { + const confirmMessage = 'feature-flags.export-all-include-lists-design.confirmation-text.text'; + switch (event) { + case 'Import Include List': + this.dialogService + .openImportFeatureFlagIncludeListModal(flag.id) + .afterClosed() + .subscribe(() => this.featureFlagService.fetchFeatureFlagById(flag.id)); + break; + case 'Export All Include Lists': + if (flag.featureFlagSegmentInclusion.length) { + this.dialogService + .openExportDesignModal('Export All Include Lists', confirmMessage) + .afterClosed() + .subscribe((isExportClicked: boolean) => { + if (isExportClicked) { + this.featureFlagService.exportAllIncludeListsData(flag.id); + } + }); + } + break; + default: + console.log('Unknown action'); + } } onSectionCardExpandChange(isSectionCardExpanded: boolean) { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-table/feature-flag-inclusions-table.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-table/feature-flag-inclusions-table.component.ts index 5f89142211..003cef6a95 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-table/feature-flag-inclusions-table.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-table/feature-flag-inclusions-table.component.ts @@ -1,9 +1,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FeatureFlagsService } from '../../../../../../../../core/feature-flags/feature-flags.service'; -import { - FEATURE_FLAG_PARTICIPANT_LIST_KEY, - ParticipantListRowActionEvent, -} from '../../../../../../../../core/feature-flags/store/feature-flags.model'; +import { ParticipantListRowActionEvent } from '../../../../../../../../core/feature-flags/store/feature-flags.model'; +import { FEATURE_FLAG_LIST_FILTER_MODE } from 'upgrade_types'; import { CommonDetailsParticipantListTableComponent } from '../../../../../../../../shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; @@ -19,7 +17,7 @@ import { TranslateModule } from '@ngx-translate/core'; export class FeatureFlagInclusionsTableComponent { @Input() slideToggleDisabled?: boolean = false; @Input() actionsDisabled?: boolean = false; - tableType = FEATURE_FLAG_PARTICIPANT_LIST_KEY.INCLUDE; + tableType = FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION; dataSource$ = this.featureFlagService.selectFeatureFlagInclusions$; isLoading$ = this.featureFlagService.isLoadingSelectedFeatureFlag$; @Output() rowAction = new EventEmitter(); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts index b697c371be..212a8276eb 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts @@ -156,7 +156,7 @@ export class FeatureFlagOverviewDetailsSectionCardComponent implements OnInit, O openConfirmExportDesignModal(id: string) { const confirmMessage = 'feature-flags.export-feature-flag-design.confirmation-text.text'; this.dialogService - .openExportFeatureFlagDesignModal(confirmMessage) + .openExportDesignModal(FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EXPORT_DESIGN, confirmMessage) .afterClosed() .subscribe((isExportClicked: boolean) => { if (isExportClicked) { diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component.ts index 4289bb26df..e296e1afef 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component.ts @@ -10,12 +10,12 @@ import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { CommonStatusIndicatorChipComponent } from '../common-status-indicator-chip/common-status-indicator-chip.component'; import { - FEATURE_FLAG_PARTICIPANT_LIST_KEY, PARTICIPANT_LIST_ROW_ACTION, ParticipantListRowActionEvent, ParticipantListTableRow, } from '../../../core/feature-flags/store/feature-flags.model'; import { MemberTypes } from '../../../core/segments/store/segments.model'; +import { FEATURE_FLAG_LIST_FILTER_MODE } from 'upgrade_types'; /** * `CommonDetailsParticipantListTableComponent` is a reusable Angular component that displays a table with common details for participant lists. @@ -54,7 +54,7 @@ import { MemberTypes } from '../../../core/segments/store/segments.model'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CommonDetailsParticipantListTableComponent { - @Input() tableType: FEATURE_FLAG_PARTICIPANT_LIST_KEY; + @Input() tableType: FEATURE_FLAG_LIST_FILTER_MODE; @Input() dataSource: any[]; @Input() noDataRowText: string; @Input() slideToggleDisabled?: boolean = false; @@ -83,7 +83,7 @@ export class CommonDetailsParticipantListTableComponent { ngOnInit() { this.displayedColumns = - this.tableType === FEATURE_FLAG_PARTICIPANT_LIST_KEY.INCLUDE + this.tableType === FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION ? ['type', 'values', 'name', 'enable', 'actions'] : ['type', 'values', 'name', 'actions']; } diff --git a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index 11a9c5fa0f..8703dd83f1 100644 --- a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts +++ b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts @@ -23,6 +23,7 @@ import { ModalSize, SimpleConfirmationModalParams, } from '../../shared-standalone-component-lib/components/common-modal/common-modal.types'; +import { FEATURE_FLAG_LIST_FILTER_MODE } from 'upgrade_types'; @Injectable({ providedIn: 'root', @@ -320,9 +321,9 @@ export class DialogService { return this.dialog.open(DeleteFeatureFlagModalComponent, config); } - openExportFeatureFlagDesignModal(warning: string): MatDialogRef { + openExportDesignModal(title, warning: string): MatDialogRef { const commonModalConfig: CommonModalConfig = { - title: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EXPORT_DESIGN, + title: title, primaryActionBtnLabel: 'Export', primaryActionBtnColor: 'primary', cancelBtnLabel: 'Cancel', @@ -352,12 +353,29 @@ export class DialogService { } openImportFeatureFlagModal() { + return this.openImportModal('Import Feature Flag', null, null); + } + + openImportFeatureFlagIncludeListModal(flagId: string) { + return this.openImportModal('Import List', FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION, flagId); + } + + openImportFeatureFlagExcludeListModal(flagId: string) { + return this.openImportModal('Import List', FEATURE_FLAG_LIST_FILTER_MODE.EXCLUSION, flagId); + } + + openImportModal(title: string, listType: FEATURE_FLAG_LIST_FILTER_MODE, flagId: string) { const commonModalConfig: CommonModalConfig = { - title: 'Import Feature Flag', + title: title, primaryActionBtnLabel: 'Import', primaryActionBtnColor: 'primary', cancelBtnLabel: 'Cancel', + params: { + listType: listType, + flagId: flagId, + }, }; + const config: MatDialogConfig = { data: commonModalConfig, width: ModalSize.STANDARD, diff --git a/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts index 16aefb6ba6..dad0227db7 100644 --- a/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts +++ b/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts @@ -21,13 +21,15 @@ export class CommonExportHelpersService { if (data.length > 1) { const zip = new JSZip(); data.forEach((element, index) => { - zip.file(element.name + ' (File ' + (index + 1) + ').json', JSON.stringify(element)); + const fileName = element.name || element.segment.name; + zip.file(fileName + ' (file ' + (index + 1) + ').json', JSON.stringify(element)); }); zip.generateAsync({ type: 'base64' }).then((content) => { this.download(zipFileName + '.zip', content, true); }); } else { - this.download(data[0].name + '.json', data[0], false); + const filename = data[0].name || data[0].segment.name; + this.download(filename + '.json', data[0], false); } } } diff --git a/frontend/projects/upgrade/src/assets/i18n/en.json b/frontend/projects/upgrade/src/assets/i18n/en.json index a87d7d5381..112822aae5 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -371,6 +371,8 @@ "feature-flags.delete-flag.input-placeholder.text": "Feature flag name", "feature-flags.import-flag-modal.compatibility-description.incompatible.text": "This JSON file cannot be imported because it lacks required properties or it incorrectly formatted for the feature flag. Please ensure it is the correct file.", "feature-flags.import-flag-modal.compatibility-description.warning.text": "This JSON file can be imported, but it may contain outdated or missing properties/features. Please review the feature flag details post-import.", + "feature-flags.import-flag-list-modal.compatibility-description.incompatible.text": "This JSON file cannot be imported because it lacks required properties or it incorrectly formatted for the feature flag list. Please ensure it is the correct file.", + "feature-flags.import-flag-list-modal.compatibility-description.warning.text": "This JSON file can be imported, but it may contain outdated or missing properties/features. Please review the feature flag list details post-import.", "feature-flags.upsert-flag-modal.name-hint.text": "The name for this feature flag.", "feature-flags.upsert-flag-modal.key-hint.text": "A unique key used to retrieve this feature flag from the client application.", "feature-flags.upsert-flag-modal.duplicate-key-error.text": "Feature flag with this key already exists for this app-context.", @@ -409,13 +411,16 @@ "feature-flags.enable.text": "Enable", "feature-flags.add-feature-flag.text": "Add Feature Flag", "feature-flags.import-feature-flag.text": "Import Feature Flag", - "feature-flags.import-feature-flag.message.text": "The Feature Flag JSON file should include the required properties for it to be imported", + "feature-flags.import-feature-flag.message.text": "The Feature Flag JSON file should include the required properties for it to be imported.", + "feature-flags.import-feature-flag-list.message.text": "The List JSON file should include the required properties for it to be imported.", "feature-flags.export-all-feature-flags.text": "Export All Feature Flag Designs", "feature-flags.export-feature-flag-design.text": "Export Feature Flag Design", "feature-flags.export-feature-flags-data.text": "Email Feature Flag Data", "feature-flags.export-all-feature-flags.confirmation-text.text": "Are you sure you want to export all feature flags design (JSON)?", "feature-flags.export-feature-flag-design.confirmation-text.text": "Are you sure you want to export the feature flags design (JSON)?", "feature-flags.export-feature-flags-data.confirmation-text.text": "Are you sure you want to email the feature flags data (CSV)?", + "feature-flags.export-all-include-lists-design.confirmation-text.text": "Are you sure you want to export all include lists (JSON)?", + "feature-flags.export-all-exclude-lists-design.confirmation-text.text": "Are you sure you want to export all exclude lists (JSON)?", "feature-flags.upsert-list-modal.segment-placeholder.text": "Select segment", "feature-flags.upsert-list-modal.segment.error.message.text": "Segment must be selected from the available options.", "feature-flags.upsert-list-modal.values-label.text": "Values", @@ -464,6 +469,7 @@ "segments.import-segment.text": "IMPORT SEGMENT", "segments.import-segment.message.text": "Select the JSON file(s) to import segments:", "segments.import-segment.error.message.text": "Invalid Segment JSON data", + "segments.export-all-lists-design.confirmation-text.text": "Are you sure you want to export all lists (JSON)?", "segments.segment-experiment-list-title.text": "Used by ({{ numberOfUses }}) ", "segments.segment-experiment-list-name.text": "Name", "segments.segment-experiment-list-state.text": "Status", diff --git a/frontend/projects/upgrade/src/environments/environment-types.ts b/frontend/projects/upgrade/src/environments/environment-types.ts index 877fe95aac..65488672b3 100644 --- a/frontend/projects/upgrade/src/environments/environment-types.ts +++ b/frontend/projects/upgrade/src/environments/environment-types.ts @@ -29,11 +29,15 @@ export interface APIEndpoints { allExperimentNames: string; featureFlag: string; validateFeatureFlag: string; + validateFeatureFlagList: string; importFeatureFlag: string; + importFeatureFlagList: string; updateFlagStatus: string; updateFilterMode: string; getPaginatedFlags: string; exportFlagsDesign: string; + exportFFAllIncludeListsDesign: string; + exportFFAllExcludeListsDesign: string; emailFlagData: string; addFlagInclusionList: string; addFlagExclusionList: string; diff --git a/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts index a9731e8f28..72f956e62b 100644 --- a/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.bsnl.ts b/frontend/projects/upgrade/src/environments/environment.bsnl.ts index 474310a541..abee623819 100644 --- a/frontend/projects/upgrade/src/environments/environment.bsnl.ts +++ b/frontend/projects/upgrade/src/environments/environment.bsnl.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/email', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts index 916596ce1f..298607b665 100755 --- a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/email', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.prod.ts b/frontend/projects/upgrade/src/environments/environment.prod.ts index 1cee609963..ce859f8076 100755 --- a/frontend/projects/upgrade/src/environments/environment.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.prod.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/email', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.qa.ts b/frontend/projects/upgrade/src/environments/environment.qa.ts index 93bfba0e35..1f94489ca2 100644 --- a/frontend/projects/upgrade/src/environments/environment.qa.ts +++ b/frontend/projects/upgrade/src/environments/environment.qa.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.staging.ts b/frontend/projects/upgrade/src/environments/environment.staging.ts index 8f1ea164be..b2267cd631 100644 --- a/frontend/projects/upgrade/src/environments/environment.staging.ts +++ b/frontend/projects/upgrade/src/environments/environment.staging.ts @@ -47,8 +47,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/email', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/frontend/projects/upgrade/src/environments/environment.ts b/frontend/projects/upgrade/src/environments/environment.ts index 289a01c44b..16893e74d2 100755 --- a/frontend/projects/upgrade/src/environments/environment.ts +++ b/frontend/projects/upgrade/src/environments/environment.ts @@ -52,8 +52,12 @@ export const environment = { updateFilterMode: '/flags/filterMode', getPaginatedFlags: '/flags/paginated', validateFeatureFlag: '/flags/import/validation', + validateFeatureFlagList: '/flags/lists/import/validation', importFeatureFlag: '/flags/import', + importFeatureFlagList: '/flags/lists/import', exportFlagsDesign: '/flags/export', + exportFFAllIncludeListsDesign: '/flags/export/includeLists', + exportFFAllExcludeListsDesign: '/flags/export/excludeLists', emailFlagData: '/flags/email', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', diff --git a/types/src/index.ts b/types/src/index.ts index 8a81095a7a..18d0f4bc7a 100644 --- a/types/src/index.ts +++ b/types/src/index.ts @@ -35,6 +35,7 @@ export { FLAG_SORT_KEY, FLAG_SEARCH_KEY, FEATURE_FLAG_STATUS, + FEATURE_FLAG_PARTICIPANT_LIST_KEY, STATUS_INDICATOR_CHIP_TYPE, FILE_TYPE, } from './Experiment/enums';