diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts
index 8a8c91ba99f9..85dde7c4edc2 100644
--- a/frontend/common/constants.ts
+++ b/frontend/common/constants.ts
@@ -1,9 +1,17 @@
import { OAuthType } from './types/requests'
import { SegmentCondition } from './types/responses'
import Utils from './utils/utils'
-
import Project from './project'
import { integrationCategories } from 'components/pages/IntegrationsPage'
+import {
+ EnvironmentPermission,
+ EnvironmentPermissionDescriptions,
+ OrganisationPermission,
+ OrganisationPermissionDescriptions,
+ ProjectPermission,
+ ProjectPermissionDescriptions,
+} from './types/permissions.types'
+
const keywords = {
FEATURE_FUNCTION: 'myCoolFeature',
FEATURE_NAME: 'my_cool_feature',
@@ -252,8 +260,13 @@ const Constants = {
value: '',
} as SegmentCondition,
defaultTagColor: '#3d4db6',
- environmentPermissions: (perm: string) =>
- `To manage this feature you need the ${perm} permission for this environment. Please contact a member of this environment who has administrator privileges.`,
+ environmentPermissions: (perm: EnvironmentPermission) => {
+ const description =
+ perm === 'ADMIN'
+ ? 'Administrator'
+ : EnvironmentPermissionDescriptions[perm]
+ return `To manage this feature you need the ${description} permission for this environment. Please contact a member of this environment who has administrator privileges.`
+ },
events: {
'ACCEPT_INVITE': (org: any) => ({
'category': 'Invite',
@@ -623,8 +636,13 @@ const Constants = {
modals: {
'PAYMENT': 'Payment Modal',
},
- organisationPermissions: (perm: string) =>
- `To manage this feature you need the ${perm} permission for this organisastion. Please contact a member of this organisation who has administrator privileges.`,
+ organisationPermissions: (perm: OrganisationPermission) => {
+ const description =
+ perm === 'ADMIN'
+ ? 'Administrator'
+ : OrganisationPermissionDescriptions[perm]
+ return `To manage this feature you need the ${description} permission for this organisation. Please contact a member of this organisation who has administrator privileges.`
+ },
pages: {
'ACCOUNT': 'Account Page',
'AUDIT_LOG': 'Audit Log Page',
@@ -656,8 +674,11 @@ const Constants = {
'#FFBE71',
'#F57C78',
],
- projectPermissions: (perm: string) =>
- `To use this feature you need the ${perm} permission for this project. Please contact a member of this project who has administrator privileges.`,
+ projectPermissions: (perm: ProjectPermission) => {
+ const description =
+ perm === 'ADMIN' ? 'Administrator' : ProjectPermissionDescriptions[perm]
+ return `To use this feature you need the ${description} permission for this project. Please contact a member of this project who has administrator privileges.`
+ },
resourceTypes: {
GITHUB_ISSUE: {
id: 1,
diff --git a/frontend/common/providers/Permission.tsx b/frontend/common/providers/Permission.tsx
index e5f83db8b9ab..6e700d251658 100644
--- a/frontend/common/providers/Permission.tsx
+++ b/frontend/common/providers/Permission.tsx
@@ -1,29 +1,93 @@
import React, { FC, ReactNode, useMemo } from 'react'
import { useGetPermissionQuery } from 'common/services/usePermission'
-import { PermissionLevel } from 'common/types/requests'
import AccountStore from 'common/stores/account-store'
import intersection from 'lodash/intersection'
import { cloneDeep } from 'lodash'
+import Utils from 'common/utils/utils'
+import Constants from 'common/constants'
+import {
+ ADMIN_PERMISSION,
+ EnvironmentPermission,
+ OrganisationPermission,
+ ProjectPermission,
+} from 'common/types/permissions.types'
-type PermissionType = {
- id: any
- permission: string
+/**
+ * Base props shared across all permission levels
+ */
+type BasePermissionProps = {
+ id: number | string
tags?: number[]
- level: PermissionLevel
- children: (data: { permission: boolean; isLoading: boolean }) => ReactNode
+ children:
+ | ReactNode
+ | ((data: { permission: boolean; isLoading: boolean }) => ReactNode)
+ fallback?: ReactNode
+ permissionName?: string
+ showTooltip?: boolean
}
+/**
+ * Discriminated union types for each permission level
+ * This means we can detect a mismatch between level and permission
+ */
+type OrganisationLevelProps = BasePermissionProps & {
+ level: 'organisation'
+ permission: OrganisationPermission
+}
+
+type ProjectLevelProps = BasePermissionProps & {
+ level: 'project'
+ permission: ProjectPermission
+}
+
+type EnvironmentLevelProps = BasePermissionProps & {
+ level: 'environment'
+ permission: EnvironmentPermission
+}
+
+type PermissionType =
+ | OrganisationLevelProps
+ | ProjectLevelProps
+ | EnvironmentLevelProps
+
+type UseHasPermissionParams = {
+ id: number | string
+ level: 'organisation' | 'project' | 'environment'
+ permission: OrganisationPermission | ProjectPermission | EnvironmentPermission
+ tags?: number[]
+}
+
+/**
+ * Hook to check if the current user has a specific permission
+ *
+ * Fetches permission data and checks if the user has the requested permission.
+ * Supports tag-based permissions where additional permissions can be granted
+ * based on tag intersection.
+ *
+ * @param {Object} params - The permission check parameters
+ * @param {number | string} params.id - The resource ID to check permissions for
+ * @param {PermissionLevel} params.level - The permission level to check at
+ * @param {string} params.permission - The permission key to check
+ * @param {number[]} [params.tags] - Optional tag IDs for tag-based permission checking
+ * @returns {Object} Object containing permission status and loading state
+ * @returns {boolean} returns.isLoading - Whether the permission data is still loading
+ * @returns {boolean} returns.isSuccess - Whether the permission data was fetched successfully
+ * @returns {boolean} returns.permission - Whether the user has the requested permission
+ */
export const useHasPermission = ({
id,
level,
permission,
tags,
-}: Omit) => {
+}: UseHasPermissionParams) => {
const {
data: permissionsData,
isLoading,
isSuccess,
- } = useGetPermissionQuery({ id: `${id}`, level }, { skip: !id || !level })
+ } = useGetPermissionQuery(
+ { id: id as number, level },
+ { skip: !id || !level },
+ )
const data = useMemo(() => {
if (!tags?.length || !permissionsData?.tag_based_permissions)
return permissionsData
@@ -45,11 +109,68 @@ export const useHasPermission = ({
}
}
+/**
+ * Permission component for conditional rendering based on user permissions
+ *
+ * This component checks if the current user has a specific permission and conditionally
+ * renders its children. It supports multiple rendering patterns:
+ *
+ * @example
+ * // Basic usage with simple children
+ *
+ *
+ *
+ *
+ * @example
+ * // Using render function to access permission state
+ *
+ * {({ permission, isLoading }) => (
+ *
+ * )}
+ *
+ *
+ * @example
+ * // With tooltip on permission denial
+ *
+ *
+ *
+ *
+ * @example
+ * // With fallback content
+ * You don't have permission to delete features}
+ * >
+ *
+ *
+ *
+ * @example
+ * // With tag-based permissions
+ *
+ *
+ *
+ */
const Permission: FC = ({
children,
+ fallback,
id,
level,
permission,
+ permissionName,
+ showTooltip = false,
tags,
}) => {
const { isLoading, permission: hasPermission } = useHasPermission({
@@ -58,14 +179,63 @@ const Permission: FC = ({
permission,
tags,
})
- return (
- <>
- {children({
- isLoading,
- permission: hasPermission || AccountStore.isAdmin(),
- }) || null}
- >
- )
+
+ const finalPermission = hasPermission || AccountStore.isAdmin()
+
+ const getPermissionDescription = (): string => {
+ if (permission === ADMIN_PERMISSION) {
+ switch (level) {
+ case 'environment':
+ return Constants.environmentPermissions(ADMIN_PERMISSION)
+ case 'project':
+ return Constants.projectPermissions(ADMIN_PERMISSION)
+ default:
+ return Constants.organisationPermissions(ADMIN_PERMISSION)
+ }
+ }
+
+ switch (level) {
+ case 'environment':
+ return Constants.environmentPermissions(
+ permission as EnvironmentPermission,
+ )
+ case 'project':
+ return Constants.projectPermissions(permission as ProjectPermission)
+ default:
+ return Constants.organisationPermissions(
+ permission as OrganisationPermission,
+ )
+ }
+ }
+
+ const tooltipMessage = permissionName || getPermissionDescription()
+
+ if (typeof children === 'function') {
+ const renderedChildren = children({
+ isLoading,
+ permission: finalPermission,
+ })
+
+ if (finalPermission || !showTooltip) {
+ return <>{renderedChildren || null}>
+ }
+
+ return Utils.renderWithPermission(
+ finalPermission,
+ tooltipMessage,
+ renderedChildren,
+ )
+ }
+
+ if (finalPermission) {
+ return <>{children}>
+ }
+
+ if (showTooltip) {
+ return Utils.renderWithPermission(finalPermission, tooltipMessage, children)
+ }
+
+ return <>{fallback || null}>
}
export default Permission
diff --git a/frontend/common/providers/withSegmentOverrides.js b/frontend/common/providers/withSegmentOverrides.js
index a958e8a51e70..b3fbbbe95e62 100644
--- a/frontend/common/providers/withSegmentOverrides.js
+++ b/frontend/common/providers/withSegmentOverrides.js
@@ -1,6 +1,7 @@
import data from 'common/data/base/_data'
import ProjectStore from 'common/stores/project-store'
import FeatureListStore from 'common/stores/feature-list-store'
+import { mergeChangeSets } from 'common/services/useChangeRequest'
export default (WrappedComponent) => {
class HOC extends React.Component {
@@ -28,6 +29,7 @@ export default (WrappedComponent) => {
getOverrides = () => {
if (this.props.projectFlag) {
//todo: migrate to useSegmentFeatureState
+ const projectId = this.props.projectFlag.project
Promise.all([
data.get(
`${
@@ -43,7 +45,12 @@ export default (WrappedComponent) => {
this.props.environmentId,
)}&feature=${this.props.projectFlag.id}`,
),
- ]).then(([res, res2]) => {
+ this.props.changeRequest?.change_sets
+ ? data.get(
+ `${Project.api}projects/${projectId}/segments/?page_size=1000`,
+ )
+ : Promise.resolve({ results: [] }),
+ ]).then(([res, res2, segmentsRes]) => {
const results = res.results
const featureStates = res2.results
const environmentOverride = res2.results.find(
@@ -72,18 +79,115 @@ export default (WrappedComponent) => {
}
}
})
- const resResults = res.results || []
- const segmentOverrides = results
- .concat(
+
+ let segmentOverrides
+ if (this.props.changeRequest?.change_sets) {
+ // Add changesets to existing segment overrides
+ const mergedFeatureStates = mergeChangeSets(
+ this.props.changeRequest.change_sets,
+ featureStates,
+ this.props.changeRequest.conflicts,
+ )
+
+ // Get segment IDs marked for deletion
+ const segmentIdsToDelete =
+ this.props.changeRequest.change_sets?.flatMap(
+ (changeSet) => changeSet.segment_ids_to_delete_overrides || [],
+ ) || []
+
+ segmentOverrides = results.map((currentSegmentOverride) => {
+ const changedFeatureState = mergedFeatureStates.find(
+ (featureState) =>
+ featureState.feature_segment?.segment ===
+ currentSegmentOverride.segment,
+ )
+
+ // Any segment_ids_to_delete_overrides should be marked as toRemove
+ const toRemove = segmentIdsToDelete.includes(
+ currentSegmentOverride.segment,
+ )
+
+ if (changedFeatureState) {
+ return {
+ ...currentSegmentOverride,
+ ...changedFeatureState,
+ id: changedFeatureState.id || currentSegmentOverride.id,
+ is_feature_specific:
+ changedFeatureState.feature_segment?.is_feature_specific,
+ multivariate_options:
+ changedFeatureState.multivariate_feature_state_values || [],
+ priority: changedFeatureState.feature_segment?.priority || 0,
+ segment: changedFeatureState.feature_segment?.segment,
+ segment_name:
+ changedFeatureState.feature_segment?.segment_name ||
+ currentSegmentOverride.segment_name,
+ toRemove,
+ uuid:
+ changedFeatureState.feature_segment?.uuid ||
+ currentSegmentOverride.uuid,
+ value: Utils.featureStateToValue(
+ changedFeatureState.feature_state_value,
+ ),
+ }
+ }
+
+ return toRemove
+ ? { ...currentSegmentOverride, toRemove }
+ : currentSegmentOverride
+ })
+
+ // Add any new segment overrides from the changesets
+ mergedFeatureStates
+ .filter(
+ (featureState) =>
+ !!featureState.feature_segment?.segment &&
+ !results.find(
+ (currentOverride) =>
+ currentOverride.segment ===
+ featureState.feature_segment.segment,
+ ),
+ )
+ .forEach((newFeatureState) => {
+ // Look up segment metadata from segments API to get segment name
+ const segmentMetadata = segmentsRes.results?.find(
+ (segment) =>
+ segment.id === newFeatureState.feature_segment?.segment,
+ )
+
+ segmentOverrides.push({
+ enabled: newFeatureState.enabled,
+ environment: newFeatureState.environment,
+ feature: newFeatureState.feature,
+ id: newFeatureState.id,
+ is_feature_specific:
+ newFeatureState.feature_segment?.is_feature_specific,
+ multivariate_options:
+ newFeatureState.multivariate_feature_state_values || [],
+ priority: newFeatureState.feature_segment?.priority || 0,
+ segment: newFeatureState.feature_segment?.segment,
+ segment_name:
+ newFeatureState.feature_segment?.segment_name ||
+ segmentMetadata?.name ||
+ 'Unknown Segment',
+ uuid: newFeatureState.feature_segment?.uuid,
+ value: Utils.featureStateToValue(
+ newFeatureState.feature_state_value,
+ ),
+ })
+ })
+ } else {
+ segmentOverrides = results.concat(
(this.props.newSegmentOverrides || []).map((v, i) => ({
...v,
})),
)
- .map((v, i) => ({
- ...v,
- originalPriority: i,
- priority: i,
- }))
+ }
+ segmentOverrides = segmentOverrides.map((v, i) => ({
+ ...v,
+ originalPriority: i,
+ priority: i,
+ }))
+
const originalSegmentOverrides = _.cloneDeep(segmentOverrides)
this.setState({
environmentVariations:
diff --git a/frontend/common/types/permissions.types.ts b/frontend/common/types/permissions.types.ts
new file mode 100644
index 000000000000..67be34c6d90b
--- /dev/null
+++ b/frontend/common/types/permissions.types.ts
@@ -0,0 +1,103 @@
+export const ADMIN_PERMISSION = 'ADMIN' as const
+export const ADMIN_PERMISSION_DESCRIPTION = 'Administrator' as const
+
+// Organization Permissions
+enum OrganisationPermissionEnum {
+ CREATE_PROJECT = 'CREATE_PROJECT',
+ MANAGE_USERS = 'MANAGE_USERS',
+ MANAGE_USER_GROUPS = 'MANAGE_USER_GROUPS',
+}
+export const OrganisationPermissionDescriptions = {
+ [OrganisationPermissionEnum.CREATE_PROJECT]: 'Create project',
+ [OrganisationPermissionEnum.MANAGE_USERS]: 'Manage users',
+ [OrganisationPermissionEnum.MANAGE_USER_GROUPS]: 'Manage user groups',
+} as const
+
+export type OrganisationPermission =
+ | OrganisationPermissionEnum
+ | typeof ADMIN_PERMISSION
+export const OrganisationPermission = OrganisationPermissionEnum
+
+export type OrganisationPermissionDescription =
+ | (typeof OrganisationPermissionDescriptions)[keyof typeof OrganisationPermissionDescriptions]
+ | typeof ADMIN_PERMISSION_DESCRIPTION
+
+// Project Permissions
+enum ProjectPermissionEnum {
+ VIEW_PROJECT = 'VIEW_PROJECT',
+ CREATE_ENVIRONMENT = 'CREATE_ENVIRONMENT',
+ DELETE_FEATURE = 'DELETE_FEATURE',
+ CREATE_FEATURE = 'CREATE_FEATURE',
+ MANAGE_SEGMENTS = 'MANAGE_SEGMENTS',
+ VIEW_AUDIT_LOG = 'VIEW_AUDIT_LOG',
+ MANAGE_TAGS = 'MANAGE_TAGS',
+ MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS = 'MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS',
+ APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS = 'APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS',
+ CREATE_PROJECT_LEVEL_CHANGE_REQUESTS = 'CREATE_PROJECT_LEVEL_CHANGE_REQUESTS',
+}
+export const ProjectPermissionDescriptions = {
+ [ProjectPermissionEnum.VIEW_PROJECT]: 'View project',
+ [ProjectPermissionEnum.CREATE_ENVIRONMENT]: 'Create environment',
+ [ProjectPermissionEnum.DELETE_FEATURE]: 'Delete feature',
+ [ProjectPermissionEnum.CREATE_FEATURE]: 'Create feature',
+ [ProjectPermissionEnum.MANAGE_SEGMENTS]: 'Manage segments',
+ [ProjectPermissionEnum.VIEW_AUDIT_LOG]: 'View audit log',
+ [ProjectPermissionEnum.MANAGE_TAGS]: 'Manage tags',
+ [ProjectPermissionEnum.MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS]:
+ 'Manage project level change requests',
+ [ProjectPermissionEnum.APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS]:
+ 'Approve project level change requests',
+ [ProjectPermissionEnum.CREATE_PROJECT_LEVEL_CHANGE_REQUESTS]:
+ 'Create project level change requests',
+} as const
+
+export type ProjectPermission = ProjectPermissionEnum | typeof ADMIN_PERMISSION
+export const ProjectPermission = ProjectPermissionEnum
+
+export type ProjectPermissionDescription =
+ | (typeof ProjectPermissionDescriptions)[keyof typeof ProjectPermissionDescriptions]
+ | typeof ADMIN_PERMISSION_DESCRIPTION
+
+// Environment Permissions
+enum EnvironmentPermissionEnum {
+ VIEW_ENVIRONMENT = 'VIEW_ENVIRONMENT',
+ UPDATE_FEATURE_STATE = 'UPDATE_FEATURE_STATE',
+ MANAGE_IDENTITIES = 'MANAGE_IDENTITIES',
+ CREATE_CHANGE_REQUEST = 'CREATE_CHANGE_REQUEST',
+ APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST',
+ VIEW_IDENTITIES = 'VIEW_IDENTITIES',
+ MANAGE_SEGMENT_OVERRIDES = 'MANAGE_SEGMENT_OVERRIDES',
+}
+export const EnvironmentPermissionDescriptions = {
+ [EnvironmentPermissionEnum.VIEW_ENVIRONMENT]: 'View environment',
+ [EnvironmentPermissionEnum.UPDATE_FEATURE_STATE]: 'Update feature state',
+ [EnvironmentPermissionEnum.MANAGE_IDENTITIES]: 'Manage identities',
+ [EnvironmentPermissionEnum.CREATE_CHANGE_REQUEST]: 'Create change request',
+ [EnvironmentPermissionEnum.APPROVE_CHANGE_REQUEST]: 'Approve change request',
+ [EnvironmentPermissionEnum.VIEW_IDENTITIES]: 'View identities',
+ [EnvironmentPermissionEnum.MANAGE_SEGMENT_OVERRIDES]:
+ 'Manage segment overrides',
+} as const
+
+export type EnvironmentPermission =
+ | EnvironmentPermissionEnum
+ | typeof ADMIN_PERMISSION
+export const EnvironmentPermission = EnvironmentPermissionEnum
+
+export type EnvironmentPermissionDescription =
+ | (typeof EnvironmentPermissionDescriptions)[keyof typeof EnvironmentPermissionDescriptions]
+ | typeof ADMIN_PERMISSION_DESCRIPTION
+
+export type Permission =
+ | OrganisationPermission
+ | ProjectPermission
+ | EnvironmentPermission
+ | typeof ADMIN_PERMISSION
+
+// Combined permission descriptions record
+export const PermissionDescriptions = {
+ ...OrganisationPermissionDescriptions,
+ ...ProjectPermissionDescriptions,
+ ...EnvironmentPermissionDescriptions,
+ [ADMIN_PERMISSION]: ADMIN_PERMISSION_DESCRIPTION,
+} as const
diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx
index 25434274a790..70d12d5d5f52 100644
--- a/frontend/common/utils/utils.tsx
+++ b/frontend/common/utils/utils.tsx
@@ -9,11 +9,11 @@ import {
MultivariateFeatureStateValue,
MultivariateOption,
Organisation,
+ PConfidence,
Project as ProjectType,
ProjectFlag,
SegmentCondition,
Tag,
- PConfidence,
UserPermissions,
} from 'common/types/responses'
import flagsmith from 'flagsmith'
@@ -28,6 +28,15 @@ import { selectBuildVersion } from 'common/services/useBuildVersion'
import { getStore } from 'common/store'
import { TRACKED_UTMS, UtmsType } from 'common/types/utms'
import { TimeUnit } from 'components/release-pipelines/constants'
+import {
+ ADMIN_PERMISSION,
+ ADMIN_PERMISSION_DESCRIPTION,
+ EnvironmentPermission,
+ EnvironmentPermissionDescription,
+ EnvironmentPermissionDescriptions,
+ OrganisationPermission,
+ OrganisationPermissionDescriptions,
+} from 'common/types/permissions.types'
const semver = require('semver')
@@ -218,15 +227,15 @@ const Utils = Object.assign({}, require('./base/_utils'), {
},
getCreateProjectPermission(organisation: Organisation) {
if (organisation?.restrict_project_create_to_admin) {
- return 'ADMIN'
+ return ADMIN_PERMISSION
}
- return 'CREATE_PROJECT'
+ return OrganisationPermission.CREATE_PROJECT
},
getCreateProjectPermissionDescription(organisation: Organisation) {
if (organisation?.restrict_project_create_to_admin) {
- return 'Administrator'
+ return ADMIN_PERMISSION_DESCRIPTION
}
- return 'Create Project'
+ return OrganisationPermissionDescriptions.CREATE_PROJECT
},
getExistingWaitForTime: (
waitFor: string | undefined,
@@ -376,22 +385,15 @@ const Utils = Object.assign({}, require('./base/_utils'), {
},
getManageFeaturePermission(isChangeRequest: boolean) {
if (isChangeRequest) {
- return 'CREATE_CHANGE_REQUEST'
+ return EnvironmentPermission.CREATE_CHANGE_REQUEST
}
- return 'UPDATE_FEATURE_STATE'
+ return EnvironmentPermission.UPDATE_FEATURE_STATE
},
getManageFeaturePermissionDescription(isChangeRequest: boolean) {
if (isChangeRequest) {
- return 'Create Change Request'
+ return EnvironmentPermissionDescriptions.CREATE_CHANGE_REQUEST
}
- return 'Update Feature State'
- },
-
- getManageUserPermission() {
- return 'MANAGE_IDENTITIES'
- },
- getManageUserPermissionDescription() {
- return 'Manage Identities'
+ return EnvironmentPermissionDescriptions.UPDATE_FEATURE_STATE
},
getNextPlan: (skipFree?: boolean) => {
@@ -423,7 +425,12 @@ const Utils = Object.assign({}, require('./base/_utils'), {
const organisationId = match?.params?.organisationId
return organisationId ? parseInt(organisationId) : null
},
- getOverridePermission: (level: 'identity' | 'segment') => {
+ getOverridePermission: (
+ level: 'identity' | 'segment',
+ ): {
+ permission: EnvironmentPermission
+ permissionDescription: EnvironmentPermissionDescription
+ } => {
switch (level) {
case 'identity':
return {
@@ -433,8 +440,9 @@ const Utils = Object.assign({}, require('./base/_utils'), {
}
default:
return {
- permission: 'MANAGE_SEGMENT_OVERRIDES',
- permissionDescription: 'Manage Segment Overrides',
+ permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES,
+ permissionDescription:
+ EnvironmentPermissionDescriptions.MANAGE_SEGMENT_OVERRIDES,
}
}
},
@@ -622,9 +630,6 @@ const Utils = Object.assign({}, require('./base/_utils'), {
return utms
}, {} as UtmsType)
},
- getViewIdentitiesPermission() {
- return 'VIEW_IDENTITIES'
- },
hasEntityPermission(key: string, entityPermissions: UserPermissions) {
if (entityPermissions?.admin) return true
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index cbcc39af0b39..11b242e30ee6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -130,6 +130,7 @@
"@types/classnames": "^2.3.1",
"@types/color": "^3.0.3",
"@types/dompurify": "^3.0.2",
+ "@types/rc-switch": "^1.9.5",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/react-select": "^2.0.3",
@@ -4966,6 +4967,16 @@
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
+ "node_modules/@types/rc-switch": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/@types/rc-switch/-/rc-switch-1.9.5.tgz",
+ "integrity": "sha512-pah8pI9LwjppjzD2rAd2p9AdWxCoQzKYff0zCIHAiVpAxUI60U9vmNVbosunpEmOvbzaChhRnWgeWwTRweLAgA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "17.0.87",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.87.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8096c952e14c..a3be9c083697 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -155,6 +155,7 @@
"@types/classnames": "^2.3.1",
"@types/color": "^3.0.3",
"@types/dompurify": "^3.0.2",
+ "@types/rc-switch": "^1.9.5",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/react-select": "^2.0.3",
diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx
index 68fa9224ecd9..85d4e90ca1ef 100644
--- a/frontend/web/components/BreadcrumbSeparator.tsx
+++ b/frontend/web/components/BreadcrumbSeparator.tsx
@@ -414,7 +414,7 @@ const BreadcrumbSeparator: FC = ({
footer={Utils.renderWithPermission(
canCreateProject,
Constants.organisationPermissions(
- Utils.getCreateProjectPermissionDescription(
+ Utils.getCreateProjectPermission(
AccountStore.getOrganisation(),
),
),
diff --git a/frontend/web/components/CompareIdentities.tsx b/frontend/web/components/CompareIdentities.tsx
index e0aaf3630f0d..98c3ef1c9455 100644
--- a/frontend/web/components/CompareIdentities.tsx
+++ b/frontend/web/components/CompareIdentities.tsx
@@ -23,6 +23,7 @@ import IdentityOverridesIcon from './IdentityOverridesIcon'
import Tooltip from './Tooltip'
import PageTitle from './PageTitle'
import { getDarkMode } from 'project/darkMode'
+import { EnvironmentPermission } from 'common/types/permissions.types'
type CompareIdentitiesType = {
projectId: string
@@ -69,7 +70,7 @@ const CompareIdentities: FC = ({
const { isLoading: permissionLoading, permission } = useHasPermission({
id: environmentId,
level: 'environment',
- permission: Utils.getViewIdentitiesPermission(),
+ permission: EnvironmentPermission.VIEW_IDENTITIES,
})
const { data: leftUser } = useGetIdentityFeatureStatesAllQuery(
@@ -118,12 +119,13 @@ const CompareIdentities: FC = ({
const isEdge = Utils.getIsEdge()
const goUser = (user: IdentitySelectType['value'], feature: string) => {
+ if (!user) return
window.open(
`${
document.location.origin
}/project/${projectId}/environment/${environmentId}/users/${encodeURIComponent(
- user!.label,
- )}/${user!.value}?flag=${encodeURIComponent(feature)}`,
+ user.label,
+ )}/${user.value}?flag=${encodeURIComponent(feature)}`,
'_blank',
)
}
@@ -189,7 +191,9 @@ const CompareIdentities: FC = ({
{!permission && !permissionLoading ? (
) : (
diff --git a/frontend/web/components/Feature.js b/frontend/web/components/Feature.js
deleted file mode 100644
index 73258aca6c1b..000000000000
--- a/frontend/web/components/Feature.js
+++ /dev/null
@@ -1,209 +0,0 @@
-// import propTypes from 'prop-types';
-import React, { PureComponent } from 'react'
-import ValueEditor from './ValueEditor'
-import Constants from 'common/constants'
-import { VariationOptions } from './mv/VariationOptions'
-import { AddVariationButton } from './mv/AddVariationButton'
-import ErrorMessage from './ErrorMessage'
-import Tooltip from './Tooltip'
-import Icon from './Icon'
-import InputGroup from './base/forms/InputGroup'
-import WarningMessage from './WarningMessage'
-
-function isNegativeNumberString(str) {
- if (typeof Utils.getTypedValue(str) !== 'number') {
- return false
- }
- if (typeof str !== 'string') {
- return false
- }
- const num = parseFloat(str)
- return !isNaN(num) && num < 0
-}
-
-export default class Feature extends PureComponent {
- static displayName = 'Feature'
-
- constructor(props) {
- super(props)
- this.state = {
- isNegativeNumberString: isNegativeNumberString(
- props.environmentFlag?.feature_state_value,
- ),
- }
- }
- removeVariation = (i) => {
- const idToRemove = this.props.multivariate_options[i].id
-
- if (idToRemove) {
- openConfirm({
- body: 'This will remove the variation on your feature for all environments, if you wish to turn it off just for this environment you can set the % value to 0.',
- destructive: true,
- onYes: () => {
- this.props.removeVariation(i)
- },
- title: 'Delete variation',
- yesText: 'Confirm',
- })
- } else {
- this.props.removeVariation(i)
- }
- }
-
- render() {
- const {
- checked,
- environmentFlag,
- environmentVariations,
- error,
- identity,
- isEdit,
- multivariate_options,
- onCheckedChange,
- onValueChange,
- projectFlag,
- readOnly,
- value,
- } = this.props
-
- const enabledString = isEdit ? 'Enabled' : 'Enabled by default'
- const controlPercentage = Utils.calculateControl(multivariate_options)
- const valueString = identity
- ? 'User override'
- : !!multivariate_options && multivariate_options.length
- ? `Control Value - ${controlPercentage}%`
- : `Value`
-
- const showValue = !(
- !!identity &&
- multivariate_options &&
- !!multivariate_options.length
- )
- return (
-
-
-
-
-
- {enabledString || 'Enabled'}
-
- {!isEdit && }
-
- }
- >
- {!isEdit &&
- 'This will determine the initial enabled state for all environments. You can edit the this individually for each environment once the feature is created.'}
-
-
-
- {showValue && (
-
-
- }
- tooltip={`${Constants.strings.REMOTE_CONFIG_DESCRIPTION}${
- !isEdit
- ? ' Setting this when creating a feature will set the value for all environments. You can edit this individually for each environment once the feature is created.'
- : ''
- }`}
- title={`${valueString}`}
- />
-
- )}
- {this.state.isNegativeNumberString && (
-
- This feature currently has the value of{' '}
- "{environmentFlag?.feature_state_value}".
- Saving this feature will convert its value from a string to a
- number. If you wish to preserve this value as a string, please
- save it using the{' '}
-
- API
-
- .
-
- }
- />
- )}
-
- {!!error && (
-