diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts index dd9757c9e4..c5a6fed5fe 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts @@ -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 { @@ -18,8 +18,6 @@ export class FeatureFlagsDataService { fetchFeatureFlagsPaginated(params: FeatureFlagsPaginationParams): Observable { const url = this.environment.api.getPaginatedFlags; return this.http.post(url, params); - // mock - // // return of({ nodes: mockFeatureFlags, total: 2 }).pipe(delay(2000)); } fetchFeatureFlagById(id: string) { @@ -27,23 +25,23 @@ export class FeatureFlagsDataService { return this.http.get(url); } - addFeatureFlag(params: AddFeatureFlagRequest): Observable { - const url = this.environment.api.featureFlag; - return this.http.post(url, params); - } - updateFeatureFlagStatus(params: UpdateFeatureFlagStatusRequest): Observable { const url = this.environment.api.updateFlagStatus; return this.http.post(url, params); } - deleteFeatureFlag(id: string) { - const url = `${this.environment.api.featureFlag}/${id}`; - return this.http.delete(url); + addFeatureFlag(flag: AddFeatureFlagRequest): Observable { + const url = this.environment.api.featureFlag; + return this.http.post(url, flag); } - updateFeatureFlag(flag: FeatureFlag): Observable { + updateFeatureFlag(flag: UpdateFeatureFlagRequest): Observable { const url = `${this.environment.api.featureFlag}/${flag.id}`; return this.http.put(url, flag); } + + deleteFeatureFlag(id: string) { + const url = `${this.environment.api.featureFlag}/${id}`; + return this.http.delete(url); + } } 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 421f5e8eda..6542074908 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 @@ -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 { @@ -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)); @@ -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(), @@ -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), @@ -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 })); @@ -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 })); } diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts index da27abcadc..4cddf07568 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts @@ -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( @@ -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( 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 3ad85dcbc2..897471b221 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 @@ -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 { @@ -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 { @@ -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 { diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts index 896b1b6668..ab066d0e39 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.reducer.ts @@ -26,6 +26,7 @@ export const initialState: FeatureFlagState = adapter.getInitialState({ const reducer = createReducer( initialState, + // Feature Flags Fetching Actions on(FeatureFlagsActions.actionFetchFeatureFlags, (state) => ({ ...state, isLoadingFeatureFlags: true, @@ -43,6 +44,12 @@ const reducer = createReducer( }); }), on(FeatureFlagsActions.actionFetchFeatureFlagsFailure, (state) => ({ ...state, isLoadingFeatureFlags: false })), + + // Feature Flag Detail Actions + on(FeatureFlagsActions.actionFetchFeatureFlagById, (state) => ({ + ...state, + isLoadingSelectedFeatureFlag: true, + })), on(FeatureFlagsActions.actionFetchFeatureFlagByIdSuccess, (state, { flag }) => { return adapter.upsertOne(flag, { ...state, @@ -53,24 +60,23 @@ const reducer = createReducer( ...state, isLoadingSelectedFeatureFlag: false, })), - on(FeatureFlagsActions.actionSetIsLoadingFeatureFlags, (state, { isLoadingFeatureFlags }) => ({ + + // Feature Flag Upsert Actions (Add/Update both = upsert result) + on(FeatureFlagsActions.actionAddFeatureFlag, FeatureFlagsActions.actionUpdateFeatureFlag, (state) => ({ ...state, - isLoadingFeatureFlags, + isLoadingUpsertFeatureFlag: true, })), - on(FeatureFlagsActions.actionAddFeatureFlag, (state) => ({ ...state, isLoadingUpsertFeatureFlag: true })), - on(FeatureFlagsActions.actionAddFeatureFlagSuccess, (state, { response }) => { - return adapter.addOne(response, { - ...state, - isLoadingUpsertFeatureFlag: false, - }); - }), - on(FeatureFlagsActions.actionUpdateFeatureFlag, (state) => ({ ...state, isLoadingUpsertFeatureFlag: true })), - on(FeatureFlagsActions.actionUpdateFeatureFlagSuccess, (state, { response }) => { - return adapter.upsertOne(response, { - ...state, - isLoadingUpsertFeatureFlag: false, - }); - }), + on( + FeatureFlagsActions.actionUpdateFeatureFlagSuccess, + FeatureFlagsActions.actionAddFeatureFlagSuccess, + (state, { response }) => adapter.upsertOne(response, { ...state, isLoadingUpsertFeatureFlag: false }) + ), + on(FeatureFlagsActions.actionAddFeatureFlagFailure, FeatureFlagsActions.actionUpdateFeatureFlagFailure, (state) => ({ + ...state, + isLoadingUpsertFeatureFlag: false, + })), + + // Feature Flag Delete Actions on(FeatureFlagsActions.actionDeleteFeatureFlag, (state) => ({ ...state, isLoadingFeatureFlagDelete: true })), on(FeatureFlagsActions.actionDeleteFeatureFlagSuccess, (state, { flag }) => { return adapter.removeOne(flag.id, { @@ -82,17 +88,8 @@ const reducer = createReducer( ...state, isLoadingFeatureFlagDelete: false, })), - on(FeatureFlagsActions.actionUpdateFeatureFlagFailure, (state) => ({ ...state, isLoadingUpsertFeatureFlag: false })), - on(FeatureFlagsActions.actionAddFeatureFlagFailure, (state) => ({ ...state, isLoadingUpsertFeatureFlag: false })), - on(FeatureFlagsActions.actionSetSkipFlags, (state, { skipFlags }) => ({ ...state, skipFlags })), - on(FeatureFlagsActions.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })), - on(FeatureFlagsActions.actionSetSearchString, (state, { searchString }) => ({ ...state, searchValue: searchString })), - on(FeatureFlagsActions.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })), - on(FeatureFlagsActions.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })), - on(FeatureFlagsActions.actionSetActiveDetailsTabIndex, (state, { activeDetailsTabIndex }) => ({ - ...state, - activeDetailsTabIndex, - })), + + // Feature Flag Status Update Actions on(FeatureFlagsActions.actionUpdateFeatureFlagStatus, (state) => ({ ...state, isLoadingUpdateFeatureFlagStatus: true, @@ -108,9 +105,20 @@ const reducer = createReducer( ...state, isLoadingUpdateFeatureFlagStatus: true, })), - on(FeatureFlagsActions.actionFetchFeatureFlagById, (state) => ({ + + // UI State Update Actions + on(FeatureFlagsActions.actionSetIsLoadingFeatureFlags, (state, { isLoadingFeatureFlags }) => ({ ...state, - isLoadingSelectedFeatureFlag: true, + isLoadingFeatureFlags, + })), + on(FeatureFlagsActions.actionSetSkipFlags, (state, { skipFlags }) => ({ ...state, skipFlags })), + on(FeatureFlagsActions.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })), + on(FeatureFlagsActions.actionSetSearchString, (state, { searchString }) => ({ ...state, searchValue: searchString })), + on(FeatureFlagsActions.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })), + on(FeatureFlagsActions.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })), + on(FeatureFlagsActions.actionSetActiveDetailsTabIndex, (state, { activeDetailsTabIndex }) => ({ + ...state, + activeDetailsTabIndex, })) ); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts index fc5d8b9a1d..1dd299bb95 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts @@ -1,16 +1,10 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; -import { - EmptyPrivateSegment, - FLAG_SEARCH_KEY, - FeatureFlag, - FeatureFlagState, - ParticipantListTableRow, - PrivateSegment, -} from './feature-flags.model'; +import { FLAG_SEARCH_KEY, FeatureFlag, FeatureFlagState, ParticipantListTableRow } from './feature-flags.model'; import { selectRouterState } from '../../core.state'; import { selectAll } from './feature-flags.reducer'; -import { GroupForSegment, IndividualForSegment, Segment } from '../../segments/store/segments.model'; -import { FEATURE_FLAG_PARTICIPANT_LIST_KEY } from '../../../../../../../../types/src/Experiment/enums'; +import { MemberTypes } from '../../segments/store/segments.model'; +import { selectContextMetaData } from '../../experiments/store/experiments.selectors'; +import { CommonTextHelpersService } from '../../../shared/services/common-text-helpers.service'; export const selectFeatureFlagsState = createFeatureSelector('featureFlags'); @@ -27,6 +21,10 @@ export const selectAllFeatureFlagsSortedByDate = createSelector(selectAllFeature }); }); +export const selectAppContexts = createSelector(selectContextMetaData, (contextMetaData) => + Object.keys(contextMetaData?.contextMetadata ?? []) +); + export const selectHasInitialFeatureFlagsDataLoaded = createSelector( selectFeatureFlagsState, (state) => state.hasInitialFeatureFlagsDataLoaded @@ -125,68 +123,38 @@ export const selectIsLoadingFeatureFlagDelete = createSelector( (state) => state.isLoadingFeatureFlagDelete ); +// TODO: will need reimplementation in the list table stories export const selectFeatureFlagInclusions = createSelector( selectSelectedFeatureFlag, - (featureFlag: FeatureFlag): ParticipantListTableRow[] => - mapToParticipantTableRowStructure(featureFlag, FEATURE_FLAG_PARTICIPANT_LIST_KEY.INCLUDE) + (featureFlag: FeatureFlag): ParticipantListTableRow[] => [] + // mapToParticipantTableRowStructure(featureFlag, FEATURE_FLAG_PARTICIPANT_LIST_KEY.INCLUDE) ); export const selectFeatureFlagExclusions = createSelector( selectSelectedFeatureFlag, - (featureFlag: FeatureFlag): ParticipantListTableRow[] => - mapToParticipantTableRowStructure(featureFlag, FEATURE_FLAG_PARTICIPANT_LIST_KEY.EXCLUDE) + (featureFlag: FeatureFlag): ParticipantListTableRow[] => [] + // mapToParticipantTableRowStructure(featureFlag, FEATURE_FLAG_PARTICIPANT_LIST_KEY.EXCLUDE) ); -// TODO: can we get rid of this ui discovery work and have the backend do it? -function mapToParticipantTableRowStructure( - featureFlag: FeatureFlag, - key: FEATURE_FLAG_PARTICIPANT_LIST_KEY.INCLUDE | FEATURE_FLAG_PARTICIPANT_LIST_KEY.EXCLUDE -): ParticipantListTableRow[] { - const privateSegment: PrivateSegment | EmptyPrivateSegment = featureFlag?.[key]; - - if (!privateSegment) return []; - - // make sure this is not an empty private segment - if ('groupForSegment' in privateSegment.segment) { - const groups: GroupForSegment[] = privateSegment.segment.groupForSegment || []; - const subSegments: Segment[] = privateSegment.segment.subSegments || []; - const individuals: IndividualForSegment[] = privateSegment.segment.individualForSegment || []; - - const groupsRow = groups.map(mapGroupToRow); - const subSegmentsRow = subSegments.map(mapSubSegmentToRow); - const individualsRow = individuals.map(mapIndividualToRow); - - const allSegments: ParticipantListTableRow[] = [...groupsRow, ...subSegmentsRow, ...individualsRow]; - - return allSegments; +export const selectFeatureFlagListTypeOptions = createSelector( + selectContextMetaData, + selectSelectedFeatureFlag, + (contextMetaData, flag) => { + const flagAppContext = flag?.context?.[0]; + const groupTypes = contextMetaData?.contextMetadata?.[flagAppContext]?.GROUP_TYPES ?? []; + const groupTypeSelectOptions = CommonTextHelpersService.formatGroupTypes(groupTypes as string[]); + const listOptionTypes = [ + { + value: MemberTypes.SEGMENT, + viewValue: MemberTypes.SEGMENT, + }, + { + value: MemberTypes.INDIVIDUAL, + viewValue: MemberTypes.INDIVIDUAL, + }, + ...groupTypeSelectOptions, + ]; + + return listOptionTypes; } - - return []; -} - -function mapGroupToRow(group: GroupForSegment): ParticipantListTableRow { - return { - name: group.groupId, - type: group.type + ' (group)', - values: '?', - status: '?', - }; -} - -function mapSubSegmentToRow(subSegment: Segment): ParticipantListTableRow { - return { - name: subSegment.id, - type: 'Segment', - values: '?', - status: '?', - }; -} - -function mapIndividualToRow(individual: IndividualForSegment): ParticipantListTableRow { - return { - name: individual.userId, - type: 'Individual', - values: '?', - status: '?', - }; -} +); diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts index ddc3fc9a6c..8f80a0aa39 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts @@ -53,9 +53,12 @@ export interface experimentSegmentInclusionExclusionData { }; } -export interface GroupForSegment { +export interface Group { groupId: string; type: string; +} + +export interface GroupForSegment extends Group { segmentId: string; } @@ -93,7 +96,7 @@ export interface SegmentInput { context: string; description: string; userIds: string[]; - groups: { groupId: string; type: string }[]; + groups: Group[]; subSegmentIds: string[]; type: SEGMENT_TYPE; } 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 deleted file mode 100644 index d743ce3824..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
- -
- - Name - - {{ 'feature-flags.add-flag-modal.name-hint.text' | translate }} - - - - Key - - {{ 'feature-flags.add-flag-modal.key-hint.text' | translate - }}Learn more - - - - Description (optional) - - - - - App Context - - {{ context }} - - {{ 'feature-flags.add-flag-modal.app-context-hint.text' | translate - }}Learn more - - - -
-
-
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.scss b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.scss deleted file mode 100644 index 12f2b8238a..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -:host ::ng-deep .add-feature-flag-modal-container { - display: flex; - flex-direction: column; - align-items: space-between; - width: 100%; - max-height: 700px; -} - -.title { - font-size: 18px; - font-weight: bold; -} - -.mat-mdc-form-field { - width: 100%; -} - -.mat-mdc-form-field:not(:last-child) { - margin-bottom: 20px; -} - -.form-hint { - color: var(--grey-5); -} - -.learn-more-link { - color: var(--blue); - text-decoration: none; - - &:hover { - text-decoration: underline; - } -} \ No newline at end of file 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 deleted file mode 100644 index 6ad0cd4d42..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/modals/add-feature-flag-modal/add-feature-flag-modal.component.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; -import { - CommonModalComponent, - CommonTagsInputComponent, -} from '../../../../../shared-standalone-component-lib/components'; -import { - MAT_DIALOG_DATA, - MatDialog, - MatDialogActions, - MatDialogClose, - MatDialogContent, - MatDialogRef, - MatDialogTitle, -} from '@angular/material/dialog'; -import { CommonModalConfig } from '../../../../../shared-standalone-component-lib/components/common-modal/common-modal-config'; -import { CommonModule, NgTemplateOutlet } from '@angular/common'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIcon } from '@angular/material/icon'; -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, 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'; - -@Component({ - selector: 'app-add-feature-flag-modal', - standalone: true, - imports: [ - CommonModalComponent, - MatCardModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatDialogTitle, - MatDialogContent, - MatDialogActions, - MatDialogClose, - MatSelectModule, - CommonModule, - NgTemplateOutlet, - MatIcon, - ReactiveFormsModule, - TranslateModule, - CommonTagsInputComponent, - ], - templateUrl: './add-feature-flag-modal.component.html', - styleUrl: './add-feature-flag-modal.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AddFeatureFlagModalComponent { - isLoadingUpsertFeatureFlag$ = this.featureFlagsService.isLoadingUpsertFeatureFlag$; - appContexts$ = this.featureFlagsService.appContexts$; - featureFlagsListLengthChange$ = this.featureFlagsService.featureFlagsListLengthChange$; - subscriptions = new Subscription(); - - featureFlagForm: FormGroup; - - constructor( - @Inject(MAT_DIALOG_DATA) - public config: CommonModalConfig, - public dialog: MatDialog, - private formBuilder: FormBuilder, - private featureFlagsService: FeatureFlagsService, - private experimentService: ExperimentService, - private formHelpersService: CommonFormHelpersService, - public dialogRef: MatDialogRef - ) {} - - ngOnInit(): void { - this.experimentService.fetchContextMetaData(); - this.buildForm(); - this.listenForFeatureFlagListLengthChanges(); - this.listenOnNameChangesToUpdateKey(); - } - - buildForm(): void { - this.featureFlagForm = this.formBuilder.group({ - name: ['', Validators.required], - key: ['', Validators.required], - description: [''], - appContext: ['', Validators.required], - tags: [[]], - }); - } - - // Close the modal once the feature flag list length changes, as that indicates actual success - listenForFeatureFlagListLengthChanges(): void { - 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? - this.createAddFeatureFlagRequest(); - } else { - // If the form is invalid, manually mark all form controls as touched - this.formHelpersService.triggerTouchedToDisplayErrors(this.featureFlagForm); - } - } - - createAddFeatureFlagRequest(): void { - // temporarily use any until tags feature is added - const { name, key, description, appContext, tags }: FeatureFlagFormData = this.featureFlagForm.value; - - const addFeatureFlagRequest: AddFeatureFlagRequest = { - name, - key, - description, - status: FEATURE_FLAG_STATUS.DISABLED, - context: [appContext], - tags: tags, // it is now an array of strings - featureFlagSegmentInclusion: { - segment: { - type: SEGMENT_TYPE.PRIVATE, - }, - }, - featureFlagSegmentExclusion: { - segment: { - type: SEGMENT_TYPE.PRIVATE, - }, - }, - filterMode: FILTER_MODE.INCLUDE_ALL, - }; - - this.featureFlagsService.addFeatureFlag(addFeatureFlagRequest); - } - - closeModal() { - this.dialogRef.close(); - } - - ngOnDestroy() { - this.subscriptions.unsubscribe(); - } -} 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 8c8e9e9f76..89ef0dfe9f 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 @@ -1,33 +1,31 @@ -