diff --git a/backend/packages/Upgrade/.env.docker.local.example b/backend/packages/Upgrade/.env.docker.local.example index 903356e001..19f401f9ee 100644 --- a/backend/packages/Upgrade/.env.docker.local.example +++ b/backend/packages/Upgrade/.env.docker.local.example @@ -73,9 +73,9 @@ EMAIL_BUCKET="s3_bucket" # # Initialization # -ADMIN_USERS=user@email:role/\user2@email:role +ADMIN_USERS=user@email:admin/\user2@email:admin CLIENT_API_SECRET=secret CLIENT_API_KEY=key -CONTEXT_METADATA={"context_identifier_1":{"CONDITIONS":["potential-condition-1","potential-condition-1"],"GROUP_TYPES":["client_group_identifier_1","client_group_identifier_2","client_group_identifier_3"],"EXP_IDS":["decision_point_target_identifier_1","decision_point_target_identifier_2"],"EXP_POINTS":["decision_point_site_identifier_1","decision_point_site_identifier_2"]}} +CONTEXT_METADATA={"context_identifier_1":{"CONDITIONS":["potential-condition-1","potential-condition-2"],"GROUP_TYPES":["client_group_identifier_1","client_group_identifier_2","client_group_identifier_3"],"EXP_IDS":["decision_point_target_identifier_1","decision_point_target_identifier_2"],"EXP_POINTS":["decision_point_site_identifier_1","decision_point_site_identifier_2"]}} METRICS=[{"metric":"totalTimeSeconds","datatype":"continuous"},{"groupClass":"masteryWorkspace","allowedKeys":["calculating_area_various_figures","Compare_functions_diff_reps_quadratic"],"attributes":[{"metric":"timeSeconds","datatype":"continuous"}]}] \ No newline at end of file diff --git a/backend/packages/Upgrade/.env.example b/backend/packages/Upgrade/.env.example index afba4be77e..058737a64b 100644 --- a/backend/packages/Upgrade/.env.example +++ b/backend/packages/Upgrade/.env.example @@ -74,9 +74,9 @@ EMAIL_BUCKET="s3_bucket" # # Initialization # -ADMIN_USERS=user@email:role/\user2@email:role +ADMIN_USERS=user@email:admin/\user2@email:admin CLIENT_API_SECRET=secret CLIENT_API_KEY=key -CONTEXT_METADATA={"context_identifier_1":{"CONDITIONS":["potential-condition-1","potential-condition-1"],"GROUP_TYPES":["client_group_identifier_1","client_group_identifier_2","client_group_identifier_3"],"EXP_IDS":["decision_point_target_identifier_1","decision_point_target_identifier_2"],"EXP_POINTS":["decision_point_site_identifier_1","decision_point_site_identifier_2"]}} +CONTEXT_METADATA={"context_identifier_1":{"CONDITIONS":["potential-condition-1","potential-condition-2"],"GROUP_TYPES":["client_group_identifier_1","client_group_identifier_2","client_group_identifier_3"],"EXP_IDS":["decision_point_target_identifier_1","decision_point_target_identifier_2"],"EXP_POINTS":["decision_point_site_identifier_1","decision_point_site_identifier_2"]}} METRICS=[{"metrics":[{"metric":"totalTimeSeconds","datatype":"continuous"},{"groupClass":"masteryWorkspace","allowedKeys":["calculating_area_various_figures","Compare_functions_diff_reps_quadratic"],"attributes":[{"metric":"timeSeconds","datatype":"continuous"}]}],"contexts":["context_identifier_1"]}] diff --git a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.ts b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.ts index 9a2215f379..5713161d03 100644 --- a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.ts +++ b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.ts @@ -91,7 +91,7 @@ interface IExperimentAssignment { /** * @swagger * tags: - * - name: Client Side SDK + * - name: Client API calls * description: CRUD operations related to experiments points */ @@ -155,7 +155,7 @@ export class ExperimentClientController { * example: instructor1 * description: ExperimentUser * tags: - * - Client Side SDK + * - Client API calls * produces: * - application/json * responses: @@ -163,6 +163,15 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error + * */ @Post('init') public async init( @@ -247,8 +256,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Post('groupmembership') public async setGroupMemberShip( @@ -310,8 +325,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Post('workinggroup') public async setWorkingGroup( @@ -413,8 +434,14 @@ export class ExperimentClientController { * - enrollmentCode * - userId * - condition + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: User not defined + * description: Internal Server Error */ @Post('mark') public async markExperimentPoint( @@ -529,10 +556,14 @@ export class ExperimentClientController { * - conditionCode * - assignmentWeight * - order - * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '404': - * description: Experiment user not defined + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('assign') public async getAllExperimentConditions( @@ -631,8 +662,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log') public async log( @@ -670,9 +707,15 @@ export class ExperimentClientController { * - application/json * responses: * '200': - * description: Log data + * description: Log Caliper data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log/caliper') public async caliperLog( @@ -727,6 +770,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log blob data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('bloblog') public async blobLog(@Req() request: express.Request): Promise { @@ -796,8 +847,14 @@ export class ExperimentClientController { * responses: * '200': * description: Client side reported error + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('failed') public async failedExperimentPoint( @@ -852,6 +909,14 @@ export class ExperimentClientController { * responses: * '200': * description: Feature flags list + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('featureflag') public async getAllFlags( @@ -886,6 +951,10 @@ export class ExperimentClientController { * responses: * '200': * description: Filtered Metrics + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: Insert error in database */ @@ -962,8 +1031,14 @@ export class ExperimentClientController { * originalUser: * type: string * minLength: 1 + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('useraliases') public async setUserAliases( @@ -1008,6 +1083,10 @@ export class ExperimentClientController { * responses: * '200': * description: Database cleared + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: DEMO mode is disabled */ diff --git a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v1.ts b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v1.ts index 91db90bbb5..d36fa28c8e 100644 --- a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v1.ts +++ b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v1.ts @@ -168,6 +168,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('init') public async init( @@ -251,8 +259,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('groupmembership') public async setGroupMemberShip( @@ -319,8 +333,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('workinggroup') public async setWorkingGroup( @@ -414,8 +434,14 @@ export class ExperimentClientController { * - enrollmentCode * - userId * - condition + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: User not defined + * description: Internal Server Error */ @Post('mark') public async markExperimentPoint( @@ -493,10 +519,14 @@ export class ExperimentClientController { * condition: * type: string * minLength: 1 - * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '404': - * description: Experiment user not defined + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('assign') public async getAllExperimentConditions( @@ -594,8 +624,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log') public async log( @@ -636,9 +672,15 @@ export class ExperimentClientController { * - application/json * responses: * '200': - * description: Log data + * description: Log Caliper data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log/caliper') public async caliperLog( @@ -693,6 +735,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log blob data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('bloblog') public async blobLog(@Req() request: express.Request): Promise { @@ -762,8 +812,14 @@ export class ExperimentClientController { * responses: * '200': * description: Client side reported error + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('failed') public async failedExperimentPoint( @@ -818,6 +874,14 @@ export class ExperimentClientController { * responses: * '200': * description: Feature flags list + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('featureflag') public async getAllFlags( @@ -852,6 +916,10 @@ export class ExperimentClientController { * responses: * '200': * description: Filtered Metrics + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: Insert error in database */ @@ -911,8 +979,14 @@ export class ExperimentClientController { * required: * - userId * - userAliases + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Patch('useraliases') public async setUserAliases( @@ -942,6 +1016,10 @@ export class ExperimentClientController { * responses: * '200': * description: Database cleared + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: DEMO mode is disabled */ diff --git a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v4.ts b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v4.ts index 9b4f513583..b4ba343665 100644 --- a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v4.ts +++ b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v4.ts @@ -167,6 +167,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('init') public async init( @@ -250,8 +258,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('groupmembership') public async setGroupMemberShip( @@ -318,8 +332,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('workinggroup') public async setWorkingGroup( @@ -413,8 +433,14 @@ export class ExperimentClientController { * - enrollmentCode * - userId * - condition + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: User not defined + * description: Internal Server Error */ @Post('mark') public async markExperimentPoint( @@ -493,10 +519,14 @@ export class ExperimentClientController { * condition: * type: string * minLength: 1 - * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '404': - * description: Experiment user not defined + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('assign') public async getAllExperimentConditions( @@ -614,8 +644,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log') public async log( @@ -656,9 +692,15 @@ export class ExperimentClientController { * - application/json * responses: * '200': - * description: Log data + * description: Log Caliper data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log/caliper') public async caliperLog( @@ -713,6 +755,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log blob data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('bloblog') public async blobLog(@Req() request: express.Request): Promise { @@ -780,6 +830,14 @@ export class ExperimentClientController { * responses: * '200': * description: Feature flags list + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('featureflag') public async getAllFlags( @@ -814,6 +872,10 @@ export class ExperimentClientController { * responses: * '200': * description: Filtered Metrics + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: Insert error in database */ @@ -873,8 +935,14 @@ export class ExperimentClientController { * required: * - userId * - userAliases + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Patch('useraliases') public async setUserAliases( @@ -904,6 +972,10 @@ export class ExperimentClientController { * responses: * '200': * description: Database cleared + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: DEMO mode is disabled */ diff --git a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v5.ts b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v5.ts index ae0b6d4875..58f747b1e6 100644 --- a/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v5.ts +++ b/backend/packages/Upgrade/src/api/controllers/ExperimentClientController.v5.ts @@ -163,6 +163,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('init') public async init( @@ -246,8 +254,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('groupmembership') public async setGroupMemberShip( @@ -314,8 +328,14 @@ export class ExperimentClientController { * description: Set Group Membership * schema: * $ref: '#/definitions/initResponse' + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * description: Internal Server Error */ @Patch('workinggroup') public async setWorkingGroup( @@ -409,8 +429,14 @@ export class ExperimentClientController { * - enrollmentCode * - userId * - condition + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: User not defined + * description: Internal Server Error */ @Post('mark') public async markExperimentPoint( @@ -490,10 +516,14 @@ export class ExperimentClientController { * condition: * type: string * minLength: 1 - * '500': - * description: null value in column "id" of relation "experiment_user" violates not-null constraint + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '404': - * description: Experiment user not defined + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('assign') public async getAllExperimentConditions( @@ -616,8 +646,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Post('log') public async log( @@ -668,6 +704,14 @@ export class ExperimentClientController { * responses: * '200': * description: Log blob data + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('bloblog') public async blobLog(@Req() request: express.Request): Promise { @@ -735,6 +779,14 @@ export class ExperimentClientController { * responses: * '200': * description: Feature flags list + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined + * '500': + * description: Internal Server Error */ @Post('featureflag') public async getAllFlags( @@ -792,8 +844,14 @@ export class ExperimentClientController { * required: * - userId * - userAliases + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError + * '404': + * description: Experiment User not defined * '500': - * description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint + * description: Internal Server Error */ @Patch('useraliases') public async setUserAliases( @@ -823,6 +881,10 @@ export class ExperimentClientController { * responses: * '200': * description: Database cleared + * '400': + * description: BadRequestError - InvalidParameterValue + * '401': + * description: AuthorizationRequiredError * '500': * description: DEMO mode is disabled */ diff --git a/backend/packages/Upgrade/src/api/controllers/ExperimentController.ts b/backend/packages/Upgrade/src/api/controllers/ExperimentController.ts index 75ddfa8a48..4f81feccc7 100644 --- a/backend/packages/Upgrade/src/api/controllers/ExperimentController.ts +++ b/backend/packages/Upgrade/src/api/controllers/ExperimentController.ts @@ -1232,6 +1232,46 @@ export class ExperimentController { return this.experimentService.importExperiment(experiments, currentUser, request.logger); } + /** + * @swagger + * /experiments/{export}: + * get: + * description: Export Experiment JSON + * parameters: + * - in: body + * name: experiments + * required: true + * schema: + * type: array + * items: + * type: object + * properties: + * fileName: + * type: string + * fileContent: + * type: string + * description: Experiment Files + * tags: + * - Experiments + * produces: + * - application/json + * responses: + * '200': + * description: Experiment is exported + * schema: + * type: array + * items: + * type: object + * properties: + * fileName: + * type: string + * error: + * type: string + * '401': + * description: AuthorizationRequiredError + * '500': + * description: Internal Server Error + */ @Get('/export') public exportExperiment( @QueryParams() diff --git a/backend/packages/Upgrade/src/api/middlewares/ClientLibMiddleware.ts b/backend/packages/Upgrade/src/api/middlewares/ClientLibMiddleware.ts index af6aab6c58..2e51dc9923 100644 --- a/backend/packages/Upgrade/src/api/middlewares/ClientLibMiddleware.ts +++ b/backend/packages/Upgrade/src/api/middlewares/ClientLibMiddleware.ts @@ -38,11 +38,20 @@ export class ClientLibMiddleware implements ExpressMiddlewareInterface { req.logger.warn({ message: 'Token is not present in request header' }); const error = new Error('Token is not present in request header from client'); (error as any).type = SERVER_ERROR.TOKEN_NOT_PRESENT; - (error as any).httpCode = '401' + (error as any).httpCode = 401; throw error; } const { secret, key } = env.clientApi; - const decodeToken: any = jwt.verify(token, secret); + let decodeToken: any; + try { + decodeToken = jwt.verify(token, secret); + } catch (err) { + const error = err as ErrorWithType; + (error as any).type = SERVER_ERROR.TOKEN_VALIDATION_FAILED; + (error as any).httpCode = 401; + req.logger.error(error); + throw error; + } delete decodeToken.iat; delete decodeToken.exp; @@ -51,7 +60,7 @@ export class ClientLibMiddleware implements ExpressMiddlewareInterface { } else { const error = new Error('Provided token is invalid'); (error as any).type = SERVER_ERROR.INVALID_TOKEN; - (error as any).httpCode = '401' + (error as any).httpCode = 401; req.logger.error(error); throw error; } @@ -62,7 +71,7 @@ export class ClientLibMiddleware implements ExpressMiddlewareInterface { const err = error as ErrorWithType; if (err.message === 'jwt expired' || err.message === 'invalid signature') { err.type = SERVER_ERROR.INVALID_TOKEN; - (error as any).httpCode = '401' + (error as any).httpCode = 401; req.logger.error(err); throw err; } else { diff --git a/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts b/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts index fa39ad97c6..f8ef8ac3f4 100644 --- a/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts +++ b/backend/packages/Upgrade/src/api/models/IndividualExclusion.ts @@ -14,9 +14,6 @@ export class IndividualExclusion extends BaseModel { @ManyToOne(() => Experiment, { onDelete: 'CASCADE' }) public experiment: Experiment; - @Column({ nullable: true }) - public groupId?: string; - @IsNotEmpty() @Column({ type: 'enum', enum: EXCLUSION_CODE, nullable: true }) public exclusionCode: EXCLUSION_CODE; diff --git a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts index 2f6d6d4c61..645e5725b0 100644 --- a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts @@ -463,12 +463,12 @@ export class ExperimentRepository extends Repository { public async findOneExperiment(id: string): Promise { const experiment = await this.createBaseQueryBuilder() - .addOrderBy('conditions.order', 'ASC') - .addOrderBy('partitions.order', 'ASC') - .addOrderBy('factors.order', 'ASC') - .addOrderBy('levels.order', 'ASC') - .where({ id }) - .getOne(); + .addOrderBy('conditions.order', 'ASC') + .addOrderBy('partitions.order', 'ASC') + .addOrderBy('factors.order', 'ASC') + .addOrderBy('levels.order', 'ASC') + .where({ id }) + .getOne(); return experiment; } @@ -476,31 +476,43 @@ export class ExperimentRepository extends Repository { // Get the experiment details const experimentQuery = await this.createBaseQueryBuilder() .select([ - 'experiment.id as "experimentId"', - 'experiment.name as "experimentName"', - 'experiment.context as "context"', - 'experiment.assignmentUnit as "assignmentUnit"', - 'experiment.group as "group"', - 'experiment.consistencyRule as "consistencyRule"', - 'experiment.type as "designType"', - 'experiment.assignmentAlgorithm as "algorithmType"', - 'experiment.stratificationFactorStratificationFactorName as "stratification"', - 'experiment.postExperimentRule as "postRule"', - 'experimentRevertCondition.conditionCode as "revertTo"', - '"enrollingStateTimeLog"."timeLog" as "enrollmentStartDate"', - '"enrollmentCompleteStateTimeLog"."timeLog" as "enrollmentCompleteDate"', - '"conditionPayloadMain"."payloadValue" as "payload"', - '"decisionPointData"."excludeIfReached" as "excludeIfReached"', - '"decisionPointData"."id" as "expDecisionPointId"', - 'experimentCondition.id as "expConditionId"', - 'experimentCondition.conditionCode as "conditionName"', + 'experiment.id as "experimentId"', + 'experiment.name as "experimentName"', + 'experiment.context as "context"', + 'experiment.assignmentUnit as "assignmentUnit"', + 'experiment.group as "group"', + 'experiment.consistencyRule as "consistencyRule"', + 'experiment.type as "designType"', + 'experiment.assignmentAlgorithm as "algorithmType"', + 'experiment.stratificationFactorStratificationFactorName as "stratification"', + 'experiment.postExperimentRule as "postRule"', + 'experimentRevertCondition.conditionCode as "revertTo"', + '"enrollingStateTimeLog"."timeLog" as "enrollmentStartDate"', + '"enrollmentCompleteStateTimeLog"."timeLog" as "enrollmentCompleteDate"', + '"conditionPayloadMain"."payloadValue" as "payload"', + '"decisionPointData"."excludeIfReached" as "excludeIfReached"', + '"decisionPointData"."id" as "expDecisionPointId"', + 'experimentCondition.id as "expConditionId"', + 'experimentCondition.conditionCode as "conditionName"', ]) .leftJoin(ExperimentCondition, 'experimentCondition', 'experimentCondition.experimentId = experiment.id') .leftJoin(ExperimentCondition, 'experimentRevertCondition', 'experimentRevertCondition.id = experiment.revertTo') .leftJoin(DecisionPoint, 'decisionPointData', 'decisionPointData.experimentId = experiment.id') - .leftJoin(ConditionPayload, 'conditionPayloadMain', 'conditionPayloadMain.parentConditionId = experimentCondition.id AND conditionPayloadMain.decisionPointId = decisionPointData.id') - .leftJoin(StateTimeLog, 'enrollingStateTimeLog', 'enrollingStateTimeLog.experimentId = experiment.id AND enrollingStateTimeLog.toState = \'enrolling\'') - .leftJoin(StateTimeLog, 'enrollmentCompleteStateTimeLog', 'enrollmentCompleteStateTimeLog.experimentId = experiment.id AND enrollmentCompleteStateTimeLog.toState = \'enrollmentComplete\'') + .leftJoin( + ConditionPayload, + 'conditionPayloadMain', + 'conditionPayloadMain.parentConditionId = experimentCondition.id AND conditionPayloadMain.decisionPointId = decisionPointData.id' + ) + .leftJoin( + StateTimeLog, + 'enrollingStateTimeLog', + "enrollingStateTimeLog.experimentId = experiment.id AND enrollingStateTimeLog.toState = 'enrolling'" + ) + .leftJoin( + StateTimeLog, + 'enrollmentCompleteStateTimeLog', + "enrollmentCompleteStateTimeLog.experimentId = experiment.id AND enrollmentCompleteStateTimeLog.toState = 'enrollmentComplete'" + ) .groupBy('experiment.id') .addGroupBy('experimentCondition.id') .addGroupBy('experimentRevertCondition.conditionCode') @@ -510,7 +522,7 @@ export class ExperimentRepository extends Repository { .addGroupBy('enrollmentCompleteStateTimeLog.timeLog') .where('experiment.id = :experimentId', { experimentId }) .getRawMany(); - + return experimentQuery; } } diff --git a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts index 77fd987018..145705a363 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts @@ -1293,10 +1293,9 @@ export class ExperimentAssignmentService { } if (status === MARKED_DECISION_POINT_STATUS.CONDITION_FAILED_TO_APPLY) { - const excludeUserDoc: Pick = { + const excludeUserDoc: Pick = { user, experiment, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: EXCLUSION_CODE.EXCLUDED_BY_CLIENT, }; await this.individualExclusionRepository.saveRawJson([excludeUserDoc]); @@ -1306,10 +1305,9 @@ export class ExperimentAssignmentService { // Don't mark the experiment if user or group are in exclusion list // TODO update this with segment implementation // Create the excludeUserDoc outside of the conditional statements to avoid repetition - const excludeUserDoc: Pick = { + const excludeUserDoc: Pick = { user, experiment, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: EXCLUSION_CODE.PARTICIPANT_ON_EXCLUSION_LIST, }; if (globallyExcluded.user || globallyExcluded.group.length) { @@ -1397,18 +1395,16 @@ export class ExperimentAssignmentService { if (!individualEnrollment && !individualExclusion) { if (assignmentUnit === ASSIGNMENT_UNIT.GROUP && !groupEnrollment) { - const excludeUserDoc: Pick = { + const excludeUserDoc: Pick = { user, experiment, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: EXCLUSION_CODE.REACHED_AFTER, }; promiseArray.push(this.individualExclusionRepository.saveRawJson([excludeUserDoc])); } else if (assignmentUnit !== ASSIGNMENT_UNIT.GROUP) { - const excludeUserDoc: Pick = { + const excludeUserDoc: Pick = { user, experiment, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: EXCLUSION_CODE.REACHED_AFTER, }; promiseArray.push(this.individualExclusionRepository.saveRawJson([excludeUserDoc])); @@ -1454,7 +1450,6 @@ export class ExperimentAssignmentService { > = { experiment, user, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: EXCLUSION_CODE.EXCLUDED_DUE_TO_GROUP_LOGIC, }; individualExclusion = individualExclusionDocument as IndividualExclusion; @@ -1487,7 +1482,6 @@ export class ExperimentAssignmentService { > = { experiment, user, - groupId: user?.workingGroup?.[experiment.group], exclusionCode: invalidGroup ? EXCLUSION_CODE.INVALID_GROUP_OR_WORKING_GROUP : EXCLUSION_CODE.NO_GROUP_SPECIFIED, diff --git a/backend/packages/Upgrade/src/database/migrations/1720515933833-addTokenValidationFailedEnum.ts b/backend/packages/Upgrade/src/database/migrations/1720515933833-addTokenValidationFailedEnum.ts new file mode 100644 index 0000000000..fac72f8971 --- /dev/null +++ b/backend/packages/Upgrade/src/database/migrations/1720515933833-addTokenValidationFailedEnum.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addTokenValidationFailedEnum1720515933833 implements MigrationInterface { + name = 'addTokenValidationFailedEnum1720515933833'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."experiment_error_type_enum" RENAME TO "experiment_error_type_enum_old"` + ); + await queryRunner.query( + `CREATE TYPE "public"."experiment_error_type_enum" AS ENUM('Database not reachable', 'Database auth fail', 'Error in the assignment algorithm', 'Parameter missing in the client request', 'Parameter not in the correct format', 'User ID not found', 'Query Failed', 'Error reported from client', 'Experiment user not defined', 'Experiment user group not defined', 'Working group is not a subset of user group', 'Invalid token', 'Token is not present in request', 'JWT Token validation failed', 'Error in migration', 'Email send error', 'Condition not found', 'Experiment ID not provided for shared Decision Point', 'Experiment ID provided is invalid for shared Decision Point', 'Caliper profile or event not supported')` + ); + await queryRunner.query( + `ALTER TABLE "experiment_error" ALTER COLUMN "type" TYPE "public"."experiment_error_type_enum" USING "type"::"text"::"public"."experiment_error_type_enum"` + ); + await queryRunner.query(`DROP TYPE "public"."experiment_error_type_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."experiment_error_type_enum_old" AS ENUM('Caliper profile or event not supported', 'Condition not found', 'Database auth fail', 'Database not reachable', 'Email send error', 'Error in migration', 'Error in the assignment algorithm', 'Error reported from client', 'Experiment ID not provided for shared Decision Point', 'Experiment ID provided is invalid for shared Decision Point', 'Experiment user group not defined', 'Experiment user not defined', 'Invalid token', 'Parameter missing in the client request', 'Parameter not in the correct format', 'Query Failed', 'Token is not present in request', 'User ID not found', 'Working group is not a subset of user group')` + ); + await queryRunner.query( + `ALTER TABLE "experiment_error" ALTER COLUMN "type" TYPE "public"."experiment_error_type_enum_old" USING "type"::"text"::"public"."experiment_error_type_enum_old"` + ); + await queryRunner.query(`DROP TYPE "public"."experiment_error_type_enum"`); + await queryRunner.query( + `ALTER TYPE "public"."experiment_error_type_enum_old" RENAME TO "experiment_error_type_enum"` + ); + } +} diff --git a/backend/packages/Upgrade/src/database/migrations/1721124249413-removeGroupIdFromIndividualExclusion.ts b/backend/packages/Upgrade/src/database/migrations/1721124249413-removeGroupIdFromIndividualExclusion.ts new file mode 100644 index 0000000000..712bcd7529 --- /dev/null +++ b/backend/packages/Upgrade/src/database/migrations/1721124249413-removeGroupIdFromIndividualExclusion.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class removeGroupIdFromIndividualExclusion1721124249413 implements MigrationInterface { + name = 'removeGroupIdFromIndividualExclusion1721124249413' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "individual_exclusion" DROP COLUMN "groupId"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "individual_exclusion" ADD "groupId" character varying`); + } + +} diff --git a/backend/packages/Upgrade/test/unit/controllers/AnalyticsController.test.ts b/backend/packages/Upgrade/test/unit/controllers/AnalyticsController.test.ts index dd9455af0b..e57521d053 100644 --- a/backend/packages/Upgrade/test/unit/controllers/AnalyticsController.test.ts +++ b/backend/packages/Upgrade/test/unit/controllers/AnalyticsController.test.ts @@ -55,7 +55,7 @@ describe('Analytics Controller Testing', () => { .expect(200); }); - test('Post request for /api/stats/csv', () => { + test('Get request for /api/stats/csv', () => { return request(app) .get('/api/stats/csv') .query({ diff --git a/backend/packages/Upgrade/test/unit/controllers/ExperimentController.test.ts b/backend/packages/Upgrade/test/unit/controllers/ExperimentController.test.ts index 3b93a04564..1e526739b1 100644 --- a/backend/packages/Upgrade/test/unit/controllers/ExperimentController.test.ts +++ b/backend/packages/Upgrade/test/unit/controllers/ExperimentController.test.ts @@ -196,8 +196,10 @@ describe('Experiment Controller Testing', () => { test('Get request for /api/experiments/export', () => { return request(app) - .post('/api/experiments/import') - .send([experimentData.id]) + .get('/api/experiments/export') + .query({ + ids: [uuid()], + }) .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(200); diff --git a/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts b/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts new file mode 100644 index 0000000000..ca948d95b8 --- /dev/null +++ b/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts @@ -0,0 +1,115 @@ +import app from '../../utils/expressApp'; +import request from 'supertest'; +import { configureLogger } from '../../utils/logger'; +import { useContainer as routingUseContainer } from 'routing-controllers'; +import { Container } from 'typedi'; +import { v4 as uuid } from 'uuid'; +import SegmentServiceMock from './mocks/SegmentServiceMock'; +import { SegmentService } from '../../../src/api/services/SegmentService'; + +import { useContainer as classValidatorUseContainer } from 'class-validator'; +import { useContainer as ormUseContainer } from 'typeorm'; + +describe('Segment Controller Testing', () => { + beforeAll(() => { + configureLogger(); + routingUseContainer(Container); + ormUseContainer(Container); + classValidatorUseContainer(Container); + + // set mock container + Container.set(SegmentService, new SegmentServiceMock()); + }); + + afterAll(() => { + Container.reset(); + }); + + const segmentData = { + id: uuid(), + name: "segment1", + description: "desc", + context: "home", + type: "public", + status: "Unused", + userIds: ["user1"], + groups: { + groupId : "group1", + type: "school" + }, + subSegmentIds: ["seg2"] + }; + + test('Get request for /api/segments', () => { + return request(app) + .get('/api/segments') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Get request for /api/segments/:segmentId', () => { + return request(app) + .get(`/api/segments/${uuid()}`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Get request for /api/segments/status/:segmentId', () => { + return request(app) + .get(`/api/segments/status/${uuid()}`) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Post request for /api/segments', () => { + return request(app) + .post('/api/segments') + .send([segmentData]) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Delete request for /api/segments/:segmentId', () => { + return request(app).delete(`/api/segments/${uuid()}`).expect('Content-Type', /json/).expect(200); + }); + + test('Post request for /api/segments/import', () => { + return request(app) + .post('/api/segments/import') + .send(segmentData) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Post request for /api/segments/validation', () => { + return request(app).post('/api/segments/validation').send(segmentData).expect('Content-Type', /json/).expect(200); + }); + + test('Get request for /api/segments/export/json', () => { + return request(app) + .get('/api/segments/export/json') + .query({ + ids: [uuid()], + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Get request for /api/segments/export/csv', () => { + return request(app) + .get('/api/segments/export/csv') + .query({ + ids: [uuid()], + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + +}); diff --git a/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts b/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts new file mode 100644 index 0000000000..f4ee792779 --- /dev/null +++ b/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts @@ -0,0 +1,52 @@ +import { Service } from 'typedi'; + +@Service() +export default class ExcludeServiceMock { + public getAllSegments(): Promise<[]> { + return Promise.resolve([]); + } + + public getAllSegmentWithStatus(): Promise<[]> { + return Promise.resolve([]); + } + + public getSegmentById(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public getSegmentWithStatusById(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public getSingleSegmentWithStatus(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public upsertSegment(): Promise<[]> { + return Promise.resolve([]); + } + + public importSegments(): Promise<[]> { + return Promise.resolve([]); + } + + public deleteSegment(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public validateSegments(): Promise<[]> { + return Promise.resolve([]); + } + + public exportSegments(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public exportSegment(id: string): Promise<[]> { + return Promise.resolve([]); + } + + public exportSegmentCSV(id: string): Promise<[]> { + return Promise.resolve([]); + } +} diff --git a/frontend/projects/upgrade/src/app/app.module.ts b/frontend/projects/upgrade/src/app/app.module.ts index dcd48f9052..8290384486 100755 --- a/frontend/projects/upgrade/src/app/app.module.ts +++ b/frontend/projects/upgrade/src/app/app.module.ts @@ -33,6 +33,8 @@ export const getEnvironmentConfig = (http: HttpClient, env: Environment) => { env.withinSubjectExperimentSupportToggle = config.withinSubjectExperimentSupportToggle ?? env.withinSubjectExperimentSupportToggle ?? false; env.errorLogsToggle = config.errorLogsToggle ?? env.errorLogsToggle ?? false; + env.metricAnalyticsExperimentDisplayToggle = + config.metricAnalyticsExperimentDisplayToggle ?? env.metricAnalyticsExperimentDisplayToggle ?? false; }) .catch((error) => { console.log({ error }); diff --git a/frontend/projects/upgrade/src/app/core/analysis/analysis.service.spec.ts b/frontend/projects/upgrade/src/app/core/analysis/analysis.service.spec.ts index 3d6b0e85e3..69d5740828 100644 --- a/frontend/projects/upgrade/src/app/core/analysis/analysis.service.spec.ts +++ b/frontend/projects/upgrade/src/app/core/analysis/analysis.service.spec.ts @@ -8,6 +8,7 @@ import { actionUpsertMetrics, } from './store/analysis.actions'; import { UpsertMetrics } from './store/analysis.models'; +import { Environment } from '../../../environments/environment-types'; const mockStateStore$ = new BehaviorSubject({}); (mockStateStore$ as any).dispatch = jest.fn(); @@ -22,10 +23,12 @@ jest.mock('./store/analysis.selectors', () => ({ describe('AnalysisService', () => { const mockStore: any = mockStateStore$; + let mockEnvironment: Environment = { metricAnalyticsExperimentDisplayToggle: true } as Environment; let service: AnalysisService; beforeEach(() => { - service = new AnalysisService(mockStore); + service = new AnalysisService(mockStore, mockEnvironment); + jest.resetAllMocks(); }); describe('#queryResultById$', () => { @@ -78,12 +81,34 @@ describe('AnalysisService', () => { }); describe('#executeQuery', () => { - it('should dispatch executeQuery with the supplied string input array', () => { + let originalEnvironment; + + beforeEach(() => { + // Save the original environment to restore it after tests + originalEnvironment = { ...mockEnvironment }; + }); + + afterEach(() => { + // Restore the original environment after each test + mockEnvironment = { ...originalEnvironment }; + }); + + it('should dispatch executeQuery with the supplied string input array when metricAnalyticsExperimentDisplayToggle is true', () => { + mockEnvironment = { metricAnalyticsExperimentDisplayToggle: true } as Environment; const mockQueryIds = ['test', 'test2']; service.executeQuery(mockQueryIds); - expect(mockStore.dispatch).toHaveBeenLastCalledWith(actionExecuteQuery({ queryIds: mockQueryIds })); + expect(mockStore.dispatch).toHaveBeenCalledWith(actionExecuteQuery({ queryIds: mockQueryIds })); + }); + + it('should not dispatch executeQuery and log a warning when metricAnalyticsExperimentDisplayToggle is false', () => { + mockEnvironment = { metricAnalyticsExperimentDisplayToggle: false } as Environment; + const mockQueryIds = ['test3', 'test4']; + + service.executeQuery(mockQueryIds); + + expect(mockStore.dispatch).not.toHaveBeenCalledWith(mockQueryIds); }); }); diff --git a/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts b/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts index b527b4d521..4522bc1aca 100644 --- a/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts +++ b/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { AppState } from '../core.module'; import { Store, select } from '@ngrx/store'; import { @@ -11,10 +11,11 @@ import { import * as AnalysisActions from './store/analysis.actions'; import { UpsertMetrics } from './store/analysis.models'; import { selectExperimentQueries } from '../experiments/store/experiments.selectors'; +import { ENV, Environment } from '../../../environments/environment-types'; @Injectable() export class AnalysisService { - constructor(private store$: Store) {} + constructor(private store$: Store, @Inject(ENV) private environment: Environment) {} isMetricsLoading$ = this.store$.pipe(select(selectIsMetricsLoading)); isQueryExecuting$ = this.store$.pipe(select(selectIsQueryExecuting)); @@ -35,7 +36,14 @@ export class AnalysisService { } executeQuery(queryIds: string[]) { - this.store$.dispatch(AnalysisActions.actionExecuteQuery({ queryIds })); + if (this.environment.metricAnalyticsExperimentDisplayToggle) { + this.store$.dispatch(AnalysisActions.actionExecuteQuery({ queryIds })); + } else { + console.warn( + 'executeQuery is currently disabled via metricAnalyticsExperimentDisplayToggle:', + this.environment.metricAnalyticsExperimentDisplayToggle + ); + } } experimentQueryResult$(experimentId: string) { 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 0d94dc47c9..deaa35e600 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 @@ -19,6 +19,8 @@ import { selectFeatureFlagInclusions, selectFeatureFlagExclusions, selectIsLoadingSelectedFeatureFlag, + selectSortKey, + selectSortAs, } from './store/feature-flags.selectors'; import * as FeatureFlagsActions from './store/feature-flags.actions'; import { actionFetchContextMetaData } from '../experiments/store/experiments.actions'; @@ -39,6 +41,8 @@ export class FeatureFlagsService { isAllFlagsFetched$ = this.store$.pipe(select(selectIsAllFlagsFetched)); searchString$ = this.store$.pipe(select(selectSearchString)); searchKey$ = this.store$.pipe(select(selectSearchKey)); + sortKey$ = this.store$.pipe(select(selectSortKey)); + sortAs$ = this.store$.pipe(select(selectSortAs)); isLoadingUpsertFeatureFlag$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag)); IsLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete)); @@ -86,6 +90,12 @@ export class FeatureFlagsService { map((exclusions) => exclusions.length) ); + convertNameStringToKey(name:string):string { + let upperCaseString = name.trim().toUpperCase(); + let key = upperCaseString.replace(/ /g, '_'); + return key; +} + fetchFeatureFlags(fromStarting?: boolean) { this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting })); } diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts index 74842d3d3a..7152cd6a44 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts @@ -91,8 +91,9 @@ export class FeatureFlagsEffects { ofType(FeatureFlagsActions.actionAddFeatureFlag), switchMap((action) => { return this.featureFlagsDataService.addFeatureFlag(action.addFeatureFlagRequest).pipe( - map((response) => { - return FeatureFlagsActions.actionAddFeatureFlagSuccess({ response }); + map((response) => FeatureFlagsActions.actionAddFeatureFlagSuccess({ response })), + tap(({ response }) => { + this.router.navigate(['/featureflags', 'detail', response.id]); }), catchError(() => [FeatureFlagsActions.actionAddFeatureFlagFailure()]) ); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts index 4429f7f9c2..69a24197f0 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts @@ -104,12 +104,12 @@ export enum FLAG_SEARCH_KEY { } export const FLAG_ROOT_COLUMN_NAMES = { - NAME: 'Name', - STATUS: 'Status', - UPDATED_AT: 'Updated at', - APP_CONTEXT: 'App Context', - TAGS: 'Tags', - EXPOSURES: 'Exposures', + NAME: 'name', + STATUS: 'status', + UPDATED_AT: 'updatedAt', + APP_CONTEXT: 'appContext', + TAGS: 'tags', + EXPOSURES: 'exposures', }; export const FLAG_TRANSLATION_KEYS = { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.html index d83d38d2c0..d743ce3824 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.html @@ -4,7 +4,7 @@ [cancelBtnLabel]="config.cancelBtnLabel" [primaryActionBtnLabel]="config.primaryActionBtnLabel" [primaryActionBtnColor]="config.primaryActionBtnColor" - [primaryActionBtnDisabled$]="isLoadingUpsertFeatureFlag$" + [primaryActionBtnDisabled]="isLoadingUpsertFeatureFlag$ | async" (primaryActionBtnClicked)="onPrimaryActionBtnClicked()" >
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.ts index bc1d9f9f06..6ad0cd4d42 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.ts @@ -24,7 +24,7 @@ import { MatSelectModule } from '@angular/material/select'; import { FeatureFlagsService } from '../../../../../core/feature-flags/feature-flags.service'; import { CommonFormHelpersService } from '../../../../../shared/services/common-form-helpers.service'; import { FEATURE_FLAG_STATUS, SEGMENT_TYPE, FILTER_MODE } from '../../../../../../../../../../types/src'; -import { AddFeatureFlagRequest } from '../../../../../core/feature-flags/store/feature-flags.model'; +import { AddFeatureFlagRequest, FeatureFlagFormData } from '../../../../../core/feature-flags/store/feature-flags.model'; import { Subscription } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { ExperimentService } from '../../../../../core/experiments/experiments.service'; @@ -77,6 +77,7 @@ export class AddFeatureFlagModalComponent { this.experimentService.fetchContextMetaData(); this.buildForm(); this.listenForFeatureFlagListLengthChanges(); + this.listenOnNameChangesToUpdateKey(); } buildForm(): void { @@ -85,7 +86,7 @@ export class AddFeatureFlagModalComponent { key: ['', Validators.required], description: [''], appContext: ['', Validators.required], - tags: [], + tags: [[]], }); } @@ -94,6 +95,15 @@ export class AddFeatureFlagModalComponent { this.subscriptions = this.featureFlagsListLengthChange$.subscribe(() => this.closeModal()); } + listenOnNameChangesToUpdateKey(): void { + this.featureFlagForm.get('name')?.valueChanges.subscribe((name) => { + const keyControl = this.featureFlagForm.get('key'); + if (keyControl && !keyControl.dirty) { + keyControl.setValue(this.featureFlagsService.convertNameStringToKey(name)); + } + }); + } + onPrimaryActionBtnClicked(): void { if (this.featureFlagForm.valid) { // Handle extra form validation logic here? @@ -106,8 +116,7 @@ export class AddFeatureFlagModalComponent { createAddFeatureFlagRequest(): void { // temporarily use any until tags feature is added - // const { name, key, description, appContext, tags }: FeatureFlagFormData = this.featureFlagForm.value; - const { name, key, description, appContext, tags }: any = this.featureFlagForm.value; + const { name, key, description, appContext, tags }: FeatureFlagFormData = this.featureFlagForm.value; const addFeatureFlagRequest: AddFeatureFlagRequest = { name, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.html index 91d8574177..8c8e9e9f76 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.html @@ -5,7 +5,7 @@ [cancelBtnLabel]="data.cancelBtnLabel" [primaryActionBtnLabel]="data.primaryActionBtnLabel" [primaryActionBtnColor]="data.primaryActionBtnColor" - [primaryActionBtnDisabled$]="isDeleteActionBtnDisabled$" + [primaryActionBtnDisabled]="isDeleteActionBtnDisabled$ | async" (primaryActionBtnClicked)="onPrimaryActionBtnClicked(flag.id)" >
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.ts index c3c7883781..d54f0b7646 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component.ts @@ -44,9 +44,7 @@ export class DeleteFeatureFlagModalComponent { private inputSubject: BehaviorSubject = new BehaviorSubject(''); // Observable that emits true if inputValue is 'delete', false otherwise - isDeleteNotTyped$: Observable = this.inputSubject - .asObservable() - .pipe(map((value) => value.toLowerCase() !== 'delete')); + isDeleteNotTyped$: Observable = this.inputSubject.pipe(map((value) => value.toLowerCase() !== 'delete')); isDeleteActionBtnDisabled$: Observable = combineLatest([ this.isDeleteNotTyped$, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.html index c8944ecde9..a9096fff44 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.html @@ -4,7 +4,7 @@ [cancelBtnLabel]="config.cancelBtnLabel" [primaryActionBtnLabel]="config.primaryActionBtnLabel" [primaryActionBtnColor]="config.primaryActionBtnColor" - [primaryActionBtnDisabled$]="isLoadingUpsertFeatureFlag$" + [primaryActionBtnDisabled]="isLoadingUpsertFeatureFlag$ | async" (primaryActionBtnClicked)="onPrimaryActionBtnClicked()" > diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.ts index e5ffbf2150..4487b22bb8 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component.ts @@ -23,8 +23,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { FeatureFlagsService } from '../../../../../core/feature-flags/feature-flags.service'; import { CommonFormHelpersService } from '../../../../../shared/services/common-form-helpers.service'; -import { FEATURE_FLAG_STATUS, SEGMENT_TYPE, FILTER_MODE } from '../../../../../../../../../../types/src'; -import { AddFeatureFlagRequest, FeatureFlag, FeatureFlagFormData } from '../../../../../core/feature-flags/store/feature-flags.model'; +import { FeatureFlag, FeatureFlagFormData } from '../../../../../core/feature-flags/store/feature-flags.model'; import { Subscription } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { ExperimentService } from '../../../../../core/experiments/experiments.service'; @@ -80,6 +79,7 @@ export class EditFeatureFlagModalComponent { this.buildForm(); this.initializeFormValues(); this.listenForFeatureFlagGetUpdated(); + this.listenOnNameChangesToUpdateKey(); } buildForm(): void { @@ -109,6 +109,15 @@ export class EditFeatureFlagModalComponent { ); } + listenOnNameChangesToUpdateKey(): void { + this.featureFlagForm.get('name')?.valueChanges.subscribe((name) => { + const keyControl = this.featureFlagForm.get('key'); + if (keyControl && !keyControl.dirty) { + keyControl.setValue(this.featureFlagsService.convertNameStringToKey(name)); + } + }); + } + // Close the modal once the feature flag list length changes, as that indicates actual success listenForFeatureFlagGetUpdated(): void { this.subscriptions = this.isSelectedFeatureFlagUpdated$.subscribe(() => this.closeModal()); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html new file mode 100644 index 0000000000..6632f185b8 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.html @@ -0,0 +1,24 @@ + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.scss new file mode 100644 index 0000000000..0a13fbb9a7 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.scss @@ -0,0 +1,22 @@ +.drag-drop-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + color: grey; + + .full-width { + width: 100%; + } +} + +.import-message { + margin-top: 5px; + color: grey; +} + +a { + text-decoration: none; +} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts new file mode 100644 index 0000000000..fced0d1298 --- /dev/null +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component.ts @@ -0,0 +1,53 @@ +import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { CommonModalComponent } from '../../../../../shared-standalone-component-lib/components'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { CommonModalConfig } from '../../../../../shared-standalone-component-lib/components/common-modal/common-modal-config'; +import { BehaviorSubject } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../../../../shared/shared.module'; +import { CommonImportContainerComponent } from '../../../../../shared-standalone-component-lib/components/common-import-container/common-import-container.component'; + +@Component({ + selector: 'app-import-feature-flag-modal', + standalone: true, + imports: [CommonModalComponent, CommonModule, SharedModule, CommonImportContainerComponent], + templateUrl: './import-feature-flag-modal.component.html', + styleUrls: ['./import-feature-flag-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImportFeatureFlagModalComponent { + + isImportActionBtnDisabled = new BehaviorSubject(true); + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: CommonModalConfig, + public dialog: MatDialog, + public dialogRef: MatDialogRef + ) {} + + handleFilesSelected(files: File[]) { + if(files.length>0) { + this.isImportActionBtnDisabled.next(false); + } + console.log('Selected files:', files); + //Send files to validation endpoint to receive data for table + } + + handleFileInput(file: File) { + const reader = new FileReader(); + reader.onload = (e: any) => { + const jsonContent = e.target.result; + console.log(JSON.parse(jsonContent)); + }; + reader.readAsText(file); + } + + importFiles() { + console.log('Import feature flags'); + } + + closeModal() { + this.dialogRef.close(); + } +} 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 index fb374ba420..77db00743a 100644 --- 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 @@ -3,21 +3,29 @@ 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.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 index b352c439df..d387cd2f5b 100644 --- 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 @@ -28,6 +28,7 @@ import { SharedModule } from '../../../../../../shared/shared.module'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeatureFlagDetailsPageContentComponent implements OnInit, OnDestroy { + isSectionCardExpanded = true; activeTabIndex$ = this.featureFlagsService.activeDetailsTabIndex$; featureFlag$: Observable; @@ -42,6 +43,11 @@ export class FeatureFlagDetailsPageContentComponent implements OnInit, OnDestroy this.featureFlag$ = this.featureFlagsService.selectedFeatureFlag$; } + + onSectionCardExpandChange(expanded: boolean) { + this.isSectionCardExpanded = expanded; + } + ngOnDestroy() { this.featureFlagIdSub.unsubscribe(); } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html index 33996ac262..226fc69ad3 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.html @@ -22,7 +22,5 @@ - + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts index 1ff9f5f3b3..80aac588e1 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts @@ -26,6 +26,7 @@ import { FeatureFlagsService } from '../../../../../../../core/feature-flags/fea changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeatureFlagExclusionsSectionCardComponent { + @Input() isSectionCardExpanded; tableRowCount$ = this.featureFlagService.selectFeatureFlagExclusionsLength$; constructor(private featureFlagService: FeatureFlagsService) {} @@ -35,8 +36,6 @@ export class FeatureFlagExclusionsSectionCardComponent { { name: 'Delete', disabled: false }, ]; - isSectionCardExpanded = true; - addExcludeListClicked() { console.log('add Exclude List Clicked'); } 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-data/feature-flag-exposures-data.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-data/feature-flag-exposures-data.component.html new file mode 100644 index 0000000000..3c57c982d1 --- /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-data/feature-flag-exposures-data.component.html @@ -0,0 +1,5 @@ +
+ + Add Exposures data here! + +
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-data/feature-flag-exposures-data.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-data/feature-flag-exposures-data.component.scss new file mode 100644 index 0000000000..8b13789179 --- /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-data/feature-flag-exposures-data.component.scss @@ -0,0 +1 @@ + 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-data/feature-flag-exposures-data.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-data/feature-flag-exposures-data.component.ts new file mode 100644 index 0000000000..866aa5d595 --- /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-data/feature-flag-exposures-data.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { FeatureFlagsService } from '../../../../../../../../core/feature-flags/feature-flags.service'; +import { CommonDetailsParticipantListTableComponent } from '../../../../../../../../shared-standalone-component-lib/components/common-details-participant-list-table/common-details-participant-list-table.component'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + selector: 'app-feature-flag-exposures-data', + standalone: true, + templateUrl: './feature-flag-exposures-data.component.html', + styleUrl: './feature-flag-exposures-data.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonDetailsParticipantListTableComponent, CommonModule, TranslateModule], +}) +export class FeatureFlagExposuresDataComponent {} 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 index 791f6df23d..033462451e 100644 --- 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 @@ -1,5 +1,19 @@ -
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 index e69de29bb2..54079260c1 100644 --- 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 @@ -0,0 +1,3 @@ +.full-width { + width: 100%; +} \ 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-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 index 43f0b955e7..5ad928ccda 100644 --- 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 @@ -1,15 +1,34 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { CommonSectionCardComponent } from '../../../../../../../shared-standalone-component-lib/components'; +import { + CommonSectionCardActionButtonsComponent, + CommonSectionCardComponent, + CommonSectionCardTitleHeaderComponent, +} from '../../../../../../../shared-standalone-component-lib/components'; import { FeatureFlag } from '../../../../../../../core/feature-flags/store/feature-flags.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { FeatureFlagExposuresDataComponent } from './feature-flag-exposures-data/feature-flag-exposures-data.component'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-feature-flag-exposures-section-card', standalone: true, - imports: [CommonSectionCardComponent], + imports: [ + CommonSectionCardComponent, + CommonSectionCardTitleHeaderComponent, + CommonSectionCardActionButtonsComponent, + CommonModule, + TranslateModule, + FeatureFlagExposuresDataComponent, + ], templateUrl: './feature-flag-exposures-section-card.component.html', styleUrl: './feature-flag-exposures-section-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeatureFlagExposuresSectionCardComponent { @Input() data: FeatureFlag; + @Input() isSectionCardExpanded; + + onSectionCardExpandChange(isSectionCardExpanded: boolean) { + this.isSectionCardExpanded = isSectionCardExpanded; + } } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html index c50c7a3593..923164b3c5 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.html @@ -22,5 +22,5 @@ - + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts index 81951af67e..7dfdd8e124 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts @@ -26,6 +26,7 @@ import { FeatureFlagsService } from '../../../../../../../core/feature-flags/fea changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeatureFlagInclusionsSectionCardComponent { + @Input() isSectionCardExpanded; tableRowCount$ = this.featureFlagService.selectFeatureFlagInclusionsLength$; constructor(private featureFlagService: FeatureFlagsService) {} @@ -35,8 +36,6 @@ export class FeatureFlagInclusionsSectionCardComponent { { name: 'Delete', disabled: false }, ]; - isSectionCardExpanded = true; - addIncludeListClicked() { console.log('add Include List Clicked'); } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts index f78f82a1ff..01d235c9f6 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-section-card.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonSectionCardActionButtonsComponent, CommonSectionCardComponent, @@ -28,6 +28,8 @@ import { DialogService } from '../../../../../../../shared/services/common-dialo changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeatureFlagOverviewDetailsSectionCardComponent { + isSectionCardExpanded = true; + @Output() sectionCardExpandChange = new EventEmitter(); featureFlag$ = this.featureFlagService.selectedFeatureFlag$; flagOverviewDetails$ = this.featureFlagService.selectedFlagOverviewDetails; @@ -35,12 +37,8 @@ export class FeatureFlagOverviewDetailsSectionCardComponent { { name: 'Edit', disabled: false }, { name: 'Delete', disabled: false }, ]; - isSectionCardExpanded = true; - constructor( - private dialogService: DialogService, - private featureFlagService: FeatureFlagsService, - ) {} + constructor(private dialogService: DialogService, private featureFlagService: FeatureFlagsService) {} get FEATURE_FLAG_STATUS() { return FEATURE_FLAG_STATUS; @@ -82,5 +80,6 @@ export class FeatureFlagOverviewDetailsSectionCardComponent { onSectionCardExpandChange(isSectionCardExpanded: boolean) { this.isSectionCardExpanded = isSectionCardExpanded; + this.sectionCardExpandChange.emit(this.isSectionCardExpanded); } } 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 index 6e80bef9df..b8686c982d 100644 --- 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 @@ -1,7 +1,15 @@
- +
@@ -19,7 +27,7 @@ {{ flag.name | truncate: 30 }} @@ -39,7 +47,9 @@ - {{ FLAG_TRANSLATION_KEYS.STATUS | translate | uppercase }} + + {{ FLAG_TRANSLATION_KEYS.STATUS | translate | uppercase }} + @@ -58,7 +68,7 @@ - + {{ FLAG_TRANSLATION_KEYS.APP_CONTEXT | translate | uppercase }} @@ -69,8 +79,10 @@ - - {{ FLAG_TRANSLATION_KEYS.TAGS | translate | uppercase }} + + + {{ FLAG_TRANSLATION_KEYS.TAGS | translate | uppercase }} + {{ tag }} @@ -78,7 +90,7 @@ - + {{ FLAG_TRANSLATION_KEYS.EXPOSURES | translate | uppercase }} @@ -97,4 +109,4 @@
-
\ 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 index c0d0a70b70..a14bc2456d 100644 --- 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 @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, ViewChild } from '@angular/core'; import { Observable } from 'rxjs'; import { FLAG_ROOT_COLUMN_NAMES, @@ -7,12 +7,12 @@ import { FeatureFlag, } from '../../../../../../../../core/feature-flags/store/feature-flags.model'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { AsyncPipe, NgIf, NgFor, UpperCasePipe, DatePipe } from '@angular/common'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { TranslateModule } from '@ngx-translate/core'; -import { MatChipsModule } from '@angular/material/chips'; +import { AsyncPipe, NgIf, NgFor, UpperCasePipe } from '@angular/common'; import { RouterModule } from '@angular/router'; +import { MatSort } from '@angular/material/sort'; import { CommonStatusIndicatorChipComponent } from '../../../../../../../../shared-standalone-component-lib/components'; +import { FeatureFlagsService } from '../../../../../../../../core/feature-flags/feature-flags.service'; +import { SharedModule } from '../../../../../../../../shared/shared.module'; @Component({ selector: 'app-feature-flag-root-section-card-table', @@ -22,21 +22,30 @@ import { CommonStatusIndicatorChipComponent } from '../../../../../../../../shar AsyncPipe, NgIf, NgFor, - MatTooltipModule, - TranslateModule, + SharedModule, UpperCasePipe, - MatChipsModule, RouterModule, - DatePipe, CommonStatusIndicatorChipComponent, ], 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; +export class FeatureFlagRootSectionCardTableComponent implements OnInit { + @Input() dataSource$: MatTableDataSource; @Input() isLoading$: Observable; + flagSortKey$ = this.featureFlagsService.sortKey$; + flagSortAs$ = this.featureFlagsService.sortAs$; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + constructor(private featureFlagsService: FeatureFlagsService) {} + + ngOnInit() { + if (this.dataSource$?.data) { + this.dataSource$.sort = this.sort; + } + } get displayedColumns(): string[] { return FLAG_ROOT_DISPLAYED_COLUMNS; @@ -54,7 +63,8 @@ export class FeatureFlagRootSectionCardTableComponent { console.log('fetchFlagsOnScroll'); } - changeSorting($event) { - console.log('onSearch:', $event); + changeSorting(event) { + this.featureFlagsService.setSortingType(event.direction ? event.direction.toUpperCase() : null); + this.featureFlagsService.setSortKey(event.direction ? event.active : null); } } 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 index c7c574f264..36dc297154 100644 --- 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 @@ -14,6 +14,7 @@ [showPrimaryButton]="true" [primaryButtonText]="'feature-flags.add-feature-flag.text' | translate" [menuButtonItems]="menuButtonItems" + [showMenuButton]="true" [isSectionCardExpanded]="isSectionCardExpanded" (primaryButtonClick)="onAddFeatureFlagButtonClick()" (menuButtonItemClick)="onMenuButtonItemClick($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.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 index e8525b1306..ae07c422d8 100644 --- 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 @@ -98,7 +98,11 @@ export class FeatureFlagRootSectionCardComponent { } onMenuButtonItemClick(menuButtonItemName: string) { - console.log('onMenuButtonItemClick:', menuButtonItemName); + if (menuButtonItemName === 'Import Feature Flag') { + this.dialogService.openImportFeatureFlagModal(); + } else if (menuButtonItemName === 'Export All Feature Flags') { + console.log('onMenuButtonItemClick:', menuButtonItemName); + } } onSectionCardExpandChange(isSectionCardExpanded: boolean) { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/components/modal/import-experiment/import-experiment.component.html b/frontend/projects/upgrade/src/app/features/dashboard/home/components/modal/import-experiment/import-experiment.component.html index 1367cc07ad..d080d9959d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/components/modal/import-experiment/import-experiment.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/components/modal/import-experiment/import-experiment.component.html @@ -1,5 +1,5 @@
- {{ 'home.experiment.import-experiment.text' | translate | titlecase }} + {{ 'home.experiment.import-experiments.text' | translate | titlecase }}

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 3900306e47..de8864bb3d 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 @@ -952,7 +952,18 @@

- + + + + +
+ warning +

+ {{ 'home.view-experiment.metrics-analysis-unavailable.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 47113edc80..0c08e02c00 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 @@ -389,6 +389,17 @@ $font-size-small: 15px; margin-bottom: 24px; padding: 10px; } + + .disabled-analysis-view-messsage-container { + padding-top: 50px; + padding-bottom: 20px; + display: flex; + justify-content: center; + + .mat-icon { + margin-right: 20px; + } + } } ::ng-deep .owl-dt-calendar-table .owl-dt-calendar-cell-selected { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.ts index 0ca78e4a2c..e39c389ebe 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/pages/view-experiment/view-experiment.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewEncapsulation, Inject } from '@angular/core'; import { ExperimentService } from '../../../../../core/experiments/experiments.service'; import { MatDialog } from '@angular/material/dialog'; import { ExperimentStatusComponent } from '../../components/modal/experiment-status/experiment-status.component'; @@ -32,6 +32,7 @@ import { FactorialConditionTableDataFromConditionPayload, SimpleExperimentPayloadTableRowData, } from '../../../../../core/experiment-design-stepper/store/experiment-design-stepper.model'; +import { ENV, Environment } from '../../../../../../environments/environment-types'; // Used in view-experiment component only enum DialogType { CHANGE_STATUS = 'Change status', @@ -101,7 +102,8 @@ export class ViewExperimentComponent implements OnInit, OnDestroy { private dialog: MatDialog, private authService: AuthService, private router: Router, - private _Activatedroute: ActivatedRoute + private _Activatedroute: ActivatedRoute, + @Inject(ENV) private environment: Environment ) {} get DialogType() { @@ -124,6 +126,10 @@ export class ViewExperimentComponent implements OnInit, OnDestroy { return this.experiment.state === EXPERIMENT_STATE.CANCELLED; } + get showMetricAnalysisDisplay() { + return this.environment.metricAnalyticsExperimentDisplayToggle; + } + ngOnInit() { this.isLoadingExperimentDetailStats$ = this.experimentService.isLoadingExperimentDetailStats$; this.isPollingExperimentDetailStats$ = this.experimentService.isPollingExperimentDetailStats$; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/metrics/metrics.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/metrics/metrics.component.ts index 132bb4cc0f..82b7015960 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/metrics/metrics.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/metrics/metrics.component.ts @@ -9,17 +9,17 @@ import { } from '@angular/core'; import { UserPermission } from '../../../../../core/auth/store/auth.models'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { MetricUnit } from '../../../../../core/analysis/store/analysis.models'; +import { METRICS_JOIN_TEXT, MetricUnit } from '../../../../../core/analysis/store/analysis.models'; import { NestedTreeControl } from '@angular/cdk/tree'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; import { AnalysisService } from '../../../../../core/analysis/analysis.service'; import { AuthService } from '../../../../../core/auth/auth.service'; -import { DeleteMetricsComponent } from '../modals/delete-metrics/delete-metrics.component'; import { AddMetricsComponent } from '../modals/add-metrics/add-metrics.component'; import { METRIC_SEARCH_KEY } from '../../../../../../../../../../types/src/Experiment/enums'; import { IMetricUnit } from '../../../../../../../../../../types/src'; +import { DeleteComponent } from '../../../../../shared/components/delete/delete.component'; @Component({ selector: 'profile-metrics', @@ -108,9 +108,14 @@ export class MetricsComponent implements OnInit, OnDestroy, AfterViewInit { }; const key = this.analysisService.findParents(data, nodeToBeDeleted.id); this.selectedMetricIndex = null; - this.dialog.open(DeleteMetricsComponent, { + const dialogRef = this.dialog.open(DeleteComponent, { panelClass: 'delete-modal', - data: { key }, + }); + + dialogRef.afterClosed().subscribe((isDeleteButtonClicked) => { + if (isDeleteButtonClicked) { + this.analysisService.deleteMetric(key.join(METRICS_JOIN_TEXT)); + } }); } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.html b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.html deleted file mode 100644 index 1ad9aa5e9d..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.html +++ /dev/null @@ -1,31 +0,0 @@ -
-

- {{ 'metric.delete-metric.message.text' | translate }} -

- - - -
-
- - -
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.scss deleted file mode 100644 index 9f569caaee..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.scss +++ /dev/null @@ -1,21 +0,0 @@ -.metric-delete-dialog { - .name { - width: 100%; - line-height: 18px; - margin-top: -10px; - padding-bottom: 20px; - - &-input { - line-height: 18px; - } - } - - &-actions { - justify-content: flex-end; - margin: 0; - - .delete-btn { - background-color: var(--red) !important; - } - } -} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.spec.ts b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.spec.ts deleted file mode 100644 index 4d3b786045..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DeleteMetricsComponent } from './delete-metrics.component'; -import { TestingModule } from '../../../../../../../testing/testing.module'; -import { AnalysisService } from '../../../../../../core/analysis/analysis.service'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -xdescribe('DeleteMetricsComponent', () => { - let component: DeleteMetricsComponent; - let fixture: ComponentFixture; - - const modalData = { - key: ['key1', 'key2'], - }; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DeleteMetricsComponent], - imports: [TestingModule], - providers: [ - AnalysisService, - { provide: MatDialogRef, useValue: {} }, - { - provide: MAT_DIALOG_DATA, - useValue: modalData, - }, - ], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DeleteMetricsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.ts deleted file mode 100644 index 567f7e64c2..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/components/modals/delete-metrics/delete-metrics.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component, ChangeDetectionStrategy, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { AnalysisService } from '../../../../../../core/analysis/analysis.service'; -import { METRICS_JOIN_TEXT } from '../../../../../../core/analysis/store/analysis.models'; - -@Component({ - selector: 'app-delete-metrics', - templateUrl: './delete-metrics.component.html', - styleUrls: ['./delete-metrics.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeleteMetricsComponent { - metricName: string; - constructor( - private analysisService: AnalysisService, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any - ) {} - - onCancelClick(): void { - this.dialogRef.close(); - } - - deleteMetric() { - this.analysisService.deleteMetric(this.data.key.join(METRICS_JOIN_TEXT)); - this.onCancelClick(); - } -} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/profile/profile.module.ts b/frontend/projects/upgrade/src/app/features/dashboard/profile/profile.module.ts index 8ca0f431ff..0228a4fe28 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/profile/profile.module.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/profile/profile.module.ts @@ -8,7 +8,6 @@ import { NewUserComponent } from './components/modals/new-user/new-user.componen import { ProfileInfoComponent } from './components/profile-info/profile-info.component'; import { MetricsComponent } from './components/metrics/metrics.component'; import { AddMetricsComponent } from './components/modals/add-metrics/add-metrics.component'; -import { DeleteMetricsComponent } from './components/modals/delete-metrics/delete-metrics.component'; import { NgJsonEditorModule } from 'ang-jsoneditor'; @NgModule({ @@ -17,7 +16,6 @@ import { NgJsonEditorModule } from 'ang-jsoneditor'; NewUserComponent, ProfileInfoComponent, MetricsComponent, - DeleteMetricsComponent, AddMetricsComponent, ], imports: [CommonModule, ProfileRoutingModule, SharedModule, NgJsonEditorModule], diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/modal/import-segment/import-segment.component.html b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/modal/import-segment/import-segment.component.html index 5d4c58aaaf..b788170744 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/modal/import-segment/import-segment.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/modal/import-segment/import-segment.component.html @@ -1,5 +1,5 @@
- {{ 'segments.import-segment.text' | translate | titlecase }} + {{ 'segments.import-segments.text' | translate | titlecase }}

diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.ts index bed59a7e75..64d30e417d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segment-members/segment-members.component.ts @@ -154,30 +154,22 @@ export class SegmentMembersComponent implements OnInit, OnChanges { } selectSubSegments(): void { - if (this.allSegments) { - this.allSegments.forEach((segment) => { - if (this.segmentInfo) { - if ( - segment.type !== SEGMENT_TYPE.GLOBAL_EXCLUDE && - segment.id !== this.segmentInfo.id && - segment.context === this.currentContext - ) { - this.subSegmentIds.push(segment.name); - this.segmentNameId.set(segment.name, segment.id); - } else if ( - segment.type !== SEGMENT_TYPE.GLOBAL_EXCLUDE && - segment.id !== this.segmentInfo.id && - this.currentContext === 'ALL' - ) { - this.subSegmentIds.push(segment.name); - this.segmentNameId.set(segment.name, segment.id); - } - } else if (segment.type !== SEGMENT_TYPE.GLOBAL_EXCLUDE && segment.context === this.currentContext) { + if (!this.allSegments) { + return; + } + + const isContextAll = this.currentContext === "ALL"; + + this.allSegments + .filter((segment) => + segment.type !== SEGMENT_TYPE.GLOBAL_EXCLUDE && + (isContextAll || segment.context === this.currentContext) && + (!this.segmentInfo || segment.id !== this.segmentInfo.id) + ) + .forEach(segment => { this.subSegmentIds.push(segment.name); this.segmentNameId.set(segment.name, segment.id); - } }); - } } onFileSelected(event: any): void { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segments-list/segments-list.component.html b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segments-list/segments-list.component.html index 18401ceea5..236f546764 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segments-list/segments-list.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/components/segments-list/segments-list.component.html @@ -43,7 +43,7 @@ *ngIf="(permissions$ | async)?.segments.create" > add - {{ 'segments.import-segments.text' | translate }} + {{ 'segments.import-segment.text' | translate }} + +

\ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.scss b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.scss new file mode 100644 index 0000000000..744ffe9850 --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.scss @@ -0,0 +1,29 @@ +.input-container { + /* Add your styles here */ + height: 210px; + width: 592px; + border: 2px dashed #ccc; + padding: 20px; + text-align: center; + border-radius: 10px; + transition: background-color 0.3s; + + &.drag-over { + background-color: #f0f0f0; + } + + .drag-text { + margin-bottom: 0.7rem; + } + + mat-icon { + height: 70px; + width: 70px; + font-size: 70px; + color: grey; + } + + button { + font-size: 14px; + } +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.ts new file mode 100644 index 0000000000..250566b484 --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-import-container/common-import-container.component.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../../shared/shared.module'; +import { CommonModalComponent } from '../common-modal/common-modal.component'; +import { BehaviorSubject } from 'rxjs'; +import { FILE_TYPE } from 'upgrade_types'; + +/** + * A reusable component for drag-and-drop file import functionality. + * This component allows users to drag and drop files or select them via a file input. + * It supports specifying a file type and emits the selected files to the parent component. + * + * The component accepts the following inputs: + * - `fileType`: A string representing the accepted file type (e.g., '.json'). Only files with this extension can be selected or dropped. + * - `buttonLabel`: A string representing the label text of the button. Defaults to 'Upload File'. + * + * The component emits the following outputs: + * - `filesSelected`: An event that emits the selected files as an array of `File` objects. + * + * Example usage: + * + * ``` + * + * ``` + */ +@Component({ + selector: 'app-common-import-container', + standalone: true, + imports: [CommonModalComponent, CommonModule, SharedModule], + templateUrl: './common-import-container.component.html', + styleUrls: ['./common-import-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CommonImportContainerComponent { + @Input() fileType!: FILE_TYPE; + @Input() buttonLabel!: string; + @Output() filesSelected = new EventEmitter(); + + isDragOver = new BehaviorSubject(false); + + onDragOver(event: DragEvent) { + this.handleDragState(event, true); + } + + onDragLeave(event: DragEvent) { + this.handleDragState(event, false); + } + + onDrop(event: DragEvent) { + this.handleDragState(event, false); + this.handleFileSelection(event.dataTransfer?.files); + } + + private handleDragState(event: DragEvent, isOver: boolean) { + event.preventDefault(); + event.stopPropagation(); + this.isDragOver.next(isOver); + } + + onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + this.handleFileSelection(input.files); + } + + private handleFileSelection(files: FileList | null) { + if (files && files.length > 0) { + const validFiles = Array.from(files).filter(file => file.name.endsWith(this.fileType)); + if (validFiles.length > 0) { + this.filesSelected.emit(validFiles); + } else { + console.error('Invalid file types...'); + } + } + } +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.html b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.html index f9ef3b8b2b..c4cc2e3f90 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.html +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.html @@ -21,7 +21,7 @@

{{ title }}

class="dialog-action-btn primary-btn" mat-flat-button [color]="primaryActionBtnColor" - [disabled]="primaryActionBtnDisabled$ | async" + [disabled]="primaryActionBtnDisabled" (click)="onPrimaryActionBtnClicked()" > {{ primaryActionBtnLabel }} diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.spec.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.spec.ts deleted file mode 100644 index 7323254d65..0000000000 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CommonModalComponent } from './common-modal.component'; - -xdescribe('CommonDialogComponent', () => { - let component: CommonModalComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CommonModalComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(CommonModalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.ts index 8a791cd184..1fded529d5 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal.component.ts @@ -7,7 +7,6 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDialogActions, MatDialogClose, MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; import { CommonModule, NgTemplateOutlet } from '@angular/common'; import { MatIcon } from '@angular/material/icon'; -import { Observable } from 'rxjs'; @Component({ selector: 'app-common-dialog', @@ -35,7 +34,7 @@ export class CommonModalComponent { @Input() primaryActionBtnLabel = 'Submit'; @Input() primaryActionBtnColor = 'primary'; @Input() hideFooter = false; - @Input() primaryActionBtnDisabled$: Observable; + @Input() primaryActionBtnDisabled = false; @Output() primaryActionBtnClicked = new EventEmitter(); onPrimaryActionBtnClicked() { diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.scss b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.scss index e69de29bb2..5e794b6c31 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.scss +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.scss @@ -0,0 +1,3 @@ +::ng-deep .mat-mdc-tab-group .mat-mdc-tab-header .mat-mdc-tab-label-container .mat-mdc-tab-labels .mat-mdc-tab:first-of-type { + margin-left: 0; +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.html b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.html index db7101a866..2775d6146c 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.html +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.html @@ -1,25 +1,35 @@ - + {{ 'home.new-experiment.overview.tags.placeHolder' | translate }} - - - - {{ componentTag }} - - cancel - - - +
+ + + + {{ componentTag }} + + cancel + + + + + +
diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.scss b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.scss index c7acb4bf6e..654ab0ba87 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.scss +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.scss @@ -1,3 +1,32 @@ mat-form-field { width: 100%; } +.icon-buttons-present { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +.container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.icon-button { + background: none; + border: none; + box-shadow: none; + outline: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +.icon-button { + color: var(--grey-2); +} diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.ts index 9eee4a7096..761b793a9d 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tag-input/common-tag-input.component.ts @@ -1,4 +1,4 @@ -import { Component, forwardRef } from '@angular/core'; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl } from '@angular/forms'; import { MatChipInputEvent } from '@angular/material/chips'; import { ENTER, COMMA } from '@angular/cdk/keycodes'; @@ -15,9 +15,21 @@ import { TranslateModule } from '@ngx-translate/core'; // registerOnChange(fn: any): Registers a callback for when the value changes. // registerOnTouched(fn: any): Registers a callback for when the component is touched. -// Example Usage: +// Typical usage // +// To add Import/Export button while using component +// you can add the property 'actionButtons' which will check for +// tags value and will display Import/Export icon accordingly +// and bind it with 'actionButtonClicked' to implement action + +// Manage built-in optional action buttons +// + @Component({ selector: 'app-common-tags-input', templateUrl: './common-tag-input.component.html', @@ -32,14 +44,42 @@ import { TranslateModule } from '@ngx-translate/core'; ], imports: [CommonModule, MatChipsModule, MatFormFieldModule, MatIconModule, MatInputModule, TranslateModule], }) -export class CommonTagsInputComponent implements ControlValueAccessor { - isChipSelectable = true; +export class CommonTagsInputComponent implements ControlValueAccessor, OnInit { + showExportIcon = false; + showImportIcon = false; + @Input() actionButtons = false; + @Output() actionButtonClicked = new EventEmitter(); + + isChipSelectable = false; isChipRemovable = true; addChipOnBlur = true; readonly separatorKeysCodes: number[] = [ENTER, COMMA]; tags = new FormControl([]); + ngOnInit(): void { + this.checkTagValue(); + } + + checkTagValue(): void { + this.tags.valueChanges.subscribe((value) => { + if (this.actionButtons) { + // Update showExportIcon and showImportIcon based on tags value + if (value && value.length > 0) { + this.showExportIcon = true; + this.showImportIcon = false; + } else { + this.showImportIcon = true; + this.showExportIcon = false; + } + } + }); + } + + onActionButtonClick(): void { + this.actionButtonClicked.emit(); + } + addChip(event: MatChipInputEvent) { const input = event.chipInput; const value = (event.value || '').trim().toLowerCase(); @@ -57,19 +97,19 @@ export class CommonTagsInputComponent implements ControlValueAccessor { if (input) { input.clear(); } + + this.checkTagValue(); } removeChip(tag: string) { const currentTags = this.tags.value || []; - const index = currentTags.indexOf(tag); + const newTags = currentTags.filter((t) => t !== tag); - if (index >= 0) { - currentTags.splice(index, 1); - this.tags.setValue(currentTags); - this.tags.updateValueAndValidity(); - } - } + this.tags.setValue(newTags); + this.tags.updateValueAndValidity(); + this.checkTagValue(); + } // Implement ControlValueAccessor methods writeValue(value: string[]) { this.tags.setValue(value || []); diff --git a/frontend/projects/upgrade/src/app/shared/components/delete/delete.component.html b/frontend/projects/upgrade/src/app/shared/components/delete/delete.component.html index d16fcc022f..d356efeb25 100644 --- a/frontend/projects/upgrade/src/app/shared/components/delete/delete.component.html +++ b/frontend/projects/upgrade/src/app/shared/components/delete/delete.component.html @@ -1,5 +1,5 @@
-

+

{{ 'global.delete-confirmation.message.text' | translate }}

, @Inject(MAT_DIALOG_DATA) public data: any) {} onCancelClick(): void { - this.message = false; - this.dialogRef.close(this.message); + this.isDeleteButtonClicked = false; + this.dialogRef.close(this.isDeleteButtonClicked); } delete(): void { - this.message = true; - this.dialogRef.close(this.message); + this.isDeleteButtonClicked = true; + this.dialogRef.close(this.isDeleteButtonClicked); } } diff --git a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index eedd21cfa2..9d3b1ea15c 100644 --- a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts +++ b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts @@ -4,6 +4,8 @@ import { MatConfirmDialogComponent } from '../components/mat-confirm-dialog/mat- import { AddFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component'; import { CommonModalConfig } from '../../shared-standalone-component-lib/components/common-modal/common-modal-config'; import { DeleteFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component'; + +import { ImportFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component'; import { UpdateFlagStatusConfirmationModalComponent } from '../../features/dashboard/feature-flags/modals/update-flag-status-confirmation-modal/update-flag-status-confirmation-modal.component'; import { EditFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/edit-feature-flag-modal/edit-feature-flag-modal.component'; @@ -101,4 +103,21 @@ export class DialogService { }; return this.dialog.open(DeleteFeatureFlagModalComponent, config); } + + openImportFeatureFlagModal() { + const commonModalConfig: CommonModalConfig = { + title: 'Import Feature Flag', + primaryActionBtnLabel: 'Import', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + }; + const config: MatDialogConfig = { + data: commonModalConfig, + width: '670px', + height: '460px', + autoFocus: 'input', + disableClose: true, + }; + return this.dialog.open(ImportFeatureFlagModalComponent, config); + } } diff --git a/frontend/projects/upgrade/src/assets/i18n/en.json b/frontend/projects/upgrade/src/assets/i18n/en.json index 5e57f98da1..7c1bde17b1 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -76,6 +76,7 @@ "home.experiment.export-all-experiments.text": "EXPORT ALL", "home.experiment.add-experiment.text": "ADD EXPERIMENT", "home.experiment.import-experiment.text": "IMPORT EXPERIMENT", + "home.experiment.import-experiments.text": "IMPORT EXPERIMENT(S)", "home.experiment-query-result.title.text": "Metrics Data", "home.experiment-query-result.detail.text": "Metrics reflect participants who have provided data on the relevant metric", "home.experiment-query-result.main-effect.title.text": "Main Effect", @@ -246,6 +247,7 @@ "home.view-experiment.experiment-payloads.column-label.target": "TARGET", "home.view-experiment.experiment-payloads.column-label.condition": "CONDITION", "home.view-experiment.experiment-payloads.column-label.payload": "PAYLOAD", + "home.view-experiment.metrics-analysis-unavailable.text": "Experiment metrics analysis has been temporarily disabled in this view. This information is still available when exporting the experiment data.", "home.view-experiment.graph-type.text": "Type", "home.view-experiment.graph-conditions.text": "Conditions", "home.view-experiment.graph-decision-point.text": "Sites", @@ -259,7 +261,7 @@ "home.view-experiment.mat-tab.design.text": "Design", "home.view-experiment.mat-tab.participants.text": "Participants", "home.view-experiment.mat-tab.metrics.text": "Metrics", - "home.import-experiment.message.text": "Select the JSON to import experiment:", + "home.import-experiment.message.text": "Select the JSON file(s) to import experiments:", "home.import-experiment.error.message.text": "Invalid Experiment JSON data", "home.import-experiment.stratification-factor-error.message.text": "Missing Stratification Factor in Experiment JSON data", "home.import-experiment.invalid-stratification-factor-error.message.text": "Import Stratification Factor from Participants Menu > Stratification before using it", @@ -364,6 +366,8 @@ "feature-flags.details.exclusions.card.no-data-row.text": "No Exclude Lists defined.", "feature-flags.details.exclusions.card.title.text": "Exclude Lists", "feature-flags.details.exclusions.card.subtitle.text": "Define Exclude lists for this feature flag", + "feature-flags.details.exposures.card.title.text": "Exposures", + "feature-flags.details.exposures.card.subtitle.text": "View total exposures for the feature flag during the specific period.", "feature-flags.details.add-exclude-list.button.text": "Add Exclude List", "feature-flags.table-variation-type.text": "Variation type", "feature-flags.overview.flag-variation-type.text": "Flag Variation Type", @@ -384,12 +388,13 @@ "feature-flags.enable.text": "Enable", "feature-flags.add-feature-flag.text": "Add Feature Flag", "feature-flags.import-feature-flag.text": "Import Feature Flag", + "feature-flags.import-feature-flag.message.text": "The Feature Flag JSON file should include the required properties for it to be imported", "feature-flags.export-all-feature-flags.text": "Export All Feature Flags", "segments.title.text": "Segments", "segments.subtitle.text": "Define new segments to include or exclude from any experiment", "segments.no-segments.text": "Welcome!
Let's start by creating a new segment!", "segments.add-segments.text": "ADD SEGMENT", - "segments.import-segments.text": "IMPORT SEGMENT", + "segments.import-segments.text": "IMPORT SEGMENT(S)", "segments.export-all-segments.text": "EXPORT ALL", "segments.global-name.text": "Name", "segments.global-status.text": "Status", @@ -423,7 +428,7 @@ "segments.view-segment.members-subtitle.text": "Member(s)", "segments.global-members.segments-count-members-error.text": "Please have at least 1 valid member to move forward", "segments.import-segment.text": "IMPORT SEGMENT", - "segments.import-segment.message.text": "Select the JSON to import segment:", + "segments.import-segment.message.text": "Select the JSON file(s) to import segments:", "segments.import-segment.error.message.text": "Invalid Segment JSON data", "segments.segment-experiment-list-title.text": "Experiments using '{{ segmentName }}' ", "segments.segment-experiment-list-name.text": "Name", diff --git a/frontend/projects/upgrade/src/assets/js/demo-app.js b/frontend/projects/upgrade/src/assets/js/demo-app.js index 3366f0540e..6afc20172a 100644 --- a/frontend/projects/upgrade/src/assets/js/demo-app.js +++ b/frontend/projects/upgrade/src/assets/js/demo-app.js @@ -5,80 +5,88 @@ if (window.self !== window.top) { const getElementById = (id) => { switch (id) { // Home - case 'login-with-google-button': - return document.querySelector('div.login-container button.google-sign-in-btn'); + case 'upgrade-logo-link': + return document.querySelector('div.logo a.logo-link'); case 'experiments-tab': return document.querySelectorAll('div.list-item-container a.nav-item')[0]; case 'signout-button': - return document.querySelector('span.mat-list-item-content a.logout-link'); + return document.querySelector('mat-list.user-list a.logout-link'); case 'add-experiment-button': - return document.querySelectorAll('mat-card.mat-card button.mat-flat-button')[1]; + return document.querySelectorAll('mat-card.mat-mdc-card button.mat-mdc-unelevated-button')[1]; // Experiment Stepper - Overview Step case 'experiment-stepper-overview-name': - return document.querySelectorAll('form.experiment-overview input.mat-input-element')[0]; + return document.querySelectorAll('form.experiment-overview input.mat-mdc-input-element')[0]; case 'experiment-stepper-overview-description': - return document.querySelectorAll('form.experiment-overview input.mat-input-element')[1]; + return document.querySelectorAll('form.experiment-overview input.mat-mdc-input-element')[1]; case 'experiment-stepper-overview-app-context': - return document.querySelectorAll('form.experiment-overview div.mat-select-value')[0]; + return document.querySelectorAll('form.experiment-overview div.mat-mdc-select-value')[0]; case 'experiment-stepper-overview-unit-of-assignment': - return document.querySelectorAll('form.experiment-overview div.mat-select-value')[1]; + return document.querySelectorAll('form.experiment-overview div.mat-mdc-select-value')[1]; case 'experiment-stepper-overview-consistency-rule': - return document.querySelectorAll('form.experiment-overview div.mat-select-value')[2]; + return document.querySelectorAll('form.experiment-overview div.mat-mdc-select-value')[2]; case 'experiment-stepper-overview-next-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[1]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[1]; // Experiment Stepper - Design Step case 'experiment-stepper-design-add-decision-point-button': return document.querySelector('form.experiment-design button.add-decision-point'); case 'experiment-stepper-design-decision-points-row1-site': - return document.querySelectorAll('mat-table.decision-point-table input.mat-input-element')[0]; + return document.querySelectorAll('mat-table.decision-point-table input.mat-mdc-input-element')[0]; case 'experiment-stepper-design-decision-points-row1-target': - return document.querySelectorAll('mat-table.decision-point-table input.mat-input-element')[1]; + return document.querySelectorAll('mat-table.decision-point-table input.mat-mdc-input-element')[1]; case 'experiment-stepper-design-decision-points-row1-exclude-if-reached': - return document.querySelector('mat-table.decision-point-table input.mat-checkbox-input'); + return document.querySelector('mat-table.decision-point-table input.mdc-checkbox__native-control'); + case 'experiment-stepper-design-decision-points-row1-confirm-button': + return document.querySelectorAll('mat-table.decision-point-table button.row-action-btn')[0]; case 'experiment-stepper-design-add-condition-button': return document.querySelector('form.experiment-design button.add-condition'); case 'experiment-stepper-design-conditions-row1-condition': - return document.querySelectorAll('mat-table.condition-table input.mat-input-element')[0]; + return document.querySelectorAll('mat-table.condition-table input.mat-mdc-input-element')[0]; + case 'experiment-stepper-design-conditions-row1-confirm': + return document.querySelectorAll('mat-table.condition-table button.row-action-btn')[0]; case 'experiment-stepper-design-conditions-row2-condition': - return document.querySelectorAll('mat-table.condition-table input.mat-input-element')[3]; + return document.querySelectorAll('mat-table.condition-table input.mat-mdc-input-element')[0]; + case 'experiment-stepper-design-conditions-row2-confirm': + return document.querySelectorAll('mat-table.condition-table button.row-action-btn')[2]; case 'experiment-stepper-design-next-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[4]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[4]; // Experiment Stepper - Participants Step - case 'experiment-stepper-participants-inclusion-criteria': - return document.querySelector('form.experiment-participants div.mat-select-value'); + case 'experiment-stepper-participants-add-member-button': + return document.querySelector('form.experiment-participants button.add-member'); + case 'experiment-stepper-participants-include-row1-type': + return document.querySelectorAll('mat-table.member-table div.mat-mdc-select-value')[0]; case 'experiment-stepper-participants-next-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[7]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[7]; // Experiment Stepper - Metrics Step case 'experiment-stepper-metrics-add-metric-button': return document.querySelector('form.metric-design button.add-metric'); case 'experiment-stepper-metrics-metrics-row1-metric': - return document.querySelectorAll('mat-table.metric-table input.mat-input-element')[0]; + return document.querySelectorAll('mat-table.metric-table input.mat-mdc-input-element')[0]; case 'experiment-stepper-metrics-metrics-row1-statistic': - return document.querySelectorAll('mat-table.metric-table div.mat-select-value')[0]; + return document.querySelectorAll('mat-table.metric-table div.mat-mdc-select-value')[0]; case 'experiment-stepper-metrics-metrics-row1-display-name': - return document.querySelectorAll('mat-table.metric-table input.mat-input-element')[1]; + return document.querySelectorAll('mat-table.metric-table input.mat-mdc-input-element')[1]; case 'experiment-stepper-metrics-metrics-row2-metric': - return document.querySelectorAll('mat-table.metric-table input.mat-input-element')[2]; + return document.querySelectorAll('mat-table.metric-table input.mat-mdc-input-element')[2]; case 'experiment-stepper-metrics-metrics-row2-statistic': - return document.querySelectorAll('mat-table.metric-table div.mat-select-value')[1]; + return document.querySelectorAll('mat-table.metric-table div.mat-mdc-select-value')[1]; case 'experiment-stepper-metrics-metrics-row2-display-name': - return document.querySelectorAll('mat-table.metric-table input.mat-input-element')[3]; + return document.querySelectorAll('mat-table.metric-table input.mat-mdc-input-element')[3]; case 'experiment-stepper-metrics-next-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[10]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[10]; // Experiment Stepper - Schedule Step case 'experiment-stepper-schedule-next-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[13]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[13]; // Experiment Stepper - Post Rule Step case 'experiment-stepper-post-rule-post-rule': - return document.querySelectorAll('form.post-experiment-rule-form div.mat-select-value')[0]; + return document.querySelectorAll('form.post-experiment-rule-form div.mat-mdc-select-value')[0]; case 'experiment-stepper-post-rule-create-button': - return document.querySelectorAll('div.new-experiment-modal button.mat-raised-button')[16]; + return document.querySelectorAll('div.new-experiment-modal button.mat-mdc-raised-button')[16]; // Experiment Details - Overview Tab case 'experiment-details-overview-status': @@ -86,13 +94,13 @@ if (window.self !== window.top) { // Experiment Details - Data Tab case 'experiment-details-data-tab': - return document.querySelectorAll('div.mat-tab-list div.mat-tab-label')[4]; + return document.querySelectorAll('div.mat-mdc-tab-list div.mat-mdc-tab')[4]; // Change Experiment Status Modal case 'change-experiment-status-modal-new-status': - return document.querySelectorAll('form.experiment-status-form div.mat-select-value')[1]; + return document.querySelectorAll('form.experiment-status-form div.mat-mdc-select-value')[1]; case 'change-experiment-status-modal-save-button': - return document.querySelectorAll('div.button-container button.mat-raised-button')[1]; + return document.querySelectorAll('div.button-container button.mat-mdc-raised-button')[1]; } console.error(`Error: The element ID "${id}" is not valid.`); return null; @@ -127,7 +135,7 @@ if (window.self !== window.top) { }; const closeOpenedModals = () => { - const modalButtonsSelector = 'div.cdk-overlay-pane button.mat-raised-button'; + const modalButtonsSelector = 'div.cdk-overlay-pane button.mat-mdc-raised-button'; const modalCloseButton = Array.from(document.querySelectorAll(modalButtonsSelector)).find( (elem) => elem.innerText.toLowerCase() === 'close' ); @@ -152,16 +160,16 @@ if (window.self !== window.top) { const signOutButton = getElementById('signout-button'); if (signOutButton) { signOutButton.click(); - } else if (gapi.auth2.getAuthInstance().isSignedIn.get()) { - gapi.auth2.getAuthInstance().signOut(); } }, 'on-upgrade-tab-click': () => { - if (!gapi.auth2.getAuthInstance().isSignedIn.get()) { + if (!getElementById('upgrade-logo-link')) { return closeOpenedModals(); } const experimentsTab = getElementById('experiments-tab'); - experimentsTab.click(); + if (experimentsTab) { + experimentsTab.click(); + } }, 'remove-active-window-event': () => { removeActiveWindowEvent(); diff --git a/frontend/projects/upgrade/src/environments/environment-types.ts b/frontend/projects/upgrade/src/environments/environment-types.ts index aa189af8bf..3580d10186 100644 --- a/frontend/projects/upgrade/src/environments/environment-types.ts +++ b/frontend/projects/upgrade/src/environments/environment-types.ts @@ -59,8 +59,9 @@ export interface Environment { pollingLimit: number; api: APIEndpoints; featureFlagNavToggle: boolean; - withinSubjectExperimentSupportToggle: boolean; errorLogsToggle: boolean; + withinSubjectExperimentSupportToggle: boolean; + metricAnalyticsExperimentDisplayToggle: boolean; } export interface RuntimeEnvironmentConfig { @@ -71,4 +72,5 @@ export interface RuntimeEnvironmentConfig { featureFlagNavToggle?: boolean; withinSubjectExperimentSupportToggle?: boolean; errorLogsToggle?: boolean; + metricAnalyticsExperimentDisplayToggle?: boolean; } diff --git a/frontend/projects/upgrade/src/environments/environment.bsnl.ts b/frontend/projects/upgrade/src/environments/environment.bsnl.ts index 2efeddb526..78d11f14ff 100644 --- a/frontend/projects/upgrade/src/environments/environment.bsnl.ts +++ b/frontend/projects/upgrade/src/environments/environment.bsnl.ts @@ -13,6 +13,7 @@ export const environment = { featureFlagNavToggle: false, withinSubjectExperimentSupportToggle: false, errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: true, api: { getAllExperiments: '/experiments/paginated', createNewExperiments: '/experiments', diff --git a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts index b41b0bb018..075a02037f 100755 --- a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts @@ -13,6 +13,7 @@ export const environment = { featureFlagNavToggle: false, withinSubjectExperimentSupportToggle: false, errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, api: { getAllExperiments: '/experiments/paginated', createNewExperiments: '/experiments', diff --git a/frontend/projects/upgrade/src/environments/environment.prod.ts b/frontend/projects/upgrade/src/environments/environment.prod.ts index 962b584476..fb7aad0702 100755 --- a/frontend/projects/upgrade/src/environments/environment.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.prod.ts @@ -13,6 +13,7 @@ export const environment = { featureFlagNavToggle: false, withinSubjectExperimentSupportToggle: false, errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, api: { getAllExperiments: '/experiments/paginated', createNewExperiments: '/experiments', diff --git a/frontend/projects/upgrade/src/environments/environment.staging.ts b/frontend/projects/upgrade/src/environments/environment.staging.ts index 51c001752b..144bb38cdf 100644 --- a/frontend/projects/upgrade/src/environments/environment.staging.ts +++ b/frontend/projects/upgrade/src/environments/environment.staging.ts @@ -13,6 +13,7 @@ export const environment = { featureFlagNavToggle: false, withinSubjectExperimentSupportToggle: false, errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, api: { getAllExperiments: '/experiments/paginated', createNewExperiments: '/experiments', diff --git a/frontend/projects/upgrade/src/environments/environment.ts b/frontend/projects/upgrade/src/environments/environment.ts index 336a8e1cc0..774d1998ab 100755 --- a/frontend/projects/upgrade/src/environments/environment.ts +++ b/frontend/projects/upgrade/src/environments/environment.ts @@ -18,6 +18,7 @@ export const environment = { featureFlagNavToggle: true, withinSubjectExperimentSupportToggle: false, errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, api: { getAllExperiments: '/experiments/paginated', createNewExperiments: '/experiments', diff --git a/frontend/projects/upgrade/src/index.html b/frontend/projects/upgrade/src/index.html index 183c8a31a2..b8614f0f04 100755 --- a/frontend/projects/upgrade/src/index.html +++ b/frontend/projects/upgrade/src/index.html @@ -11,6 +11,12 @@ + + + diff --git a/types/src/Experiment/enums.ts b/types/src/Experiment/enums.ts index ad451bbeac..d7a4228b5c 100644 --- a/types/src/Experiment/enums.ts +++ b/types/src/Experiment/enums.ts @@ -58,6 +58,7 @@ export enum SERVER_ERROR { WORKING_GROUP_NOT_SUBSET_OF_GROUP = 'Working group is not a subset of user group', INVALID_TOKEN = 'Invalid token', TOKEN_NOT_PRESENT = 'Token is not present in request', + TOKEN_VALIDATION_FAILED = 'JWT Token validation failed', MIGRATION_ERROR = 'Error in migration', EMAIL_SEND_ERROR = 'Email send error', CONDITION_NOT_FOUND = 'Condition not found', @@ -212,7 +213,6 @@ export enum METRIC_SEARCH_KEY { export enum FLAG_SORT_KEY { NAME = 'name', - KEY = 'key', STATUS = 'status', UPDATED_AT = 'updatedAt', } @@ -271,3 +271,8 @@ export enum FEATURE_FLAG_PARTICIPANT_LIST_KEY { INCLUDE = 'featureFlagSegmentInclusion', EXCLUDE = 'featureFlagSegmentExclusion', } + +export enum FILE_TYPE { + JSON = '.json', + CSV = '.csv', +} diff --git a/types/src/index.ts b/types/src/index.ts index 56f95714d2..2d92cf032e 100644 --- a/types/src/index.ts +++ b/types/src/index.ts @@ -34,6 +34,7 @@ export { FLAG_SEARCH_KEY, FEATURE_FLAG_STATUS, STATUS_INDICATOR_CHIP_TYPE, + FILE_TYPE, } from './Experiment/enums'; export { IEnrollmentCompleteCondition,