From 58f1e63e7c60d15a58c5d86445201ac3e29eb84a Mon Sep 17 00:00:00 2001 From: Pratik Prajapati <33730817+ppratikcr7@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:16:26 +0530 Subject: [PATCH] Feature: Experiment Archive State (#987) * working archive feature frontend changes * ignoring storing logs for archived experiments * add few more missing readonly properties for archive state * missed readonly for archive state * Create API and database for archive * resolved failing test cases for archive state * import change * not allowing archived state change from enrolling state * merge analysis and archieve api * added remaining file * fix peer review comments for archive state * Solve review comments --------- Co-authored-by: RidhamShah Co-authored-by: danoswaltCL <97542869+danoswaltCL@users.noreply.github.com> --- .../Upgrade/src/api/models/ArchivedStats.ts | 18 +++++ .../packages/Upgrade/src/api/models/Query.ts | 8 +- .../repositories/ArchivedStatsRepository.ts | 24 ++++++ .../src/api/repositories/MetricRepository.ts | 3 + .../src/api/services/ExperimentService.ts | 27 ++++++- .../Upgrade/src/api/services/QueryService.ts | 22 +++++- .../migrations/1692936809279-archivedState.ts | 79 +++++++++++++++++++ .../repositories/MetricRepository.test.ts | 8 +- .../test/unit/services/QueryService.test.ts | 32 ++++++++ .../src/app/core/analysis/analysis.service.ts | 1 + .../core/analysis/store/analysis.actions.ts | 1 + .../core/analysis/store/analysis.selectors.ts | 2 +- .../experiment-list.component.html | 43 +++++++--- .../experiment-list.component.ts | 78 ++++++++++++------ .../experiment-query-result.component.ts | 3 +- .../experiment-status.component.html | 13 ++- .../experiment-status.component.ts | 23 +++++- .../view-experiment.component.html | 32 +++++--- .../pipes/experiment-state.pipe.spec.ts | 8 ++ .../app/shared/pipes/experiment-state.pipe.ts | 2 + .../projects/upgrade/src/assets/i18n/en.json | 3 + types/src/Experiment/enums.ts | 1 + 22 files changed, 373 insertions(+), 58 deletions(-) create mode 100644 backend/packages/Upgrade/src/api/models/ArchivedStats.ts create mode 100644 backend/packages/Upgrade/src/api/repositories/ArchivedStatsRepository.ts create mode 100644 backend/packages/Upgrade/src/database/migrations/1692936809279-archivedState.ts diff --git a/backend/packages/Upgrade/src/api/models/ArchivedStats.ts b/backend/packages/Upgrade/src/api/models/ArchivedStats.ts new file mode 100644 index 0000000000..91702fccd4 --- /dev/null +++ b/backend/packages/Upgrade/src/api/models/ArchivedStats.ts @@ -0,0 +1,18 @@ +import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm'; +import { IsNotEmpty } from 'class-validator'; +import { BaseModel } from './base/BaseModel'; +import { Query } from './Query'; + +@Entity() +export class ArchivedStats extends BaseModel { + @PrimaryColumn('uuid') + public id: string; + + @IsNotEmpty() + @Column('jsonb') + public result: object; + + @OneToOne(() => Query, (query) => query.archivedStats, { onDelete: 'CASCADE' }) + @JoinColumn() + public query: Query; +} diff --git a/backend/packages/Upgrade/src/api/models/Query.ts b/backend/packages/Upgrade/src/api/models/Query.ts index 1c584826e8..ef639b1975 100644 --- a/backend/packages/Upgrade/src/api/models/Query.ts +++ b/backend/packages/Upgrade/src/api/models/Query.ts @@ -1,9 +1,11 @@ -import { Entity, ManyToOne, Column, PrimaryColumn } from 'typeorm'; +import { Entity, ManyToOne, Column, PrimaryColumn, OneToOne } from 'typeorm'; import { BaseModel } from './base/BaseModel'; import { Metric } from './Metric'; import { Experiment } from './Experiment'; import { IsDefined } from 'class-validator'; import { REPEATED_MEASURE } from 'upgrade_types'; +import { ArchivedStats } from './ArchivedStats'; +import { Type } from 'class-transformer'; @Entity() export class Query extends BaseModel { @@ -23,6 +25,10 @@ export class Query extends BaseModel { @ManyToOne(() => Experiment, (experiment) => experiment.queries, { onDelete: 'CASCADE' }) public experiment: Experiment; + @OneToOne(() => ArchivedStats, (archivedStats) => archivedStats.query, { onDelete: 'CASCADE' }) + @Type(() => ArchivedStats) + public archivedStats: ArchivedStats; + @Column({ type: 'enum', enum: REPEATED_MEASURE, diff --git a/backend/packages/Upgrade/src/api/repositories/ArchivedStatsRepository.ts b/backend/packages/Upgrade/src/api/repositories/ArchivedStatsRepository.ts new file mode 100644 index 0000000000..4f78267a0a --- /dev/null +++ b/backend/packages/Upgrade/src/api/repositories/ArchivedStatsRepository.ts @@ -0,0 +1,24 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { ArchivedStats } from '../models/ArchivedStats'; +import repositoryError from './utils/repositoryError'; + +@EntityRepository(ArchivedStats) +export class ArchivedStatsRepository extends Repository { + public async saveRawJson( + rawDataArray: Array> + ): Promise { + const result = await this.createQueryBuilder('ArchivedStats') + .insert() + .into(ArchivedStats) + .values(rawDataArray) + .onConflict(`DO NOTHING`) + .returning('*') + .execute() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError(this.constructor.name, 'saveRawJson', { rawDataArray }, errorMsg); + throw errorMsgString; + }); + + return result.raw; + } +} \ No newline at end of file diff --git a/backend/packages/Upgrade/src/api/repositories/MetricRepository.ts b/backend/packages/Upgrade/src/api/repositories/MetricRepository.ts index 98705b851f..794963e74f 100644 --- a/backend/packages/Upgrade/src/api/repositories/MetricRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/MetricRepository.ts @@ -1,6 +1,7 @@ import { Metric } from '../models/Metric'; import { EntityRepository, Repository } from 'typeorm'; import repositoryError from './utils/repositoryError'; +import { EXPERIMENT_STATE } from 'upgrade_types'; @EntityRepository(Metric) export class MetricRepository extends Repository { @@ -32,7 +33,9 @@ export class MetricRepository extends Repository { public async findMetricsWithQueries(ids: string[]): Promise { return this.createQueryBuilder('metrics') .innerJoin('metrics.queries', 'queries') + .innerJoin('queries.experiment', 'experiment') .where('key IN (:...ids)', { ids }) + .andWhere('experiment.state NOT IN (:...archived)', { archived: [EXPERIMENT_STATE.ARCHIVED] }) .getMany() .catch((errorMsg: any) => { const errorMsgString = repositoryError(this.constructor.name, 'findMetricsWithQueries', { ids }, errorMsg); diff --git a/backend/packages/Upgrade/src/api/services/ExperimentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentService.ts index fb8e3bb87a..dbf8cca6da 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentService.ts @@ -72,6 +72,9 @@ import { import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO'; import { FactorDTO } from '../DTO/FactorDTO'; import { LevelDTO } from '../DTO/LevelDTO'; +import { QueryService } from './QueryService'; +import { ArchivedStats } from '../models/ArchivedStats'; +import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository'; @Service() export class ExperimentService { @@ -93,10 +96,12 @@ export class ExperimentService { @OrmRepository() private factorRepository: FactorRepository, @OrmRepository() private levelRepository: LevelRepository, @OrmRepository() private levelCombinationElementsRepository: LevelCombinationElementRepository, + @OrmRepository() private archivedStatsRepository: ArchivedStatsRepository, public previewUserService: PreviewUserService, public segmentService: SegmentService, public scheduledJobService: ScheduledJobService, - public errorService: ErrorService + public errorService: ErrorService, + public queryService: QueryService ) {} public async find(logger?: UpgradeLogger): Promise { @@ -384,7 +389,7 @@ export class ExperimentService { ): Promise { const oldExperiment = await this.experimentRepository.findOne( { id: experimentId }, - { relations: ['stateTimeLogs'] } + { relations: ['stateTimeLogs', 'queries', 'queries.metric'] } ); if ( @@ -394,6 +399,24 @@ export class ExperimentService { await this.populateExclusionTable(experimentId, state, logger); } + if (state === EXPERIMENT_STATE.ARCHIVED) { + const queryIds = oldExperiment.queries.map((query) => query.id); + const results = await this.queryService.analyze(queryIds, logger); + const archivedStatsData: Array> = results.map( + (result) => { + const queryId = result.id; + delete result.id; + const archivedStats: Partial = { + id: uuid(), + result: result, + query: queryId, + }; + return archivedStats; + } + ); + await this.archivedStatsRepository.saveRawJson(archivedStatsData); + } + let data: AuditLogData = { experimentId, experimentName: oldExperiment.name, diff --git a/backend/packages/Upgrade/src/api/services/QueryService.ts b/backend/packages/Upgrade/src/api/services/QueryService.ts index 7ed2cd4e75..e349d1eecb 100644 --- a/backend/packages/Upgrade/src/api/services/QueryService.ts +++ b/backend/packages/Upgrade/src/api/services/QueryService.ts @@ -3,11 +3,13 @@ import { OrmRepository } from 'typeorm-typedi-extensions'; import { QueryRepository } from '../repositories/QueryRepository'; import { Query } from '../models/Query'; import { LogRepository } from '../repositories/LogRepository'; -import { EXPERIMENT_TYPE, SERVER_ERROR } from 'upgrade_types'; +import { EXPERIMENT_STATE, EXPERIMENT_TYPE, SERVER_ERROR } from 'upgrade_types'; import { ErrorService } from './ErrorService'; import { ExperimentError } from '../models/ExperimentError'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; import { Experiment } from '../models/Experiment'; +import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository'; +import { In } from 'typeorm'; interface queryResult { conditionId?: string; @@ -21,6 +23,7 @@ export class QueryService { constructor( @OrmRepository() private queryRepository: QueryRepository, @OrmRepository() private logRepository: LogRepository, + @OrmRepository() private archivedStatsRepository: ArchivedStatsRepository, public errorService: ErrorService ) {} @@ -35,6 +38,17 @@ export class QueryService { }); } + public async getArchivedStats(queryIds: string[], logger: UpgradeLogger): Promise { + logger.info({ message: `Get archivedStats of query with queryIds ${queryIds}` }); + const archiveData = await this.archivedStatsRepository.find({ + relations: ['query'], + where: { query: In(queryIds) }, + }); + return archiveData.map((data) => { + return { ...data.result, id: data.query.id }; + }); + } + public async analyze(queryIds: string[], logger: UpgradeLogger): Promise { logger.info({ message: `Get analysis of query with queryIds ${queryIds}` }); const promiseArray = queryIds.map((queryId) => @@ -52,6 +66,12 @@ export class QueryService { const promiseResult = await Promise.all(promiseArray); const experiments: Experiment[] = []; + + // checks for archieve state experiment + if (promiseResult[0].experiment?.state === EXPERIMENT_STATE.ARCHIVED) { + return this.getArchivedStats(queryIds, logger); + } + const analyzePromise = promiseResult.map((query) => { experiments.push(query.experiment); if (query.experiment?.type === EXPERIMENT_TYPE.FACTORIAL) { diff --git a/backend/packages/Upgrade/src/database/migrations/1692936809279-archivedState.ts b/backend/packages/Upgrade/src/database/migrations/1692936809279-archivedState.ts new file mode 100644 index 0000000000..b32bfd8075 --- /dev/null +++ b/backend/packages/Upgrade/src/database/migrations/1692936809279-archivedState.ts @@ -0,0 +1,79 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class archivedState1692936809279 implements MigrationInterface { + name = 'archivedState1692936809279'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "archived_stats" ("createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "versionNumber" integer NOT NULL, "id" uuid NOT NULL, "result" jsonb NOT NULL, "queryId" uuid, CONSTRAINT "REL_df18fe5ea7298a1dc7bfb33b06" UNIQUE ("queryId"), CONSTRAINT "PK_afc8a335c8d9d48cefaf1aa2c09" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `ALTER TYPE "public"."state_time_log_fromstate_enum" RENAME TO "state_time_log_fromstate_enum_old"` + ); + await queryRunner.query( + `CREATE TYPE "public"."state_time_log_fromstate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')` + ); + await queryRunner.query( + `ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum"` + ); + await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum_old"`); + await queryRunner.query( + `ALTER TYPE "public"."state_time_log_tostate_enum" RENAME TO "state_time_log_tostate_enum_old"` + ); + await queryRunner.query( + `CREATE TYPE "public"."state_time_log_tostate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')` + ); + await queryRunner.query( + `ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum" USING "toState"::"text"::"public"."state_time_log_tostate_enum"` + ); + await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum_old"`); + await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum" RENAME TO "experiment_state_enum_old"`); + await queryRunner.query( + `CREATE TYPE "public"."experiment_state_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')` + ); + await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`); + await queryRunner.query( + `ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum" USING "state"::"text"::"public"."experiment_state_enum"` + ); + await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`); + await queryRunner.query(`DROP TYPE "public"."experiment_state_enum_old"`); + await queryRunner.query( + `ALTER TABLE "archived_stats" ADD CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f" FOREIGN KEY ("queryId") REFERENCES "query"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "archived_stats" DROP CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f"`); + await queryRunner.query( + `CREATE TYPE "public"."experiment_state_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')` + ); + await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`); + await queryRunner.query( + `ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum_old" USING "state"::"text"::"public"."experiment_state_enum_old"` + ); + await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`); + await queryRunner.query(`DROP TYPE "public"."experiment_state_enum"`); + await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum_old" RENAME TO "experiment_state_enum"`); + await queryRunner.query( + `CREATE TYPE "public"."state_time_log_tostate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')` + ); + await queryRunner.query( + `ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum_old" USING "toState"::"text"::"public"."state_time_log_tostate_enum_old"` + ); + await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum"`); + await queryRunner.query( + `ALTER TYPE "public"."state_time_log_tostate_enum_old" RENAME TO "state_time_log_tostate_enum"` + ); + await queryRunner.query( + `CREATE TYPE "public"."state_time_log_fromstate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')` + ); + await queryRunner.query( + `ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum_old" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum_old"` + ); + await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum"`); + await queryRunner.query( + `ALTER TYPE "public"."state_time_log_fromstate_enum_old" RENAME TO "state_time_log_fromstate_enum"` + ); + await queryRunner.query(`DROP TABLE "archived_stats"`); + } +} diff --git a/backend/packages/Upgrade/test/unit/repositories/MetricRepository.test.ts b/backend/packages/Upgrade/test/unit/repositories/MetricRepository.test.ts index d6ab581d98..e3929d06cf 100644 --- a/backend/packages/Upgrade/test/unit/repositories/MetricRepository.test.ts +++ b/backend/packages/Upgrade/test/unit/repositories/MetricRepository.test.ts @@ -26,7 +26,7 @@ afterEach(() => { }); describe('MetricRepository Testing', () => { - it('should delete a metric', async () => { + it('should delete a metric', async () => { createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(deleteQueryBuilder); const result = { identifiers: [{ id: metric.key }], @@ -106,8 +106,9 @@ describe('MetricRepository Testing', () => { raw: [metric], }; - selectMock.expects('innerJoin').once().returns(selectQueryBuilder); + selectMock.expects('innerJoin').twice().returns(selectQueryBuilder); selectMock.expects('where').once().returns(selectQueryBuilder); + selectMock.expects('andWhere').once().returns(selectQueryBuilder); selectMock.expects('getMany').once().returns(Promise.resolve(result)); const res = await repo.findMetricsWithQueries([metric.key]); @@ -121,8 +122,9 @@ describe('MetricRepository Testing', () => { it('should throw an error when get monitored experiment metric by date range fails', async () => { createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(selectQueryBuilder); - selectMock.expects('innerJoin').once().returns(selectQueryBuilder); + selectMock.expects('innerJoin').twice().returns(selectQueryBuilder); selectMock.expects('where').once().returns(selectQueryBuilder); + selectMock.expects('andWhere').once().returns(selectQueryBuilder); selectMock.expects('getMany').once().returns(Promise.reject(err)); expect(async () => { diff --git a/backend/packages/Upgrade/test/unit/services/QueryService.test.ts b/backend/packages/Upgrade/test/unit/services/QueryService.test.ts index b0a675d586..595bfb4a93 100644 --- a/backend/packages/Upgrade/test/unit/services/QueryService.test.ts +++ b/backend/packages/Upgrade/test/unit/services/QueryService.test.ts @@ -9,12 +9,15 @@ import { Query } from '../../../src/api/models/Query'; import { Experiment } from '../../../src/api/models/Experiment'; import { ErrorService } from '../../../src/api/services/ErrorService'; import { ErrorRepository } from '../../../src/api/repositories/ErrorRepository'; +import { ArchivedStatsRepository } from '../../../src/api/repositories/ArchivedStatsRepository'; +import { ArchivedStats } from '../../../src/api/models/ArchivedStats'; const logger = new UpgradeLogger(); describe('Query Service Testing', () => { let service: QueryService; let queryRepo: Repository; + let archivedStatsRepo: Repository; let module: TestingModule; const exp1 = new Experiment(); @@ -43,12 +46,19 @@ describe('Query Service Testing', () => { result: 0, participantsLogged: 0, }; + + const mockArchiveData = new ArchivedStats(); + mockArchiveData.id = 'id1'; + mockArchiveData.query = mockquery1; + mockArchiveData.result = { mainEffect: logResult, interactionEffect: null }; + beforeEach(async () => { module = await Test.createTestingModule({ providers: [ QueryService, QueryRepository, LogRepository, + ArchivedStatsRepository, ErrorService, ErrorRepository, { @@ -69,6 +79,12 @@ describe('Query Service Testing', () => { }), }, }, + { + provide: getRepositoryToken(ArchivedStatsRepository), + useValue: { + find: jest.fn().mockResolvedValue([mockArchiveData]), + }, + }, { provide: ErrorService, useValue: { @@ -80,6 +96,7 @@ describe('Query Service Testing', () => { service = module.get(QueryService); queryRepo = module.get>(getRepositoryToken(QueryRepository)); + archivedStatsRepo = module.get>(getRepositoryToken(ArchivedStatsRepository)); }); it('should be defined', async () => { @@ -90,6 +107,10 @@ describe('Query Service Testing', () => { expect(await queryRepo.find()).toEqual(queryArr); }); + it('should have the repo mocked', async () => { + expect(await archivedStatsRepo.find()).toEqual([mockArchiveData]); + }); + it('should find and map queries to experiments', async () => { const response = await service.find(logger); expect(response).toEqual([ @@ -134,4 +155,15 @@ describe('Query Service Testing', () => { }, ]); }); + + it('should find and map archivedStats to queries', async () => { + const response = await service.getArchivedStats(['id1'], logger); + expect(response).toEqual([ + { + id: mockquery1.id, + interactionEffect: null, + mainEffect: logResult, + }, + ]); + }); }); 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 c6a9f7c342..5903d37869 100644 --- a/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts +++ b/frontend/projects/upgrade/src/app/core/analysis/analysis.service.ts @@ -10,6 +10,7 @@ import { } from './store/analysis.selectors'; import * as AnalysisActions from './store/analysis.actions'; import { UpsertMetrics } from './store/analysis.models'; +import { EXPERIMENT_STATE } from '../experiments/store/experiments.model'; @Injectable() export class AnalysisService { diff --git a/frontend/projects/upgrade/src/app/core/analysis/store/analysis.actions.ts b/frontend/projects/upgrade/src/app/core/analysis/store/analysis.actions.ts index d6594344e4..64b06f667f 100644 --- a/frontend/projects/upgrade/src/app/core/analysis/store/analysis.actions.ts +++ b/frontend/projects/upgrade/src/app/core/analysis/store/analysis.actions.ts @@ -1,5 +1,6 @@ import { createAction, props } from '@ngrx/store'; import { MetricUnit, UpsertMetrics } from './analysis.models'; +import { EXPERIMENT_STATE } from '../../experiments/store/experiments.model'; export const actionFetchMetrics = createAction('[Analysis] Fetch Metrics'); diff --git a/frontend/projects/upgrade/src/app/core/analysis/store/analysis.selectors.ts b/frontend/projects/upgrade/src/app/core/analysis/store/analysis.selectors.ts index ce1368c164..a6d06d29da 100644 --- a/frontend/projects/upgrade/src/app/core/analysis/store/analysis.selectors.ts +++ b/frontend/projects/upgrade/src/app/core/analysis/store/analysis.selectors.ts @@ -1,5 +1,5 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; -import { AnalysisState, State } from './analysis.models'; +import { AnalysisState } from './analysis.models'; export const selectAnalysisState = createFeatureSelector('analysis'); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html b/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html index 2ff389401a..30b0e7fa1f 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/home/components/experiment-list/experiment-list.component.html @@ -13,14 +13,37 @@ - +
+ +
+
+ + + + {{ filterOption | titlecase }} + + +
search
@@ -32,7 +55,7 @@ (click)="openExportAllExperimentsDialog()" > download - {{ 'segments.export-all-segments.text' | translate }} + {{ 'home.experiment.export-all-experiments.text' | translate }} @@ -105,7 +109,9 @@ @@ -131,7 +137,9 @@
@@ -204,10 +212,7 @@
- + {{ 'home-global.consistency-rule.text' | translate | uppercase }} @@ -215,7 +220,9 @@ {{ 'home-global.condition-order.text' | translate | uppercase }} - {{ experiment.consistencyRule || experiment.conditionOrder | titlecase }} + {{ + experiment.consistencyRule || experiment.conditionOrder | titlecase + }}
@@ -232,7 +239,7 @@ ( ) - + create
@@ -290,7 +297,7 @@ {{ experiment.endOn | formatDate: 'medium date' }} @@ -309,7 +316,7 @@ @@ -387,6 +394,7 @@ class="ft-16-600" color="primary" (change)="toggleVerboseLogging($event)" + [disabled]="experiment.state === ExperimentState.CANCELLED || experiment.state === ExperimentState.ARCHIVED" > {{ 'home.view-experiment.logging.text' | translate | uppercase }} diff --git a/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.spec.ts b/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.spec.ts index af62ab103b..d3ba91f6b7 100644 --- a/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.spec.ts +++ b/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.spec.ts @@ -28,6 +28,10 @@ describe('ExperimentStatePipe', () => { expect(experimentStatePipe.transform(EXPERIMENT_STATE.INACTIVE)).toBe('Inactive'); }); + it('should return Archived State', () => { + expect(experimentStatePipe.transform(EXPERIMENT_STATE.ARCHIVED)).toBe('Archived'); + }); + it('should return #000 color for Preview State', () => { expect(experimentStatePipe.transform(EXPERIMENT_STATE.PREVIEW, ExperimentStatePipeType.COLOR)).toBe('#000'); }); @@ -53,4 +57,8 @@ describe('ExperimentStatePipe', () => { it('should return #d8d8d8 color for Inactive State', () => { expect(experimentStatePipe.transform(EXPERIMENT_STATE.INACTIVE, ExperimentStatePipeType.COLOR)).toBe('#d8d8d8'); }); + + it('should return #fd9099 color for Archived State', () => { + expect(experimentStatePipe.transform(EXPERIMENT_STATE.ARCHIVED, ExperimentStatePipeType.COLOR)).toBe('#fd9099'); + }); }); diff --git a/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.ts b/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.ts index ae0afb8dc0..7fe5d486f4 100644 --- a/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.ts +++ b/frontend/projects/upgrade/src/app/shared/pipes/experiment-state.pipe.ts @@ -24,6 +24,8 @@ export class ExperimentStatePipe implements PipeTransform { return type === ExperimentStatePipeType.TEXT ? 'Enrolling' : '#7b9cff'; case EXPERIMENT_STATE.CANCELLED: return type === ExperimentStatePipeType.TEXT ? 'Cancelled' : '#ff0000'; + case EXPERIMENT_STATE.ARCHIVED: + return type === ExperimentStatePipeType.TEXT ? 'Archived' : '#fd9099'; } } } diff --git a/frontend/projects/upgrade/src/assets/i18n/en.json b/frontend/projects/upgrade/src/assets/i18n/en.json index 0ca3539ebb..d5eb8783c6 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -71,6 +71,7 @@ "home-global.assignment-weight.text": "WEIGHT (%)", "home.experiment.text.subtitle": "Create and analyze experiments", "home.no-experiment.text": "Welcome!
Let's start by creating a new experiment!", + "home.experiment.export-all-experiments.text": "EXPORT ALL", "home.experiment.add-experiment.text": "ADD EXPERIMENT", "home.experiment.import-experiment.text": "IMPORT EXPERIMENT", "home.experiment-query-result.title.text": "Metrics Data", @@ -267,6 +268,8 @@ "home.change-experiment-status.enrolling-info.text": "*Once the experiment is started you won’t be allowed to update Experiment’s Condition and Sites", "home.change-experiment-status.enrolling-error.text": "*Experiment start date should not be greater than experiment end date {{ endDate }}", "home.change-experiment-status.condition-count-error.text": "*Please have at least 2 conditions to move forward", + "home.change-experiment-status.archiving-confirmation.text": "Please acknowledge that you agree to permanently transition this experiment to archive status.", + "home.change-experiment-status.archiving-info.text": "*Archiving the experiment will cancel the post rule. The archived experiments are hidden from the experiment list and can be found by explicitly searching for 'Archived' experiments.", "home.change-post-experiment-rule.title.text": "Change Post Experiment Condition", "logs-global.log-zero-state.text": "No Logs", "logs-global.log-view-diff.text": "view", diff --git a/types/src/Experiment/enums.ts b/types/src/Experiment/enums.ts index 95eb419e6d..8b1ca9912b 100644 --- a/types/src/Experiment/enums.ts +++ b/types/src/Experiment/enums.ts @@ -30,6 +30,7 @@ export enum EXPERIMENT_STATE { ENROLLING = 'enrolling', ENROLLMENT_COMPLETE = 'enrollmentComplete', CANCELLED = 'cancelled', + ARCHIVED = 'archived', } export enum SERVER_ERROR {