diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index 7b98bf388829..1012667780b0 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -146,6 +146,7 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { segmentOverrides, changeRequest, commit, + mode, ) { Dispatcher.handleViewAction({ actionType: Actions.EDIT_ENVIRONMENT_FLAG_CHANGE_REQUEST, @@ -154,6 +155,7 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { environmentFlag, environmentId, flag, + mode, projectFlag, projectId, segmentOverrides, diff --git a/frontend/common/providers/FeatureListProvider.js b/frontend/common/providers/FeatureListProvider.js index dabc92859d3d..038c8f47d990 100644 --- a/frontend/common/providers/FeatureListProvider.js +++ b/frontend/common/providers/FeatureListProvider.js @@ -31,8 +31,8 @@ const FeatureListProvider = class extends React.Component { }) }) - this.listenTo(FeatureListStore, 'saved', (isCreate) => { - this.props.onSave && this.props.onSave(isCreate) + this.listenTo(FeatureListStore, 'saved', (data) => { + this.props.onSave && this.props.onSave(data) }) this.listenTo(FeatureListStore, 'problem', () => { @@ -174,7 +174,7 @@ const FeatureListProvider = class extends React.Component { }), () => { FeatureListStore.isSaving = false - FeatureListStore.trigger('saved') + FeatureListStore.trigger('saved', {}) FeatureListStore.trigger('change') }, ) @@ -189,6 +189,7 @@ const FeatureListProvider = class extends React.Component { segmentOverrides, changeRequest, commit, + mode, ) => { AppActions.editFeatureMv( projectId, @@ -220,6 +221,7 @@ const FeatureListProvider = class extends React.Component { segmentOverrides, changeRequest, commit, + mode, ) }, ) diff --git a/frontend/common/services/useFeatureSegment.ts b/frontend/common/services/useFeatureSegment.ts index 15173421e3a1..6e98284bc5ba 100644 --- a/frontend/common/services/useFeatureSegment.ts +++ b/frontend/common/services/useFeatureSegment.ts @@ -17,6 +17,15 @@ export const featureSegmentService = service url: `features/feature-segments/${query.id}/`, }), }), + getFeatureSegment: builder.query< + Res['featureSegment'], + Req['getFeatureSegment'] + >({ + providesTags: (res) => [{ id: res?.id, type: 'FeatureSegment' }], + query: (query: Req['getFeatureSegment']) => ({ + url: `features/feature-segments/${query.id}/`, + }), + }), // END OF ENDPOINTS }), }) @@ -35,10 +44,22 @@ export async function deleteFeatureSegment( ), ) } +export async function getFeatureSegment( + store: any, + data: Req['getFeatureSegment'], + options?: Parameters< + typeof featureSegmentService.endpoints.getFeatureSegment.initiate + >[1], +) { + return store.dispatch( + featureSegmentService.endpoints.getFeatureSegment.initiate(data, options), + ) +} // END OF FUNCTION_EXPORTS export const { useDeleteFeatureSegmentMutation, + useGetFeatureSegmentQuery, // END OF EXPORTS } = featureSegmentService diff --git a/frontend/common/services/useFeatureState.ts b/frontend/common/services/useFeatureState.ts new file mode 100644 index 000000000000..dfa1cbcfe115 --- /dev/null +++ b/frontend/common/services/useFeatureState.ts @@ -0,0 +1,79 @@ +import { FeatureState, PagedResponse, Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' +import Utils from 'common/utils/utils' +import { getFeatureSegment } from './useFeatureSegment' +import { getStore } from 'common/store' +const _data = require('../data/base/_data') +export const addFeatureSegmentsToFeatureStates = async (v) => { + if (!v.feature_segment) { + return v + } + const featureSegmentData = await getFeatureSegment(getStore(), { + id: v.feature_segment, + }) + return { + ...v, + feature_segment: featureSegmentData.data, + } +} +export const featureStateService = service + .enhanceEndpoints({ addTagTypes: ['FeatureState'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + getFeatureStates: builder.query< + Res['featureStates'], + Req['getFeatureStates'] + >({ + providesTags: [{ id: 'LIST', type: 'FeatureState' }], + queryFn: async (query, baseQueryApi, extraOptions, baseQuery) => { + //This endpoint returns feature_segments as a number, so it fetches the feature segments and appends + const { + data, + }: { + data: PagedResponse< + Omit & { + feature_segment: number | null + } + > + } = await baseQuery({ + url: `features/featurestates/?${Utils.toParam(query)}`, + }) + const results = await Promise.all( + data.results.map(addFeatureSegmentsToFeatureStates), + ) + return { + data: { + ...data, + results, + }, + } + }, + }), + // END OF ENDPOINTS + }), + }) + +export async function getFeatureStates( + store: any, + data: Req['getFeatureStates'], + options?: Parameters< + typeof featureStateService.endpoints.getFeatureStates.initiate + >[1], +) { + return store.dispatch( + featureStateService.endpoints.getFeatureStates.initiate(data, options), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useGetFeatureStatesQuery, + // END OF EXPORTS +} = featureStateService + +/* Usage examples: +const { data, isLoading } = useGetFeatureStatesQuery({ id: 2 }, {}) //get hook +const [createFeatureStates, { isLoading, data, isSuccess }] = useCreateFeatureStatesMutation() //create hook +featureStateService.endpoints.getFeatureStates.select({id: 2})(store.getState()) //access data from any function +*/ diff --git a/frontend/common/services/useFeatureVersion.ts b/frontend/common/services/useFeatureVersion.ts index 625d93a29fda..2950ee378ee5 100644 --- a/frontend/common/services/useFeatureVersion.ts +++ b/frontend/common/services/useFeatureVersion.ts @@ -10,6 +10,7 @@ import { import { deleteFeatureSegment } from './useFeatureSegment' import transformCorePaging from 'common/transformCorePaging' import Utils from 'common/utils/utils' +import { updateSegmentPriorities } from './useSegmentPriority' export const featureVersionService = service .enhanceEndpoints({ addTagTypes: ['FeatureVersion'] }) @@ -27,6 +28,7 @@ export const featureVersionService = service environmentId: query.environmentId, featureId: query.featureId, }) + // Step 2: Get the feature states for the live version const currentFeatureStates: { data: FeatureState[] } = await getVersionFeatureState(getStore(), { @@ -34,9 +36,10 @@ export const featureVersionService = service featureId: query.featureId, sha: versionRes.data.uuid, }) - const res = await Promise.all( + + // Step 3: update, create or delete feature states from the new version + const res: { data: FeatureState }[] = await Promise.all( query.featureStates.map((featureState) => { - // Step 3: update, create or delete feature states from the new version const matchingVersionState = currentFeatureStates.data.find( (feature) => { return ( @@ -110,6 +113,25 @@ export const featureVersionService = service } }), ) + + //Step 4: Update feature segment priorities before saving feature states + const prioritiesToUpdate = query.featureStates + .filter((v) => !v.toRemove && !!v.feature_segment) + .map((v) => { + const matchingFeatureSegment = res?.find( + (currentFeatureState) => + v.feature_segment?.segment === + currentFeatureState.data.feature_segment?.segment, + ) + return { + id: matchingFeatureSegment!.data.feature_segment!.id!, + priority: v.feature_segment!.priority, + } + }) + if (prioritiesToUpdate.length) { + await updateSegmentPriorities(getStore(), prioritiesToUpdate) + } + const ret = { data: res.map((item) => ({ ...item, @@ -117,7 +139,8 @@ export const featureVersionService = service })), error: res.find((v) => !!v.error)?.error, } - // Step 4: Publish the feature version + + // Step 5: Publish the feature version if (!query.skipPublish) { await publishFeatureVersion(getStore(), { environmentId: query.environmentId, diff --git a/frontend/common/stores/change-requests-store.js b/frontend/common/stores/change-requests-store.js index 73bb46265c0c..b9c4aaf31b8d 100644 --- a/frontend/common/stores/change-requests-store.js +++ b/frontend/common/stores/change-requests-store.js @@ -2,20 +2,31 @@ const Dispatcher = require('../dispatcher/dispatcher') const BaseStore = require('./base/_store') const data = require('../data/base/_data') const { flatten } = require('lodash') +const { + addFeatureSegmentsToFeatureStates, +} = require('../services/useFeatureState') const PAGE_SIZE = 20 -const transformChangeRequest = (changeRequest) => { - if (changeRequest?.environment_feature_versions?.length) { - return { - ...changeRequest, - feature_states: flatten( - changeRequest.environment_feature_versions.map( - (featureVersion) => featureVersion.feature_states, +const transformChangeRequest = async (changeRequest) => { + const res = changeRequest?.environment_feature_versions?.length + ? { + ...changeRequest, + feature_states: flatten( + changeRequest.environment_feature_versions.map( + (featureVersion) => featureVersion.feature_states, + ), ), - ), - } + } + : changeRequest + + const feature_states = await Promise.all( + res.feature_states.map(addFeatureSegmentsToFeatureStates), + ) + + return { + ...res, + feature_states, } - return changeRequest } const controller = { actionChangeRequest: (id, action, cb) => { @@ -25,8 +36,8 @@ const controller = { .then(() => { data .get(`${Project.api}features/workflows/change-requests/${id}/`) - .then((res) => { - store.model[id] = transformChangeRequest(res) + .then(async (res) => { + store.model[id] = await transformChangeRequest(res) cb && cb() store.loaded() }) @@ -47,8 +58,8 @@ const controller = { store.loading() data .get(`${Project.api}features/workflows/change-requests/${id}/`) - .then((apiResponse) => { - const res = transformChangeRequest(apiResponse) + .then(async (apiResponse) => { + const res = await transformChangeRequest(apiResponse) return Promise.all([ data.get( `${Project.api}environments/${environmentId}/featurestates/?feature=${res.feature_states[0].feature}`, @@ -114,15 +125,32 @@ const controller = { updateChangeRequest: (changeRequest) => { store.loading() data - .put( + .get( `${Project.api}features/workflows/change-requests/${changeRequest.id}/`, - changeRequest, ) .then((res) => { - store.model[changeRequest.id] = transformChangeRequest(res) - store.loaded() + data + .put( + `${Project.api}features/workflows/change-requests/${changeRequest.id}/`, + { + ...res, + approvals: changeRequest.approvals, + description: changeRequest.description, + environment_feature_versions: + changeRequest?.environment_feature_versions?.map((v) => v.uuid), + group_assignments: changeRequest.group_assignments, + title: changeRequest.title, + }, + ) + .then(async () => { + const res = await data.get( + `${Project.api}features/workflows/change-requests/${changeRequest.id}/`, + ) + store.model[changeRequest.id] = await transformChangeRequest(res) + store.loaded() + }) + .catch((e) => API.ajaxHandler(store, e)) }) - .catch((e) => API.ajaxHandler(store, e)) }, } diff --git a/frontend/common/stores/feature-list-store.js b/frontend/common/stores/feature-list-store.ts similarity index 93% rename from frontend/common/stores/feature-list-store.js rename to frontend/common/stores/feature-list-store.ts index 392bdc3b7191..291f5be5a550 100644 --- a/frontend/common/stores/feature-list-store.js +++ b/frontend/common/stores/feature-list-store.ts @@ -4,13 +4,25 @@ import ProjectStore from './project-store' import { createAndSetFeatureVersion } from 'common/services/useFeatureVersion' import { updateSegmentPriorities } from 'common/services/useSegmentPriority' import OrganisationStore from './organisation-store' - +import { + Approval, + Environment, + FeatureState, + MultivariateOption, + ProjectFlag, +} from 'common/types/responses' +import Utils from 'common/utils/utils' +import Actions from 'common/dispatcher/action-constants' +import Project from 'common/project' const Dispatcher = require('common/dispatcher/dispatcher') const BaseStore = require('./base/_store') const data = require('../data/base/_data') const { createSegmentOverride } = require('../services/useSegmentOverride') const { getStore } = require('../store') - +import flagsmith from 'flagsmith' +import API from 'project/api' +import segmentOverrides from 'components/SegmentOverrides' +import { Req } from 'common/types/requests' let createdFirstFeature = false const PAGE_SIZE = 200 function recursivePageGet(url, parentRes) { @@ -30,6 +42,26 @@ function recursivePageGet(url, parentRes) { return Promise.resolve(response) }) } + +const convertSegmentOverrideToFeatureState = ( + override, + i, + changeRequest?: Req['createChangeRequest'], +) => { + return { + enabled: override.enabled, + feature_segment: { + id: override.id, + priority: i, + segment: override.segment, + uuid: override.uuid, + }, + feature_state_value: override.value, + id: override.id, + live_from: changeRequest?.live_from, + toRemove: override.toRemove, + } as Partial +} const controller = { createFlag(projectId, environmentId, flag) { store.saving() @@ -95,7 +127,7 @@ const controller = { _.keyBy(environmentFeatures.results, 'feature'), } store.model.lastSaved = new Date().valueOf() - store.saved(flag.name) + store.saved({ createdFlag: flag.name }) }), ) .catch((e) => API.ajaxHandler(store, e)) @@ -319,7 +351,6 @@ const controller = { Object.assign({}, environmentFlag, { enabled: flag.default_enabled, feature_state_value: flag.initial_value, - hide_from_client: flag.hide_from_client, }), ) }) @@ -399,33 +430,41 @@ const controller = { store.model.lastSaved = new Date().valueOf() } onComplete && onComplete() - store.saved() + store.saved({}) }) }) }, editFeatureStateChangeRequest: async ( - projectId, - environmentId, - flag, - projectFlag, - environmentFlag, - segmentOverrides, - changeRequest, - commit, + projectId: string, + environmentId: string, + flag: ProjectFlag, + projectFlag: ProjectFlag, + environmentFlag: FeatureState, + segmentOverrides: any, + changeRequest: Req['createChangeRequest'], + commit: boolean, + mode: 'VALUE' | 'SEGMENT', ) => { store.saving() API.trackEvent(Constants.events.EDIT_FEATURE) - const env = ProjectStore.getEnvironment(environmentId) + const env: Environment = ProjectStore.getEnvironment(environmentId) as any let environment_feature_versions = [] if (env.use_v2_feature_versioning) { - const featureStates = [ - Object.assign({}, environmentFlag, { - enabled: flag.default_enabled, - feature_state_value: flag.initial_value, - hide_from_client: flag.hide_from_client, - live_from: flag.live_from, - }), - ] + let featureStates + if (mode === 'SEGMENT') { + featureStates = segmentOverrides?.map((override: any, i: number) => + convertSegmentOverrideToFeatureState(override, i, changeRequest), + ) + } else { + featureStates = [ + Object.assign({}, environmentFlag, { + enabled: flag.default_enabled, + feature_state_value: flag.initial_value, + live_from: flag.live_from, + }), + ] + } + const version = await createAndSetFeatureVersion(getStore(), { environmentId: env.id, featureId: projectFlag.id, @@ -456,7 +495,7 @@ const controller = { return keys.includes('group') }) - let req = { + const req = { approvals: userApprovals, environment_feature_versions, feature_states: !env.use_v2_feature_versioning @@ -533,10 +572,7 @@ const controller = { }) Promise.all([prom]).then(() => { - store.saved() - if (typeof closeModal !== 'undefined') { - closeModal() - } + store.saved({ changeRequest: true, isCreate: true }) }) }, editVersionedFeatureState: ( @@ -553,21 +589,9 @@ const controller = { if (mode !== 'VALUE') { // Create a new version with segment overrides - const featureStates = segmentOverrides?.map((override, i) => { - return { - enabled: override.enabled, - feature_segment: { - id: override.id, - priority: i, - segment: override.segment, - uuid: override.uuid, - }, - feature_state_value: override.value, - hide_from_client: flag.hide_from_client, - id: override.id, - toRemove: override.toRemove, - } - }) + const featureStates = segmentOverrides?.map( + convertSegmentOverrideToFeatureState, + ) prom = ProjectStore.getEnvironmentIdFromKeyAsync( projectId, environmentId, @@ -614,7 +638,6 @@ const controller = { const data = Object.assign({}, environmentFlag, { enabled: flag.default_enabled, feature_state_value: flag.initial_value, - hide_from_client: flag.hide_from_client, }) return createAndSetFeatureVersion(getStore(), { environmentId: res, @@ -650,7 +673,7 @@ const controller = { store.model.lastSaved = new Date().valueOf() } onComplete && onComplete() - store.saved() + store.saved({}) }) }, getFeatureUsage(projectId, environmentId, flag, period) { @@ -800,7 +823,7 @@ const controller = { (f) => f.id !== flag.id, ) store.model.lastSaved = new Date().valueOf() - store.saved() + store.saved({}) }) }, searchFeatures: _.throttle( @@ -898,12 +921,7 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { ) break case Actions.CREATE_FLAG: - controller.createFlag( - action.projectId, - action.environmentId, - action.flag, - action.segmentOverrides, - ) + controller.createFlag(action.projectId, action.environmentId, action.flag) break case Actions.EDIT_ENVIRONMENT_FLAG: controller.editFeatureState( @@ -927,6 +945,7 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { action.segmentOverrides, action.changeRequest, action.commit, + action.mode, ) break case Actions.EDIT_FEATURE: diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 242f0c8bfbfc..46162d98c1dd 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -4,6 +4,9 @@ import { FeatureState, FeatureStateValue, ImportStrategy, + APIKey, + Approval, + MultivariateOption, Segment, Tag, UserGroup, @@ -17,7 +20,7 @@ export type PagedRequest = T & { export type OAuthType = 'github' | 'saml' | 'google' export type PermissionLevel = 'organisation' | 'project' | 'environment' export type CreateVersionFeatureState = { - environmentId: string + environmentId: number featureId: number sha: string featureState: FeatureState @@ -247,18 +250,27 @@ export type Req = { getGroupWithRole: { org_id: number; group_id: number } deleteGroupWithRole: { org_id: number; group_id: number; role_id: number } createAndSetFeatureVersion: { - environmentId: string + environmentId: number featureId: number skipPublish?: boolean - featureStates: (FeatureState & { toRemove: boolean })[] + featureStates: Pick< + FeatureState, + | 'enabled' + | 'feature_segment' + | 'uuid' + | 'feature_state_value' + | 'id' + | 'toRemove' + | 'multivariate_feature_state_values' + >[] } createFeatureVersion: { - environmentId: string + environmentId: number featureId: number } publishFeatureVersion: { sha: string - environmentId: string + environmentId: number featureId: number } createVersionFeatureState: CreateVersionFeatureState @@ -269,7 +281,7 @@ export type Req = { } getVersionFeatureState: { sha: string - environmentId: string + environmentId: number featureId: number } updateSegmentPriorities: { id: number; priority: number }[] @@ -397,5 +409,17 @@ export type Req = { usersToRemoveAdmin: number[] | null usersToRemove: number[] | null } + createChangeRequest: { + approvals: Approval[] + live_from: string | undefined + description: string + multivariate_options: MultivariateOption[] + title: string + } + getFeatureStates: { + environment?: number + feature?: number + } + getFeatureSegment: { id: string } // END OF TYPES } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index b224ccd18d1f..62c010a3ba6b 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -4,6 +4,13 @@ export type EdgePagedResponse = PagedResponse & { last_evaluated_key?: string pages?: (string | undefined)[] } +export type Approval = + | { + user: number + } + | { + group: number + } export type PagedResponse = { count?: number next?: string @@ -488,7 +495,6 @@ export type FeatureState = { environment_feature_version: string version?: number live_from?: string - hide_from_client?: string feature: number environment: number feature_segment?: { @@ -498,6 +504,8 @@ export type FeatureState = { uuid: string } change_request?: number + //Added by FE + toRemove?: boolean } export type ProjectFlag = { @@ -594,6 +602,31 @@ export type RolePermissionGroup = { id: number role_name: string } +export type ChangeRequest = { + id: number + created_at: string + updated_at: string + environment: number + title: string + description: string | number + feature_states: FeatureState[] + user: number + committed_at: number | null + committed_by: number | null + deleted_at: null + approvals: { + id: number + user: number + approved_at: null | string + }[] + is_approved: boolean + is_committed: boolean + group_assignments: { group: number }[] + environment_feature_versions: { + uuid: string + feature_states: FeatureState[] + }[] +} export type FeatureVersion = { created_at: string updated_at: string @@ -697,7 +730,7 @@ export type Res = { githubPulls: PullRequest[] githubRepos: GithubPaginatedRepos segmentPriorities: {} - featureSegment: { id: string } + featureSegment: FeatureState['feature_segment'] featureVersions: PagedResponse users: User[] enableFeatureVersioning: { id: string } @@ -710,5 +743,6 @@ export type Res = { userGroupPermissions: GroupPermission[] identityFeatureStates: PagedResponse cloneidentityFeatureStates: IdentityFeatureState + featureStates: PagedResponse // END OF TYPES } diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index fa89927cdf73..d3bf10a545fd 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -169,7 +169,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { description: projectFlag.description, enabled: false, feature_state_value: projectFlag.initial_value, - hide_from_client: false, is_archived: projectFlag.is_archived, is_server_key_only: projectFlag.is_server_key_only, multivariate_options: projectFlag.multivariate_options, @@ -183,7 +182,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { description: projectFlag.description, enabled: identityFlag.enabled, feature_state_value: identityFlag.feature_state_value, - hide_from_client: environmentFlag.hide_from_client, is_archived: projectFlag.is_archived, is_server_key_only: projectFlag.is_server_key_only, multivariate_options: projectFlag.multivariate_options, @@ -195,7 +193,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { description: projectFlag.description, enabled: environmentFlag.enabled, feature_state_value: environmentFlag.feature_state_value, - hide_from_client: environmentFlag.hide_from_client, is_archived: projectFlag.is_archived, is_server_key_only: projectFlag.is_server_key_only, multivariate_options: projectFlag.multivariate_options.map((v) => { diff --git a/frontend/web/components/Feature.js b/frontend/web/components/Feature.js index 4aecb868daaa..aa883c053e38 100644 --- a/frontend/web/components/Feature.js +++ b/frontend/web/components/Feature.js @@ -35,7 +35,6 @@ export default class Feature extends PureComponent { environmentFlag, environmentVariations, error, - hide_from_client, identity, isEdit, multivariate_options, @@ -47,7 +46,6 @@ export default class Feature extends PureComponent { } = this.props const enabledString = isEdit ? 'Enabled' : 'Enabled by default' - const disabled = hide_from_client const controlPercentage = Utils.calculateControl(multivariate_options) const valueString = identity ? 'User override' @@ -69,8 +67,8 @@ export default class Feature extends PureComponent { @@ -98,7 +96,7 @@ export default class Feature extends PureComponent { typeof value === 'undefined' || value === null ? '' : value }`} onChange={onValueChange} - disabled={hide_from_client || readOnly} + disabled={readOnly} placeholder="e.g. 'big' " /> } diff --git a/frontend/web/components/diff/DiffChangeRequest.tsx b/frontend/web/components/diff/DiffChangeRequest.tsx new file mode 100644 index 000000000000..58a91a2393da --- /dev/null +++ b/frontend/web/components/diff/DiffChangeRequest.tsx @@ -0,0 +1,47 @@ +import { FC } from 'react' +import { ChangeRequest, ChangeRequestSummary } from 'common/types/responses' +import { useGetFeatureStatesQuery } from 'common/services/useFeatureState' +import DiffFeature from './DiffFeature' + +type DiffChangeRequestType = { + changeRequest: ChangeRequest | null + feature: number + projectId: string +} + +const DiffChangeRequest: FC = ({ + changeRequest, + feature, + projectId, +}) => { + const { data, isLoading } = useGetFeatureStatesQuery( + { + environment: changeRequest?.environment, + feature, + }, + { refetchOnMountOrArgChange: true, skip: !changeRequest }, + ) + if (!changeRequest) { + return null + } + + if (isLoading) { + return ( +
+ +
+ ) + } + return ( +
+ +
+ ) +} + +export default DiffChangeRequest diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx index 6d5e1877fd99..858ece306519 100644 --- a/frontend/web/components/diff/DiffFeature.tsx +++ b/frontend/web/components/diff/DiffFeature.tsx @@ -86,6 +86,11 @@ const DiffFeature: FC = ({ } > + {!totalChanges && ( +
+ No Changes Found +
+ )}
diff --git a/frontend/web/components/diff/DiffSegments.tsx b/frontend/web/components/diff/DiffSegments.tsx index 0f9d1405c0c6..39fd2727172f 100644 --- a/frontend/web/components/diff/DiffSegments.tsx +++ b/frontend/web/components/diff/DiffSegments.tsx @@ -11,11 +11,14 @@ type DiffSegment = { diff: TDiffSegment } -const widths = [80, 105] +const widths = [200, 80, 105] const DiffSegment: FC = ({ diff }) => { return (
-
+
+ {diff.segment?.name} +
+
= ({ diff }) => { newValue={diff.deleted ? diff.oldValue : diff.newValue} />
-
+
= ({ diffs }) => { const tableHeader = (
+ Segment +
+
Priority
Value
-
+
Enabled
diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index b6e476f3dcf9..9ad6e4d1e101 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -52,7 +52,6 @@ const CreateFlag = class extends Component { description, enabled, feature_state_value, - hide_from_client, is_archived, is_server_key_only, multivariate_options, @@ -82,7 +81,6 @@ const CreateFlag = class extends Component { externalResources: [], githubId: '', hasIntegrationWithGithub: false, - hide_from_client, identityVariations: this.props.identityFlag && this.props.identityFlag.multivariate_feature_state_values @@ -272,7 +270,6 @@ const CreateFlag = class extends Component { default_enabled, description, environmentFlag, - hide_from_client, initial_value, is_archived, is_server_key_only, @@ -313,7 +310,6 @@ const CreateFlag = class extends Component { { default_enabled, description, - hide_from_client, initial_value, is_archived, is_server_key_only, @@ -512,7 +508,6 @@ const CreateFlag = class extends Component { featureExternalResource, githubId, hasIntegrationWithGithub, - hide_from_client, initial_value, isEdit, multivariate_options, @@ -527,12 +522,11 @@ const CreateFlag = class extends Component { const Provider = identity ? IdentityProvider : FeatureListProvider const environmentVariations = this.props.environmentVariations const environment = ProjectStore.getEnvironment(this.props.environmentId) + const isVersioned = !!environment?.use_v2_feature_versioning const is4Eyes = !!environment && Utils.changeRequestsEnabled(environment.minimum_change_request_approvals) const canSchedule = Utils.getPlansPermission('SCHEDULE_FLAGS') - const is4EyesSegmentOverrides = - is4Eyes && Utils.getFlagsmithHasFeature('4eyes_segment_overrides') // const project = ProjectStore.model const caseSensitive = project?.only_allow_lower_case_feature_names const regex = project?.feature_name_regex @@ -773,32 +767,6 @@ const CreateFlag = class extends Component { )} - - {!identity && Utils.getFlagsmithHasFeature('hide_flag') && ( - - - - this.setState({ hide_from_client }) - } - className='ml-0' - /> - - Hide from SDKs - - } - place='top' - > - {Constants.strings.HIDE_FROM_SDKS_DESCRIPTION} - - - - )} ) @@ -893,7 +861,6 @@ const CreateFlag = class extends Component { > { this.setState({ segmentsChanged: false }) - this.save(editFeatureSegments, isSaving) + if (is4Eyes && isVersioned && !identity) { + openModal2( + this.props.changeRequest + ? 'Update Change Request' + : 'New Change Request', + { + closeModal2() + this.save( + ( + projectId, + environmentId, + flag, + projectFlag, + environmentFlag, + segmentOverrides, + ) => { + createChangeRequest( + projectId, + environmentId, + flag, + projectFlag, + environmentFlag, + segmentOverrides, + { + approvals, + description, + featureStateId: + this.props.changeRequest && + this.props.changeRequest.feature_states[0].id, + id: + this.props.changeRequest && + this.props.changeRequest.id, + live_from, + multivariate_options: this.props + .multivariate_options + ? this.props.multivariate_options.map((v) => { + const matching = + this.state.multivariate_options.find( + (m) => + m.id === + v.multivariate_feature_option, + ) + return { + ...v, + percentage_allocation: + matching.default_percentage_allocation, + } + }) + : this.state.multivariate_options, + title, + }, + !is4Eyes, + 'SEGMENT', + ) + }, + ) + }} + />, + ) + } else { + this.save(editFeatureSegments, isSaving) + } } const onCreateFeature = () => { @@ -1392,8 +1429,7 @@ const CreateFlag = class extends Component { {!this.state.showCreateSegment && (

- {is4Eyes && - is4EyesSegmentOverrides + {is4Eyes && isVersioned ? 'This will create a change request for the environment' : 'This will update the segment overrides for the environment'}{' '} @@ -1434,6 +1470,41 @@ const CreateFlag = class extends Component { permission: manageSegmentsOverrides, }) => { + if ( + isVersioned && + is4Eyes + ) { + return Utils.renderWithPermission( + savePermission, + Utils.getManageFeaturePermissionDescription( + is4Eyes, + identity, + ), + , + ) + } + return Utils.renderWithPermission( manageSegmentsOverrides, Constants.environmentPermissions( @@ -1923,27 +1994,39 @@ const FeatureProvider = (WrappedComponent) => { componentDidMount() { ES6Component(this) - this.listenTo(FeatureListStore, 'saved', (createdFlag) => { - if (createdFlag) { - const projectFlag = FeatureListStore.getProjectFlags()?.find?.( - (flag) => flag.name === createdFlag, + this.listenTo( + FeatureListStore, + 'saved', + ({ changeRequest, createdFlag, isCreate } = {}) => { + toast( + `${createdFlag || isCreate ? 'Created' : 'Updated'} ${ + changeRequest ? 'Change Request' : 'Feature' + }`, ) - window.history.replaceState( - {}, - `${document.location.pathname}?feature=${projectFlag.id}`, - ) - const envFlags = FeatureListStore.getEnvironmentFlags() - const newEnvironmentFlag = envFlags?.[projectFlag.id] || {} - setModalTitle(`Edit Feature ${projectFlag.name}`) - this.setState({ - environmentFlag: { - ...this.state.environmentFlag, - ...(newEnvironmentFlag || {}), - }, - projectFlag, - }) - } - }) + if (createdFlag) { + const projectFlag = FeatureListStore.getProjectFlags()?.find?.( + (flag) => flag.name === createdFlag, + ) + window.history.replaceState( + {}, + `${document.location.pathname}?feature=${projectFlag.id}`, + ) + const envFlags = FeatureListStore.getEnvironmentFlags() + const newEnvironmentFlag = envFlags?.[projectFlag.id] || {} + setModalTitle(`Edit Feature ${projectFlag.name}`) + this.setState({ + environmentFlag: { + ...this.state.environmentFlag, + ...(newEnvironmentFlag || {}), + }, + projectFlag, + }) + } + if (changeRequest) { + closeModal() + } + }, + ) } render() { diff --git a/frontend/web/components/pages/ChangeRequestPage.js b/frontend/web/components/pages/ChangeRequestPage.js index 06c552174a29..59402feca6dc 100644 --- a/frontend/web/components/pages/ChangeRequestPage.js +++ b/frontend/web/components/pages/ChangeRequestPage.js @@ -16,15 +16,11 @@ import MyGroupsSelect from 'components/MyGroupsSelect' import { getMyGroups } from 'common/services/useMyGroup' import { getStore } from 'common/store' import PageTitle from 'components/PageTitle' -import Icon from 'components/Icon' import { close } from 'ionicons/icons' import { IonIcon } from '@ionic/react' -import { useGetSegmentsQuery } from 'common/services/useSegment' -import DiffFeature from 'components/diff/DiffFeature' import Breadcrumb from 'components/Breadcrumb' import SettingsButton from 'components/SettingsButton' - -const labelWidth = 120 +import DiffChangeRequest from 'components/diff/DiffChangeRequest' const ChangeRequestsPage = class extends Component { static displayName = 'ChangeRequestsPage' @@ -275,7 +271,7 @@ const ChangeRequestsPage = class extends Component { const environment = ProjectStore.getEnvironment( this.props.match.params.environmentId, ) - + const isVersioned = environment?.use_v2_feature_versioning const minApprovals = environment.minimum_change_request_approvals || 0 const isYourChangeRequest = changeRequest.user === AccountStore.getUser().id @@ -323,14 +319,19 @@ const ChangeRequestsPage = class extends Component { > Delete - + {!isVersioned && ( + + )} ) } @@ -476,12 +477,9 @@ const ChangeRequestsPage = class extends Component { } className='no-pad mb-2' > -

+
-
+
Feature:
@@ -502,29 +500,11 @@ const ChangeRequestsPage = class extends Component {
- {environmentFlag && changeRequest ? ( - - ) : ( -
- -
- )} +
@@ -558,7 +538,7 @@ const ChangeRequestsPage = class extends Component { by {committedBy.first_name} {committedBy.last_name}
) : ( - + {!isYourChangeRequest && Utils.renderWithPermission( diff --git a/frontend/web/components/pages/FeatureHistoryPage.tsx b/frontend/web/components/pages/FeatureHistoryPage.tsx index 9af850f5a853..6102272795a0 100644 --- a/frontend/web/components/pages/FeatureHistoryPage.tsx +++ b/frontend/web/components/pages/FeatureHistoryPage.tsx @@ -15,8 +15,6 @@ import { import PageTitle from 'components/PageTitle' import Button from 'components/base/forms/Button' import FeatureVersion from 'components/FeatureVersion' -import { IonIcon } from '@ionic/react' -import { chevronDown } from 'ionicons/icons' import InlineModal from 'components/InlineModal' import TableFilterItem from 'components/tables/TableFilterItem' import moment from 'moment' diff --git a/frontend/web/components/pages/FeaturesPage.js b/frontend/web/components/pages/FeaturesPage.js index 34af62e86c30..4232b7ee5e89 100644 --- a/frontend/web/components/pages/FeaturesPage.js +++ b/frontend/web/components/pages/FeaturesPage.js @@ -150,11 +150,6 @@ const FeaturesPage = class extends Component { : this.state.tags.join(','), value_search: this.state.value_search ? this.state.value_search : undefined, }) - - onSave = (isCreate) => { - toast(`${isCreate ? 'Created' : 'Updated'} Feature`) - } - onError = (error) => { // Kick user back out to projects this.setState({ error })