From 8a46154fb80a6ce3f4a790064eadbda32a39e722 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 25 Mar 2026 16:04:39 +0100 Subject: [PATCH 1/2] feat: added-tracking-on-code-references-actions --- frontend/package-lock.json | 8 +++---- frontend/package.json | 2 +- .../FeatureCodeReferencesContainer.tsx | 21 ++++++++++++++++++- .../components/CodeReferenceItem.tsx | 13 +++++++++++- .../components/RepoCodeReferencesSection.tsx | 16 +++++++++++++- .../components/modals/create-feature/index.js | 9 ++++++++ 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1616c5760533..574be94e218a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@babel/register": "^7.12.1", "@datadog/ui-extensions-react": "0.32.0", "@datadog/ui-extensions-sdk": "0.32.0", - "@flagsmith/flagsmith": "^11.0.0-internal.5", + "@flagsmith/flagsmith": "^11.0.0-internal.6", "@ionic/react": "^7.5.3", "@material-ui/core": "4.12.4", "@react-oauth/google": "^0.2.8", @@ -2779,9 +2779,9 @@ } }, "node_modules/@flagsmith/flagsmith": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@flagsmith/flagsmith/-/flagsmith-11.0.0.tgz", - "integrity": "sha512-jJB+1O/ctU7TCoIBsV2lgYAUOpShjcSqHkH4nlyqUGeQCGC0ZHto8IvGf+nZwpFAF5Czaphn/anm9MDoZIS3rw==", + "version": "11.0.0-internal.6", + "resolved": "https://registry.npmjs.org/@flagsmith/flagsmith/-/flagsmith-11.0.0-internal.6.tgz", + "integrity": "sha512-Vz719LOLC6h6KDHqlULOJTCth5RuX2iT4GPxcTZTGxOrxM9q4XwE8AxxLqoExgucXrd2neKUMU0PDmB1Q3LE0Q==", "license": "BSD-3-Clause" }, "node_modules/@floating-ui/core": { diff --git a/frontend/package.json b/frontend/package.json index 65524c4356de..fd51803dc501 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,7 +51,7 @@ "@babel/register": "^7.12.1", "@datadog/ui-extensions-react": "0.32.0", "@datadog/ui-extensions-sdk": "0.32.0", - "@flagsmith/flagsmith": "^11.0.0-internal.5", + "@flagsmith/flagsmith": "^11.0.0-internal.6", "@ionic/react": "^7.5.3", "@material-ui/core": "4.12.4", "@react-oauth/google": "^0.2.8", diff --git a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer.tsx b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer.tsx index 42227eb39165..4ede1c2b5bfc 100644 --- a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer.tsx +++ b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer.tsx @@ -1,4 +1,5 @@ -import React, { useMemo } from 'react' +import React, { useEffect, useMemo, useRef } from 'react' +import flagsmith from '@flagsmith/flagsmith' import { useGetFeatureCodeReferencesQuery } from 'common/services/useCodeReferences' import RepoCodeReferencesSection from './components/RepoCodeReferencesSection' import { FeatureCodeReferences } from 'common/types/responses' @@ -34,6 +35,23 @@ const FeatureCodeReferencesContainer: React.FC< [data], ) + const hasTrackedView = useRef(false) + useEffect(() => { + if (data.length > 0 && !hasTrackedView.current) { + hasTrackedView.current = true + const totalRefs = data.reduce( + (sum, repo) => sum + repo.code_references.length, + 0, + ) + flagsmith.trackEvent('code_references_view', { + feature_id: featureId, + project_id: projectId, + repos_count: data.length, + total_refs_count: totalRefs, + }) + } + }, [data, featureId, projectId]) + if (isLoading) { return (
@@ -59,6 +77,7 @@ const FeatureCodeReferencesContainer: React.FC< key={codeReferencesByRepo[repo].repository_url} repositoryName={codeReferencesByRepo[repo].repository_url} repositoryScan={codeReferencesByRepo[repo]} + featureId={featureId} /> ))}
diff --git a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/CodeReferenceItem.tsx b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/CodeReferenceItem.tsx index edeb324c30c0..334a16903db8 100644 --- a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/CodeReferenceItem.tsx +++ b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/CodeReferenceItem.tsx @@ -1,12 +1,17 @@ +import flagsmith from '@flagsmith/flagsmith' import Icon from 'components/Icon' -import { CodeReference } from 'common/types/responses' +import { CodeReference, VCSProvider } from 'common/types/responses' interface CodeReferenceItemProps { codeReference: CodeReference + featureId: number + vcsProvider: VCSProvider } const CodeReferenceItem: React.FC = ({ codeReference, + featureId, + vcsProvider, }) => { return ( @@ -25,6 +30,12 @@ const CodeReferenceItem: React.FC = ({ href={codeReference.permalink} target='_blank' rel='noreferrer' + onClick={() => { + flagsmith.trackEvent('code_references_click_permalink', { + feature_id: featureId, + vcs_provider: vcsProvider, + }) + }} > {codeReference.file_path}:{codeReference.line_number} diff --git a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/RepoCodeReferencesSection.tsx b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/RepoCodeReferencesSection.tsx index 48adc5f7c5f8..cc8018dea91a 100644 --- a/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/RepoCodeReferencesSection.tsx +++ b/frontend/web/components/feature-page/FeatureNavTab/CodeReferences/components/RepoCodeReferencesSection.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import flagsmith from '@flagsmith/flagsmith' import { FeatureCodeReferences } from 'common/types/responses' import moment from 'moment' import CodeReferenceItem from './CodeReferenceItem' @@ -9,9 +10,11 @@ import CodeReferenceScanIndicator from './CodeReferenceScanIndicator' interface RepoCodeReferencesSectionProps { repositoryScan: FeatureCodeReferences repositoryName: string + featureId: number } const RepoCodeReferencesSection: React.FC = ({ + featureId, repositoryName, repositoryScan, }) => { @@ -42,7 +45,16 @@ const RepoCodeReferencesSection: React.FC = ({ > setIsOpen(!isOpen)} + onClick={() => { + if (!isOpen) { + flagsmith.trackEvent('code_references_expand_repo', { + feature_id: featureId, + refs_count: repositoryScan?.code_references?.length, + vcs_provider: repositoryScan?.vcs_provider, + }) + } + setIsOpen(!isOpen) + }} > = ({ {repositoryScan?.code_references?.map((codeReference) => ( ))} diff --git a/frontend/web/components/modals/create-feature/index.js b/frontend/web/components/modals/create-feature/index.js index efdc235dda61..5b34d0d62bae 100644 --- a/frontend/web/components/modals/create-feature/index.js +++ b/frontend/web/components/modals/create-feature/index.js @@ -40,6 +40,7 @@ import { getChangeRequests } from 'common/services/useChangeRequest' import FeatureHealthTabContent from 'components/feature-health/FeatureHealthTabContent' import FeaturePipelineStatus from 'components/release-pipelines/FeaturePipelineStatus' import FeatureInPipelineGuard from 'components/release-pipelines/FeatureInPipelineGuard' +import flagsmith from '@flagsmith/flagsmith' import FeatureCodeReferencesContainer from 'components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer' import ProjectProvider from 'common/providers/ProjectProvider' import CreateFeature from './tabs/CreateFeature' @@ -1610,6 +1611,14 @@ const Index = class extends Component { target='_blank' href='https://docs.flagsmith.com/managing-flags/code-references' rel='noreferrer' + onClick={() => { + flagsmith.trackEvent( + 'code_references_click_docs', + { + feature_id: projectFlag.id, + }, + ) + }} > Learn more From 1959553b592961dd7cd5b75984ab5e3e3f1b05dc Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 30 Mar 2026 09:43:29 +0200 Subject: [PATCH 2/2] feat: removed-unwanted-committed-file --- .../components/modals/create-feature/index.js | 2069 ----------------- 1 file changed, 2069 deletions(-) delete mode 100644 frontend/web/components/modals/create-feature/index.js diff --git a/frontend/web/components/modals/create-feature/index.js b/frontend/web/components/modals/create-feature/index.js deleted file mode 100644 index 08372eea3de7..000000000000 --- a/frontend/web/components/modals/create-feature/index.js +++ /dev/null @@ -1,2069 +0,0 @@ -import React, { Component } from 'react' -import withSegmentOverrides from 'common/providers/withSegmentOverrides' -import moment from 'moment' -import Constants from 'common/constants' -import data from 'common/data/base/_data' -import ProjectStore from 'common/stores/project-store' -import ConfigProvider from 'common/providers/ConfigProvider' -import FeatureListStore from 'common/stores/feature-list-store' -import IdentityProvider from 'common/providers/IdentityProvider' -import Tabs from 'components/navigation/TabMenu/Tabs' -import TabItem from 'components/navigation/TabMenu/TabItem' -import SegmentOverrides from 'components/SegmentOverrides' -import ChangeRequestModal from 'components/modals/ChangeRequestModal' -import classNames from 'classnames' -import InfoMessage from 'components/InfoMessage' -import JSONReference from 'components/JSONReference' -import ErrorMessage from 'components/ErrorMessage' -import Permission from 'common/providers/Permission' -import IdentitySelect from 'components/IdentitySelect' -import { - setInterceptClose, - setModalTitle, -} from 'components/modals/base/ModalDefault' -import Icon from 'components/Icon' -import ModalHR from 'components/modals/ModalHR' -import FeatureValue from 'components/feature-summary/FeatureValue' -import { getStore } from 'common/store' -import Button from 'components/base/forms/Button' -import { getSupportedContentType } from 'common/services/useSupportedContentType' -import { getGithubIntegration } from 'common/services/useGithubIntegration' -import { removeUserOverride } from 'components/RemoveUserOverride' -import ExternalResourcesLinkTab from 'components/ExternalResourcesLinkTab' -import { saveFeatureWithValidation } from 'components/saveFeatureWithValidation' -import FeatureHistory from 'components/FeatureHistory' -import WarningMessage from 'components/WarningMessage' -import FeatureAnalytics from 'components/feature-page/FeatureNavTab/FeatureAnalytics' -import { FlagValueFooter } from 'components/modals/FlagValueFooter' -import { getPermission } from 'common/services/usePermission' -import { getChangeRequests } from 'common/services/useChangeRequest' -import FeatureHealthTabContent from 'components/feature-health/FeatureHealthTabContent' -import FeaturePipelineStatus from 'components/release-pipelines/FeaturePipelineStatus' -import FeatureInPipelineGuard from 'components/release-pipelines/FeatureInPipelineGuard' -import FeatureCodeReferencesContainer from 'components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer' -import ProjectProvider from 'common/providers/ProjectProvider' -import CreateFeature from './tabs/CreateFeature' -import FeatureSettings from './tabs/FeatureSettings' -import FeatureValueTab from './tabs/FeatureValue' -import FeatureLimitAlert from './FeatureLimitAlert' -import FeatureUpdateSummary from './FeatureUpdateSummary' -import FeatureNameInput from './FeatureNameInput' -import { - EnvironmentPermission, - ProjectPermission, -} from 'common/types/permissions.types' - -const Index = class extends Component { - static displayName = 'create-feature' - - constructor(props, context) { - super(props, context) - if (this.props.projectFlag) { - this.userOverridesPage(1, true) - } - - const projectFlagData = this.props.projectFlag - ? _.cloneDeep(this.props.projectFlag) - : { - description: undefined, - is_archived: undefined, - is_server_key_only: undefined, - metadata: [], - multivariate_options: [], - name: undefined, - tags: [], - } - - const sourceFlag = this.props.identityFlag || this.props.environmentFlag - const environmentFlagData = sourceFlag ? _.cloneDeep(sourceFlag) : {} - - this.state = { - changeRequests: [], - enabledIndentity: false, - enabledSegment: false, - environmentFlag: environmentFlagData, - externalResource: {}, - externalResources: [], - featureContentType: {}, - featureLimitAlert: { percentage: 0 }, - githubId: '', - hasIntegrationWithGithub: false, - hasMetadataRequired: false, - isEdit: !!this.props.projectFlag, - period: 30, - projectFlag: projectFlagData, - scheduledChangeRequests: [], - segmentsChanged: false, - selectedIdentity: null, - settingsChanged: false, - userOverridesError: false, - userOverridesNoPermission: false, - valueChanged: false, - } - } - - close() { - closeModal() - } - - componentDidUpdate(prevProps) { - ES6Component(this) - - const environmentFlagSource = - this.props.identityFlag || this.props.environmentFlag - const prevEnvironmentFlagSource = - prevProps.identityFlag || prevProps.environmentFlag - - if ( - environmentFlagSource && - prevEnvironmentFlagSource && - environmentFlagSource.updated_at && - prevEnvironmentFlagSource.updated_at && - environmentFlagSource.updated_at !== prevEnvironmentFlagSource.updated_at - ) { - this.setState({ - environmentFlag: _.cloneDeep(environmentFlagSource), - }) - } - - if ( - this.props.projectFlag && - prevProps.projectFlag && - this.props.projectFlag.updated_at && - prevProps.projectFlag.updated_at && - this.props.projectFlag.updated_at !== prevProps.projectFlag.updated_at - ) { - this.setState({ - projectFlag: _.cloneDeep(this.props.projectFlag), - }) - } - - if ( - !this.props.identity && - this.props.environmentVariations !== prevProps.environmentVariations - ) { - if ( - this.props.environmentVariations && - this.props.environmentVariations.length - ) { - this.setState({ - projectFlag: { - ...this.state.projectFlag, - multivariate_options: - this.state.projectFlag.multivariate_options && - this.state.projectFlag.multivariate_options.map((v) => { - const matchingVariation = ( - this.props.multivariate_options || - this.props.environmentVariations - ).find((e) => e.multivariate_feature_option === v.id) - return { - ...v, - default_percentage_allocation: - (matchingVariation && - matchingVariation.percentage_allocation) || - v.default_percentage_allocation || - 0, - } - }), - }, - }) - } - } - } - - onClosing = () => { - if (this.state.isEdit) { - return new Promise((resolve) => { - const projectFlagChanged = this.state.settingsChanged - const environmentFlagChanged = this.state.valueChanged - const segmentOverridesChanged = this.state.segmentsChanged - if ( - projectFlagChanged || - environmentFlagChanged || - segmentOverridesChanged - ) { - openConfirm({ - body: 'Closing this will discard your unsaved changes.', - noText: 'Cancel', - onNo: () => resolve(false), - onYes: () => resolve(true), - title: 'Discard changes', - yesText: 'Ok', - }) - } else { - resolve(true) - } - }) - } - return Promise.resolve(true) - } - - componentDidMount = () => { - setInterceptClose(this.onClosing) - if (Utils.getPlansPermission('METADATA')) { - getSupportedContentType(getStore(), { - organisation_id: AccountStore.getOrganisation().id, - }).then((res) => { - const featureContentType = Utils.getContentType( - res.data, - 'model', - 'feature', - ) - this.setState({ featureContentType: featureContentType }) - }) - } - - this.fetchChangeRequests() - this.fetchScheduledChangeRequests() - - getGithubIntegration(getStore(), { - organisation_id: AccountStore.getOrganisation().id, - }).then((res) => { - this.setState({ - githubId: res?.data?.results[0]?.id, - hasIntegrationWithGithub: !!res?.data?.results?.length, - }) - }) - } - - componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout) - } - } - - setUserOverridesError = () => { - this.setState({ - userOverrides: [], - userOverridesError: true, - userOverridesNoPermission: false, - userOverridesPaging: { count: 0, currentPage: 1, next: null }, - }) - } - - setUserOverridesNoPermission = () => { - this.setState({ - userOverrides: [], - userOverridesError: false, - userOverridesNoPermission: true, - userOverridesPaging: { count: 0, currentPage: 1, next: null }, - }) - } - - userOverridesPage = (page, forceRefetch) => { - if (Utils.getIsEdge()) { - // Early return if tab should be hidden - if (Utils.getShouldHideIdentityOverridesTab(ProjectStore.model)) { - this.setState({ - userOverrides: [], - userOverridesPaging: { - count: 0, - currentPage: 1, - next: null, - }, - }) - return - } - - getPermission( - getStore(), - { - id: this.props.environmentId, - level: 'environment', - permissions: EnvironmentPermission.VIEW_IDENTITIES, - }, - { forceRefetch }, - ) - .then((permissions) => { - const hasViewIdentitiesPermission = - permissions[EnvironmentPermission.VIEW_IDENTITIES] || - permissions.ADMIN - // Early return if user doesn't have permission - if (!hasViewIdentitiesPermission) { - this.setUserOverridesNoPermission() - return - } - - data - .get( - `${Project.api}environments/${this.props.environmentId}/edge-identity-overrides?feature=${this.props.projectFlag.id}&page=${page}`, - ) - .then((userOverrides) => { - this.setState({ - userOverrides: userOverrides.results.map((v) => ({ - ...v.feature_state, - identity: { - id: v.identity_uuid, - identifier: v.identifier, - }, - })), - userOverridesError: false, - userOverridesNoPermission: false, - userOverridesPaging: { - count: userOverrides.count, - currentPage: page, - next: userOverrides.next, - }, - }) - }) - .catch((response) => { - if (response?.status === 403) { - this.setUserOverridesNoPermission() - } else { - this.setUserOverridesError() - } - }) - }) - .catch(() => { - this.setUserOverridesError() - }) - - return - } - - data - .get( - `${Project.api}environments/${ - this.props.environmentId - }/${Utils.getFeatureStatesEndpoint()}/?anyIdentity=1&feature=${ - this.props.projectFlag.id - }&page=${page}`, - ) - .then((userOverrides) => { - this.setState({ - userOverrides: userOverrides.results, - userOverridesError: false, - userOverridesNoPermission: false, - userOverridesPaging: { - count: userOverrides.count, - currentPage: page, - next: userOverrides.next, - }, - }) - }) - .catch((response) => { - if (response?.status === 403) { - this.setUserOverridesNoPermission() - } else { - this.setUserOverridesError() - } - }) - } - - renderUserOverridesNoResults = () => { - if (this.state.userOverridesError) { - return ( -
- Failed to load identity overrides. -
- ) - } - if (this.state.userOverridesNoPermission) { - return ( -
- You do not have permission to view identity overrides. -
- ) - } - return ( - -
- No identities are overriding this feature. -
-
- ) - } - - save = (func, isSaving) => { - const { - environmentFlag, - environmentId, - identity, - identityFlag, - projectFlag: _projectFlag, - segmentOverrides, - } = this.props - const { environmentFlag: stateEnvironmentFlag, projectFlag } = this.state - const hasMultivariate = - environmentFlag && - environmentFlag.multivariate_feature_state_values && - environmentFlag.multivariate_feature_state_values.length - if (identity) { - !isSaving && - projectFlag.name && - func({ - environmentFlag, - environmentId, - identity, - identityFlag: Object.assign({}, identityFlag || {}, { - enabled: stateEnvironmentFlag.enabled, - feature_state_value: hasMultivariate - ? environmentFlag.feature_state_value - : this.cleanInputValue(stateEnvironmentFlag.feature_state_value), - multivariate_options: - stateEnvironmentFlag.multivariate_feature_state_values, - }), - projectFlag, - }) - } else { - FeatureListStore.isSaving = true - FeatureListStore.trigger('change') - !isSaving && - projectFlag.name && - func( - this.props.projectId, - this.props.environmentId, - { - default_enabled: stateEnvironmentFlag.enabled, - description: projectFlag.description, - initial_value: this.cleanInputValue( - stateEnvironmentFlag.feature_state_value, - ), - is_archived: projectFlag.is_archived, - is_server_key_only: projectFlag.is_server_key_only, - metadata: - !this.props.projectFlag?.metadata || - (this.props.projectFlag.metadata !== projectFlag.metadata && - projectFlag.metadata.length) - ? projectFlag.metadata - : this.props.projectFlag.metadata, - multivariate_options: projectFlag.multivariate_options, - name: projectFlag.name, - tags: projectFlag.tags, - }, - { - skipSaveProjectFeature: this.state.skipSaveProjectFeature, - ..._projectFlag, - }, - { - ...environmentFlag, - multivariate_feature_state_values: - this.props.environmentVariations || - environmentFlag?.multivariate_feature_state_values, - }, - segmentOverrides, - ) - } - } - - changeSegment = (items) => { - const { enabledSegment } = this.state - items.forEach((item) => { - item.enabled = enabledSegment - }) - this.props.updateSegments(items) - this.setState({ enabledSegment: !enabledSegment }) - } - - changeIdentity = (items) => { - const { environmentId } = this.props - const { enabledIndentity } = this.state - - Promise.all( - items.map( - (item) => - new Promise((resolve) => { - AppActions.changeUserFlag({ - environmentId, - identity: item.identity.id, - identityFlag: item.id, - onSuccess: resolve, - payload: { - enabled: enabledIndentity, - id: item.identity.id, - value: item.identity.identifier, - }, - }) - }), - ), - ).then(() => { - this.userOverridesPage(1) - }) - - this.setState({ enabledIndentity: !enabledIndentity }) - } - - toggleUserFlag = ({ enabled, id, identity }) => { - const { environmentId } = this.props - - AppActions.changeUserFlag({ - environmentId, - identity: identity.id, - identityFlag: id, - onSuccess: () => { - this.userOverridesPage(1) - }, - payload: { - enabled: !enabled, - id: identity.id, - value: identity.identifier, - }, - }) - } - parseError = (error) => { - const { projectFlag } = this.props - let featureError = - error?.metadata?.flatMap((m) => m.non_field_errors ?? []).join('\n') || - error?.message || - error?.name?.[0] || - error - let featureWarning = '' - //Treat multivariate no changes as warnings - if ( - featureError?.includes?.('no changes') && - projectFlag?.multivariate_options?.length - ) { - featureWarning = `Your feature contains no changes to its value, enabled state or environment weights. If you have adjusted any variation values this will have been saved for all environments.` - featureError = '' - } - return { featureError, featureWarning } - } - cleanInputValue = (value) => { - if (value && typeof value === 'string') { - return value.trim() - } - return value - } - - addItem = () => { - const { environmentFlag, environmentId, identity, projectFlag } = this.props - this.setState({ isLoading: true }) - const selectedIdentity = this.state.selectedIdentity.value - const identities = identity ? identity.identifier : [] - - if (!_.find(identities, (v) => v.identifier === selectedIdentity)) { - data - .post( - `${ - Project.api - }environments/${environmentId}/${Utils.getIdentitiesEndpoint()}/${selectedIdentity}/${Utils.getFeatureStatesEndpoint()}/`, - { - enabled: !environmentFlag.enabled, - feature: projectFlag.id, - feature_state_value: environmentFlag.value || null, - }, - ) - .then(() => { - this.setState({ - isLoading: false, - selectedIdentity: null, - }) - this.userOverridesPage(1) - }) - .catch((e) => { - this.setState({ error: e, isLoading: false }) - }) - } else { - this.setState({ - isLoading: false, - selectedIdentity: null, - }) - } - } - - fetchChangeRequests = (forceRefetch) => { - const { environmentId, projectFlag } = this.props - if (!projectFlag?.id) return - - getChangeRequests( - getStore(), - { - committed: false, - environmentId, - feature_id: projectFlag?.id, - }, - { forceRefetch }, - ).then((res) => { - this.setState({ changeRequests: res.data?.results }) - }) - } - - fetchScheduledChangeRequests = (forceRefetch) => { - const { environmentId, projectFlag } = this.props - if (!projectFlag?.id) return - - const date = moment().toISOString() - - getChangeRequests( - getStore(), - { - environmentId, - feature_id: projectFlag.id, - live_from_after: date, - }, - { forceRefetch }, - ).then((res) => { - this.setState({ scheduledChangeRequests: res.data?.results }) - }) - } - - render() { - const { - enabledIndentity, - enabledSegment, - environmentFlag, - featureContentType, - githubId, - hasIntegrationWithGithub, - isEdit, - projectFlag, - } = this.state - const { identity, identityName } = this.props - const Provider = identity ? IdentityProvider : FeatureListProvider - 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 project = ProjectStore.model - const caseSensitive = project?.only_allow_lower_case_feature_names - const regex = project?.feature_name_regex - const controlValue = Utils.calculateControl( - projectFlag.multivariate_options, - ) - const invalid = - !!projectFlag.multivariate_options && - projectFlag.multivariate_options.length && - controlValue < 0 - const existingChangeRequest = this.props.changeRequest - const isVersionedChangeRequest = existingChangeRequest && isVersioned - const hideIdentityOverridesTab = Utils.getShouldHideIdentityOverridesTab() - const noPermissions = this.props.noPermissions - let regexValid = true - - const hasCodeReferences = projectFlag?.code_references_counts?.length > 0 - - try { - if (!isEdit && projectFlag.name && regex) { - regexValid = projectFlag.name.match(new RegExp(regex)) - } - } catch (e) { - regexValid = false - } - - return ( - - {({ project }) => ( - { - if (identity) { - this.close() - } - AppActions.refreshFeatures( - this.props.projectId, - this.props.environmentId, - ) - - if (is4Eyes && !identity) { - this.fetchChangeRequests(true) - this.fetchScheduledChangeRequests(true) - } - - if (this.props.changeRequest) { - this.close() - } - }} - > - {( - { error, isSaving }, - { - createChangeRequest, - createFlag, - editFeatureSegments, - editFeatureSettings, - editFeatureValue, - }, - ) => { - const saveFeatureValue = saveFeatureWithValidation((schedule) => { - if ((is4Eyes || schedule) && !identity) { - this.setState({ segmentsChanged: false, valueChanged: false }) - // Until this page and feature-list-store are refactored, this is the best way of parsing feature states - const featureStates = (this.props.segmentOverrides || []) - .filter((override) => !override.toRemove) - .map((override) => { - return { - enabled: override.enabled, - feature: override.feature, - feature_segment: { - environment: override.environment, - id: override.id, - is_feature_specific: override.is_feature_specific, - priority: override.priority, - segment: override.segment, - segment_name: override.segment_name, - uuid: override.uuid, - }, - feature_state_value: Utils.valueToFeatureState( - override.value, - ), - id: override.id, - multivariate_feature_state_values: - override.multivariate_options, - } - }) - .concat([ - Object.assign({}, this.props.environmentFlag, { - enabled: environmentFlag.enabled, - feature_state_value: Utils.valueToFeatureState( - environmentFlag.feature_state_value, - ), - multivariate_feature_state_values: - environmentFlag.multivariate_feature_state_values, - }), - ]) - - const getModalTitle = () => { - if (schedule) { - return 'New Scheduled Flag Update' - } - if (this.props.changeRequest) { - return 'Update Change Request' - } - return 'New Change Request' - } - - let modalTitle = 'New Change Request' - if (schedule) { - modalTitle = 'New Scheduled Flag Update' - } else if (this.props.changeRequest) { - modalTitle = 'Update Change Request' - } - - openModal2( - getModalTitle(), - { - 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, - ignore_conflicts, - live_from, - multivariate_options: flag.multivariate_options, - title, - }, - !is4Eyes, - ) - }, - ) - }} - />, - ) - } else { - this.setState({ valueChanged: false }) - this.save(editFeatureValue, isSaving) - } - }) - - const saveSettings = () => { - this.setState({ settingsChanged: false }) - this.save(editFeatureSettings, isSaving) - } - - const saveFeatureSegments = saveFeatureWithValidation( - (schedule) => { - this.setState({ segmentsChanged: false }) - - if ((is4Eyes || schedule) && isVersioned && !identity) { - return saveFeatureValue(schedule) - } else { - this.save(editFeatureSegments, isSaving) - } - }, - ) - - const onCreateFeature = saveFeatureWithValidation(() => { - this.save(createFlag, isSaving) - }) - const isLimitReached = false - - const { featureError, featureWarning } = this.parseError(error) - - return ( - - {({ permission: createFeature }) => ( - - {({ permission: projectAdmin }) => { - this.state.skipSaveProjectFeature = !createFeature - - return ( -
- {isEdit && !identity ? ( - <> - - this.forceUpdate()} - urlParam='tab' - history={this.props.history} - overflowX - > - - Value{' '} - {this.state.valueChanged && ( -
- {'*'} -
- )} - - } - > - { - this.setState({ - environmentFlag: { - ...this.state.environmentFlag, - ...changes, - }, - valueChanged: true, - }) - }} - onProjectFlagChange={(changes) => { - this.setState({ - projectFlag: { - ...this.state.projectFlag, - ...changes, - }, - }) - }} - onRemoveMultivariateOption={ - this.props.removeMultivariateOption - } - /> - - - -
- {(!existingChangeRequest || - isVersionedChangeRequest) && ( - - Segment Overrides{' '} - {this.state.segmentsChanged && ( -
- * -
- )} - - } - > - - ( - <> -
- Segment Overrides{' '} -
- - This feature is in{' '} - - { - matchingReleasePipeline?.name - } - {' '} - release pipeline and no segment - overrides can be created - - - )} - > -
- -
- - Segment Overrides{' '} - - - } - place='top' - > - { - Constants.strings - .SEGMENT_OVERRIDES_DESCRIPTION - } - -
- - {({ - permission: - manageSegmentOverrides, - }) => - !this.state - .showCreateSegment && - !!manageSegmentOverrides && - !this.props.disableCreate && ( -
- -
- ) - } -
- {!this.state.showCreateSegment && - !noPermissions && ( - - )} -
- {this.props.segmentOverrides ? ( - - {({ - permission: - manageSegmentOverrides, - }) => { - const isReadOnly = - !manageSegmentOverrides - return ( - <> - - - - this.setState({ - showCreateSegment, - }) - } - readOnly={isReadOnly} - is4Eyes={is4Eyes} - showEditSegment - showCreateSegment={ - this.state - .showCreateSegment - } - feature={projectFlag.id} - projectId={ - this.props.projectId - } - multivariateOptions={ - projectFlag.multivariate_options - } - environmentId={ - this.props - .environmentId - } - value={ - this.props - .segmentOverrides - } - controlValue={ - environmentFlag.feature_state_value - } - onChange={(v) => { - this.setState({ - segmentsChanged: true, - }) - this.props.updateSegments( - v, - ) - }} - highlightSegmentId={ - this.props - .highlightSegmentId - } - /> - - ) - }} - - ) : ( -
- -
- )} - {!this.state.showCreateSegment && ( - - )} - {!this.state.showCreateSegment && ( -
-

- {is4Eyes && isVersioned - ? 'This will create a change request with any value and segment override changes for the environment' - : 'This will update the segment overrides for the environment'}{' '} - - { - _.find( - project.environments, - { - api_key: - this.props - .environmentId, - }, - ).name - } - -

-
- - {({ - permission: - savePermission, - }) => ( - - {({ - permission: - manageSegmentsOverrides, - }) => { - const getButtonText = - () => { - if (isSaving) { - return existingChangeRequest - ? 'Updating Change Request' - : 'Creating Change Request' - } - return existingChangeRequest - ? 'Update Change Request' - : 'Create Change Request' - } - - const getScheduleButtonText = - () => { - if (isSaving) { - return existingChangeRequest - ? 'Updating Change Request' - : 'Scheduling Update' - } - return existingChangeRequest - ? 'Update Change Request' - : 'Schedule Update' - } - - if ( - isVersioned && - is4Eyes - ) { - return Utils.renderWithPermission( - savePermission, - Utils.getManageFeaturePermissionDescription( - is4Eyes, - identity, - ), - , - ) - } - - return Utils.renderWithPermission( - manageSegmentsOverrides, - Constants.environmentPermissions( - EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES, - ), - <> - {!is4Eyes && - isVersioned && ( - <> - - - )} - - , - ) - }} - - )} - -
-
- )} -
-
-
-
- )} - - {({ permission: viewIdentities }) => - !existingChangeRequest && - !hideIdentityOverridesTab && ( - - {viewIdentities ? ( - <> - - - - Identity Overrides{' '} - - - } - place='top' - > - { - Constants.strings - .IDENTITY_OVERRIDES_DESCRIPTION - } - -
- - Identity overrides - override feature - values for individual - identities. The - overrides take - priority over an - segment overrides and - environment defaults. - Identity overrides - will only apply when - you identify via the - SDK.{' '} - - Check the Docs for - more details - - . - -
- - } - action={ - !Utils.getIsEdge() && ( - - ) - } - items={ - this.state.userOverrides - } - paging={ - this.state - .userOverridesPaging - } - renderSearchWithNoResults - nextPage={() => - this.userOverridesPage( - this.state - .userOverridesPaging - .currentPage + 1, - ) - } - prevPage={() => - this.userOverridesPage( - this.state - .userOverridesPaging - .currentPage - 1, - ) - } - goToPage={(page) => - this.userOverridesPage(page) - } - searchPanel={ - !Utils.getIsEdge() && ( -
- - - v.identity?.id, - )} - environmentId={ - this.props - .environmentId - } - data-test='select-identity' - placeholder='Create an Identity Override...' - value={ - this.state - .selectedIdentity - } - onChange={( - selectedIdentity, - ) => - this.setState( - { - selectedIdentity, - }, - this.addItem, - ) - } - /> - -
- ) - } - renderRow={(identityFlag) => { - const { - enabled, - feature_state_value, - id, - identity, - } = identityFlag - return ( - - -
- - this.toggleUserFlag( - { - enabled, - id, - identity, - }, - ) - } - disabled={Utils.getIsEdge()} - /> -
-
- { - identity.identifier - } -
-
- -
- {feature_state_value !== - null && ( - - )} -
-
- - -
-
-
- ) - }} - renderNoResults={this.renderUserOverridesNoResults()} - isLoading={ - !this.state.userOverrides - } - /> -
- - ) : ( - -
- - )} - - ) - } - - {(!Project.disableAnalytics || - hasCodeReferences) && ( - - Usage - - } - > - {!Project.disableAnalytics && ( -
- -
- )} - {hasCodeReferences && ( - -
-
- Code references -
- - New - -
-
- Code references allow you to track - where feature flags are being used - within your code.{' '} - { - flagsmith.trackEvent( - 'code_references_click_docs', - { - feature_id: projectFlag.id, - }, - ) - }} - > - Learn more - -
- -
- )} -
- )} - { - - - - } - {hasIntegrationWithGithub && - projectFlag?.id && ( - - Links - - } - > - - - )} - {!existingChangeRequest && - this.props.flagId && - isVersioned && ( - - - - )} - {!existingChangeRequest && ( - - Settings{' '} - {this.state.settingsChanged && ( -
- {'*'} -
- )} - - } - > - { - const updates = {} - - // Update projectFlag with changes - updates.projectFlag = { - ...this.state.projectFlag, - ...changes, - } - - // Set settingsChanged flag unless it's only metadata changing - if (changes.metadata === undefined) { - updates.settingsChanged = true - } - - this.setState(updates) - }} - onHasMetadataRequiredChange={( - hasMetadataRequired, - ) => - this.setState({ - hasMetadataRequired, - }) - } - /> - - - {isEdit && ( -
- {!!createFeature && ( - <> -

- This will save the above - settings{' '} - - all environments - - . -

- - - )} -
- )} -
- )} - - - ) : ( -
- - this.setState({ featureLimitAlert }) - } - /> -
- - this.setState({ - projectFlag: { - ...this.state.projectFlag, - name, - }, - }) - } - caseSensitive={caseSensitive} - regex={regex} - regexValid={regexValid} - autoFocus - /> -
- { - this.setState({ - environmentFlag: { - ...this.state.environmentFlag, - ...changes, - }, - valueChanged: true, - }) - }} - onProjectFlagChange={(changes) => { - this.setState({ - projectFlag: { - ...this.state.projectFlag, - ...changes, - }, - }) - }} - onRemoveMultivariateOption={ - this.props.removeMultivariateOption - } - onHasMetadataRequiredChange={( - hasMetadataRequired, - ) => { - this.setState({ - hasMetadataRequired, - }) - }} - featureError={ - this.parseError(error).featureError - } - featureWarning={ - this.parseError(error).featureWarning - } - /> - -
- )} - {identity && ( -
- {identity ? ( -
-

- This will update the feature value for the - user {identityName} in - - {' '} - { - _.find(project.environments, { - api_key: this.props.environmentId, - }).name - } - . - - { - ' Any segment overrides for this feature will now be ignored.' - } -

-
- ) : ( - '' - )} - -
- {identity && ( - - {({ permission: savePermission }) => - Utils.renderWithPermission( - savePermission, - EnvironmentPermission.UPDATE_FEATURE_STATE, -
- -
, - ) - } -
- )} -
-
- )} -
- ) - }} -
- )} - - ) - }} - - )} - - ) - } -} - -Index.propTypes = {} - -//This will remount the modal when a feature is created -const FeatureProvider = (WrappedComponent) => { - class HOC extends Component { - constructor(props) { - super(props) - this.state = { - ...props, - } - ES6Component(this) - } - - componentDidMount() { - // toast update feature - ES6Component(this) - this.listenTo( - FeatureListStore, - 'saved', - ({ - changeRequest, - createdFlag, - error, - isCreate, - updatedChangeRequest, - } = {}) => { - if (error?.data?.metadata) { - error.data.metadata?.forEach((m) => { - if (Object.keys(m).length > 0) { - toast(m.non_field_errors[0], 'danger') - } - }) - } else if (error?.data) { - toast('Error updating the Flag', 'danger') - return - } else { - const isEditingChangeRequest = - this.props.changeRequest && changeRequest - const operation = createdFlag || isCreate ? 'Created' : 'Updated' - const type = changeRequest ? 'Change Request' : 'Feature' - - const toastText = isEditingChangeRequest - ? `Updated ${type}` - : `${operation} ${type}` - const toastAction = changeRequest - ? { - buttonText: 'Open', - onClick: () => { - closeModal() - this.props.history.push( - `/project/${this.props.projectId}/environment/${this.props.environmentId}/change-requests/${updatedChangeRequest?.id}`, - ) - }, - } - : undefined - - toast(toastText, 'success', undefined, toastAction) - } - const envFlags = FeatureListStore.getEnvironmentFlags() - - if (createdFlag) { - const projectFlag = FeatureListStore.getProjectFlags()?.find?.( - (flag) => flag.name === createdFlag, - ) - window.history.replaceState( - {}, - `${document.location.pathname}?feature=${projectFlag.id}`, - ) - const newEnvironmentFlag = envFlags?.[projectFlag.id] || {} - setModalTitle(`Edit Feature ${projectFlag.name}`) - this.setState({ - environmentFlag: { - ...this.state.environmentFlag, - ...(newEnvironmentFlag || {}), - }, - projectFlag, - segmentsChanged: false, - settingsChanged: false, - valueChanged: false, - }) - } else if (this.props.projectFlag) { - //update the environmentFlag and projectFlag to the new values - const newEnvironmentFlag = - envFlags?.[this.props.projectFlag.id] || {} - const newProjectFlag = FeatureListStore.getProjectFlags()?.find?.( - (flag) => flag.id === this.props.projectFlag.id, - ) - this.setState({ - environmentFlag: { - ...this.state.environmentFlag, - ...(newEnvironmentFlag || {}), - }, - projectFlag: newProjectFlag, - segmentsChanged: false, - settingsChanged: false, - valueChanged: false, - }) - } - }, - ) - } - - render() { - return ( - - ) - } - } - return HOC -} - -const WrappedCreateFlag = ConfigProvider(withSegmentOverrides(Index)) - -export default FeatureProvider(WrappedCreateFlag)