diff --git a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts index 832a7230aa..a7370c5f42 100644 --- a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts @@ -462,12 +462,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; } @@ -475,31 +475,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') @@ -509,7 +521,7 @@ export class ExperimentRepository extends Repository { .addGroupBy('enrollmentCompleteStateTimeLog.timeLog') .where('experiment.id = :experimentId', { experimentId }) .getRawMany(); - + return experimentQuery; } } 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..259e49ebe8 --- /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,39 @@ + 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..8ee757f43a --- /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,60 @@ +.drag-drop-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + color: grey; + + mat-icon { + height: 70px; + width: 70px; + font-size: 70px; + color: grey; + } +} + +.input-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border: 1px dashed grey; + border-radius: 4px; + height: 206px; + width: 592px; + position: relative; + + button { + font-size: 14px; + } +} + +.input-container-header { + position: absolute; + top: 5px; + right: 5px; + + mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + } +} + +.input-container-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +p { + color: grey; + font-size: 12px; +} + +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..e9cb49b01a --- /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,86 @@ +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 { FeatureFlagsService } from '../../../../../core/feature-flags/feature-flags.service'; +import { BehaviorSubject } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../../../../shared/shared.module'; + +@Component({ + selector: 'app-import-feature-flag-modal', + standalone: true, + imports: [CommonModalComponent, CommonModule, SharedModule], + templateUrl: './import-feature-flag-modal.component.html', + styleUrls: ['./import-feature-flag-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImportFeatureFlagModalComponent { + @ViewChild('fileInput') fileInput: ElementRef; + + isImportActionBtnDisabled = new BehaviorSubject(true); + isDragOver = new BehaviorSubject(false); + fileName = new BehaviorSubject(null); + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: CommonModalConfig, + public dialog: MatDialog, + public dialogRef: MatDialogRef + ) {} + + onDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + this.isDragOver.next(true); + } + + onDragLeave(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + this.isDragOver.next(false); + } + + onDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + this.isDragOver.next(false); + + const files = event.dataTransfer?.files; + if (files && files.length > 0) { + this.processFile(files[0]); + } + } + + onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + this.processFile(input.files[0]); + } + } + + processFile(file: File) { + if (file.type === 'application/json') { + this.fileName.next(file.name); + this.isImportActionBtnDisabled.next(false); + this.handleFileInput(file); + } else { + alert('Please upload a valid JSON file.'); + this.fileName.next(null); + this.isImportActionBtnDisabled.next(true); + } + } + + handleFileInput(file: File) { + const reader = new FileReader(); + reader.onload = (e: any) => { + const jsonContent = e.target.result; + console.log(JSON.parse(jsonContent)); + }; + reader.readAsText(file); + } + + closeModal() { + this.dialogRef.close(); + } +} 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/shared/services/common-dialog.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index eedd21cfa2..51ea502f5f 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: '450px', + 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 c646543e96..7c1bde17b1 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -388,6 +388,7 @@ "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",