diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b953045d740a..c69dfe57f993 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",
@@ -2780,9 +2780,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 7a1e53625582..e22927246a51 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
new file mode 100644
index 000000000000..08372eea3de7
--- /dev/null
+++ b/frontend/web/components/modals/create-feature/index.js
@@ -0,0 +1,2069 @@
+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.setState({
+ showCreateSegment: true,
+ })
+ }}
+ theme='outline'
+ disabled={
+ !!isLimitReached
+ }
+ >
+ Create Feature-Specific
+ Segment
+
+
+ )
+ }
+
+ {!this.state.showCreateSegment &&
+ !noPermissions && (
+
+ this.changeSegment(
+ this.props
+ .segmentOverrides,
+ )
+ }
+ type='button'
+ theme='secondary'
+ size='small'
+ >
+ {enabledSegment
+ ? 'Enable All'
+ : 'Disable All'}
+
+ )}
+
+ {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,
+ ),
+
+ saveFeatureSegments(
+ false,
+ )
+ }
+ type='button'
+ data-test='update-feature-segments-btn'
+ id='update-feature-segments-btn'
+ disabled={
+ isSaving ||
+ !projectFlag.name ||
+ invalid ||
+ !savePermission
+ }
+ >
+ {getButtonText()}
+ ,
+ )
+ }
+
+ return Utils.renderWithPermission(
+ manageSegmentsOverrides,
+ Constants.environmentPermissions(
+ EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES,
+ ),
+ <>
+ {!is4Eyes &&
+ isVersioned && (
+ <>
+
+ saveFeatureSegments(
+ true,
+ )
+ }
+ className='mr-2'
+ type='button'
+ data-test='create-change-request'
+ id='create-change-request-btn'
+ disabled={
+ isSaving ||
+ !projectFlag.name ||
+ invalid ||
+ !savePermission
+ }
+ >
+ {getScheduleButtonText()}
+
+ >
+ )}
+
+ saveFeatureSegments(
+ false,
+ )
+ }
+ type='button'
+ data-test='update-feature-segments-btn'
+ id='update-feature-segments-btn'
+ disabled={
+ isSaving ||
+ !projectFlag.name ||
+ invalid ||
+ !manageSegmentsOverrides
+ }
+ >
+ {isSaving
+ ? 'Updating'
+ : 'Update Segment Overrides'}
+
+ >,
+ )
+ }}
+
+ )}
+
+
+
+ )}
+
+
+
+
+ )}
+
+ {({ 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() && (
+
+ this.changeIdentity(
+ this.state
+ .userOverrides,
+ )
+ }
+ type='button'
+ theme='secondary'
+ size='small'
+ >
+ {enabledIndentity
+ ? 'Enable All'
+ : 'Disable All'}
+
+ )
+ }
+ 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 && (
+
+ )}
+
+
+
+ {' '}
+ Edit
+
+ {
+ e.stopPropagation()
+ removeUserOverride(
+ {
+ cb: () =>
+ this.userOverridesPage(
+ 1,
+ true,
+ ),
+ environmentId:
+ this.props
+ .environmentId,
+ identifier:
+ identity.identifier,
+ identity:
+ identity.id,
+ identityFlag,
+ projectFlag,
+ },
+ )
+ }}
+ className='btn ml-2 btn-with-icon'
+ >
+
+
+
+
+
+ )
+ }}
+ renderNoResults={this.renderUserOverridesNoResults()}
+ isLoading={
+ !this.state.userOverrides
+ }
+ />
+
+ >
+ ) : (
+
+
+
+ )}
+
+ )
+ }
+
+ {(!Project.disableAnalytics ||
+ hasCodeReferences) && (
+
+ Usage
+
+ }
+ >
+ {!Project.disableAnalytics && (
+
+
+
+ )}
+ {hasCodeReferences && (
+
+
+
+ Code references
+
+
+ New
+
+
+
+
+
+ )}
+
+ )}
+ {
+
+
+
+ }
+ {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
+
+ .
+
+
+ {isSaving
+ ? 'Updating'
+ : 'Update Settings'}
+
+ >
+ )}
+
+ )}
+
+ )}
+
+ >
+ ) : (
+
+
+ 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,
+
+ saveFeatureValue()}
+ data-test='update-feature-btn'
+ id='update-feature-btn'
+ disabled={
+ !savePermission ||
+ isSaving ||
+ !projectFlag.name ||
+ invalid
+ }
+ >
+ {isSaving
+ ? 'Updating'
+ : 'Update Feature'}
+
+
,
+ )
+ }
+
+ )}
+
+
+ )}
+
+ )
+ }}
+
+ )}
+
+ )
+ }}
+
+ )}
+
+ )
+ }
+}
+
+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)
diff --git a/frontend/web/components/modals/create-feature/tabs/UsageTab.tsx b/frontend/web/components/modals/create-feature/tabs/UsageTab.tsx
index c59793c75307..f35df112c9b3 100644
--- a/frontend/web/components/modals/create-feature/tabs/UsageTab.tsx
+++ b/frontend/web/components/modals/create-feature/tabs/UsageTab.tsx
@@ -1,4 +1,5 @@
import React, { FC } from 'react'
+import flagsmith from '@flagsmith/flagsmith'
import Project from 'common/project'
import FeatureAnalytics from 'components/feature-page/FeatureNavTab/FeatureAnalytics'
import FeatureCodeReferencesContainer from 'components/feature-page/FeatureNavTab/CodeReferences/FeatureCodeReferencesContainer'
@@ -48,6 +49,11 @@ const UsageTab: FC = ({
target='_blank'
href='https://docs.flagsmith.com/managing-flags/code-references'
rel='noreferrer'
+ onClick={() => {
+ flagsmith.trackEvent('code_references_click_docs', {
+ feature_id: featureId,
+ })
+ }}
>
Learn more
diff --git a/frontend/web/components/navigation/navbars/ProjectNavbar.tsx b/frontend/web/components/navigation/navbars/ProjectNavbar.tsx
index 19a96788b2c1..e2666ff3355a 100644
--- a/frontend/web/components/navigation/navbars/ProjectNavbar.tsx
+++ b/frontend/web/components/navigation/navbars/ProjectNavbar.tsx
@@ -51,18 +51,18 @@ const ProjectNavbar: FC = ({ environmentId, projectId }) => {
>
Segments
- {Utils.getFlagsmithHasFeature('feature_lifecycle') && (
- }
- id='lifecycle-link'
- to={`/project/${projectId}/lifecycle`}
- isActive={(_, location) =>
- location.pathname.startsWith(`/project/${projectId}/lifecycle`)
- }
- >
- Feature Lifecycle
-
- )}
+ {Utils.getFlagsmithHasFeature('feature_lifecycle') && (
+ }
+ id='lifecycle-link'
+ to={`/project/${projectId}/lifecycle`}
+ isActive={(_, location) =>
+ location.pathname.startsWith(`/project/${projectId}/lifecycle`)
+ }
+ >
+ Feature Lifecycle
+
+ )}