diff --git a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts index 2366422a97..a0af65710d 100644 --- a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts +++ b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts @@ -1,4 +1,4 @@ -import { JsonController, Authorized, Post, Body, CurrentUser, Delete, Param, Put, Req } from 'routing-controllers'; +import { JsonController, Authorized, Post, Body, CurrentUser, Delete, Param, Put, Req, Get } from 'routing-controllers'; import { FeatureFlagService } from '../services/FeatureFlagService'; import { FeatureFlag } from '../models/FeatureFlag'; import { User } from '../models/User'; @@ -6,6 +6,7 @@ import { FeatureFlagStatusUpdateValidator } from './validators/FeatureFlagStatus import { FeatureFlagPaginatedParamsValidator } from './validators/FeatureFlagsPaginatedParamsValidator'; import { AppRequest, PaginationResponse } from '../../types'; import { SERVER_ERROR } from 'upgrade_types'; +import { isUUID } from 'class-validator'; import { FeatureFlagValidation } from './validators/FeatureFlagValidator'; interface FeatureFlagsPaginationInfo extends PaginationResponse { @@ -21,9 +22,8 @@ interface FeatureFlagsPaginationInfo extends PaginationResponse { * - name * - key * - description - * - variationType * - status - * - variations + * - context * properties: * id: * type: string @@ -33,29 +33,101 @@ interface FeatureFlagsPaginationInfo extends PaginationResponse { * type: string * description: * type: string - * variationType: - * type: string * status: - * type: boolean - * variations: - * type: array - * items: - * type: object - * properties: - * value: - * type: string - * name: - * type: string - * description: - * type: string - * defaultVariation: - * type: boolean[] + * type: string + * enum: [archived, enabled, disabled] + * context: + * type: array + * items: + * type: string + * tags: + * type: array + * items: + * type: string + * featureFlagSegmentInclusion: + * type: object + * properties: + * segment: + * type: object + * properties: + * type: + * type: string + * example: private + * individualForSegment: + * type: array + * items: + * type: object + * properties: + * userId: + * type: string + * example: user1 + * groupForSegment: + * type: array + * items: + * type: object + * properties: + * groupId: + * type: string + * example: school1 + * type: + * type: string + * example: schoolId + * subSegments: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * context: + * type: string + * featureFlagSegmentExclusion: + * type: object + * properties: + * segment: + * type: object + * properties: + * type: + * type: string + * example: private + * individualForSegment: + * type: array + * items: + * type: object + * properties: + * userId: + * type: string + * example: user1 + * groupForSegment: + * type: array + * items: + * type: object + * properties: + * groupId: + * type: string + * example: school1 + * type: + * type: string + * example: schoolId + * subSegments: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * context: + * type: string */ /** * @swagger * flags: - * - name: Feature flags + * - name: Feature Flags * description: Get Feature flags related data */ @@ -64,6 +136,71 @@ interface FeatureFlagsPaginationInfo extends PaginationResponse { export class FeatureFlagsController { constructor(public featureFlagService: FeatureFlagService) {} + /** + * @swagger + * /flags: + * get: + * description: Get all the feature flags + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Feature Flag List + * schema: + * type: array + * items: + * $ref: '#/definitions/FeatureFlag' + * '401': + * description: AuthorizationRequiredError + */ + + @Get() + public find(@Req() request: AppRequest): Promise { + return this.featureFlagService.find(request.logger); + } + + /** + * @swagger + * /flags/{id}: + * get: + * description: Get feature flag by id + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Feature Flag Id + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Get Feature Flag By Id + * schema: + * $ref: '#/definitions/FeatureFlag' + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Feature Flag not found + * '500': + * description: id should be of type UUID + */ + @Get('/:id') + public findOne(@Param('id') id: string, @Req() request: AppRequest): Promise { + if (!isUUID(id)) { + return Promise.reject( + new Error( + JSON.stringify({ type: SERVER_ERROR.INCORRECT_PARAM_FORMAT, message: ' : id should be of type UUID.' }) + ) + ); + } + return this.featureFlagService.findOne(id, request.logger); + } + /** * @swagger * /flags/paginated: @@ -90,7 +227,7 @@ export class FeatureFlagsController { * properties: * key: * type: string - * enum: [all, name, key, status, variation Type] + * enum: [all, name, key, status, tag, context] * string: * type: string * sortParams: @@ -98,18 +235,19 @@ export class FeatureFlagsController { * properties: * key: * type: string - * enum: [name, key, status, variationType] + * enum: [name, key, status, updatedAt] * sortAs: * type: string * enum: [ASC, DESC] * tags: - * - Feature flags + * - Feature Flags * produces: * - application/json * responses: * '200': - * description: Get Paginated Experiments + * description: Get Paginated Feature Flags */ + @Post('/paginated') public async paginatedFind( @Body({ validate: true }) @@ -157,7 +295,7 @@ export class FeatureFlagsController { * $ref: '#/definitions/FeatureFlag' * description: Feature flag structure * tags: - * - Feature flags + * - Feature Flags * produces: * - application/json * responses: @@ -182,19 +320,21 @@ export class FeatureFlagsController { * - application/json * parameters: * - in: body - * name: flagId - * required: true + * name: statusUpdate + * description: Updating the featur flag's status * schema: - * type: string - * description: Flag ID - * - in: body - * name: status - * required: true - * schema: - * type: boolean - * description: Flag State + * type: object + * required: + * - flagId + * - status + * properties: + * flagId: + * type: string + * status: + * type: string + * enum: [archived, enabled, disabled] * tags: - * - Feature flags + * - Feature Flags * produces: * - application/json * responses: @@ -222,7 +362,7 @@ export class FeatureFlagsController { * type: string * description: Feature flag Id * tags: - * - Feature flags + * - Feature Flags * produces: * - application/json * responses: @@ -265,7 +405,7 @@ export class FeatureFlagsController { * $ref: '#/definitions/FeatureFlag' * description: Feature Flag Structure * tags: - * - Feature flags + * - Feature Flags * produces: * - application/json * responses: diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts index 11bf780a61..0572cd1abf 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts @@ -1,10 +1,11 @@ -import { IsNotEmpty, IsDefined, IsString, IsArray, IsEnum } from 'class-validator'; +import { IsNotEmpty, IsDefined, IsString, IsArray, IsEnum, IsOptional, ValidateNested } from 'class-validator'; +import { ParticipantsValidator } from '../../DTO/ExperimentDTO'; import { FILTER_MODE } from 'upgrade_types'; import { FEATURE_FLAG_STATUS } from 'upgrade_types'; +import { Type } from 'class-transformer'; export class FeatureFlagValidation { - @IsNotEmpty() - @IsDefined() + @IsOptional() @IsString() id: string; @@ -13,8 +14,7 @@ export class FeatureFlagValidation { @IsString() name: string; - @IsNotEmpty() - @IsDefined() + @IsOptional() @IsString() description: string; @@ -40,4 +40,14 @@ export class FeatureFlagValidation { @IsNotEmpty() @IsArray() public tags: string[]; + + @IsNotEmpty() + @ValidateNested() + @Type(() => ParticipantsValidator) + public featureFlagSegmentInclusion: ParticipantsValidator; + + @IsNotEmpty() + @ValidateNested() + @Type(() => ParticipantsValidator) + public featureFlagSegmentExclusion: ParticipantsValidator; } diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator.ts index cdbcfaf59d..bfd0c2d54f 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator.ts @@ -4,26 +4,34 @@ import { SORT_AS_DIRECTION } from 'upgrade_types'; // TODO: Move to upgrade types export interface IFeatureFlagSearchParams { - key: FLAG_SEARCH_SORT_KEY; + key: FLAG_SEARCH_KEY; string: string; } export interface IFeatureFlagSortParams { - key: FLAG_SEARCH_SORT_KEY; + key: FLAG_SORT_KEY; sortAs: SORT_AS_DIRECTION; } -export enum FLAG_SEARCH_SORT_KEY { +export enum FLAG_SORT_KEY { + NAME = 'name', + KEY = 'key', + STATUS = 'status', + UPDATED_AT = 'updatedAt', +} + +export enum FLAG_SEARCH_KEY { ALL = 'all', NAME = 'name', KEY = 'key', STATUS = 'status', - VARIATION_TYPE = 'variationType', + TAG = 'tag', + CONTEXT = 'context', } class IFeatureFlagSortParamsValidator { @IsNotEmpty() - @IsEnum(FLAG_SEARCH_SORT_KEY) - key: FLAG_SEARCH_SORT_KEY; + @IsEnum(FLAG_SORT_KEY) + key: FLAG_SORT_KEY; @IsNotEmpty() @IsEnum(SORT_AS_DIRECTION) @@ -32,8 +40,8 @@ class IFeatureFlagSortParamsValidator { class IFeatureFlagSearchParamsValidator { @IsNotEmpty() - @IsEnum(FLAG_SEARCH_SORT_KEY) - key: FLAG_SEARCH_SORT_KEY; + @IsEnum(FLAG_SEARCH_KEY) + key: FLAG_SEARCH_KEY; @IsNotEmpty() @IsString() diff --git a/backend/packages/Upgrade/src/api/models/FeatureFlag.ts b/backend/packages/Upgrade/src/api/models/FeatureFlag.ts index b43adf8259..43a52c5078 100644 --- a/backend/packages/Upgrade/src/api/models/FeatureFlag.ts +++ b/backend/packages/Upgrade/src/api/models/FeatureFlag.ts @@ -1,8 +1,7 @@ -import { Column, Entity, PrimaryColumn, OneToMany, OneToOne } from 'typeorm'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; +import { Column, Entity, PrimaryColumn, OneToOne } from 'typeorm'; +import { IsNotEmpty } from 'class-validator'; import { BaseModel } from './base/BaseModel'; import { Type } from 'class-transformer'; -import { FlagVariation } from './FlagVariation'; import { FeatureFlagSegmentInclusion } from './FeatureFlagSegmentInclusion'; import { FeatureFlagSegmentExclusion } from './FeatureFlagSegmentExclusion'; import { FEATURE_FLAG_STATUS, FILTER_MODE } from 'upgrade_types'; @@ -43,11 +42,6 @@ export class FeatureFlag extends BaseModel { }) public filterMode: FILTER_MODE; - @OneToMany(() => FlagVariation, (variation) => variation.featureFlag) - @ValidateNested() - @Type(() => FlagVariation) - public variations: FlagVariation[]; - @OneToOne(() => FeatureFlagSegmentInclusion, (featureFlagSegmentInclusion) => featureFlagSegmentInclusion.featureFlag) @Type(() => FeatureFlagSegmentInclusion) public featureFlagSegmentInclusion: FeatureFlagSegmentInclusion; diff --git a/backend/packages/Upgrade/src/api/models/FlagVariation.ts b/backend/packages/Upgrade/src/api/models/FlagVariation.ts deleted file mode 100644 index bf36e27903..0000000000 --- a/backend/packages/Upgrade/src/api/models/FlagVariation.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; -import { IsNotEmpty } from 'class-validator'; -import { BaseModel } from './base/BaseModel'; -import { FeatureFlag } from './FeatureFlag'; - -@Entity() -export class FlagVariation extends BaseModel { - @PrimaryColumn('uuid') - public id: string; - - @Column() - @IsNotEmpty() - public value: string; - - @Column({ - nullable: true, - }) - public name: string; - - @Column({ - nullable: true, - }) - public description: string; - - @Column('boolean', { - nullable: true, - array: true, - }) - public defaultVariation: boolean[]; - - @ManyToOne(() => FeatureFlag, (flag) => flag.variations, { onDelete: 'CASCADE' }) - public featureFlag: FeatureFlag; -} diff --git a/backend/packages/Upgrade/src/api/models/IndividualEnrollment.ts b/backend/packages/Upgrade/src/api/models/IndividualEnrollment.ts index 038d89d122..7c376a6b90 100644 --- a/backend/packages/Upgrade/src/api/models/IndividualEnrollment.ts +++ b/backend/packages/Upgrade/src/api/models/IndividualEnrollment.ts @@ -31,6 +31,7 @@ export class IndividualEnrollment extends BaseModel { @Column({ type: 'enum', enum: ENROLLMENT_CODE, nullable: true }) public enrollmentCode: ENROLLMENT_CODE; + @Index() @ManyToOne(() => ExperimentCondition, { onDelete: 'CASCADE' }) public condition: ExperimentCondition; } diff --git a/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts b/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts index 4611bb46ed..fa39ad97c6 100644 --- a/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts +++ b/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts @@ -1,6 +1,6 @@ import { EXCLUSION_CODE } from 'upgrade_types'; import { IsNotEmpty } from 'class-validator'; -import { Entity, PrimaryColumn, ManyToOne, Column } from 'typeorm'; +import { Entity, PrimaryColumn, ManyToOne, Column, Index } from 'typeorm'; import { BaseModel } from './base/BaseModel'; import { Experiment } from './Experiment'; import { ExperimentUser } from './ExperimentUser'; @@ -10,6 +10,7 @@ export class IndividualExclusion extends BaseModel { @PrimaryColumn() public id: string; + @Index() @ManyToOne(() => Experiment, { onDelete: 'CASCADE' }) public experiment: Experiment; @@ -20,6 +21,7 @@ export class IndividualExclusion extends BaseModel { @Column({ type: 'enum', enum: EXCLUSION_CODE, nullable: true }) public exclusionCode: EXCLUSION_CODE; + @Index() @ManyToOne(() => ExperimentUser, { onDelete: 'CASCADE' }) public user: ExperimentUser; } diff --git a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts new file mode 100644 index 0000000000..8e78046141 --- /dev/null +++ b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts @@ -0,0 +1,75 @@ +import { Repository, EntityRepository, EntityManager } from 'typeorm'; +import repositoryError from './utils/repositoryError'; +import { UpgradeLogger } from 'src/lib/logger/UpgradeLogger'; +import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; + +@EntityRepository(FeatureFlagSegmentExclusion) +export class FeatureFlagSegmentExclusionRepository extends Repository { + public async insertData( + data: Partial, + logger: UpgradeLogger, + entityManager: EntityManager + ): Promise { + const result = await entityManager + .createQueryBuilder() + .insert() + .into(FeatureFlagSegmentExclusion) + .values(data) + .onConflict(`DO NOTHING`) + .returning('*') + .execute() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'FeatureFlagSegmentExclusionRepository', + 'insertFeatureFlagSegmentExclusion', + { data }, + errorMsg + ); + logger.error(errorMsg); + throw errorMsgString; + }); + + return result.raw; + } + + public async getFeatureFlagSegmentExclusionData(): Promise[]> { + return this.createQueryBuilder('featureFlagSegmentExclusion') + .leftJoin('featureFlagSegmentExclusion.featureFlag', 'featureFlag') + .leftJoin('featureFlagSegmentExclusion.segment', 'segment') + .leftJoinAndSelect('segment.subSegments', 'subSegments') + .addSelect('featureFlag.name') + .addSelect('featureFlag.state') + .addSelect('featureFlag.context') + .addSelect('segment.id') + .getMany() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError('FeatureFlagSegmentExclusion', 'getdata', {}, errorMsg); + throw errorMsgString; + }); + } + + public async deleteData( + segmentId: string, + featureFlagId: string, + logger: UpgradeLogger + ): Promise { + const result = await this.createQueryBuilder() + .delete() + .from(FeatureFlagSegmentExclusion) + .where('segmentId=:segmentId AND featureFlagId=:featureFlagId', { segmentId, featureFlagId }) + .returning('*') + .execute() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'FeatureFlagSegmentExclusionRepository', + 'deleteFeatureFlagSegmentExclusion', + { segmentId, featureFlagId }, + errorMsg + ); + logger.error(errorMsg); + throw errorMsgString; + }); + + return result.raw; + } +} diff --git a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts new file mode 100644 index 0000000000..d6f29b9a46 --- /dev/null +++ b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts @@ -0,0 +1,74 @@ +import { Repository, EntityRepository, EntityManager } from 'typeorm'; +import repositoryError from './utils/repositoryError'; +import { UpgradeLogger } from 'src/lib/logger/UpgradeLogger'; +import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; + +@EntityRepository(FeatureFlagSegmentInclusion) +export class FeatureFlagSegmentInclusionRepository extends Repository { + public async insertData( + data: Partial, + logger: UpgradeLogger, + entityManager: EntityManager + ): Promise { + const result = await entityManager + .createQueryBuilder() + .insert() + .into(FeatureFlagSegmentInclusion) + .values(data) + .onConflict(`DO NOTHING`) + .returning('*') + .execute() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'FeatureFlagSegmentInclusionRepository', + 'insertFeatureFlagSegmentInclusion', + { data }, + errorMsg + ); + logger.error(errorMsg); + throw errorMsgString; + }); + return result.raw; + } + + public async getFeatureFlagSegmentInclusionData(): Promise[]> { + return this.createQueryBuilder('featureFlagSegmentInclusion') + .leftJoin('featureFlagSegmentInclusion.featureFlag', 'featureFlag') + .leftJoin('featureFlagSegmentInclusion.segment', 'segment') + .leftJoinAndSelect('segment.subSegments', 'subSegments') + .addSelect('featureFlag.name') + .addSelect('featureFlag.state') + .addSelect('featureFlag.context') + .addSelect('segment.id') + .getMany() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError('FeatureFlagSegmentInclusion', 'getdata', {}, errorMsg); + throw errorMsgString; + }); + } + + public async deleteData( + segmentId: string, + featureFlagId: string, + logger: UpgradeLogger + ): Promise { + const result = await this.createQueryBuilder() + .delete() + .from(FeatureFlagSegmentInclusion) + .where('segmentId=:segmentId AND featureFlagId=:featureFlagId', { segmentId, featureFlagId }) + .returning('*') + .execute() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'FeatureFlagSegmentInclusionRepository', + 'deleteFeatureFlagSegmentInclusion', + { segmentId, featureFlagId }, + errorMsg + ); + logger.error(errorMsg); + throw errorMsgString; + }); + + return result.raw; + } +} diff --git a/backend/packages/Upgrade/src/api/repositories/FlagVariationRepository.ts b/backend/packages/Upgrade/src/api/repositories/FlagVariationRepository.ts deleted file mode 100644 index 2ef6004ed6..0000000000 --- a/backend/packages/Upgrade/src/api/repositories/FlagVariationRepository.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Repository, EntityRepository, EntityManager } from 'typeorm'; -import repositoryError from './utils/repositoryError'; -import { FlagVariation } from '../models/FlagVariation'; - -@EntityRepository(FlagVariation) -export class FlagVariationRepository extends Repository { - public async insertVariations( - variationDocs: Array>, - entityManager: EntityManager - ): Promise { - const result = await entityManager - .createQueryBuilder() - .insert() - .into(FlagVariation) - .values(variationDocs) - .returning('*') - .execute() - .catch((errorMsg: any) => { - const errorMsgString = repositoryError( - 'FlagVariationRepository', - 'insertVariations', - { variationDocs }, - errorMsg - ); - throw errorMsgString; - }); - - return result.raw; - } - - public async deleteVariation(id: string, entityManager: EntityManager): Promise { - await entityManager - .createQueryBuilder() - .delete() - .from(FlagVariation) - .where('id=:id', { id }) - .execute() - .catch((errorMsg: any) => { - const errorMsgString = repositoryError('FlagVariationRepository', 'deleteVariation', { id }, errorMsg); - throw errorMsgString; - }); - } - - public async upsertFlagVariation( - variationDoc: Partial, - entityManager: EntityManager - ): Promise { - const result = await entityManager - .createQueryBuilder() - .insert() - .into(FlagVariation) - .values(variationDoc) - .onConflict( - `("id") DO UPDATE SET "value" = :value, "name" = :name, "description" = :description, "defaultVariation" = :defaultVariation` - ) - .setParameter('value', variationDoc.value) - .setParameter('name', variationDoc.name) - .setParameter('description', variationDoc.description) - .setParameter('defaultVariation', variationDoc.defaultVariation) - .returning('*') - .execute() - .catch((errorMsg: any) => { - const errorMsgString = repositoryError( - 'FlagVariationRepository', - 'upsertFlagVariation', - { variationDoc }, - errorMsg - ); - throw errorMsgString; - }); - - return result.raw[0]; - } -} diff --git a/backend/packages/Upgrade/src/api/services/ExperimentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentService.ts index 46606ebf74..d431b98efb 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentService.ts @@ -84,6 +84,8 @@ import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { StratificationFactorRepository } from '../repositories/StratificationFactorRepository'; +import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; +import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; const errorRemovePart = 'instance of ExperimentDTO has failed the validation:\n - '; const stratificationErrorMessage = @@ -1879,9 +1881,13 @@ export class ExperimentService { return { ...experiment, factors: updatedFactors, conditionPayloads: updatedConditionPayloads }; } - private includeExcludeSegmentCreation( + public includeExcludeSegmentCreation( experimentSegment: ParticipantsValidator, - experimentDocSegmentData: ExperimentSegmentInclusion | ExperimentSegmentExclusion, + experimentDocSegmentData: + | ExperimentSegmentInclusion + | ExperimentSegmentExclusion + | FeatureFlagSegmentExclusion + | FeatureFlagSegmentInclusion, experimentId: string, context: string[], isIncludeMode: boolean diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index 20022875bb..02ea236ac0 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -4,31 +4,63 @@ import { InjectRepository } from 'typeorm-typedi-extensions'; import { FeatureFlagRepository } from '../repositories/FeatureFlagRepository'; import { getConnection } from 'typeorm'; import { v4 as uuid } from 'uuid'; -import { FlagVariation } from '../models/FlagVariation'; -import { FlagVariationRepository } from '../repositories/FlagVariationRepository'; import { IFeatureFlagSearchParams, IFeatureFlagSortParams, - FLAG_SEARCH_SORT_KEY, + FLAG_SEARCH_KEY, } from '../controllers/validators/FeatureFlagsPaginatedParamsValidator'; -import { SERVER_ERROR, FEATURE_FLAG_STATUS } from 'upgrade_types'; +import { SERVER_ERROR, FEATURE_FLAG_STATUS, SEGMENT_TYPE } from 'upgrade_types'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; import { FeatureFlagValidation } from '../controllers/validators/FeatureFlagValidator'; +import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; +import { Segment } from '../models/Segment'; +import { SegmentInputValidator } from '../controllers/validators/SegmentInputValidator'; +import { ErrorWithType } from '../errors/ErrorWithType'; +import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; +import { FeatureFlagSegmentExclusionRepository } from '../repositories/FeatureFlagSegmentExclusionRepository'; +import { FeatureFlagSegmentInclusionRepository } from '../repositories/FeatureFlagSegmentInclusionRepository'; +import { SegmentService } from './SegmentService'; +import { ExperimentService } from './ExperimentService'; @Service() export class FeatureFlagService { constructor( @InjectRepository() private featureFlagRepository: FeatureFlagRepository, - @InjectRepository() private flagVariationRepository: FlagVariationRepository + @InjectRepository() private featureFlagSegmentInclusionRepository: FeatureFlagSegmentInclusionRepository, + @InjectRepository() private featureFlagSegmentExclusionRepository: FeatureFlagSegmentExclusionRepository, + public segmentService: SegmentService, + public experimentService: ExperimentService ) {} public find(logger: UpgradeLogger): Promise { logger.info({ message: 'Get all feature flags' }); - return this.featureFlagRepository.find({ relations: ['variations'] }); + return this.featureFlagRepository.find(); + } + + public async findOne(id: string, logger?: UpgradeLogger): Promise { + if (logger) { + logger.info({ message: `Find feature flag by id => ${id}` }); + } + const featureFlag = await this.featureFlagRepository + .createQueryBuilder('feature_flag') + .leftJoinAndSelect('feature_flag.featureFlagSegmentInclusion', 'featureFlagSegmentInclusion') + .leftJoinAndSelect('featureFlagSegmentInclusion.segment', 'segmentInclusion') + .leftJoinAndSelect('segmentInclusion.individualForSegment', 'individualForSegment') + .leftJoinAndSelect('segmentInclusion.groupForSegment', 'groupForSegment') + .leftJoinAndSelect('segmentInclusion.subSegments', 'subSegment') + .leftJoinAndSelect('feature_flag.featureFlagSegmentExclusion', 'featureFlagSegmentExclusion') + .leftJoinAndSelect('featureFlagSegmentExclusion.segment', 'segmentExclusion') + .leftJoinAndSelect('segmentExclusion.individualForSegment', 'individualForSegmentExclusion') + .leftJoinAndSelect('segmentExclusion.groupForSegment', 'groupForSegmentExclusion') + .leftJoinAndSelect('segmentExclusion.subSegments', 'subSegmentExclusion') + .where({ id }) + .getOne(); + + return featureFlag; } public create(flagDTO: FeatureFlagValidation, logger: UpgradeLogger): Promise { - logger.info({ message: 'Create a new feature flag' }); + logger.info({ message: 'Create a new feature flag', details: flagDTO }); return this.addFeatureFlagInDB(this.featureFlagValidatorToFlag(flagDTO), logger); } @@ -45,9 +77,7 @@ export class FeatureFlagService { ): Promise { logger.info({ message: 'Find paginated Feature flags' }); - let queryBuilder = this.featureFlagRepository - .createQueryBuilder('feature_flag') - .innerJoinAndSelect('feature_flag.variations', 'variations'); + let queryBuilder = this.featureFlagRepository.createQueryBuilder('feature_flag'); if (searchParams) { const customSearchString = searchParams.string.split(' ').join(`:*&`); // add search query @@ -69,7 +99,6 @@ export class FeatureFlagService { logger.info({ message: `Delete Feature Flag => ${featureFlagId}` }); const featureFlag = await this.featureFlagRepository.find({ where: { id: featureFlagId }, - relations: ['variations'], }); if (featureFlag) { @@ -96,8 +125,9 @@ export class FeatureFlagService { private async addFeatureFlagInDB(flag: FeatureFlag, logger: UpgradeLogger): Promise { const createdFeatureFlag = await getConnection().transaction(async (transactionalEntityManager) => { flag.id = uuid(); - const { variations, ...flagDoc } = flag; - // saving experiment doc + // saving feature flag doc + const { featureFlagSegmentExclusion, featureFlagSegmentInclusion, ...flagDoc } = flag; + let featureFlagDoc: FeatureFlag; try { featureFlagDoc = ( @@ -110,36 +140,55 @@ export class FeatureFlagService { throw error; } - // creating variations docs - const variationDocsToSave = - variations && - variations.length > 0 && - variations.map((variation: FlagVariation) => { - variation.id = variation.id || uuid(); - variation.featureFlag = featureFlagDoc; - return variation; - }); - - // saving variations - let variationDocs: FlagVariation[]; + const { + segmentExists: includeSegmentExists, + segmentDoc: segmentIncludeDoc, + segmentDocToSave: segmentIncludeDocToSave, + } = await this.addPrivateSegmentToDB(featureFlagSegmentInclusion, flag, 'Inclusion', logger); + const { + segmentExists: excludeSegmentExists, + segmentDoc: segmentExcludeDoc, + segmentDocToSave: segmentExcludeDocToSave, + } = await this.addPrivateSegmentToDB(featureFlagSegmentExclusion, flag, 'Exclusion', logger); + + let featureFlagSegmentInclusionDoc: FeatureFlagSegmentInclusion; + let featureFlagSegmentExclusionDoc: FeatureFlagSegmentExclusion; + try { - variationDocs = await this.flagVariationRepository.insertVariations( - variationDocsToSave, - transactionalEntityManager - ); + [featureFlagSegmentInclusionDoc, featureFlagSegmentExclusionDoc] = await Promise.all([ + includeSegmentExists + ? this.featureFlagSegmentInclusionRepository.insertData( + segmentIncludeDocToSave, + logger, + transactionalEntityManager + ) + : (Promise.resolve([]) as any), + excludeSegmentExists + ? this.featureFlagSegmentExclusionRepository.insertData( + segmentExcludeDocToSave, + logger, + transactionalEntityManager + ) + : (Promise.resolve([]) as any), + ]); } catch (err) { - const error = new Error(`Error in creating variation "addFeatureFlagInDB" ${err}`); - (error as any).type = SERVER_ERROR.QUERY_FAILED; + const error = err as Error; + error.message = `Error in creating inclusion or exclusion segments "addFeatureFlagInDB"`; logger.error(error); throw error; } - const variationDocToReturn = variationDocs.map((variationDoc) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { featureFlagId, ...rest } = variationDoc as any; - return rest; - }); - return { ...featureFlagDoc, variations: variationDocToReturn as any }; + const newFeatureFlagObject = { + ...featureFlagDoc, + ...(includeSegmentExists && { + featureFlagSegmentInclusion: { ...featureFlagSegmentInclusionDoc, segment: segmentIncludeDoc } as any, + }), + ...(excludeSegmentExists && { + featureFlagSegmentExclusion: { ...featureFlagSegmentExclusionDoc, segment: segmentExcludeDoc } as any, + }), + }; + + return newFeatureFlagObject; }); // TODO: Add log for feature flag creation @@ -148,15 +197,18 @@ export class FeatureFlagService { private async updateFeatureFlagInDB(flag: FeatureFlag, logger: UpgradeLogger): Promise { // get old feature flag document - const oldFeatureFlag = await this.featureFlagRepository.find({ - where: { id: flag.id }, - relations: ['variations'], - }); - const oldVariations = oldFeatureFlag[0].variations; + const oldFeatureFlag = await this.findOne(flag.id); return getConnection().transaction(async (transactionalEntityManager) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { variations, versionNumber, createdAt, updatedAt, ...flagDoc } = flag; + const { + featureFlagSegmentExclusion, + featureFlagSegmentInclusion, + versionNumber, + createdAt, + updatedAt, + ...flagDoc + } = flag; let featureFlagDoc: FeatureFlag; try { featureFlagDoc = (await this.featureFlagRepository.updateFeatureFlag(flagDoc, transactionalEntityManager))[0]; @@ -166,92 +218,72 @@ export class FeatureFlagService { logger.error(error); throw error; } + featureFlagDoc.featureFlagSegmentInclusion = oldFeatureFlag.featureFlagSegmentInclusion; + const segmentIncludeData = this.experimentService.includeExcludeSegmentCreation( + featureFlagSegmentInclusion, + featureFlagDoc.featureFlagSegmentInclusion, + flag.id, + flag.context, + true + ); - // creating variations docs - const variationDocToSave: Array> = - (variations && - variations.length > 0 && - variations.map((variation: FlagVariation) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt, updatedAt, versionNumber, ...rest } = variation; - rest.featureFlag = featureFlagDoc; - rest.id = rest.id || uuid(); - return rest; - })) || - []; - - // delete variations which don't exist in new feature flag document - const toDeleteVariations = []; - oldVariations.forEach(({ id }) => { - if ( - !variationDocToSave.find((doc) => { - return doc.id === id; - }) - ) { - toDeleteVariations.push(this.flagVariationRepository.deleteVariation(id, transactionalEntityManager)); - } - }); - - // delete old variations - await Promise.all(toDeleteVariations); + featureFlagDoc.featureFlagSegmentExclusion = oldFeatureFlag.featureFlagSegmentExclusion; + const segmentExcludeData = this.experimentService.includeExcludeSegmentCreation( + featureFlagSegmentExclusion, + featureFlagDoc.featureFlagSegmentExclusion, + flag.id, + flag.context, + false + ); - // saving variations - let variationDocs: FlagVariation[]; + let segmentIncludeDoc: Segment; try { - [variationDocs] = await Promise.all([ - Promise.all( - variationDocToSave.map(async (variationDoc) => { - return this.flagVariationRepository.upsertFlagVariation(variationDoc, transactionalEntityManager); - }) - ) as any, - ]); + segmentIncludeDoc = await this.segmentService.upsertSegment(segmentIncludeData, logger); } catch (err) { - const error = new Error(`Error in creating variations "updateFeatureFlagInDB" ${err}`); - (error as any).type = SERVER_ERROR.QUERY_FAILED; + const error = err as ErrorWithType; + error.details = 'Error in updating IncludeSegment in DB'; + error.type = SERVER_ERROR.QUERY_FAILED; logger.error(error); throw error; } - const variationDocToReturn = variationDocs.map((variationDoc) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { featureFlagId, ...rest } = variationDoc as any; - return { ...rest, featureFlag: variationDoc.featureFlag }; - }); - - const newFeatureFlag = { - ...featureFlagDoc, - variations: variationDocToReturn as any, - }; + let segmentExcludeDoc: Segment; + try { + segmentExcludeDoc = await this.segmentService.upsertSegment(segmentExcludeData, logger); + } catch (err) { + const error = err as ErrorWithType; + error.details = 'Error in updating ExcludeSegment in DB'; + error.type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + throw error; + } - // add log of diff of new and old feature flag doc - return newFeatureFlag; + featureFlagDoc.featureFlagSegmentInclusion.segment = segmentIncludeDoc; + featureFlagDoc.featureFlagSegmentExclusion.segment = segmentExcludeDoc; + return featureFlagDoc; }); } - private postgresSearchString(type: FLAG_SEARCH_SORT_KEY): string { + private postgresSearchString(type: FLAG_SEARCH_KEY): string { const searchString: string[] = []; switch (type) { - case FLAG_SEARCH_SORT_KEY.NAME: + case FLAG_SEARCH_KEY.NAME: searchString.push("coalesce(feature_flag.name::TEXT,'')"); - searchString.push("coalesce(variations.value::TEXT,'')"); break; - case FLAG_SEARCH_SORT_KEY.KEY: + case FLAG_SEARCH_KEY.KEY: searchString.push("coalesce(feature_flag.key::TEXT,'')"); break; - case FLAG_SEARCH_SORT_KEY.STATUS: + case FLAG_SEARCH_KEY.STATUS: searchString.push("coalesce(feature_flag.status::TEXT,'')"); break; - case FLAG_SEARCH_SORT_KEY.VARIATION_TYPE: - // TODO: Update column name - // searchString.push("coalesce(feature_flag.variationType::TEXT,'')"); + case FLAG_SEARCH_KEY.CONTEXT: + searchString.push("coalesce(feature_flag.context::TEXT,'')"); break; default: searchString.push("coalesce(feature_flag.name::TEXT,'')"); - searchString.push("coalesce(variations.value::TEXT,'')"); searchString.push("coalesce(feature_flag.key::TEXT,'')"); searchString.push("coalesce(feature_flag.status::TEXT,'')"); - // TODO: Update column name - // searchString.push("coalesce(feature_flag.variationType::TEXT,'')"); + searchString.push("coalesce(feature_flag.context::TEXT,'')"); break; } const stringConcat = searchString.join(','); @@ -266,7 +298,75 @@ export class FeatureFlagService { featureFlag.id = flagDTO.id; featureFlag.key = flagDTO.key; featureFlag.status = flagDTO.status; + featureFlag.context = flagDTO.context; + featureFlag.tags = flagDTO.tags; + const newExclusion = new FeatureFlagSegmentExclusion(); + const newInclusion = new FeatureFlagSegmentInclusion(); + featureFlag.featureFlagSegmentExclusion = { ...flagDTO.featureFlagSegmentExclusion, ...newExclusion }; + featureFlag.featureFlagSegmentInclusion = { ...flagDTO.featureFlagSegmentInclusion, ...newInclusion }; featureFlag.filterMode = flagDTO.filterMode; return featureFlag; } + + private getSegmentDoc(doc: FeatureFlagSegmentInclusion | FeatureFlagSegmentExclusion) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { createdAt, updatedAt, versionNumber, ...newDoc } = doc; + return newDoc; + } + + private async addPrivateSegmentToDB( + segmentInclusionExclusion: FeatureFlagSegmentExclusion | FeatureFlagSegmentInclusion, + flag: FeatureFlag, + type: string, + logger: UpgradeLogger + ) { + let segmentExists = true; + let segmentDoc: Segment; + let segmentDocToSave: Partial = {}; + if (segmentInclusionExclusion) { + const segment: any = this.setSegmentInclusionOrExclusion(segmentInclusionExclusion); + const segmentData: SegmentInputValidator = { + ...segment, + id: segment.id || uuid(), + name: flag.id + ' ' + type + ' Segment', + description: flag.id + ' ' + type + ' Segment', + context: flag.context[0], + type: SEGMENT_TYPE.PRIVATE, + }; + try { + segmentDoc = await this.segmentService.upsertSegment(segmentData, logger); + } catch (err) { + const error = err as ErrorWithType; + error.details = 'Error in adding segment in DB'; + error.type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + throw error; + } + // creating segment doc + const tempDoc = type === 'Inclusion' ? new FeatureFlagSegmentInclusion() : new FeatureFlagSegmentExclusion(); + tempDoc.segment = segmentDoc; + tempDoc.featureFlag = flag; + segmentDocToSave = this.getSegmentDoc(tempDoc); + } else { + segmentExists = false; + } + return { segmentExists, segmentDoc, segmentDocToSave }; + } + + private setSegmentInclusionOrExclusion( + inclusionOrExclusion: FeatureFlagSegmentExclusion | FeatureFlagSegmentInclusion + ) { + const segment = inclusionOrExclusion.segment; + return segment + ? { + type: segment.type, + userIds: segment.individualForSegment?.map((x) => x.userId) || [], + groups: + segment.groupForSegment?.map((x) => { + return { type: x.type, groupId: x.groupId }; + }) || [], + subSegmentIds: segment.subSegments?.map((x) => x.id) || [], + } + : inclusionOrExclusion; + } } diff --git a/backend/packages/Upgrade/src/database/migrations/1716191003726-addingIndex.ts b/backend/packages/Upgrade/src/database/migrations/1716191003726-addingIndex.ts new file mode 100644 index 0000000000..ea44ef43b9 --- /dev/null +++ b/backend/packages/Upgrade/src/database/migrations/1716191003726-addingIndex.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addingIndex1716191003726 implements MigrationInterface { + name = 'addingIndex1716191003726'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX "IDX_4f4d2f2ec491226d4692ed400f" ON "individual_enrollment" ("conditionId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_43238be19de9500c290393b907" ON "individual_exclusion" ("experimentId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_de26ba8251f4ebf8b9c8ccf623" ON "individual_exclusion" ("userId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_de26ba8251f4ebf8b9c8ccf623"`); + await queryRunner.query(`DROP INDEX "public"."IDX_43238be19de9500c290393b907"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4f4d2f2ec491226d4692ed400f"`); + } +} diff --git a/backend/packages/Upgrade/test/unit/controllers/FeatureFlagController.test.ts b/backend/packages/Upgrade/test/unit/controllers/FeatureFlagController.test.ts index 178c50a2a7..0aa85b59b8 100644 --- a/backend/packages/Upgrade/test/unit/controllers/FeatureFlagController.test.ts +++ b/backend/packages/Upgrade/test/unit/controllers/FeatureFlagController.test.ts @@ -48,10 +48,19 @@ describe('Feature Flag Controller Testing', () => { name: 'string', key: 'string', description: 'string', - variationType: 'string', status: 'enabled', context: ['foo'], tags: ['bar'], + featureFlagSegmentInclusion: { + segment: { + type: 'private', + }, + }, + featureFlagSegmentExclusion: { + segment: { + type: 'private', + }, + }, filterMode: 'includeAll', }) .set('Accept', 'application/json') @@ -87,10 +96,25 @@ describe('Feature Flag Controller Testing', () => { name: 'string', key: 'string', description: 'string', - variationType: 'string', status: 'enabled', context: ['foo'], tags: ['bar'], + featureFlagSegmentInclusion: { + segment: { + type: 'private', + individualForSegment: [], + groupForSegment: [], + subSegments: [], + }, + }, + featureFlagSegmentExclusion: { + segment: { + type: 'private', + individualForSegment: [], + groupForSegment: [], + subSegments: [], + }, + }, filterMode: 'includeAll', }) .set('Accept', 'application/json') diff --git a/backend/packages/Upgrade/test/unit/repositories/FlagVariationRepository.test.ts b/backend/packages/Upgrade/test/unit/repositories/FlagVariationRepository.test.ts deleted file mode 100644 index 2ca7c979dc..0000000000 --- a/backend/packages/Upgrade/test/unit/repositories/FlagVariationRepository.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Connection, DeleteQueryBuilder, EntityManager, InsertQueryBuilder } from 'typeorm'; -import * as sinon from 'sinon'; -import { FlagVariationRepository } from '../../../src/api/repositories/FlagVariationRepository'; -import { FlagVariation } from '../../../src/api/models/FlagVariation'; - -let sandbox; -let connection; -let manager; -let createQueryBuilderStub; -let insertMock, deleteMock; -const insertQueryBuilder = new InsertQueryBuilder(null); -const deleteQueryBuilder = new DeleteQueryBuilder(null); -const repo = new FlagVariationRepository(); -const err = new Error('test error'); - -const flag = new FlagVariation(); -flag.id = 'id1'; - -beforeEach(() => { - sandbox = sinon.createSandbox(); - connection = sinon.createStubInstance(Connection); - manager = new EntityManager(connection); - - insertMock = sandbox.mock(insertQueryBuilder); - deleteMock = sandbox.mock(deleteQueryBuilder); -}); - -afterEach(() => { - sandbox.restore(); -}); - -describe('FlagVariationRepository Testing', () => { - it('should insert a new flag', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(insertQueryBuilder); - const result = { - identifiers: [{ id: flag.id }], - generatedMaps: [flag], - raw: [flag], - }; - - insertMock.expects('insert').once().returns(insertQueryBuilder); - insertMock.expects('into').once().returns(insertQueryBuilder); - insertMock.expects('values').once().returns(insertQueryBuilder); - insertMock.expects('returning').once().returns(insertQueryBuilder); - insertMock.expects('execute').once().returns(Promise.resolve(result)); - - const res = await repo.insertVariations([flag], manager); - - sinon.assert.calledOnce(createQueryBuilderStub); - insertMock.verify(); - - expect(res).toEqual([flag]); - }); - - it('should throw an error when insert fails', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(insertQueryBuilder); - - insertMock.expects('insert').once().returns(insertQueryBuilder); - insertMock.expects('into').once().returns(insertQueryBuilder); - insertMock.expects('values').once().returns(insertQueryBuilder); - insertMock.expects('returning').once().returns(insertQueryBuilder); - insertMock.expects('execute').once().returns(Promise.reject(err)); - - expect(async () => { - await repo.insertVariations([flag], manager); - }).rejects.toThrow(err); - - sinon.assert.calledOnce(createQueryBuilderStub); - insertMock.verify(); - }); - - it('should delete a flag', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(deleteQueryBuilder); - const result = { - identifiers: [{ id: flag.id }], - generatedMaps: [flag], - raw: [flag], - }; - - deleteMock.expects('delete').once().returns(deleteQueryBuilder); - deleteMock.expects('from').once().returns(deleteQueryBuilder); - deleteMock.expects('where').once().returns(deleteQueryBuilder); - deleteMock.expects('execute').once().returns(Promise.resolve(result)); - - await repo.deleteVariation(flag.id, manager); - - sinon.assert.calledOnce(createQueryBuilderStub); - deleteMock.verify(); - }); - - it('should throw an error when delete fails', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(deleteQueryBuilder); - - deleteMock.expects('delete').once().returns(deleteQueryBuilder); - deleteMock.expects('from').once().returns(deleteQueryBuilder); - deleteMock.expects('where').once().returns(deleteQueryBuilder); - deleteMock.expects('execute').once().returns(Promise.reject(err)); - - expect(async () => { - await repo.deleteVariation(flag.id, manager); - }).rejects.toThrow(err); - - sinon.assert.calledOnce(createQueryBuilderStub); - deleteMock.verify(); - }); - - it('should update flag', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(insertQueryBuilder); - const result = { - identifiers: [{ id: flag.id }], - generatedMaps: [flag], - raw: [flag], - }; - - insertMock.expects('insert').once().returns(insertQueryBuilder); - insertMock.expects('into').once().returns(insertQueryBuilder); - insertMock.expects('values').once().returns(insertQueryBuilder); - insertMock.expects('onConflict').once().returns(insertQueryBuilder); - insertMock.expects('setParameter').exactly(4).returns(insertQueryBuilder); - insertMock.expects('returning').once().returns(insertQueryBuilder); - insertMock.expects('execute').once().returns(Promise.resolve(result)); - - const res = await repo.upsertFlagVariation(flag, manager); - - sinon.assert.calledOnce(createQueryBuilderStub); - insertMock.verify(); - - expect(res).toEqual(flag); - }); - - it('should throw an error when update flag fails', async () => { - createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(insertQueryBuilder); - - insertMock.expects('insert').once().returns(insertQueryBuilder); - insertMock.expects('into').once().returns(insertQueryBuilder); - insertMock.expects('values').once().returns(insertQueryBuilder); - insertMock.expects('onConflict').once().returns(insertQueryBuilder); - insertMock.expects('setParameter').exactly(4).returns(insertQueryBuilder); - insertMock.expects('returning').once().returns(insertQueryBuilder); - insertMock.expects('execute').once().returns(Promise.reject(err)); - - expect(async () => { - await repo.upsertFlagVariation(flag, manager); - }).rejects.toThrow(err); - - sinon.assert.calledOnce(createQueryBuilderStub); - insertMock.verify(); - }); -}); diff --git a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts index 0b0d7d5744..2ae02bc5a9 100644 --- a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts +++ b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts @@ -1,52 +1,87 @@ -import { FeatureFlagService } from '../../../src/api/services/FeatureFlagService'; import * as sinon from 'sinon'; import { Connection, ConnectionManager } from 'typeorm'; import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { UpgradeLogger } from '../../../src/lib/logger/UpgradeLogger'; -import { ErrorService } from '../../../src/api/services/ErrorService'; -import { FeatureFlagRepository } from '../../../src/api/repositories/FeatureFlagRepository'; + import { FeatureFlag } from '../../../src/api/models/FeatureFlag'; -import { FlagVariationRepository } from '../../../src/api/repositories/FlagVariationRepository'; -import { FLAG_SEARCH_SORT_KEY } from '../../../src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator'; +import { Segment } from '../../../src/api/models/Segment'; + +import { FeatureFlagRepository } from '../../../src/api/repositories/FeatureFlagRepository'; +import { FeatureFlagSegmentInclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentInclusionRepository'; +import { FeatureFlagSegmentExclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentExclusionRepository'; + +import { ErrorService } from '../../../src/api/services/ErrorService'; +import { FeatureFlagService } from '../../../src/api/services/FeatureFlagService'; +import { SegmentService } from '../../../src/api/services/SegmentService'; +import { ExperimentService } from '../../../src/api/services/ExperimentService'; + +import { UpgradeLogger } from '../../../src/lib/logger/UpgradeLogger'; + +import { + FLAG_SEARCH_KEY, + FLAG_SORT_KEY, +} from '../../../src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator'; import { SORT_AS_DIRECTION } from '../../../../../../types/src'; -import { FlagVariation } from '../../../src/api/models/FlagVariation'; import { isUUID } from 'class-validator'; import { v4 as uuid } from 'uuid'; import { FEATURE_FLAG_STATUS } from 'upgrade_types'; -// Skip these tests until the API work is done and variations are removed -describe.skip('Feature Flag Service Testing', () => { +describe('Feature Flag Service Testing', () => { let service: FeatureFlagService; let flagRepo: FeatureFlagRepository; - let flagVariationRepo: FlagVariationRepository; + let flagSegmentInclusionRepo: FeatureFlagSegmentInclusionRepository; + let flagSegmentExclusionRepo: FeatureFlagSegmentExclusionRepository; + let segmentService: SegmentService; + let module: Awaited>; const logger = new UpgradeLogger(); - const var1 = new FlagVariation(); - var1.id = uuid(); - var1.value = 'value1'; - const var2 = new FlagVariation(); - var2.id = uuid(); - var1.value = 'value2'; - const var3 = new FlagVariation(); + + const seg1 = new Segment(); const mockFlag1 = new FeatureFlag(); mockFlag1.id = uuid(); mockFlag1.name = 'name'; mockFlag1.key = 'key'; mockFlag1.description = 'description'; + mockFlag1.context = ['context']; mockFlag1.status = FEATURE_FLAG_STATUS.ENABLED; - mockFlag1.variations = [var1, var2, var3]; + mockFlag1.featureFlagSegmentExclusion = { + createdAt: new Date(), + updatedAt: new Date(), + versionNumber: 1, + segment: seg1, + featureFlag: mockFlag1, + }; + mockFlag1.featureFlagSegmentInclusion = { + createdAt: new Date(), + updatedAt: new Date(), + versionNumber: 1, + segment: seg1, + featureFlag: mockFlag1, + }; const mockFlag2 = new FeatureFlag(); mockFlag2.id = uuid(); mockFlag2.name = 'name'; mockFlag2.key = 'key'; mockFlag2.description = 'description'; + mockFlag2.context = ['context']; mockFlag2.status = FEATURE_FLAG_STATUS.ENABLED; - - mockFlag1.variations = [var2, var3]; + mockFlag2.featureFlagSegmentExclusion = { + createdAt: new Date(), + updatedAt: new Date(), + versionNumber: 1, + segment: seg1, + featureFlag: mockFlag2, + }; + mockFlag2.featureFlagSegmentInclusion = { + createdAt: new Date(), + updatedAt: new Date(), + versionNumber: 1, + segment: seg1, + featureFlag: mockFlag2, + }; const mockFlag3 = new FeatureFlag(); @@ -79,8 +114,20 @@ describe.skip('Feature Flag Service Testing', () => { module = await Test.createTestingModule({ providers: [ FeatureFlagService, - FeatureFlagRepository, - FlagVariationRepository, + { + provide: ExperimentService, + useValue: { + includeExcludeSegmentCreation: jest.fn().mockResolvedValue({ subSegmentIds: [], userIds: [], groups: [] }), + }, + }, + { + provide: SegmentService, + useValue: { + upsertSegment: jest.fn().mockResolvedValue({ id: uuid() }), + addSegmentDataInDB: jest.fn().mockResolvedValue({ id: uuid() }), + find: jest.fn().mockResolvedValue([]), + }, + }, { provide: getRepositoryToken(FeatureFlagRepository), useValue: { @@ -107,18 +154,31 @@ describe.skip('Feature Flag Service Testing', () => { offset: offsetSpy, limit: limitSpy, innerJoinAndSelect: jest.fn().mockReturnThis(), + leftJoinAndSelect: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(mockFlagArr), + getOne: jest.fn().mockResolvedValue(mockFlag1), })), }, }, { - provide: getRepositoryToken(FlagVariationRepository), + provide: getRepositoryToken(FeatureFlagSegmentInclusionRepository), useValue: { - find: jest.fn().mockResolvedValue(mockFlagArr), - insertVariations: jest.fn().mockResolvedValue(mockFlagArr), - upsertFlagVariation: jest.fn().mockResolvedValue(mockFlagArr), - deleteVariation: jest.fn().mockImplementation((flag) => { - return flag; + find: jest.fn().mockResolvedValue(''), + insertData: jest.fn().mockResolvedValue(''), + getFeatureFlagSegmentInclusionData: jest.fn().mockResolvedValue(''), + deleteData: jest.fn().mockImplementation((seg) => { + return seg; + }), + }, + }, + { + provide: getRepositoryToken(FeatureFlagSegmentExclusionRepository), + useValue: { + find: jest.fn().mockResolvedValue(''), + insertData: jest.fn().mockResolvedValue(''), + getFeatureFlagSegmentExclusionData: jest.fn().mockResolvedValue(''), + deleteData: jest.fn().mockImplementation((seg) => { + return seg; }), }, }, @@ -133,7 +193,13 @@ describe.skip('Feature Flag Service Testing', () => { service = module.get(FeatureFlagService); flagRepo = module.get(getRepositoryToken(FeatureFlagRepository)); - flagVariationRepo = module.get(getRepositoryToken(FlagVariationRepository)); + flagSegmentInclusionRepo = module.get( + getRepositoryToken(FeatureFlagSegmentInclusionRepository) + ); + flagSegmentExclusionRepo = module.get( + getRepositoryToken(FeatureFlagSegmentExclusionRepository) + ); + segmentService = module.get(SegmentService); }); it('should be defined', async () => { @@ -151,7 +217,8 @@ describe.skip('Feature Flag Service Testing', () => { it('should create a feature flag with uuid', async () => { const results = await service.create(mockFlag1, logger); - expect(isUUID(results.variations[0].id)).toBeTruthy(); + expect(isUUID(results.featureFlagSegmentInclusion.segment.id)).toBeTruthy(); + expect(isUUID(results.featureFlagSegmentExclusion.segment.id)).toBeTruthy(); }); it('should throw an error when create flag fails', async () => { @@ -162,12 +229,20 @@ describe.skip('Feature Flag Service Testing', () => { }).rejects.toThrow(new Error('Error in creating feature flag document "addFeatureFlagInDB" Error: insert error')); }); - it('should throw an error when create variation fails', async () => { + it('should throw an error when create segment inclusion fails', async () => { const err = new Error('insert error'); - flagVariationRepo.insertVariations = jest.fn().mockRejectedValue(err); + flagSegmentInclusionRepo.insertData = jest.fn().mockRejectedValue(err); expect(async () => { await service.create(mockFlag1, logger); - }).rejects.toThrow(new Error('Error in creating variation "addFeatureFlagInDB" Error: insert error')); + }).rejects.toThrow(new Error('Error in creating inclusion or exclusion segments "addFeatureFlagInDB"')); + }); + + it('should throw an error when create segment exclusion fails', async () => { + const err = new Error('insert error'); + flagSegmentExclusionRepo.insertData = jest.fn().mockRejectedValue(err); + expect(async () => { + await service.create(mockFlag1, logger); + }).rejects.toThrow(new Error('Error in creating inclusion or exclusion segments "addFeatureFlagInDB"')); }); it('should return a count of feature flags', async () => { @@ -181,11 +256,11 @@ describe.skip('Feature Flag Service Testing', () => { 2, logger, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SEARCH_KEY.ALL, string: '', }, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SORT_KEY.NAME, sortAs: SORT_AS_DIRECTION.ASCENDING, } ); @@ -198,11 +273,11 @@ describe.skip('Feature Flag Service Testing', () => { 2, logger, { - key: FLAG_SEARCH_SORT_KEY.KEY, + key: FLAG_SEARCH_KEY.KEY, string: '', }, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SORT_KEY.NAME, sortAs: SORT_AS_DIRECTION.ASCENDING, } ); @@ -215,11 +290,11 @@ describe.skip('Feature Flag Service Testing', () => { 2, logger, { - key: FLAG_SEARCH_SORT_KEY.NAME, + key: FLAG_SEARCH_KEY.NAME, string: '', }, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SORT_KEY.NAME, sortAs: SORT_AS_DIRECTION.ASCENDING, } ); @@ -232,28 +307,28 @@ describe.skip('Feature Flag Service Testing', () => { 2, logger, { - key: FLAG_SEARCH_SORT_KEY.STATUS, + key: FLAG_SEARCH_KEY.STATUS, string: '', }, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SORT_KEY.NAME, sortAs: SORT_AS_DIRECTION.ASCENDING, } ); expect(results).toEqual(mockFlagArr); }); - it('should find all paginated feature flags with search string variation type', async () => { + it('should find all paginated feature flags with search string context', async () => { const results = await service.findPaginated( 1, 2, logger, { - key: FLAG_SEARCH_SORT_KEY.VARIATION_TYPE, + key: FLAG_SEARCH_KEY.CONTEXT, string: '', }, { - key: FLAG_SEARCH_SORT_KEY.ALL, + key: FLAG_SORT_KEY.NAME, sortAs: SORT_AS_DIRECTION.ASCENDING, } ); @@ -270,13 +345,7 @@ describe.skip('Feature Flag Service Testing', () => { expect(isUUID(results.id)).toBeTruthy(); }); - it('should update the flag with no id and no variations', async () => { - const results = await service.update(mockFlag3, logger); - expect(isUUID(results.id)).toBeTruthy(); - }); - - it('should update the flag with no id', async () => { - mockFlag3.variations = [var3]; + it('should update the flag with no id and no context', async () => { const results = await service.update(mockFlag3, logger); expect(isUUID(results.id)).toBeTruthy(); }); @@ -291,12 +360,12 @@ describe.skip('Feature Flag Service Testing', () => { ); }); - it('should throw an error when unable to update flag variation', async () => { + it('should throw an error when unable to update segment (for inclusion or exclusion', async () => { const err = new Error('insert error'); - flagVariationRepo.upsertFlagVariation = jest.fn().mockRejectedValue(err); + segmentService.upsertSegment = jest.fn().mockRejectedValue(err); expect(async () => { await service.update(mockFlag1, logger); - }).rejects.toThrow(new Error('Error in creating variations "updateFeatureFlagInDB" Error: insert error')); + }).rejects.toThrow(err); }); it('should update the flag state', async () => { diff --git a/frontend/projects/upgrade/src/app/app.module.ts b/frontend/projects/upgrade/src/app/app.module.ts index 40af9a283e..dcd48f9052 100755 --- a/frontend/projects/upgrade/src/app/app.module.ts +++ b/frontend/projects/upgrade/src/app/app.module.ts @@ -32,6 +32,7 @@ export const getEnvironmentConfig = (http: HttpClient, env: Environment) => { env.featureFlagNavToggle = config.featureFlagNavToggle ?? env.featureFlagNavToggle ?? false; env.withinSubjectExperimentSupportToggle = config.withinSubjectExperimentSupportToggle ?? env.withinSubjectExperimentSupportToggle ?? false; + env.errorLogsToggle = config.errorLogsToggle ?? env.errorLogsToggle ?? false; }) .catch((error) => { console.log({ error }); diff --git a/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.spec.ts b/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.spec.ts index ff83510ae3..8bc9a59164 100644 --- a/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.spec.ts +++ b/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.spec.ts @@ -15,43 +15,12 @@ describe('AppErrorHandler', () => { service = new AppErrorHandler(mockNotificationsService, mockEnvironment); }); - it('should call notification service with an error of "An error occured. See console for details." when not in production and not 401', () => { + it('should call notification service with an error of "An error occurred. See console for details."', () => { const mockError = { status: 400 } as any; const expectedValue = 'An error occurred. See console for details.'; - mockEnvironment.production = false; service.handleError(mockError); expect(mockNotificationsService.showError).toHaveBeenCalledWith(expectedValue); }); - - it('should not call notification service with an error when not in production and is 401', () => { - const mockError = { status: 401 } as any; - const expectedValue = 'An error occurred. See console for details.'; - mockEnvironment.production = false; - - service.handleError(mockError); - - expect(mockNotificationsService.showError).not.toHaveBeenCalledWith(expectedValue); - }); - - it('should not call when in production mode and 401', () => { - const mockError = { status: 401 } as any; - const expectedValue = 'An error occurred.'; - mockEnvironment.production = true; - - service.handleError(mockError); - - expect(mockNotificationsService.showError).not.toHaveBeenCalledWith(expectedValue); - }); - - it('should not call when in production mode and 400', () => { - const mockError = { status: 400 } as any; - const expectedValue = 'An error occurred.'; - mockEnvironment.production = true; - - service.handleError(mockError); - - expect(mockNotificationsService.showError).not.toHaveBeenCalledWith(expectedValue); - }); }); diff --git a/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.ts b/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.ts index 0b42d615be..82080e3c87 100755 --- a/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.ts +++ b/frontend/projects/upgrade/src/app/core/error-handler/app-error-handler.service.ts @@ -13,16 +13,8 @@ export class AppErrorHandler extends ErrorHandler { } handleError(error: Error | HttpErrorResponse) { - let displayMessage = 'An error occurred.'; - - if (!this.environment.production) { - displayMessage += ' See console for details.'; - } - - if (!((error as any).status === 401) && !this.environment.production) { - this.notificationsService.showError(displayMessage); - } - + const displayMessage = 'An error occurred. See console for details.'; + this.notificationsService.showError(displayMessage); super.handleError(error); } } diff --git a/frontend/projects/upgrade/src/app/core/experiment-design-stepper/experiment-design-stepper.service.ts b/frontend/projects/upgrade/src/app/core/experiment-design-stepper/experiment-design-stepper.service.ts index dbac94f1d4..c7a0cafa9e 100644 --- a/frontend/projects/upgrade/src/app/core/experiment-design-stepper/experiment-design-stepper.service.ts +++ b/frontend/projects/upgrade/src/app/core/experiment-design-stepper/experiment-design-stepper.service.ts @@ -338,7 +338,7 @@ export class ExperimentDesignStepperService { const payloadTableData = this.getSimpleExperimentPayloadTableData(); payloadTableData.forEach((payloadRowData: SimpleExperimentPayloadTableRowData) => { - const parentCondition = conditions.find((condition) => condition.conditionCode === payloadRowData.condition); + const parentCondition = conditions.find((condition) => condition.conditionCode === payloadRowData.condition).id; const decisionPoint = decisionPoints.find( (decisionPoint) => decisionPoint.target === payloadRowData.target && decisionPoint.site === payloadRowData.site diff --git a/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts b/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts index 53acc93f7d..5221749570 100644 --- a/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts +++ b/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts @@ -13,7 +13,7 @@ import { ExperimentStateInfo, EXPERIMENT_STATE, POST_EXPERIMENT_RULE, - segmentNew, + SegmentNew, } from './store/experiments.model'; import { ExperimentFile } from '../../features/dashboard/home/components/modal/import-experiment/import-experiment.component'; @@ -60,14 +60,14 @@ describe('ExperimentDataService', () => { status: 'segment-status', }; - const dummyInclusionData: segmentNew = { + const dummyInclusionData: SegmentNew = { updatedAt: '2022-06-20T13:14:52.900Z', createdAt: '2022-06-20T13:14:52.900Z', versionNumber: 1, segment: segmentData, }; - const dummyExclusionData: segmentNew = { + const dummyExclusionData: SegmentNew = { updatedAt: '2022-06-20T13:14:52.900Z', createdAt: '2022-06-20T13:14:52.900Z', versionNumber: 1, diff --git a/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts b/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts index 1dbc25af1b..1c7df5d195 100644 --- a/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts +++ b/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts @@ -37,7 +37,7 @@ import { import { Environment } from '../../../environments/environment-types'; import { environment } from '../../../environments/environment'; import { ASSIGNMENT_ALGORITHM, CONDITION_ORDER, EXPERIMENT_TYPE, FILTER_MODE, SEGMENT_TYPE } from 'upgrade_types'; -import { segmentNew } from './store/experiments.model'; +import { SegmentNew } from './store/experiments.model'; import { Segment } from '../segments/store/segments.model'; const MockStateStore$ = new BehaviorSubject({}); @@ -74,14 +74,14 @@ describe('ExperimentService', () => { status: 'segment-status', }; - const dummyInclusionData: segmentNew = { + const dummyInclusionData: SegmentNew = { updatedAt: '2022-06-20T13:14:52.900Z', createdAt: '2022-06-20T13:14:52.900Z', versionNumber: 1, segment: segmentData, }; - const dummyExclusionData: segmentNew = { + const dummyExclusionData: SegmentNew = { updatedAt: '2022-06-20T13:14:52.900Z', createdAt: '2022-06-20T13:14:52.900Z', versionNumber: 1, diff --git a/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts b/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts index 888485c9c6..fe4ee1b4a5 100644 --- a/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts +++ b/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts @@ -210,7 +210,7 @@ export interface ExperimentStateTimeLog { versionNumber: number; } -export interface segmentNew { +export interface SegmentNew { updatedAt: string; createdAt: string; versionNumber: number; @@ -247,8 +247,8 @@ export interface Experiment { queries: any[]; stateTimeLogs: ExperimentStateTimeLog[]; filterMode: FILTER_MODE; - experimentSegmentInclusion: segmentNew; - experimentSegmentExclusion: segmentNew; + experimentSegmentInclusion: SegmentNew; + experimentSegmentExclusion: SegmentNew; groupSatisfied?: number; backendVersion: string; } 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 784f45007c..4b7e078933 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 @@ -1,8 +1,96 @@ import { Inject, Injectable } from '@angular/core'; import { ENV, Environment } from '../../../environments/environment-types'; import { HttpClient } from '@angular/common/http'; +import { FeatureFlagsPaginationParams } from './store/feature-flags.model'; +import { delay, of } from 'rxjs'; +import { FEATURE_FLAG_STATUS, FILTER_MODE } from '../../../../../../../types/src'; @Injectable() export class FeatureFlagsDataService { constructor(private http: HttpClient, @Inject(ENV) private environment: Environment) {} + + fetchFeatureFlagsPaginated(params: FeatureFlagsPaginationParams) { + const url = this.environment.api.getPaginatedFlags; + // return this.http.post(url, params); + // mock + return of({ nodes: mockFeatureFlags, total: 2 }).pipe(delay(2000)); + } } + +const mockFeatureFlags = [ + { + createdAt: '2021-09-08T08:00:00.000Z', + updatedAt: '2021-09-08T08:00:00.000Z', + versionNumber: 1, + id: '1', + name: 'Feature Flag 1', + key: 'feature_flag_1', + description: 'Feature Flag 1 Description', + status: FEATURE_FLAG_STATUS.ENABLED, + filterMode: FILTER_MODE.INCLUDE_ALL, + context: ['context1', 'context2'], + tags: ['tag1', 'tag2'], + featureFlagSegmentInclusion: null, + featureFlagSegmentExclusion: null, + }, + { + createdAt: '2021-09-08T08:00:00.000Z', + updatedAt: '2021-09-08T08:00:00.000Z', + versionNumber: 1, + id: '2', + name: 'Feature Flag 2', + key: 'feature_flag_2', + description: 'Feature Flag 2 Description', + status: FEATURE_FLAG_STATUS.ENABLED, + filterMode: FILTER_MODE.INCLUDE_ALL, + context: ['context1', 'context2'], + tags: ['tag1', 'tag2'], + featureFlagSegmentInclusion: null, + featureFlagSegmentExclusion: null, + }, + { + createdAt: '2021-09-08T08:00:00.000Z', + updatedAt: '2021-09-08T08:00:00.000Z', + versionNumber: 1, + id: '3', + name: 'Feature Flag 2', + key: 'feature_flag_2', + description: 'Feature Flag 2 Description', + status: FEATURE_FLAG_STATUS.ENABLED, + filterMode: FILTER_MODE.INCLUDE_ALL, + context: ['context1', 'context2'], + tags: ['tag1', 'tag2'], + featureFlagSegmentInclusion: null, + featureFlagSegmentExclusion: null, + }, + { + createdAt: '2021-09-08T08:00:00.000Z', + updatedAt: '2021-09-08T08:00:00.000Z', + versionNumber: 1, + id: '4', + name: 'Feature Flag 4', + key: 'feature_flag_4', + description: 'Feature Flag 4 Description', + status: FEATURE_FLAG_STATUS.ENABLED, + filterMode: FILTER_MODE.INCLUDE_ALL, + context: ['context1', 'context2'], + tags: ['tag1', 'tag2'], + featureFlagSegmentInclusion: null, + featureFlagSegmentExclusion: null, + }, + { + createdAt: '2021-09-08T08:00:00.000Z', + updatedAt: '2021-09-08T08:00:00.000Z', + versionNumber: 1, + id: '5', + name: 'Feature Flag 5', + key: 'feature_flag_5', + description: 'Feature Flag 5 Description', + status: FEATURE_FLAG_STATUS.ENABLED, + filterMode: FILTER_MODE.INCLUDE_ALL, + context: ['context1', 'context2'], + tags: ['tag1', 'tag2'], + featureFlagSegmentInclusion: null, + featureFlagSegmentExclusion: null, + }, +]; diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.module.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.module.ts index b16f523c64..4f3f243f47 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.module.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.module.ts @@ -2,10 +2,18 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FeatureFlagsService } from './feature-flags.service'; import { FeatureFlagsDataService } from './feature-flags.data.service'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; +import { featureFlagsReducer } from './store/feature-flags.reducer'; +import { FeatureFlagsEffects } from './store/feature-flags.effects'; @NgModule({ declarations: [], - imports: [CommonModule], + imports: [ + CommonModule, + EffectsModule.forFeature([FeatureFlagsEffects]), + StoreModule.forFeature('featureFlags', featureFlagsReducer), + ], providers: [FeatureFlagsService, FeatureFlagsDataService], }) export class FeatureFlagsModule {} 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 4a6fac4548..4c981ab896 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 @@ -1,8 +1,46 @@ import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { Store, select } from '@ngrx/store'; import { AppState } from '../core.state'; +import { + selectAllFeatureFlagsSortedByDate, + selectIsAllFlagsFetched, + selectIsLoadingFeatureFlags, + selectHasInitialFeatureFlagsDataLoaded, + selectActiveDetailsTabIndex, +} from './store/feature-flags.selectors'; +import * as FeatureFlagsActions from './store/feature-flags.actions'; +import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types'; @Injectable() export class FeatureFlagsService { constructor(private store$: Store) {} + isInitialFeatureFlagsLoading$ = this.store$.pipe(select(selectHasInitialFeatureFlagsDataLoaded)); + isLoadingFeatureFlags$ = this.store$.pipe(select(selectIsLoadingFeatureFlags)); + allFeatureFlags$ = this.store$.pipe(select(selectAllFeatureFlagsSortedByDate)); + isAllFlagsFetched$ = this.store$.pipe(select(selectIsAllFlagsFetched)); + activeDetailsTabIndex$ = this.store$.pipe(select(selectActiveDetailsTabIndex)); + + fetchFeatureFlags(fromStarting?: boolean) { + this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting })); + } + + setSearchKey(searchKey: FLAG_SEARCH_KEY) { + this.store$.dispatch(FeatureFlagsActions.actionSetSearchKey({ searchKey })); + } + + setSearchString(searchString: string) { + this.store$.dispatch(FeatureFlagsActions.actionSetSearchString({ searchString })); + } + + setSortKey(sortKey: FLAG_SORT_KEY) { + this.store$.dispatch(FeatureFlagsActions.actionSetSortKey({ sortKey })); + } + + setSortingType(sortingType: SORT_AS_DIRECTION) { + this.store$.dispatch(FeatureFlagsActions.actionSetSortingType({ sortingType })); + } + + setActiveDetailsTab(activeDetailsTabIndex: number) { + this.store$.dispatch(FeatureFlagsActions.actionSetActiveDetailsTabIndex({ activeDetailsTabIndex })); + } } 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 new file mode 100644 index 0000000000..f62ccdf40a --- /dev/null +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts @@ -0,0 +1,44 @@ +import { createAction, props } from '@ngrx/store'; +import { FeatureFlag } from './feature-flags.model'; +import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types'; + +export const actionFetchFeatureFlags = createAction( + '[Feature Flags] Fetch Feature Flags Paginated', + props<{ fromStarting?: boolean }>() +); + +export const actionFetchFeatureFlagsSuccess = createAction( + '[Feature Flags] Fetch Feature Flags Paginated Success', + props<{ flags: FeatureFlag[]; totalFlags: number }>() +); + +export const actionFetchFeatureFlagsFailure = createAction('[Feature Flags] Fetch Feature Flags Paginated Failure'); + +export const actionSetIsLoadingFeatureFlags = createAction( + '[Feature Flags] Set Is Loading Flags', + props<{ isLoadingFeatureFlags: boolean }>() +); + +export const actionSetSkipFlags = createAction('[Feature Flags] Set Skip Flags', props<{ skipFlags: number }>()); + +export const actionSetSearchKey = createAction( + '[Feature Flags] Set Search key value', + props<{ searchKey: FLAG_SEARCH_KEY }>() +); + +export const actionSetSearchString = createAction( + '[Feature Flags] Set Search String', + props<{ searchString: string }>() +); + +export const actionSetSortKey = createAction('[Feature Flags] Set Sort key value', props<{ sortKey: FLAG_SORT_KEY }>()); + +export const actionSetSortingType = createAction( + '[Feature Flags] Set Sorting type', + props<{ sortingType: SORT_AS_DIRECTION }>() +); + +export const actionSetActiveDetailsTabIndex = createAction( + '[Feature Flags] Set Active Details Tab Index', + props<{ activeDetailsTabIndex: number }>() +); 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 new file mode 100644 index 0000000000..f96175f525 --- /dev/null +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts @@ -0,0 +1,116 @@ +import { FeatureFlagsDataService } from '../feature-flags.data.service'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Injectable } from '@angular/core'; +import * as FeatureFlagsActions from './feature-flags.actions'; +import { catchError, switchMap, map, filter, withLatestFrom, tap, first } from 'rxjs/operators'; +import { FeatureFlagsPaginationParams, NUMBER_OF_FLAGS } from './feature-flags.model'; +import { Router } from '@angular/router'; +import { Store, select } from '@ngrx/store'; +import { AppState } from '../../core.module'; +import { + selectTotalFlags, + selectSearchKey, + selectSkipFlags, + selectSortKey, + selectSortAs, + selectSearchString, +} from './feature-flags.selectors'; + +@Injectable() +export class FeatureFlagsEffects { + constructor( + private store$: Store, + private actions$: Actions, + private featureFlagsDataService: FeatureFlagsDataService, + private router: Router + ) {} + + fetchFeatureFlags$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionFetchFeatureFlags), + map((action) => action.fromStarting), + withLatestFrom( + this.store$.pipe(select(selectSkipFlags)), + this.store$.pipe(select(selectTotalFlags)), + this.store$.pipe(select(selectSearchKey)), + this.store$.pipe(select(selectSortKey)), + this.store$.pipe(select(selectSortAs)) + ), + filter(([fromStarting, skip, total]) => skip < total || total === null || fromStarting), + tap(() => { + this.store$.dispatch(FeatureFlagsActions.actionSetIsLoadingFeatureFlags({ isLoadingFeatureFlags: true })); + }), + switchMap(([fromStarting, skip, _, searchKey, sortKey, sortAs]) => { + let searchString = null; + // As withLatestFrom does not support more than 5 arguments + // TODO: Find alternative + this.getSearchString$().subscribe((searchInput) => { + searchString = searchInput; + }); + let params: FeatureFlagsPaginationParams = { + skip: fromStarting ? 0 : skip, + take: NUMBER_OF_FLAGS, + }; + if (sortKey) { + params = { + ...params, + sortParams: { + key: sortKey, + sortAs, + }, + }; + } + if (searchString) { + params = { + ...params, + searchParams: { + key: searchKey, + string: searchString, + }, + }; + } + return this.featureFlagsDataService.fetchFeatureFlagsPaginated(params).pipe( + switchMap((data: any) => { + const actions = fromStarting ? [FeatureFlagsActions.actionSetSkipFlags({ skipFlags: 0 })] : []; + return [ + ...actions, + FeatureFlagsActions.actionFetchFeatureFlagsSuccess({ flags: data.nodes, totalFlags: data.total }), + ]; + }), + catchError(() => [FeatureFlagsActions.actionFetchFeatureFlagsFailure()]) + ); + }) + ) + ); + + fetchFeatureFlagsOnSearchString$ = createEffect( + () => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionSetSearchString), + map((action) => action.searchString), + tap((searchString) => { + // Allow empty string as we erasing text from search input + if (searchString !== null) { + this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting: true })); + } + }) + ), + { dispatch: false } + ); + + fetchFlagsOnSearchKeyChange$ = createEffect( + () => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionSetSearchKey), + withLatestFrom(this.store$.pipe(select(selectSearchString))), + tap(([_, searchString]) => { + if (searchString) { + this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting: true })); + } + }) + ), + { dispatch: false } + ); + + 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 new file mode 100644 index 0000000000..bd5770969b --- /dev/null +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts @@ -0,0 +1,84 @@ +import { AppState } from '../../core.state'; +import { EntityState } from '@ngrx/entity'; +import { FEATURE_FLAG_STATUS, FILTER_MODE, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types'; +import { SegmentNew } from '../../experiments/store/experiments.model'; + +export interface FeatureFlag { + createdAt: string; + updatedAt: string; + versionNumber: number; + id: string; + name: string; + key: string; + description: string; + status: FEATURE_FLAG_STATUS; + filterMode: FILTER_MODE; + context: string[]; + tags: string[]; + featureFlagSegmentInclusion: SegmentNew; + featureFlagSegmentExclusion: SegmentNew; +} + +export const NUMBER_OF_FLAGS = 20; + +interface IFeatureFlagsSearchParams { + key: FLAG_SEARCH_KEY; + string: string; +} + +interface IFeatureFlagsSortParams { + key: FLAG_SORT_KEY; + sortAs: SORT_AS_DIRECTION; +} + +export interface FeatureFlagsPaginationParams { + skip: number; + take: number; + searchParams?: IFeatureFlagsSearchParams; + sortParams?: IFeatureFlagsSortParams; +} + +export enum FLAG_SEARCH_KEY { + ALL = 'all', + NAME = 'name', + KEY = 'key', + STATUS = 'status', + TAG = 'tag', + CONTEXT = 'context', +} + +export const FLAG_ROOT_COLUMN_NAMES = { + NAME: 'Name', + STATUS: 'Status', + UPDATED_AT: 'Updated at', + APP_CONTEXT: 'App Context', + TAGS: 'Tags', + EXPOSURES: 'Exposures', +}; + +export const FLAG_TRANSLATION_KEYS = { + NAME: 'feature-flags.global-name.text', + STATUS: 'feature-flags.global-status.text', + UPDATED_AT: 'feature-flags.global-updated-at.text', + APP_CONTEXT: 'feature-flags.global-app-context.text', + TAGS: 'feature-flags.global-tags.text', + EXPOSURES: 'feature-flags.global-exposures.text', +}; + +export const FLAG_ROOT_DISPLAYED_COLUMNS = Object.values(FLAG_ROOT_COLUMN_NAMES); + +export interface FeatureFlagState extends EntityState { + isLoadingFeatureFlags: boolean; + hasInitialFeatureFlagsDataLoaded: boolean; + activeDetailsTabIndex: number; + skipFlags: number; + totalFlags: number; + searchKey: FLAG_SEARCH_KEY; + searchString: string; + sortKey: FLAG_SORT_KEY; + sortAs: SORT_AS_DIRECTION; +} + +export interface State extends AppState { + featureFlags: FeatureFlagState; +} diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts new file mode 100644 index 0000000000..0e43b0541f --- /dev/null +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts @@ -0,0 +1,58 @@ +import { createReducer, Action, on } from '@ngrx/store'; +import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; +import { FeatureFlagState, FeatureFlag, FLAG_SEARCH_KEY } from './feature-flags.model'; +import * as FeatureFlagsActions from './feature-flags.actions'; + +export const adapter: EntityAdapter = createEntityAdapter(); + +export const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + +export const initialState: FeatureFlagState = adapter.getInitialState({ + isLoadingFeatureFlags: false, + hasInitialFeatureFlagsDataLoaded: false, + activeDetailsTabIndex: 0, + skipFlags: 0, + totalFlags: null, + searchKey: FLAG_SEARCH_KEY.ALL, + searchString: null, + sortKey: null, + sortAs: null, +}); + +const reducer = createReducer( + initialState, + on(FeatureFlagsActions.actionFetchFeatureFlags, (state) => ({ + ...state, + isLoadingFeatureFlags: true, + })), + on(FeatureFlagsActions.actionFetchFeatureFlagsSuccess, (state, { flags, totalFlags }) => { + const newState: FeatureFlagState = { + ...state, + totalFlags, + skipFlags: state.skipFlags + flags.length, + }; + return adapter.upsertMany(flags, { + ...newState, + isLoadingFeatureFlags: false, + hasInitialFeatureFlagsDataLoaded: true, + }); + }), + on(FeatureFlagsActions.actionFetchFeatureFlagsFailure, (state) => ({ ...state, isLoadingFeatureFlags: false })), + on(FeatureFlagsActions.actionSetIsLoadingFeatureFlags, (state, { isLoadingFeatureFlags }) => ({ + ...state, + isLoadingFeatureFlags, + })), + on(FeatureFlagsActions.actionSetSkipFlags, (state, { skipFlags }) => ({ ...state, skipFlags })), + on(FeatureFlagsActions.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })), + on(FeatureFlagsActions.actionSetSearchString, (state, { searchString }) => ({ ...state, searchString })), + on(FeatureFlagsActions.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })), + on(FeatureFlagsActions.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })), + on(FeatureFlagsActions.actionSetActiveDetailsTabIndex, (state, { activeDetailsTabIndex }) => ({ + ...state, + activeDetailsTabIndex, + })) +); + +export function featureFlagsReducer(state: FeatureFlagState | undefined, action: Action) { + return reducer(state, action); +} diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts new file mode 100644 index 0000000000..bd77bc739c --- /dev/null +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts @@ -0,0 +1,64 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { FeatureFlagState } from './feature-flags.model'; +import { selectRouterState } from '../../core.state'; +import { selectAll } from './feature-flags.reducer'; + +export const selectFeatureFlagsState = createFeatureSelector('featureFlags'); + +export const selectAllFeatureFlags = createSelector(selectFeatureFlagsState, selectAll); + +export const selectAllFeatureFlagsSortedByDate = createSelector(selectAllFeatureFlags, (featureFlags) => { + if (!featureFlags) { + return []; + } + return featureFlags.sort((a, b) => { + const d1 = new Date(a.createdAt); + const d2 = new Date(b.createdAt); + return d1 < d2 ? 1 : d1 > d2 ? -1 : 0; + }); +}); + +export const selectHasInitialFeatureFlagsDataLoaded = createSelector( + selectFeatureFlagsState, + (state) => state.hasInitialFeatureFlagsDataLoaded +); + +export const selectIsLoadingFeatureFlags = createSelector( + selectFeatureFlagsState, + (state) => state.isLoadingFeatureFlags +); + +export const selectIsInitialFeatureFlagsLoading = createSelector( + selectIsLoadingFeatureFlags, + selectAllFeatureFlagsSortedByDate, + (isLoading, featureFlags) => !isLoading || !!featureFlags.length +); + +export const selectSelectedFeatureFlag = createSelector( + selectRouterState, + selectFeatureFlagsState, + ({ state: { params } }, featureFlagState) => featureFlagState.entities[params.flagId] +); + +export const selectSkipFlags = createSelector(selectFeatureFlagsState, (state) => state.skipFlags); + +export const selectTotalFlags = createSelector(selectFeatureFlagsState, (state) => state.totalFlags); + +export const selectIsAllFlagsFetched = createSelector( + selectSkipFlags, + selectTotalFlags, + (skipFlags, totalFlags) => skipFlags === totalFlags +); + +export const selectSearchKey = createSelector(selectFeatureFlagsState, (state) => state.searchKey); + +export const selectSearchString = createSelector(selectFeatureFlagsState, (state) => state.searchString); + +export const selectSortKey = createSelector(selectFeatureFlagsState, (state) => state.sortKey); + +export const selectSortAs = createSelector(selectFeatureFlagsState, (state) => state.sortAs); + +export const selectActiveDetailsTabIndex = createSelector( + selectFeatureFlagsState, + (state) => state.activeDetailsTabIndex +); diff --git a/frontend/projects/upgrade/src/app/core/http-interceptors/http-error.interceptor.ts b/frontend/projects/upgrade/src/app/core/http-interceptors/http-error.interceptor.ts index f248470108..c8aa7faa2f 100755 --- a/frontend/projects/upgrade/src/app/core/http-interceptors/http-error.interceptor.ts +++ b/frontend/projects/upgrade/src/app/core/http-interceptors/http-error.interceptor.ts @@ -17,16 +17,11 @@ export class HttpErrorInterceptor implements HttpInterceptor { openPopup(error) { const temp = { type: NotificationType.Error, - title: 'Network call failed.', + title: 'Network call failed. See console for details.', content: error.url, animate: 'fromRight', }; - if (!this.environment.production) { - temp.title += ' See console for details.'; - } - if (!(error.status === 401) && !this.environment.production) { - this._notifications.create(temp.title, temp.content, temp.type, temp); - } + this._notifications.create(temp.title, temp.content, temp.type, temp); } intercept(request: HttpRequest, next: HttpHandler): Observable> { diff --git a/frontend/projects/upgrade/src/app/core/notifications/notification.service.ts b/frontend/projects/upgrade/src/app/core/notifications/notification.service.ts index ba70af05df..b8817b4cf7 100755 --- a/frontend/projects/upgrade/src/app/core/notifications/notification.service.ts +++ b/frontend/projects/upgrade/src/app/core/notifications/notification.service.ts @@ -56,8 +56,6 @@ export class NotificationService { if (snackBarDetail.type === NotificationType.Error) { snackBarDetail.title += ' See console for details.'; } - if (!this.environment.production) { - this._notifications.create(snackBarDetail.title, snackBarDetail.content, snackBarDetail.type, snackBarDetail); - } + this._notifications.create(snackBarDetail.title, snackBarDetail.content, snackBarDetail.type, snackBarDetail); } } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/common/tabs-view.scss b/frontend/projects/upgrade/src/app/features/dashboard/common/tabs-view.scss index 403ec025db..f5c9e2f36d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/common/tabs-view.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/common/tabs-view.scss @@ -3,7 +3,7 @@ flex-direction: column; box-shadow: unset !important; background: unset !important; - padding: 0 2.75rem 2.75rem; + padding: 25px 2.75rem 2.75rem; .title { margin-bottom: 0; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.html b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.html index a2e8c33c2c..672d2c7c38 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.html @@ -41,8 +41,7 @@ - -
+ diff --git a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.scss index 9eeb0e27e6..d29cb87dd4 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.component.scss @@ -53,7 +53,8 @@ mat-drawer { border-radius: 7px; background-color: var(--blue); - .icon, .list-item-label { + .icon, + .list-item-label { color: var(--white); } } @@ -109,15 +110,6 @@ mat-drawer { } } -.drawer__main-content { - &-header { - display: flex; - align-items: center; - justify-content: flex-end; - height: 25px; - } -} - .version { display: flex; flex-direction: column; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.theme.scss b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.theme.scss index 428c7cf0bc..202101d187 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.theme.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-root/dashboard-root.theme.scss @@ -20,8 +20,4 @@ } } } - - .drawer__main-content { - background-color: mat.get-color-from-palette($background, background); - } } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-routing.module.ts b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-routing.module.ts index 028c96b243..6c3100b89c 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/dashboard-routing.module.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/dashboard-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardRootComponent } from './dashboard-root/dashboard-root.component'; -import { FeatureFlagRootPageComponent } from './feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component'; const routes: Routes = [ { @@ -35,11 +34,22 @@ const routes: Routes = [ }, }, // feature-flags is built with standalone components instead of an ngModule, so we need to lazy load the component directly + // TODO: figure out how to load lazy-loaded child feature routes for feature flags if needed { path: 'featureflags', loadComponent: () => import('./feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component').then( - (m) => m.FeatureFlagRootPageComponent + (c) => c.FeatureFlagRootPageComponent + ), + data: { + title: 'app-header.title.feature-flag', + }, + }, + { + path: 'featureflags/detail/:flagId', + loadComponent: () => + import('./feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component').then( + (c) => c.FeatureFlagDetailsPageComponent ), data: { title: 'app-header.title.feature-flag', diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiment-users/experiment-users-root/experiment-users-root.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/experiment-users/experiment-users-root/experiment-users-root.component.scss index e60e743a03..f200f63be6 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiment-users/experiment-users-root/experiment-users-root.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/experiment-users/experiment-users-root/experiment-users-root.component.scss @@ -2,5 +2,5 @@ @import '../../common/tabs-view.scss'; .users-container { - height: calc(100% - 25px); // Subtract header height + height: 100%; } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/components/flag-variations/flag-variations.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/components/flag-variations/flag-variations.component.html index b41c5b8f0a..258f1e1e43 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/components/flag-variations/flag-variations.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/components/flag-variations/flag-variations.component.html @@ -17,9 +17,7 @@ {{ 'feature-flags.global-variation-value.text' | translate }} - + - + @@ -49,9 +45,7 @@ {{ 'feature-flags.variation-description.text' | translate }} - + {{ 'feature-flags.variation-default-variation.text' | translate }} - + {{ 'feature-flags.variation-on.text' | translate }} @@ -112,9 +104,7 @@ - + {{ 'feature-flags.variation-off.text' | translate }} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-root/feature-flags-root.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-root/feature-flags-root.component.scss index bfa73635ad..5d9fccc1d1 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-root/feature-flags-root.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-root/feature-flags-root.component.scss @@ -3,8 +3,8 @@ flex-direction: column; box-shadow: unset; background: unset; - padding: 0 2.75rem 2.75rem; - height: calc(100% - 25px); // Subtract theme selector's height + padding: 25px 2.75rem 2.75rem; + height: 100%; .title { margin-bottom: 0; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-routing.module.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-routing.module--LEGACY.ts similarity index 100% rename from frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-routing.module.ts rename to frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags-routing.module--LEGACY.ts diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags.module.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags.module.ts index 1cd2930234..4b3a1832d3 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags.module.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/feature-flags.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FeatureFlagsRoutingModule } from './feature-flags-routing.module'; +import { FeatureFlagsRoutingModule } from './feature-flags-routing.module--LEGACY'; import { FeatureFlagsRootComponent } from './feature-flags-root/feature-flags-root.component'; import { SharedModule } from '../../../shared/shared.module'; import { FeatureFlagsListComponent } from './components/feature-flags-list/feature-flags-list.component'; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/pages/view-feature-flag/view-feature-flag.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/pages/view-feature-flag/view-feature-flag.component.scss index 9f81f3c99e..546839021b 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/pages/view-feature-flag/view-feature-flag.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags-legacy/pages/view-feature-flag/view-feature-flag.component.scss @@ -3,7 +3,7 @@ $font-size-small: 15px; .flag-container { box-shadow: unset !important; - padding: 0 2.75rem 2.75rem; + padding: 25px 2.75rem 2.75rem; .flag-link { text-decoration: none; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.html new file mode 100644 index 0000000000..6c7d747348 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.html @@ -0,0 +1,7 @@ + + + inclusions-card + exclusions-card + exposures-card + + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.ts new file mode 100644 index 0000000000..335cfd54df --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-details-page-content.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonSectionCardListComponent } from '../../../../../../shared-standalone-component-lib/components'; +import { AsyncPipe, CommonModule, NgIf, NgSwitch, NgSwitchCase } from '@angular/common'; +import { FeatureFlagInclusionsSectionCardComponent } from './feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component'; +import { FeatureFlagExclusionsSectionCardComponent } from './feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component'; +import { FeatureFlagExposuresSectionCardComponent } from './feature-flag-exposures-section-card/feature-flag-exposures-section-card.component'; +import { FeatureFlagOverviewDetailsSectionCardComponent } from './feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component'; +import { FeatureFlagsService } from '../../../../../../core/feature-flags/feature-flags.service'; + +@Component({ + selector: 'app-feature-flag-details-page-content', + standalone: true, + imports: [ + CommonSectionCardListComponent, + CommonModule, + FeatureFlagInclusionsSectionCardComponent, + FeatureFlagExclusionsSectionCardComponent, + FeatureFlagExposuresSectionCardComponent, + FeatureFlagOverviewDetailsSectionCardComponent, + ], + templateUrl: './feature-flag-details-page-content.component.html', + styleUrl: './feature-flag-details-page-content.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagDetailsPageContentComponent { + activeTabIndex$ = this.featureFlagsService.activeDetailsTabIndex$; + + constructor(private featureFlagsService: FeatureFlagsService) { + console.log('in the ff content component'); + this.activeTabIndex$.subscribe((activeTabIndex) => { + console.log('activeTabIndex', activeTabIndex); + }); + } +} 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 new file mode 100644 index 0000000000..49e6f5fa82 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ + +
header-left
+
header-right
+
content: Exclusions
+
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.scss 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.scss new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..0c25764c2e --- /dev/null +++ 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 @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonSectionCardComponent } from '../../../../../../../shared-standalone-component-lib/components'; + +@Component({ + selector: 'app-feature-flag-exclusions-section-card', + standalone: true, + imports: [CommonSectionCardComponent], + templateUrl: './feature-flag-exclusions-section-card.component.html', + styleUrl: './feature-flag-exclusions-section-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagExclusionsSectionCardComponent {} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-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-exposures-section-card/feature-flag-exposures-section-card.component.html new file mode 100644 index 0000000000..791f6df23d --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-section-card.component.html @@ -0,0 +1,5 @@ + +
header-left
+
header-right
+
content: Exposures
+
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-section-card.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-section-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-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-exposures-section-card/feature-flag-exposures-section-card.component.ts new file mode 100644 index 0000000000..c32f0d52b0 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exposures-section-card/feature-flag-exposures-section-card.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonSectionCardComponent } from '../../../../../../../shared-standalone-component-lib/components'; + +@Component({ + selector: 'app-feature-flag-exposures-section-card', + standalone: true, + imports: [CommonSectionCardComponent], + templateUrl: './feature-flag-exposures-section-card.component.html', + styleUrl: './feature-flag-exposures-section-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagExposuresSectionCardComponent {} 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 new file mode 100644 index 0000000000..dcd4b88584 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ + +
header-left
+
header-right
+
content: Inclusions
+
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.scss 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.scss new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..67d9197337 --- /dev/null +++ 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 @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonSectionCardComponent } from '../../../../../../../shared-standalone-component-lib/components'; + +@Component({ + selector: 'app-feature-flag-inclusions-section-card', + standalone: true, + imports: [CommonSectionCardComponent], + templateUrl: './feature-flag-inclusions-section-card.component.html', + styleUrl: './feature-flag-inclusions-section-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagInclusionsSectionCardComponent {} 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-footer/feature-flag-overview-details-footer.component.html 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-footer/feature-flag-overview-details-footer.component.html new file mode 100644 index 0000000000..97313efe5b --- /dev/null +++ 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-footer/feature-flag-overview-details-footer.component.html @@ -0,0 +1,4 @@ + \ No newline at end of file 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-footer/feature-flag-overview-details-footer.component.scss 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-footer/feature-flag-overview-details-footer.component.scss new file mode 100644 index 0000000000..e69de29bb2 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-footer/feature-flag-overview-details-footer.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-footer/feature-flag-overview-details-footer.component.ts new file mode 100644 index 0000000000..912beb8858 --- /dev/null +++ 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-footer/feature-flag-overview-details-footer.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonTabbedSectionCardFooterComponent } from '../../../../../../../../shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component'; +import { FeatureFlagsService } from '../../../../../../../../core/feature-flags/feature-flags.service'; + +@Component({ + selector: 'app-feature-flag-overview-details-footer', + standalone: true, + imports: [CommonTabbedSectionCardFooterComponent], + templateUrl: './feature-flag-overview-details-footer.component.html', + styleUrl: './feature-flag-overview-details-footer.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagOverviewDetailsFooterComponent { + tabLabels = ['Participants', 'Data']; + + constructor(private featureFlagsService: FeatureFlagsService) {} + + onSelectedTabChange(selectedTabIndex: number): void { + this.featureFlagsService.setActiveDetailsTab(selectedTabIndex); + } +} 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.html 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.html new file mode 100644 index 0000000000..3fccbe140f --- /dev/null +++ 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.html @@ -0,0 +1,6 @@ + +
header-left
+
header-right
+
content: Details Overview
+ +
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.scss 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.scss new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..7d20ebbe2d --- /dev/null +++ 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 @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonSectionCardComponent } from '../../../../../../../shared-standalone-component-lib/components'; +import { FeatureFlagOverviewDetailsFooterComponent } from './feature-flag-overview-details-footer/feature-flag-overview-details-footer.component'; + +@Component({ + selector: 'app-feature-flag-overview-details-section-card', + standalone: true, + imports: [CommonSectionCardComponent, FeatureFlagOverviewDetailsFooterComponent], + templateUrl: './feature-flag-overview-details-section-card.component.html', + styleUrl: './feature-flag-overview-details-section-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagOverviewDetailsSectionCardComponent {} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.html new file mode 100644 index 0000000000..04deed9397 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.html @@ -0,0 +1,6 @@ + + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.ts new file mode 100644 index 0000000000..bad4626bac --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-header/feature-flag-details-page-header.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { CommonDetailsPageHeaderComponent } from '../../../../../../shared-standalone-component-lib/components'; + +@Component({ + selector: 'app-feature-flag-details-page-header', + standalone: true, + imports: [CommonDetailsPageHeaderComponent], + templateUrl: './feature-flag-details-page-header.component.html', + styleUrl: './feature-flag-details-page-header.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagDetailsPageHeaderComponent { + flagName = 'feature flag 1'; +} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.html new file mode 100644 index 0000000000..a643791269 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.ts new file mode 100644 index 0000000000..b4a5386c85 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { CommonPageComponent } from '../../../../../shared-standalone-component-lib/components'; +import { FeatureFlagDetailsPageHeaderComponent } from './feature-flag-details-page-header/feature-flag-details-page-header.component'; +import { FeatureFlagDetailsPageContentComponent } from './feature-flag-details-page-content/feature-flag-details-page-content.component'; + +@Component({ + selector: 'app-feature-flag-details-page', + standalone: true, + templateUrl: './feature-flag-details-page.component.html', + styleUrl: './feature-flag-details-page.component.scss', + imports: [CommonPageComponent, FeatureFlagDetailsPageHeaderComponent, FeatureFlagDetailsPageContentComponent], +}) +export class FeatureFlagDetailsPageComponent {} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.html index 8232320822..0dffa7be4f 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.html @@ -1,3 +1,3 @@ -
Hi I'm a section card 1
+
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.ts index 7db9c9bc9a..69cb4cb4bd 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-page-content.component.ts @@ -1,10 +1,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { CommonSectionCardListComponent } from '../../../../../../shared-standalone-component-lib/components'; +import { FeatureFlagRootSectionCardComponent } from './feature-flag-root-section-card/feature-flag-root-section-card.component'; @Component({ selector: 'app-feature-flag-root-page-content', standalone: true, - imports: [CommonSectionCardListComponent], + imports: [CommonSectionCardListComponent, FeatureFlagRootSectionCardComponent], templateUrl: './feature-flag-root-page-content.component.html', styleUrl: './feature-flag-root-page-content.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.html new file mode 100644 index 0000000000..fc14acace7 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.html @@ -0,0 +1,98 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{ FLAG_TRANSLATION_KEYS.NAME | translate | uppercase }} + + + + {{ flag.name }} + + + + {{ flag.name | truncate: 30 }} + + +
+ + {{ flag.description }} + + + + {{ flag.description | truncate: 35 }} + + +
+ {{ FLAG_TRANSLATION_KEYS.STATUS | translate | uppercase }} + {{ flag.status }} + + {{ FLAG_TRANSLATION_KEYS.UPDATED_AT | translate | uppercase }} + + + {{ flag.updatedAt }} + + + {{ FLAG_TRANSLATION_KEYS.APP_CONTEXT | translate | uppercase }} + + + {{ flag.context[0] }} + + {{ FLAG_TRANSLATION_KEYS.TAGS | translate | uppercase }} + + {{ tag }} + + + {{ FLAG_TRANSLATION_KEYS.EXPOSURES | translate | uppercase }} + + + {{ exposure }} +
+ {{ 'feature-flags.no-flags-in-table.text' | translate }} +
+
+
\ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.scss new file mode 100644 index 0000000000..cfc990b95a --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.scss @@ -0,0 +1,65 @@ +:host ::ng-deep .flags-list-container { + height: 100%; + width: 100%; + + .flags-list-table-container { + position: relative; + margin-top: 8px; + overflow: auto; + width: 100%; + + .spinner { + position: sticky; + top: 0; + z-index: 1111; + } + } + + .flags-list { + width: 100%; + + th { + color: var(--grey-2); + } + + tr.mat-mdc-footer-row, + tr.mat-mdc-row { + height: 55px; + } + + .mat-mdc-cell, + .mat-mdc-header-cell { + padding: 10px 5px; + padding-left: 20px; + width: 10%; + word-break: break-word; + } + + .mat-mdc-header-cell { + background-color: var(--zircon); + margin-bottom: 10px; + } + + .flag { + &-name { + text-decoration: underline; + cursor: pointer; + } + + &-description { + color: var(--grey-3); + } + } + + .mat-mdc-form-field { + width: 40%; + } + } + + .no-data-row { + text-align: center; + font-size: 16px; + color: var(--dark-grey); + padding: 16px; + } +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.ts new file mode 100644 index 0000000000..202f29600d --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card-table/feature-flag-root-section-card-table.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + FLAG_ROOT_COLUMN_NAMES, + FLAG_ROOT_DISPLAYED_COLUMNS, + FLAG_TRANSLATION_KEYS, + FeatureFlag, +} from '../../../../../../../../core/feature-flags/store/feature-flags.model'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { AsyncPipe, NgIf, NgFor, UpperCasePipe } from '@angular/common'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatChipsModule } from '@angular/material/chips'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-feature-flag-root-section-card-table', + standalone: true, + imports: [ + MatTableModule, + AsyncPipe, + NgIf, + NgFor, + MatTooltipModule, + TranslateModule, + UpperCasePipe, + MatChipsModule, + RouterModule, + ], + templateUrl: './feature-flag-root-section-card-table.component.html', + styleUrl: './feature-flag-root-section-card-table.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagRootSectionCardTableComponent { + @Input() dataSource$: MatTableDataSource; + @Input() isLoading$: Observable; + + get displayedColumns(): string[] { + return FLAG_ROOT_DISPLAYED_COLUMNS; + } + + get FLAG_TRANSLATION_KEYS() { + return FLAG_TRANSLATION_KEYS; + } + + get FLAG_ROOT_COLUMN_NAMES() { + return FLAG_ROOT_COLUMN_NAMES; + } + + fetchFlagsOnScroll() { + console.log('fetchFlagsOnScroll'); + } + + changeSorting($event) { + console.log('onSearch:', $event); + } +} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.html new file mode 100644 index 0000000000..0695f0c4cb --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.scss new file mode 100644 index 0000000000..926168f52e --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.scss @@ -0,0 +1,3 @@ +.full-width { + width: 100%; // TODO try not to set here, should be in the common parent... +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts new file mode 100644 index 0000000000..d7b46c6cff --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts @@ -0,0 +1,75 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + CommonSectionCardComponent, + CommonSectionCardSearchHeaderComponent, + CommonSectionCardActionButtonsComponent, +} from '../../../../../../../shared-standalone-component-lib/components'; +import { FeatureFlagsService } from '../../../../../../../core/feature-flags/feature-flags.service'; +import { AsyncPipe, JsonPipe, NgIf } from '@angular/common'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { FeatureFlagRootSectionCardTableComponent } from './feature-flag-root-section-card-table/feature-flag-root-section-card-table.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { IMenuButtonItem } from 'upgrade_types'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-feature-flag-root-section-card', + standalone: true, + imports: [ + CommonSectionCardComponent, + CommonSectionCardSearchHeaderComponent, + CommonSectionCardActionButtonsComponent, + FeatureFlagRootSectionCardTableComponent, + AsyncPipe, + JsonPipe, + NgIf, + MatProgressSpinnerModule, + RouterModule, + TranslateModule, + ], + templateUrl: './feature-flag-root-section-card.component.html', + styleUrl: './feature-flag-root-section-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureFlagRootSectionCardComponent { + isLoadingFeatureFlags$ = this.featureFlagService.isLoadingFeatureFlags$; + isInitialLoading$ = this.featureFlagService.isInitialFeatureFlagsLoading$; + allFeatureFlags$ = this.featureFlagService.allFeatureFlags$; + isAllFlagsFetched$ = this.featureFlagService.isAllFlagsFetched$; + isSectionCardExpanded = true; + + menuButtonItems: IMenuButtonItem[] = [ + { + name: this.translateService.instant('feature-flags.import-feature-flag.text'), + disabled: false, + }, + { + name: this.translateService.instant('feature-flags.export-all-feature-flags.text'), + disabled: true, + }, + ]; + + constructor(private featureFlagService: FeatureFlagsService, private translateService: TranslateService) {} + + ngOnInit() { + this.featureFlagService.fetchFeatureFlags(); + } + + onSearch(searchString: string) { + console.log('searchString', searchString); + // this.featureFlagService.setSearchString(searchString); + } + + onAddFeatureFlagButtonClick() { + console.log('onAddFeatureFlagButtonClick'); + } + + onMenuButtonItemClick(menuButtonItemName: string) { + console.log('onMenuButtonItemClick:', menuButtonItemName); + } + + onSectionCardExpandChange(isSectionCardExpanded: boolean) { + console.log('onSectionCardExpandChange:', isSectionCardExpanded); + this.isSectionCardExpanded = isSectionCardExpanded; + } +} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.html index 690536fb25..c8bcdd152d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.html @@ -1,4 +1,2 @@ - + + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.ts index 80dfc20fb9..dbfc977bf7 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-header/feature-flag-root-page-header.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { CommonRootPageHeaderContainerComponent } from '../../../../../../shared-standalone-component-lib/components/'; +import { CommonRootPageHeaderComponent } from '../../../../../../shared-standalone-component-lib/components'; @Component({ selector: 'app-feature-flag-root-page-header', standalone: true, - imports: [CommonRootPageHeaderContainerComponent], + imports: [CommonRootPageHeaderComponent], templateUrl: './feature-flag-root-page-header.component.html', styleUrl: './feature-flag-root-page-header.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.html index 02a294b164..34bdac627e 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.html @@ -1,4 +1,4 @@ - + - + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.ts index 24981bf7b8..f3e80a384f 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { CommonRootPageComponent } from '../../../../../shared-standalone-component-lib/components'; +import { CommonPageComponent } from '../../../../../shared-standalone-component-lib/components'; import { FeatureFlagRootPageHeaderComponent } from './feature-flag-root-page-header/feature-flag-root-page-header.component'; import { FeatureFlagRootPageContentComponent } from './feature-flag-root-page-content/feature-flag-root-page-content.component'; @@ -8,6 +8,6 @@ import { FeatureFlagRootPageContentComponent } from './feature-flag-root-page-co standalone: true, templateUrl: './feature-flag-root-page.component.html', styleUrl: './feature-flag-root-page.component.scss', - imports: [CommonRootPageComponent, FeatureFlagRootPageHeaderComponent, FeatureFlagRootPageContentComponent], + imports: [CommonPageComponent, FeatureFlagRootPageHeaderComponent, FeatureFlagRootPageContentComponent], }) export class FeatureFlagRootPageComponent {} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html b/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html index ca6b2f155e..fba56d8e99 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html @@ -1,10 +1,7 @@
- + - +
- + - + - + - + - + - +
- + {{ measure | titlecase }}
@@ -107,24 +113,28 @@ selectedNode[queryIndex]?.metadata?.type === IMetricMetadata.CATEGORICAL " > - + {{ measure | titlecase }}
- + - + {{ operation.viewValue | titlecase }} @@ -165,7 +175,11 @@ [placeholder]="'query.form-compare-value.text' | translate | titlecase" formControlName="compareValue" > - + {{ value | titlecase }} @@ -179,11 +193,7 @@ {{ 'home.new-experiment.metrics.description-header.placeholder.text' | translate }} - + - +
- +
- + {{ 'home.view-experiment.export-experiment.text' | translate | titlecase }}
- + {{ 'home.view-experiment.export-method.text' | translate }} @@ -36,9 +32,13 @@ mat-raised-button [ngClass]="{ 'default-button--disabled': - (exportForm.get('exportMethod').value === 'Email Experiment Data (CSV)' && experiments.length > 1) || !isExportMethodSelected + (exportForm.get('exportMethod').value === 'Email Experiment Data (CSV)' && experiments.length > 1) || + !isExportMethodSelected }" - [disabled]="(exportForm.get('exportMethod').value === 'Email Experiment Data (CSV)' && experiments.length > 1) || !isExportMethodSelected" + [disabled]=" + (exportForm.get('exportMethod').value === 'Email Experiment Data (CSV)' && experiments.length > 1) || + !isExportMethodSelected + " class="shared-modal--modal-btn default-button" (click)="exportExperiment()" > diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.html b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.html index e2645b5fb3..3900306e47 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.html @@ -80,16 +80,11 @@
- + - {{ 'home.view-experiment.mat-tab.overview.text' | translate }} + {{ 'home.view-experiment.mat-tab.overview.text' | translate }}
@@ -202,9 +197,7 @@
- + {{ 'home-global.design-type.text' | translate | uppercase }} {{ experiment.type | titlecase }} Experiment @@ -254,9 +247,7 @@
- + {{ 'home-global.assignment-algorithm.text' | translate | uppercase }} {{ experiment.assignmentAlgorithm | titlecase }} @@ -276,7 +267,9 @@ - + create @@ -334,7 +334,12 @@ {{ experiment.endOn | formatDate: 'medium date' }} @@ -353,7 +358,9 @@ @@ -376,8 +383,7 @@ {{ 'home.new-experiment.design.exclude-if-reached.text' | translate }} - - + @@ -395,14 +401,13 @@
-
- +
+ {{ 'home-global.stratification-factor.text' | translate | uppercase }} - {{ experiment.stratificationFactor.stratificationFactorName }} + {{ experiment.stratificationFactor.stratificationFactorName }} +
@@ -412,10 +417,7 @@ {{ 'global.context.text' | translate }} - + {{ context }} @@ -427,10 +429,7 @@ {{ 'global.tags.text' | translate }} - + {{ tag }} @@ -443,7 +442,9 @@ [checked]="experiment.logging" color="primary" (change)="toggleVerboseLogging($event)" - [disabled]="experiment.state === ExperimentState.CANCELLED || experiment.state === ExperimentState.ARCHIVED" + [disabled]=" + experiment.state === ExperimentState.CANCELLED || experiment.state === ExperimentState.ARCHIVED + " > {{ 'home.view-experiment.logging.text' | translate | uppercase }} @@ -456,7 +457,7 @@ - {{ 'home.view-experiment.mat-tab.design.text' | translate }} + {{ 'home.view-experiment.mat-tab.design.text' | translate }}
@@ -504,8 +505,7 @@ {{ 'home.new-experiment.design.exclude-if-reached.text' | translate }} - - + @@ -774,7 +774,7 @@ - {{ 'home.view-experiment.mat-tab.participants.text' | translate }} + {{ 'home.view-experiment.mat-tab.participants.text' | translate }}
@@ -869,7 +869,7 @@ - {{ 'home.view-experiment.mat-tab.metrics.text' | translate }} + {{ 'home.view-experiment.mat-tab.metrics.text' | translate }}
@@ -932,7 +932,7 @@ - {{ 'home.view-experiment.mat-tab.data.text' | translate }} + {{ 'home.view-experiment.mat-tab.data.text' | translate }}
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.scss index 37087558b9..47113edc80 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.scss @@ -2,7 +2,7 @@ $dark-grey-text-color: #646e7b; $font-size-small: 15px; .experiment-container { - padding: 0 2.75rem 2.75rem; + padding: 25px 2.75rem 2.75rem; box-shadow: 0px 0px #888888 !important; .experiment-link { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/root/home.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/home/root/home.component.scss index 24221a46e9..ab85d41816 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/root/home.component.scss +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/root/home.component.scss @@ -3,8 +3,8 @@ flex-direction: column; box-shadow: unset; background: unset; - padding: 0 2.75rem 2.75rem; - height: calc(100% - 25px); // Subtract theme selector's height + padding: 25px 2.75rem 2.75rem; + height: 100%; .title { margin-bottom: 0; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/validators/experiment-form.validators.ts b/frontend/projects/upgrade/src/app/features/dashboard/home/validators/experiment-form.validators.ts index 4a13addc06..56a8093f16 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/validators/experiment-form.validators.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/validators/experiment-form.validators.ts @@ -9,7 +9,7 @@ export class ExperimentFormValidators { return { assignmentWeightsSumError: false }; } else if (conditions.length >= 1) { const conditionWeight = conditions.map((condition) => condition.assignmentWeight); - if (!conditionWeight[0]) { + if (conditionWeight[0] === undefined || conditionWeight[0] === null) { return { assignmentWeightsSumError: false }; } else { // handling sum of decimal values for assignment weights: diff --git a/frontend/projects/upgrade/src/app/features/dashboard/logs/components/error-logs/error-logs.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/logs/components/error-logs/error-logs.component.ts index 0abe6bf81c..bf2f2e1fd1 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/logs/components/error-logs/error-logs.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/logs/components/error-logs/error-logs.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit, Inject } from '@angular/core'; import { LogType, ErrorLogs, LogDateFormatType } from '../../../../../core/logs/store/logs.model'; import { LogsService } from '../../../../../core/logs/logs.service'; import * as groupBy from 'lodash.groupby'; import { KeyValue } from '@angular/common'; import { Subscription } from 'rxjs'; import { SettingsService } from '../../../../../core/settings/settings.service'; +import { ENV, Environment } from '../../../../../../environments/environment-types'; @Component({ selector: 'error-logs', @@ -19,7 +20,11 @@ export class ErrorLogsComponent implements OnInit, OnDestroy, AfterViewInit { isAllErrorLogFetchedSub: Subscription; isErrorLogLoading$ = this.logsService.isErrorLogLoading$; - constructor(private logsService: LogsService, private settingsService: SettingsService) {} + constructor( + private logsService: LogsService, + private settingsService: SettingsService, + @Inject(ENV) public environment: Environment + ) {} get LogType() { return LogType; @@ -30,8 +35,6 @@ export class ErrorLogsComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnInit() { - // temporarily disabled - this.errorLogSubscription = this.logsService.getAllErrorLogs$.subscribe((errorLogs) => { errorLogs.sort((a, b) => (a.createdAt > b.createdAt ? -1 : a.createdAt < b.createdAt ? 1 : 0)); this.errorLogData = groupBy(errorLogs, (log) => { @@ -53,7 +56,7 @@ export class ErrorLogsComponent implements OnInit, OnDestroy, AfterViewInit { fetchErrorLogOnScroll() { if (!this.isAllErrorLogFetched) { - this.logsService.fetchErrorLogs(); // temporarily disabled + this.logsService.fetchErrorLogs(); } } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/logs/root/logs.component.html b/frontend/projects/upgrade/src/app/features/dashboard/logs/root/logs.component.html index b71425698c..77be17946c 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/logs/root/logs.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/logs/root/logs.component.html @@ -13,26 +13,22 @@

{{ 'logs.main-heading.text' | translate }}

- {{ 'logs.audit-logs-tab.text' | translate }} + {{ 'logs.audit-logs-tab.text' | translate }} - - + - + {{ 'global.import.text' | translate }} @@ -57,4 +54,4 @@
- \ No newline at end of file + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.html b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.html index 673d315347..c9c8cd63f6 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.html @@ -8,12 +8,12 @@ {{ 'segments.global-members-type.text' | translate }} - - + + {{ val.name }} @@ -31,11 +31,7 @@ - + {{ subSegmentId }} @@ -44,15 +40,12 @@ - + @@ -60,10 +53,14 @@ -