Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import feature flag validation API and related UI changes, validation table #1798

Merged
merged 43 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
84387a9
import ff validation api and frontend integration part1 WIP
ppratikcr7 Jul 26, 2024
163ac7a
merge conflict resolved
Yagnik56 Jul 26, 2024
855ee4d
merge conflict resolved
Yagnik56 Jul 26, 2024
61ae289
import validation to return compatibility
Jul 29, 2024
637b9c4
resolved merge conflicts
Yagnik56 Aug 5, 2024
480e16e
validation req, table, loading spinner and import action button code
Yagnik56 Aug 5, 2024
042415b
resolved merge conflicts
Yagnik56 Aug 5, 2024
2807b75
solve git comments
Aug 6, 2024
98d1ee4
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 7, 2024
bd3b41b
resolved the review cmt
Yagnik56 Aug 7, 2024
f96ee40
update compatibility warning code
Aug 7, 2024
27a0174
segment checks to return incompatible
Aug 7, 2024
5b6b920
resolved the review comment
Yagnik56 Aug 7, 2024
4c9ab74
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 7, 2024
fa168dd
adding segment list for import
Aug 12, 2024
0b15ae2
Add IFeatureFlagFile interface for feature flag file handling
Aug 12, 2024
e47369a
Merge branch 'featureflag/import-validation-api' into featureflag/imp…
Aug 12, 2024
c5e3975
Update import return format for feature flag
Aug 13, 2024
7111383
fetch feature flags after successful import
Yagnik56 Aug 13, 2024
92377f0
removed topromise from import segment comp
Yagnik56 Aug 13, 2024
d5e2326
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 13, 2024
674f815
added validator to validate import json
Aug 20, 2024
9361c54
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 20, 2024
3828069
update import API validations
Aug 20, 2024
41f4031
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 21, 2024
eb8d69c
Merge branch 'featureflag/import-validation-api' into featureflag/imp…
Aug 21, 2024
cc21f55
make all function of import in a transaction
Aug 22, 2024
9bc40e3
Solve failing testcases
Aug 22, 2024
661b1a8
Update import in FeatureFlagService.ts
Aug 23, 2024
07f7755
updated code to have transaction per import file
Aug 26, 2024
675650f
Merge pull request #1825 from CarnegieLearningWeb/featureflag/import-api
Yagnik56 Aug 26, 2024
a3da3cb
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Aug 26, 2024
8bd7cb1
Rename 'list' to 'segment' in FeatureFlagListValidator and related files
Aug 29, 2024
fb02b2a
revert unwanted changes
Aug 30, 2024
0aac418
solve failing testcases
Aug 30, 2024
c2f5295
Merge pull request #1879 from CarnegieLearningWeb/featureflag/validat…
RidhamShah Aug 30, 2024
17cd315
refractor the import data format to match exported file format
Sep 2, 2024
bb62ccf
Merge branch 'dev' into featureflag/import-validation-api
Sep 2, 2024
b7baef5
Solve feature flag throwing error on update (#1885)
RidhamShah Sep 3, 2024
a4f6025
Make interface for common properties of validator
Sep 4, 2024
c1115fb
Merge branch 'dev' into featureflag/import-validation-api
Sep 5, 2024
e19b162
Solve error and merge conflicts for import
Sep 6, 2024
020314f
Merge branch 'dev' into featureflag/import-validation-api
Yagnik56 Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { FeatureFlag } from '../models/FeatureFlag';
import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion';
import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion';
import { FeatureFlagStatusUpdateValidator } from './validators/FeatureFlagStatusUpdateValidator';
import { FeatureFlagPaginatedParamsValidator } from './validators/FeatureFlagsPaginatedParamsValidator';
import {
FeatureFlagFile,
FeatureFlagPaginatedParamsValidator,
ValidatedFeatureFlagsError,
} from './validators/FeatureFlagsPaginatedParamsValidator';
import { AppRequest, PaginationResponse } from '../../types';
import { SERVER_ERROR } from 'upgrade_types';
import { FeatureFlagValidation, IdValidator, UserParamsValidator } from './validators/FeatureFlagValidator';
Expand Down Expand Up @@ -613,4 +617,98 @@ export class FeatureFlagsController {
): Promise<Segment> {
return this.featureFlagService.deleteList(id, request.logger);
}

Yagnik56 marked this conversation as resolved.
Show resolved Hide resolved
/**
* @swagger
* /experiments/{validation}:
* post:
* description: Validating Experiment
* consumes:
* - application/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: Validations are completed
* schema:
* type: array
* items:
* type: object
* properties:
* fileName:
* type: string
* error:
* type: string
* '401':
* description: AuthorizationRequiredError
* '500':
* description: Internal Server Error
*/

/**
* @swagger
* /flags/{validation}:
Yagnik56 marked this conversation as resolved.
Show resolved Hide resolved
* post:
* description: Validating Feature Flag
* consumes:
* - application/json
* parameters:
* - in: body
* name: featureFlags
* required: true
* schema:
* type: array
* items:
* type: object
* properties:
* fileName:
* type: string
* fileContent:
* type: string
* description: Import FeatureFlag Files
* tags:
* - Feature Flags
* produces:
* - application/json
* responses:
* '200':
* description: Validations are completed
* schema:
* type: array
* items:
* type: object
* properties:
* fileName:
* type: string
* compatibilityType:
* type: string
* enum: [compatible, warning, incompatible]
* '401':
* description: AuthorizationRequiredError
* '500':
* description: Internal Server Error
*/
@Post('/import/validation')
public async validateImportFeatureFlags(
@Body({ validate: true }) featureFlags: FeatureFlagFile[],
RidhamShah marked this conversation as resolved.
Show resolved Hide resolved
@Req() request: AppRequest
): Promise<ValidatedFeatureFlagsError[]> {
return await this.featureFlagService.validateImportFeatureFlags(featureFlags, request.logger);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ export interface IFeatureFlagSortParams {
sortAs: SORT_AS_DIRECTION;
}

export interface FeatureFlagFile {
Yagnik56 marked this conversation as resolved.
Show resolved Hide resolved
fileName: string;
fileContent: string;
}

export interface ValidatedFeatureFlagsError {
fileName: string;
compatibilityType: FF_COMPATIBILITY_TYPE;
}

export enum FF_COMPATIBILITY_TYPE {
COMPATIBLE = 'compatible',
WARNING = 'warning',
INCOMPATIBLE = 'incompatible',
}

export enum FLAG_SORT_KEY {
NAME = 'name',
KEY = 'key',
Expand Down
71 changes: 69 additions & 2 deletions backend/packages/Upgrade/src/api/services/FeatureFlagService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
IFeatureFlagSearchParams,
IFeatureFlagSortParams,
FLAG_SEARCH_KEY,
ValidatedFeatureFlagsError,
FF_COMPATIBILITY_TYPE,
FeatureFlagFile,
} from '../controllers/validators/FeatureFlagsPaginatedParamsValidator';
import { FeatureFlagListValidator } from '../controllers/validators/FeatureFlagListValidator';
import { SERVER_ERROR, FEATURE_FLAG_STATUS, FILTER_MODE, SEGMENT_TYPE } from 'upgrade_types';
Expand Down Expand Up @@ -274,12 +277,12 @@ export class FeatureFlagService {
if (filterType === 'inclusion') {
existingRecord = await this.featureFlagSegmentInclusionRepository.findOne({
where: { featureFlag: { id: listInput.flagId }, segment: { id: listInput.list.id } },
relations: ['featureFlag', 'segment']
relations: ['featureFlag', 'segment'],
});
} else {
existingRecord = await this.featureFlagSegmentExclusionRepository.findOne({
where: { featureFlag: { id: listInput.flagId }, segment: { id: listInput.list.id } },
relations: ['featureFlag', 'segment']
relations: ['featureFlag', 'segment'],
});
}

Expand Down Expand Up @@ -397,4 +400,68 @@ export class FeatureFlagService {
const includedFeatureFlags = featureFlags.filter(({ id }) => includedFeatureFlagIds.includes(id));
return includedFeatureFlags;
}

public async validateImportFeatureFlags(
featureFlagFiles: FeatureFlagFile[],
logger: UpgradeLogger
): Promise<ValidatedFeatureFlagsError[]> {
logger.info({ message: 'Validate feature flags' });
const validationErrors = await Promise.allSettled(
featureFlagFiles.map(async (featureFlagFile) => {
let featureFlag: FeatureFlag;
try {
featureFlag = JSON.parse(featureFlagFile.fileContent);
} catch (parseError) {
logger.error({ message: 'Error in parsing feature flag file', details: parseError });
return {
fileName: featureFlagFile.fileName,
compatibilityType: FF_COMPATIBILITY_TYPE.INCOMPATIBLE,
};
}

const error = await this.validateImportFeatureFlag(featureFlagFile.fileName, featureFlag);
return error;
})
);
// Filter out the files that have no promise rejection errors
return validationErrors
.map((result) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
const { fileName, compatibilityType } = result.reason;
return { fileName: fileName, compatibilityType: compatibilityType };
}
})
.filter((error) => error !== null);
}

private async validateImportFeatureFlag(fileName: string, flag: FeatureFlag) {
RidhamShah marked this conversation as resolved.
Show resolved Hide resolved
let compatibilityType = FF_COMPATIBILITY_TYPE.COMPATIBLE;

if (!flag.name || !flag.key || !flag.context) {
compatibilityType = FF_COMPATIBILITY_TYPE.INCOMPATIBLE;
} else {
const segmentIds = [
...flag.featureFlagSegmentInclusion.flatMap((segmentInclusion) =>
segmentInclusion.segment.subSegments.map((subSegment) => subSegment.id)
),
...flag.featureFlagSegmentExclusion.flatMap((segmentExclusion) =>
segmentExclusion.segment.subSegments.map((subSegment) => subSegment.id)
),
];

const segments = await this.segmentService.getSegmentByIds(segmentIds);
segments.forEach((segment) => {
if (segment == undefined) {
compatibilityType = FF_COMPATIBILITY_TYPE.WARNING;
}
});
}

return {
fileName: fileName,
compatibilityType: compatibilityType,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import {
AddFeatureFlagRequest,
FeatureFlag,
FeatureFlagFile,
FeatureFlagSegmentListDetails,
FeatureFlagsPaginationInfo,
FeatureFlagsPaginationParams,
Expand Down Expand Up @@ -43,6 +44,11 @@ export class FeatureFlagsDataService {
return this.http.put<FeatureFlag>(url, flag);
}

validateFeatureFlag(featureFlag: FeatureFlagFile[]) {
const url = this.environment.api.validateFeatureFlag;
return this.http.post(url, featureFlag);
}

emailFeatureFlagData(flagId: string, email: string){
let featureFlagInfoParams = new HttpParams();
featureFlagInfoParams = featureFlagInfoParams.append('experimentId', flagId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
selectSortKey,
selectSortAs,
selectAppContexts,
selectIsLoadingImportFeatureFlag,
selectFeatureFlagIds,
} from './store/feature-flags.selectors';
import * as FeatureFlagsActions from './store/feature-flags.actions';
Expand All @@ -47,7 +48,8 @@ export class FeatureFlagsService {
isLoadingFeatureFlags$ = this.store$.pipe(select(selectIsLoadingFeatureFlags));
isLoadingSelectedFeatureFlag$ = this.store$.pipe(select(selectIsLoadingSelectedFeatureFlag));
isLoadingUpsertFeatureFlag$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag));
IsLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete));
isLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete));
isLoadingImportFeatureFlag$ = this.store$.pipe(select(selectIsLoadingImportFeatureFlag));
isLoadingUpdateFeatureFlagStatus$ = this.store$.pipe(select(selectIsLoadingUpdateFeatureFlagStatus));
isLoadingUpsertPrivateSegmentList$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag));
allFeatureFlags$ = this.store$.pipe(select(selectAllFeatureFlagsSortedByDate));
Expand All @@ -57,6 +59,7 @@ export class FeatureFlagsService {
searchKey$ = this.store$.pipe(select(selectSearchKey));
sortKey$ = this.store$.pipe(select(selectSortKey));
sortAs$ = this.store$.pipe(select(selectSortAs));


hasFeatureFlagsCountChanged$ = this.allFeatureFlags$.pipe(
pairwise(),
Expand Down Expand Up @@ -128,6 +131,10 @@ export class FeatureFlagsService {
this.store$.dispatch(FeatureFlagsActions.actionDeleteFeatureFlag({ flagId }));
}

setIsLoadingImportFeatureFlag(isLoadingImportFeatureFlag: boolean) {
this.store$.dispatch(FeatureFlagsActions.actionSetIsLoadingImportFeatureFlag({ isLoadingImportFeatureFlag }));
}

emailFeatureFlagData(featureFlagId: string) {
this.store$.dispatch(FeatureFlagsActions.actionEmailFeatureFlagData({ featureFlagId }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export const actionUpdateFeatureFlagSuccess = createAction(

export const actionUpdateFeatureFlagFailure = createAction('[Feature Flags] Update Feature Flag Failure');

export const actionSetIsLoadingImportFeatureFlag = createAction(
'[Feature Flags] Set Is Loading for Flag Import',
props<{ isLoadingImportFeatureFlag: boolean }>()
);


export const actionEmailFeatureFlagData = createAction(
'[Feature Flags] Email Feature Flag Data',
props<{ featureFlagId: string }>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ export enum UPSERT_FEATURE_FLAG_LIST_ACTION {
EDIT = 'edit',
}

export interface FeatureFlagFile {
Yagnik56 marked this conversation as resolved.
Show resolved Hide resolved
fileName: string;
fileContent: string | ArrayBuffer;
}

export interface ValidateFeatureFlagError {
fileName: string;
compatibilityType: string;
}

export interface FeatureFlagsPaginationInfo {
nodes: FeatureFlag[];
total: number;
Expand Down Expand Up @@ -170,6 +180,7 @@ export const FLAG_ROOT_DISPLAYED_COLUMNS = Object.values(FLAG_ROOT_COLUMN_NAMES)

export interface FeatureFlagState extends EntityState<FeatureFlag> {
isLoadingUpsertFeatureFlag: boolean;
isLoadingImportFeatureFlag: boolean;
isLoadingSelectedFeatureFlag: boolean;
isLoadingFeatureFlags: boolean;
isLoadingUpdateFeatureFlagStatus: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const { selectIds, selectEntities, selectAll, selectTotal } = adapter.get

export const initialState: FeatureFlagState = adapter.getInitialState({
isLoadingUpsertFeatureFlag: false,
isLoadingImportFeatureFlag: false,
isLoadingFeatureFlags: false,
isLoadingUpdateFeatureFlagStatus: false,
isLoadingFeatureFlagDetail: false,
Expand Down Expand Up @@ -109,6 +110,10 @@ const reducer = createReducer(
...state,
isLoadingFeatureFlags,
})),
on(FeatureFlagsActions.actionSetIsLoadingImportFeatureFlag, (state, { isLoadingImportFeatureFlag }) => ({
...state,
isLoadingImportFeatureFlag,
})),
on(FeatureFlagsActions.actionSetSkipFlags, (state, { skipFlags }) => ({ ...state, skipFlags })),
on(FeatureFlagsActions.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })),
on(FeatureFlagsActions.actionSetSearchString, (state, { searchString }) => ({ ...state, searchValue: searchString })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export const selectFeatureFlagsListLength = createSelector(
(featureFlags) => featureFlags.length
);

export const selectIsLoadingImportFeatureFlag = createSelector(
selectFeatureFlagsState,
(state) => state.isLoadingImportFeatureFlag
);

export const selectIsLoadingUpdateFeatureFlagStatus = createSelector(
selectFeatureFlagsState,
(state) => state.isLoadingUpdateFeatureFlagStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ export class DeleteFeatureFlagModalComponent {
inputValue = '';
subscriptions = new Subscription();
isSelectedFeatureFlagRemoved$ = this.featureFlagsService.isSelectedFeatureFlagRemoved$;
IsLoadingFeatureFlagDelete$ = this.featureFlagsService.IsLoadingFeatureFlagDelete$;
isLoadingFeatureFlagDelete$ = this.featureFlagsService.isLoadingFeatureFlagDelete$;
private inputSubject: BehaviorSubject<string> = new BehaviorSubject<string>('');

// Observable that emits true if inputValue is 'delete', false otherwise
isDeleteNotTyped$: Observable<boolean> = this.inputSubject.pipe(map((value) => value.toLowerCase() !== 'delete'));

isDeleteActionBtnDisabled$: Observable<boolean> = combineLatest([
this.isDeleteNotTyped$,
this.IsLoadingFeatureFlagDelete$,
this.isLoadingFeatureFlagDelete$,
]).pipe(map(([isDeleteNotTyped, isLoading]) => isDeleteNotTyped || isLoading));

constructor(
Expand Down
Loading
Loading