Skip to content

Commit

Permalink
consolidate add-edit-duplicate modals (#1732)
Browse files Browse the repository at this point in the history
* consolidate add-edit-duplicate modals

* adjustments to backend updates, code cleanup

* fixed messed up interfaces after merge

* fix up better selectors and some refactor for readability

* remove debugging thing

* Update Feature Flags Modal Style (#1769)

* param to flag naming

* remove unused en.jsonkey, refactored selector logic, better names

* Update createRequest to sendRequest

* use in-line logic instead of functions in form initialization

---------

Co-authored-by: Zack Lee <[email protected]>
Co-authored-by: Zack Lee <[email protected]>
Co-authored-by: Yagnik Hingrajiya <[email protected]>
  • Loading branch information
4 people authored Jul 22, 2024
1 parent d4c86d8 commit 7c3f819
Show file tree
Hide file tree
Showing 42 changed files with 797 additions and 983 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
FeatureFlag,
FeatureFlagsPaginationInfo,
FeatureFlagsPaginationParams,
UpdateFeatureFlagRequest,
UpdateFeatureFlagStatusRequest,
} from './store/feature-flags.model';
import { Observable } from 'rxjs';
import { FEATURE_FLAG_STATUS, FILTER_MODE } from '../../../../../../../types/src';

@Injectable()
export class FeatureFlagsDataService {
Expand All @@ -18,32 +18,30 @@ export class FeatureFlagsDataService {
fetchFeatureFlagsPaginated(params: FeatureFlagsPaginationParams): Observable<FeatureFlagsPaginationInfo> {
const url = this.environment.api.getPaginatedFlags;
return this.http.post<FeatureFlagsPaginationInfo>(url, params);
// mock
// // return of({ nodes: mockFeatureFlags, total: 2 }).pipe(delay(2000));
}

fetchFeatureFlagById(id: string) {
const url = `${this.environment.api.featureFlag}/${id}`;
return this.http.get(url);
}

addFeatureFlag(params: AddFeatureFlagRequest): Observable<FeatureFlag> {
const url = this.environment.api.featureFlag;
return this.http.post<FeatureFlag>(url, params);
}

updateFeatureFlagStatus(params: UpdateFeatureFlagStatusRequest): Observable<FeatureFlag> {
const url = this.environment.api.updateFlagStatus;
return this.http.post<FeatureFlag>(url, params);
}

deleteFeatureFlag(id: string) {
const url = `${this.environment.api.featureFlag}/${id}`;
return this.http.delete(url);
addFeatureFlag(flag: AddFeatureFlagRequest): Observable<FeatureFlag> {
const url = this.environment.api.featureFlag;
return this.http.post<FeatureFlag>(url, flag);
}

updateFeatureFlag(flag: FeatureFlag): Observable<FeatureFlag> {
updateFeatureFlag(flag: UpdateFeatureFlagRequest): Observable<FeatureFlag> {
const url = `${this.environment.api.featureFlag}/${flag.id}`;
return this.http.put<FeatureFlag>(url, flag);
}

deleteFeatureFlag(id: string) {
const url = `${this.environment.api.featureFlag}/${id}`;
return this.http.delete(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,21 @@ import {
selectIsLoadingSelectedFeatureFlag,
selectSortKey,
selectSortAs,
selectFeatureFlagListTypeOptions,
selectAppContexts,
} from './store/feature-flags.selectors';
import * as FeatureFlagsActions from './store/feature-flags.actions';
import { actionFetchContextMetaData } from '../experiments/store/experiments.actions';
import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types';
import {
AddFeatureFlagRequest,
FeatureFlag,
LIST_OPTION_TYPE,
UpdateFeatureFlagStatusRequest,
AddFeatureFlagRequest,
UpdateFeatureFlagRequest,
} from './store/feature-flags.model';
import { ExperimentService } from '../experiments/experiments.service';
import { filter, map, pairwise, withLatestFrom } from 'rxjs';
import { selectContextMetaData } from '../experiments/store/experiments.selectors';
import isEqual from 'lodash.isequal';

@Injectable()
export class FeatureFlagsService {
Expand All @@ -44,6 +46,7 @@ export class FeatureFlagsService {
isLoadingSelectedFeatureFlag$ = this.store$.pipe(select(selectIsLoadingSelectedFeatureFlag));
isLoadingUpdateFeatureFlagStatus$ = this.store$.pipe(select(selectIsLoadingUpdateFeatureFlagStatus));
allFeatureFlags$ = this.store$.pipe(select(selectAllFeatureFlagsSortedByDate));
appContexts$ = this.store$.pipe(select(selectAppContexts));
isAllFlagsFetched$ = this.store$.pipe(select(selectIsAllFlagsFetched));
searchString$ = this.store$.pipe(select(selectSearchString));
searchKey$ = this.store$.pipe(select(selectSearchKey));
Expand All @@ -52,10 +55,11 @@ export class FeatureFlagsService {
isLoadingUpsertFeatureFlag$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag));
IsLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete));

featureFlagsListLengthChange$ = this.allFeatureFlags$.pipe(
hasFeatureFlagsCountChanged$ = this.allFeatureFlags$.pipe(
pairwise(),
filter(([prevEntities, currEntities]) => prevEntities.length !== currEntities.length)
);

selectedFeatureFlagStatusChange$ = this.store$.pipe(
select(selectSelectedFeatureFlag),
pairwise(),
Expand All @@ -71,20 +75,18 @@ export class FeatureFlagsService {
isSelectedFeatureFlagUpdated$ = this.store$.pipe(
select(selectSelectedFeatureFlag),
pairwise(),
filter(([prev, curr]) => prev && curr && JSON.stringify(prev) !== JSON.stringify(curr)),
map(([prev, curr]) => curr)
filter(([prev, curr]) => {
return prev && curr && !isEqual(prev, curr);
}),
map(([, curr]) => curr)
);

selectFeatureFlagListTypeOptions$ = this.store$.pipe(select(selectFeatureFlagListTypeOptions));
selectedFlagOverviewDetails = this.store$.pipe(select(selectFeatureFlagOverviewDetails));
selectedFeatureFlag$ = this.store$.pipe(select(selectSelectedFeatureFlag));
searchParams$ = this.store$.pipe(select(selectSearchFeatureFlagParams));
selectRootTableState$ = this.store$.select(selectRootTableState);
activeDetailsTabIndex$ = this.store$.pipe(select(selectActiveDetailsTabIndex));
appContexts$ = this.experimentService.contextMetaData$.pipe(
map((contextMetaData) => {
return Object.keys(contextMetaData?.contextMetadata ?? []);
})
);
selectFeatureFlagInclusions$ = this.store$.pipe(select(selectFeatureFlagInclusions));
selectFeatureFlagInclusionsLength$ = this.store$.pipe(
select(selectFeatureFlagInclusions),
Expand All @@ -95,46 +97,6 @@ export class FeatureFlagsService {
select(selectFeatureFlagExclusions),
map((exclusions) => exclusions.length)
);
// note: this comes from experiment service!
selectFeatureFlagListTypeOptions$ = this.store$.pipe(
select(selectContextMetaData),
withLatestFrom(this.store$.pipe(select(selectSelectedFeatureFlag))),
map(([contextMetaData, flag]) => {
// TODO: straighten out contextmetadata and it's selectors with a dedicated service
const flagAppContext = flag?.context?.[0];
const groupTypes = contextMetaData?.contextMetadata?.[flagAppContext]?.GROUP_TYPES ?? [];
const groupTypeSelectOptions = this.formatGroupTypes(groupTypes as string[]);
const listOptionTypes = [
{
value: LIST_OPTION_TYPE.SEGMENT,
viewValue: LIST_OPTION_TYPE.SEGMENT,
},
{
value: LIST_OPTION_TYPE.INDIVIDUAL,
viewValue: LIST_OPTION_TYPE.INDIVIDUAL,
},
...groupTypeSelectOptions,
];

return listOptionTypes;
})
);

formatGroupTypes(groupTypes: string[]): { value: string; viewValue: string }[] {
if (Array.isArray(groupTypes) && groupTypes.length > 0) {
return groupTypes.map((groupType) => {
return { value: groupType, viewValue: 'Group: "' + groupType + '"' };
});
} else {
return [];
}
}

convertNameStringToKey(name: string): string {
const upperCaseString = name.trim().toUpperCase();
const key = upperCaseString.replace(/ /g, '_');
return key;
}

fetchFeatureFlags(fromStarting?: boolean) {
this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting }));
Expand All @@ -152,7 +114,7 @@ export class FeatureFlagsService {
this.store$.dispatch(FeatureFlagsActions.actionAddFeatureFlag({ addFeatureFlagRequest }));
}

updateFeatureFlag(flag: FeatureFlag) {
updateFeatureFlag(flag: UpdateFeatureFlagRequest) {
this.store$.dispatch(FeatureFlagsActions.actionUpdateFeatureFlag({ flag }));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createAction, props } from '@ngrx/store';
import { AddFeatureFlagRequest, FeatureFlag, UpdateFeatureFlagStatusRequest } from './feature-flags.model';
import {
FeatureFlag,
UpdateFeatureFlagStatusRequest,
AddFeatureFlagRequest,
UpdateFeatureFlagRequest,
} from './feature-flags.model';
import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types';

export const actionFetchFeatureFlags = createAction(
Expand Down Expand Up @@ -48,7 +53,7 @@ export const actionDeleteFeatureFlagFailure = createAction('[Feature Flags] Dele

export const actionUpdateFeatureFlag = createAction(
'[Feature Flags] Update Feature Flag',
props<{ flag: FeatureFlag }>()
props<{ flag: UpdateFeatureFlagRequest }>()
);

export const actionUpdateFeatureFlagSuccess = createAction(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
import { AppState } from '../../core.state';
import { EntityState } from '@ngrx/entity';
import { FEATURE_FLAG_STATUS, FILTER_MODE, FLAG_SORT_KEY, SEGMENT_TYPE, SORT_AS_DIRECTION } from 'upgrade_types';
import { GroupForSegment, IndividualForSegment, Segment } from '../../segments/store/segments.model';
import { MemberTypes, Segment } from '../../segments/store/segments.model';

export interface FeatureFlag {
// This obviously is a more global type, but for now we're not about to refactor all of the things, so I'm just putting it here so I can create some more dev-friendly types to catch the small differences between some of these formats
export interface GeneralCRUDResponseFields {
createdAt: string;
updatedAt: string;
versionNumber: number;
id: string;
}

// Fields belonging to the FeatureFlag entity itself that are not part of the CRUD response
export interface BaseFeatureFlag {
id?: string;
name: string;
key: string;
description: string;
status: FEATURE_FLAG_STATUS;
filterMode: FILTER_MODE;
description?: string;
context: string[];
tags: string[];
featureFlagSegmentInclusion: PrivateSegment | EmptyPrivateSegment;
featureFlagSegmentExclusion: PrivateSegment | EmptyPrivateSegment;
status: FEATURE_FLAG_STATUS;
filterMode: FILTER_MODE;
featureFlagSegmentInclusion: FeatureFlagSegmentListDetails[];
featureFlagSegmentExclusion: FeatureFlagSegmentListDetails[];
}

export interface FeatureFlagsPaginationInfo {
nodes: FeatureFlag[];
total: number;
skip: number;
take: number;
// Feature Flag entity = base + db-generated fields (we depend on createdOn for sorting, for instance, but it's not truly part of the feature flag base)
export type FeatureFlag = BaseFeatureFlag & GeneralCRUDResponseFields;

// Currently there is no difference between these types, but they semantically different and could diverge later
export type AddFeatureFlagRequest = BaseFeatureFlag;

// so that we can throw an error if we try to update the id
export interface UpdateFeatureFlagRequest extends AddFeatureFlagRequest {
readonly id: string;
}

export interface AddFeatureFlagRequest {
name: string;
key: string;
description: string;
status: FEATURE_FLAG_STATUS;
context: string[];
tags: string[];
featureFlagSegmentInclusion: PrivateSegment | EmptyPrivateSegment;
featureFlagSegmentExclusion: PrivateSegment | EmptyPrivateSegment;
filterMode: FILTER_MODE;
export interface FeatureFlagSegmentListDetails {
segment: Segment;
featureFlag: FeatureFlag;
enabled: boolean;
listType: MemberTypes | string;
}

export interface UpdateFeatureFlagStatusRequest {
flagId: string;
status: FEATURE_FLAG_STATUS;
export enum UPSERT_FEATURE_FLAG_ACTION {
ADD = 'add',
EDIT = 'edit',
DUPLICATE = 'duplicate',
}

export interface UpsertFeatureFlagParams {
sourceFlag: FeatureFlag;
action: UPSERT_FEATURE_FLAG_ACTION;
}

export enum UPSERT_FEATURE_FLAG_LIST_ACTION {
Expand All @@ -49,13 +59,20 @@ export enum UPSERT_FEATURE_FLAG_LIST_ACTION {
}

export interface UpsertFeatureFlagListParams {
sourceList: any; // TODO define me
sourceList: FeatureFlagSegmentListDetails;
action: UPSERT_FEATURE_FLAG_LIST_ACTION;
}

export enum LIST_OPTION_TYPE {
INDIVIDUAL = 'Individual',
SEGMENT = 'Segment',
export interface FeatureFlagsPaginationInfo {
nodes: FeatureFlag[];
total: number;
skip: number;
take: number;
}

export interface UpdateFeatureFlagStatusRequest {
flagId: string;
status: FEATURE_FLAG_STATUS;
}

export interface FeatureFlagFormData {
Expand All @@ -76,8 +93,7 @@ export interface EmptyPrivateSegment {
};
}

export type AnySegmentType = Segment | PrivateSegment | EmptyPrivateSegment | GroupForSegment | IndividualForSegment;

// TODO: This should be probably be a part of env config
export const NUMBER_OF_FLAGS = 20;

interface IFeatureFlagsSearchParams {
Expand Down
Loading

0 comments on commit 7c3f819

Please sign in to comment.