diff --git a/frontend/common/providers/Permission.tsx b/frontend/common/providers/Permission.tsx index 212f41bee1ee..2e965c165838 100644 --- a/frontend/common/providers/Permission.tsx +++ b/frontend/common/providers/Permission.tsx @@ -1,11 +1,13 @@ -import React, { FC, ReactNode } from 'react' +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' // we need this to make JSX compile +import AccountStore from 'common/stores/account-store' +import intersection from 'lodash/intersection' // we need this to make JSX compile type PermissionType = { id: any permission: string + tags?: number[] level: PermissionLevel children: (data: { permission: boolean; isLoading: boolean }) => ReactNode } @@ -14,11 +16,23 @@ export const useHasPermission = ({ id, level, permission, + tags, }: Omit) => { - const { data, isLoading, isSuccess } = useGetPermissionQuery( - { id: `${id}`, level }, - { skip: !id || !level }, - ) + const { + data: permissionsData, + isLoading, + isSuccess, + } = useGetPermissionQuery({ id: `${id}`, level }, { skip: !id || !level }) + const data = useMemo(() => { + if (!tags?.length || !permissionsData?.tag_based_permissions) return permissionsData + const addedPermissions = permissionsData + permissionsData.tag_based_permissions.forEach((tagBasedPermission) => { + if (intersection(tagBasedPermission.tags, tags).length) { + addedPermissions[tagBasedPermission.key] = true + } + }) + return addedPermissions + }, [permissionsData, tags]) const hasPermission = !!data?.[permission] || !!data?.ADMIN return { isLoading, @@ -32,11 +46,13 @@ const Permission: FC = ({ id, level, permission, + tags, }) => { const { isLoading, permission: hasPermission } = useHasPermission({ id, level, permission, + tags, }) return ( <> diff --git a/frontend/common/services/usePermission.ts b/frontend/common/services/usePermission.ts index cbaf91f05a52..b91586aa9ee7 100644 --- a/frontend/common/services/usePermission.ts +++ b/frontend/common/services/usePermission.ts @@ -13,16 +13,25 @@ export const permissionService = service query: ({ id, level }: Req['getPermission']) => ({ url: `${level}s/${id}/my-permissions/`, }), - transformResponse(baseQueryReturnValue: { - admin: boolean - permissions: string[] - }) { + transformResponse( + baseQueryReturnValue: { + admin: boolean + permissions: string[] + tag_based_permissions?: Res['permission']['tag_based_permissions'] + }, + _, + ) { const res: Res['permission'] = { ADMIN: baseQueryReturnValue.admin, } + if (baseQueryReturnValue.tag_based_permissions) { + res.tag_based_permissions = + baseQueryReturnValue.tag_based_permissions + } baseQueryReturnValue.permissions.forEach((v) => { res[v] = true }) + return res }, }), diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index bb0414226b1b..6d47b5caa27d 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -628,7 +628,10 @@ export type Res = { } identity: { id: string } //todo: we don't consider this until we migrate identity-store identities: EdgePagedResponse - permission: Record + permission: Record & { + ADMIN: boolean + tag_based_permissions?: { key: string; tags: number[] }[] + } availablePermissions: AvailablePermission[] tag: Tag tags: Tag[] diff --git a/frontend/web/components/CompareEnvironments.js b/frontend/web/components/CompareEnvironments.js index 780418e16a9a..cf11c7ca3590 100644 --- a/frontend/web/components/CompareEnvironments.js +++ b/frontend/web/components/CompareEnvironments.js @@ -242,6 +242,7 @@ class CompareEnvironments extends Component { = ({ projectId, protectedTags, readOnly, + tags, }) => { const [isOpen, setIsOpen] = useState(false) @@ -147,6 +149,7 @@ export const FeatureAction: FC = ({ {({ permission: removeFeaturePermission }) => diff --git a/frontend/web/components/FeatureRow.js b/frontend/web/components/FeatureRow.js index 5c609d44e63f..0b55c9fc9db9 100644 --- a/frontend/web/components/FeatureRow.js +++ b/frontend/web/components/FeatureRow.js @@ -141,20 +141,20 @@ class TheComponent extends Component { const changeRequestsEnabled = Utils.changeRequestsEnabled( environment && environment.minimum_change_request_approvals, ) - const onChange = ()=> { - if(disableControls) { - return; - } - if ( - projectFlag?.multivariate_options?.length || - Utils.changeRequestsEnabled( - environment.minimum_change_request_approvals, - ) - ) { - this.editFeature(projectFlag, environmentFlags[id]) - return - } - this.confirmToggle() + const onChange = () => { + if (disableControls) { + return + } + if ( + projectFlag?.multivariate_options?.length || + Utils.changeRequestsEnabled( + environment.minimum_change_request_approvals, + ) + ) { + this.editFeature(projectFlag, environmentFlags[id]) + return + } + this.confirmToggle() } const isCompact = getViewMode() === 'compact' if (this.props.condensed) { @@ -381,6 +381,7 @@ class TheComponent extends Component { featureIndex={this.props.index} readOnly={readOnly} protectedTags={protectedTags} + tags={projectFlag.tags} isCompact={isCompact} hideAudit={ AccountStore.getOrganisationRole() !== 'ADMIN' || diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 99dd33991c6f..c8ebf85be707 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -45,7 +45,7 @@ import ExternalResourcesLinkTab from 'components/ExternalResourcesLinkTab' import { saveFeatureWithValidation } from 'components/saveFeatureWithValidation' import PlanBasedBanner from 'components/PlanBasedAccess' import FeatureHistory from 'components/FeatureHistory' -import WarningMessage from 'components/WarningMessage'; +import WarningMessage from 'components/WarningMessage' const CreateFlag = class extends Component { static displayName = 'CreateFlag' @@ -403,17 +403,20 @@ const CreateFlag = class extends Component { }, }) } -parseError = (error)=>{ + parseError = (error) => { const { projectFlag } = this.props - let featureError = 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 = '' + let featureError = 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 } } - return {featureError, featureWarning} -} drawChart = (data) => { return data?.length ? ( @@ -724,131 +727,134 @@ parseError = (error)=>{ ) const Value = (error, projectAdmin, createFeature, hideValue) => { - const {featureError, featureWarning}= this.parseError(error) + const { featureError, featureWarning } = this.parseError(error) return ( - <> - {!!isEdit && ( - - )} - {!isEdit && ( - - (this.input = e)} - data-test='featureID' - inputProps={{ - className: 'full-width', - maxLength: FEATURE_ID_MAXLENGTH, - name: 'featureID', - readOnly: isEdit, - }} - value={name} - onChange={(e) => { - const newName = Utils.safeParseEventValue(e).replace(/ /g, '_') - this.setState({ - name: caseSensitive ? newName.toLowerCase() : newName, - }) - }} - isValid={!!name && regexValid} - type='text' + <> + {!!isEdit && ( + + )} + {!isEdit && ( + + (this.input = e)} + data-test='featureID' + inputProps={{ + className: 'full-width', + maxLength: FEATURE_ID_MAXLENGTH, + name: 'featureID', + readOnly: isEdit, + }} + value={name} + onChange={(e) => { + const newName = Utils.safeParseEventValue(e).replace( + / /g, + '_', + ) + this.setState({ + name: caseSensitive ? newName.toLowerCase() : newName, + }) + }} + isValid={!!name && regexValid} + type='text' + title={ + <> + - - - {isEdit ? 'ID / Name' : 'ID / Name*'} - - - - } - > - The ID that will be used by SDKs to retrieve the feature - value and enabled state. This cannot be edited once the - feature has been created. - - {!!regex && !isEdit && ( -
- {' '} - - {' '} - This must conform to the regular expression{' '} -
{regex}
-
-
- )} - - } - placeholder='E.g. header_size' - /> -
- )} - - - {identity && description && ( - - - this.setState({ description: Utils.safeParseEventValue(e) }) + + + {isEdit ? 'ID / Name' : 'ID / Name*'} + + + } - type='text' - title={identity ? 'Description' : 'Description (optional)'} - placeholder='No description' - /> - - )} - {!hideValue && ( -
- { - this.setState({ identityVariations, valueChanged: true }) - }} - environmentFlag={this.props.environmentFlag} - projectFlag={projectFlag} - onValueChange={(e) => { - const initial_value = Utils.getTypedValue( - Utils.safeParseEventValue(e), - ) - this.setState({ initial_value, valueChanged: true }) - }} - onCheckedChange={(default_enabled) => - this.setState({ default_enabled }) - } - /> -
- )} - {!isEdit && - !identity && - Settings(projectAdmin, createFeature, featureContentType)} - + > + The ID that will be used by SDKs to retrieve the feature + value and enabled state. This cannot be edited once the + feature has been created. + + {!!regex && !isEdit && ( +
+ {' '} + + {' '} + This must conform to the regular expression{' '} +
{regex}
+
+
+ )} + + } + placeholder='E.g. header_size' + /> +
+ )} + + + {identity && description && ( + + + this.setState({ description: Utils.safeParseEventValue(e) }) + } + type='text' + title={identity ? 'Description' : 'Description (optional)'} + placeholder='No description' + /> + + )} + {!hideValue && ( +
+ { + this.setState({ identityVariations, valueChanged: true }) + }} + environmentFlag={this.props.environmentFlag} + projectFlag={projectFlag} + onValueChange={(e) => { + const initial_value = Utils.getTypedValue( + Utils.safeParseEventValue(e), + ) + this.setState({ initial_value, valueChanged: true }) + }} + onCheckedChange={(default_enabled) => + this.setState({ default_enabled }) + } + /> +
+ )} + {!isEdit && + !identity && + Settings(projectAdmin, createFeature, featureContentType)} + ) } return ( @@ -1041,9 +1047,7 @@ parseError = (error)=>{ project.total_features, project.max_features_allowed, ) - const {featureError, featureWarning}= this.parseError(error) - - + const { featureError, featureWarning } = this.parseError(error) return ( { { return ( <> {
{({ permission: publishPermission }) => ( diff --git a/frontend/web/components/pages/FeaturesPage.js b/frontend/web/components/pages/FeaturesPage.js index fb20e7820ddb..d0675c7fc0c7 100644 --- a/frontend/web/components/pages/FeaturesPage.js +++ b/frontend/web/components/pages/FeaturesPage.js @@ -328,242 +328,227 @@ const FeaturesPage = class extends Component { {' '} for your selected environment. - - {({ permission }) => ( - - -
- { + +
+ { + this.setState( + { + search: Utils.safeParseEventValue(e), + }, + this.filter, + ) + }} + value={this.state.search} + /> + + { + this.setState( + { + tag_strategy, + }, + this.filter, + ) + }} + value={this.state.tags} + onToggleArchived={(value) => { + if (value !== this.state.showArchived) { + FeatureListStore.isLoading = true this.setState( { - search: - Utils.safeParseEventValue(e), + showArchived: + !this.state.showArchived, }, this.filter, ) - }} - value={this.state.search} - /> - - { - this.setState( - { - tag_strategy, - }, - this.filter, - ) - }} - value={this.state.tags} - onToggleArchived={(value) => { - if ( - value !== this.state.showArchived - ) { - FeatureListStore.isLoading = true - this.setState( - { - showArchived: - !this.state.showArchived, - }, - this.filter, - ) - } - }} - showArchived={this.state.showArchived} - onClearAll={() => { - FeatureListStore.isLoading = true - this.setState( - { showArchived: false, tags: [] }, - this.filter, - ) - }} - onChange={(tags, isAutomated) => { - FeatureListStore.isLoading = true - if ( - tags.includes('') && - tags.length > 1 - ) { - if ( - !this.state.tags.includes('') - ) { - this.setState( - { tags: [''] }, - this.filter, - ) - } else { - this.setState( - { - tags: tags.filter( - (v) => !!v, - ), - }, - this.filter, - ) - } - } else { - this.setState( - { tags }, - this.filter, - ) - } - }} - /> - { + } + }} + showArchived={this.state.showArchived} + onClearAll={() => { + FeatureListStore.isLoading = true + this.setState( + { showArchived: false, tags: [] }, + this.filter, + ) + }} + onChange={(tags, isAutomated) => { + FeatureListStore.isLoading = true + if ( + tags.includes('') && + tags.length > 1 + ) { + if (!this.state.tags.includes('')) { this.setState( - { - is_enabled: enabled, - value_search: valueSearch, - }, + { tags: [''] }, this.filter, ) - }} - /> - { - FeatureListStore.isLoading = true + } else { this.setState( { - owners: owners, + tags: tags.filter((v) => !!v), }, this.filter, ) - }} - /> - { - FeatureListStore.isLoading = true - this.setState( - { - group_owners: group_owners, - }, - this.filter, - ) - }} - /> - - { - FeatureListStore.isLoading = true - this.setState({ sort }, this.filter) - }} - /> - -
- - } - nextPage={() => - this.filter(FeatureListStore.paging.next) - } - prevPage={() => - this.filter(FeatureListStore.paging.previous) - } - goToPage={(page) => this.filter(page)} - items={projectFlags?.filter((v) => !v.ignore)} - renderFooter={() => ( - <> - + { + this.setState( + { + is_enabled: enabled, + value_search: valueSearch, + }, + this.filter, + ) + }} + /> + { + FeatureListStore.isLoading = true + this.setState( + { + owners: owners, + }, + this.filter, + ) + }} + /> + { + FeatureListStore.isLoading = true + this.setState( + { + group_owners: group_owners, + }, + this.filter, + ) + }} + /> + - { + FeatureListStore.isLoading = true + this.setState({ sort }, this.filter) + }} /> - + +
+ + } + nextPage={() => + this.filter(FeatureListStore.paging.next) + } + prevPage={() => + this.filter(FeatureListStore.paging.previous) + } + goToPage={(page) => this.filter(page)} + items={projectFlags?.filter((v) => !v.ignore)} + renderFooter={() => ( + <> + + + + )} + renderRow={(projectFlag, i) => ( + ( + id={this.props.match.params.environmentId} + > + {({ permission }) => ( )} - /> -
- )} -
+
+ )} + /> + = (props) => { id={environmentId} > {({ permission: manageUserPermission }) => ( - - {({ permission }) => ( -
- - {( - { - environmentFlags, - identity, - identityFlags, - isLoading, - projectFlags, - traits, - }: any, - { toggleFlag }: any, - ) => - isLoading && - !tags.length && - !showArchived && - typeof search !== 'string' && - (!identityFlags || !actualFlags || !projectFlags) ? ( -
- -
- ) : ( - <> - + + {( + { + environmentFlags, + identity, + identityFlags, + isLoading, + projectFlags, + traits, + }: any, + { toggleFlag }: any, + ) => + isLoading && + !tags.length && + !showArchived && + typeof search !== 'string' && + (!identityFlags || !actualFlags || !projectFlags) ? ( +
+ +
+ ) : ( + <> + + } + > + {showAliases && ( + <> +
+ + Alias:{' '} + } + > + Aliases allow you to add searchable names to an + identity + + - } - > - {showAliases && ( - <> -
- - Alias:{' '} - - } - > - Aliases allow you to add searchable names to - an identity - - -
- - )} - View and manage feature states and traits for this - user. -
- -
-
- - - - Features -
- - Overriding features here will take - priority over any segment override. - Any features that are not overridden - for this user will fallback to any - segment overrides or the environment - defaults. - -
-
- } - renderFooter={() => ( - <> - + + )} + View and manage feature states and traits for this user. +
+ +
+
+ + + + Features +
+ + Overriding features here will take + priority over any segment override. Any + features that are not overridden for this + user will fallback to any segment + overrides or the environment defaults. + +
+
+ } + renderFooter={() => ( + <> + + + + + )} + header={ + +
+ { + FeatureListStore.isLoading = true + setSearch(Utils.safeParseEventValue(e)) + }} + value={search} + /> + + { + setTagStrategy(strategy) + }} + isLoading={FeatureListStore.isLoading} + onToggleArchived={(value) => { + if (value !== showArchived) { + FeatureListStore.isLoading = true + setShowArchived(!showArchived) + } + }} + showArchived={showArchived} + onChange={(newTags) => { + FeatureListStore.isLoading = true + setTags( + newTags.includes('') && + newTags.length > 1 + ? [''] + : newTags, + ) + }} /> - { + setIsEnabled(enabled) + setValueSearch(valueSearch) + }} + /> + { + FeatureListStore.isLoading = true + setOwners(newOwners) + }} /> - { + FeatureListStore.isLoading = true + setGroupOwners(newGroupOwners) + }} + /> + + { + FeatureListStore.isLoading = true + setSort(newSort) + }} /> - - )} - header={ - -
- { - FeatureListStore.isLoading = true - setSearch( - Utils.safeParseEventValue(e), - ) - }} - value={search} - /> - - { - setTagStrategy(strategy) - }} - isLoading={ - FeatureListStore.isLoading - } - onToggleArchived={(value) => { - if (value !== showArchived) { - FeatureListStore.isLoading = - true - setShowArchived(!showArchived) - } - }} - showArchived={showArchived} - onChange={(newTags) => { - FeatureListStore.isLoading = true - setTags( - newTags.includes('') && - newTags.length > 1 - ? [''] - : newTags, - ) - }} - /> - { - setIsEnabled(enabled) - setValueSearch(valueSearch) - }} - /> - { - FeatureListStore.isLoading = true - setOwners(newOwners) - }} - /> - { - FeatureListStore.isLoading = true - setGroupOwners(newGroupOwners) - }} - /> - - { - FeatureListStore.isLoading = true - setSort(newSort) - }} - /> - -
- } - isLoading={FeatureListStore.isLoading} - items={projectFlags} - renderRow={( - { description, id: featureId, name }: any, - i: number, - ) => { - const identityFlag = - identityFlags[featureId] || {} - const environmentFlag = - (environmentFlags && - environmentFlags[featureId]) || - {} - const hasUserOverride = - identityFlag.identity || - identityFlag.identity_uuid - const flagEnabled = hasUserOverride - ? identityFlag.enabled - : environmentFlag.enabled - const flagValue = hasUserOverride - ? identityFlag.feature_state_value - : environmentFlag.feature_state_value - const actualEnabled = - actualFlags && actualFlags[name]?.enabled - const actualValue = - actualFlags && - actualFlags[name]?.feature_state_value - const flagEnabledDifferent = hasUserOverride - ? false - : actualEnabled !== flagEnabled - const flagValueDifferent = hasUserOverride - ? false - : !valuesEqual(actualValue, flagValue) - const projectFlag = projectFlags?.find( - (p: any) => - p.id === environmentFlag.feature, - ) - const isMultiVariateOverride = - flagValueDifferent && - projectFlag?.multivariate_options?.find( - (v: any) => - Utils.featureStateToValue(v) === - actualValue, +
+
+ } + isLoading={FeatureListStore.isLoading} + items={projectFlags} + renderRow={( + { description, id: featureId, name, tags }: any, + i: number, + ) => { + return ( + + {({ permission }) => { + const identityFlag = + identityFlags[featureId] || {} + const environmentFlag = + (environmentFlags && + environmentFlags[featureId]) || + {} + const hasUserOverride = + identityFlag.identity || + identityFlag.identity_uuid + const flagEnabled = hasUserOverride + ? identityFlag.enabled + : environmentFlag.enabled + const flagValue = hasUserOverride + ? identityFlag.feature_state_value + : environmentFlag.feature_state_value + const actualEnabled = + actualFlags && + actualFlags[name]?.enabled + const actualValue = + actualFlags && + actualFlags[name]?.feature_state_value + const flagEnabledDifferent = + hasUserOverride + ? false + : actualEnabled !== flagEnabled + const flagValueDifferent = hasUserOverride + ? false + : !valuesEqual(actualValue, flagValue) + const projectFlag = projectFlags?.find( + (p: any) => + p.id === environmentFlag.feature, ) - const flagDifferent = - flagEnabledDifferent || flagValueDifferent - - const onClick = () => { - if (permission) { - editFeature( - projectFlag, - environmentFlags[featureId], - identityFlags[featureId] || - actualFlags![name], - identityFlags[featureId] - ?.multivariate_feature_state_values, + const isMultiVariateOverride = + flagValueDifferent && + projectFlag?.multivariate_options?.find( + (v: any) => + Utils.featureStateToValue(v) === + actualValue, ) + const flagDifferent = + flagEnabledDifferent || + flagValueDifferent + + const onClick = () => { + if (permission) { + editFeature( + projectFlag, + environmentFlags[featureId], + identityFlags[featureId] || + actualFlags![name], + identityFlags[featureId] + ?.multivariate_feature_state_values, + ) + } } - } - const isCompact = - getViewMode() === 'compact' - if (name === preselect && actualFlags) { - setPreselect(null) - onClick() - } + const isCompact = + getViewMode() === 'compact' + if (name === preselect && actualFlags) { + setPreselect(null) + onClick() + } - return ( -
- - - - - - - {description ? ( - {name} - } - > - {description} - - ) : ( - name - )} - - - - - - {hasUserOverride ? ( -
- Overriding defaults -
- ) : flagEnabledDifferent ? ( -
+ + + + - - {isMultiVariateOverride ? ( - - This flag is being - overridden by a - variation defined on - your feature, the - control value is{' '} - - {flagEnabled - ? 'on' - : 'off'} - {' '} - for this user - + + {description ? ( + {name} + } + > + {description} + ) : ( - - This flag is being - overridden by segments - and would normally be{' '} - - {flagEnabled - ? 'on' - : 'off'} - {' '} - for this user - + name )} - - -
- ) : flagValueDifferent ? ( - isMultiVariateOverride ? ( -
- - This feature is being - overridden by a % - variation in the - environment, the control - value of this feature is{' '} - + + + + + {hasUserOverride ? ( +
+ Overriding defaults
- ) : ( + ) : flagEnabledDifferent ? (
- - This feature is being - overridden by segments and - would normally be{' '} - {' '} - for this user - + + + {isMultiVariateOverride ? ( + + This flag is being + overridden by a + variation defined on + your feature, the + control value is{' '} + + {flagEnabled + ? 'on' + : 'off'} + {' '} + for this user + + ) : ( + + This flag is being + overridden by + segments and would + normally be{' '} + + {flagEnabled + ? 'on' + : 'off'} + {' '} + for this user + + )} + +
- ) - ) : ( - getViewMode() === 'default' && ( -
- Using environment defaults -
- ) - )} - - - -
- -
-
e.stopPropagation()} - > - {Utils.renderWithPermission( - permission, - Constants.environmentPermissions( - Utils.getManageFeaturePermissionDescription( - false, - true, + ) : flagValueDifferent ? ( + isMultiVariateOverride ? ( +
+ + This feature is being + overridden by a % + variation in the + environment, the control + value of this feature is{' '} + + +
+ ) : ( +
+ + This feature is being + overridden by segments + and would normally be{' '} + {' '} + for this user + +
+ ) + ) : ( + getViewMode() === + 'default' && ( +
+ Using environment defaults +
+ ) + )} + + + +
+ +
+
e.stopPropagation()} + > + {Utils.renderWithPermission( + permission, + Constants.environmentPermissions( + Utils.getManageFeaturePermissionDescription( + false, + true, + ), ), - ), - - confirmToggle( - projectFlag, - actualFlags![name], - () => - toggleFlag({ - environmentFlag: - actualFlags![name], - environmentId, - identity: id, - identityFlag, - projectFlag: { - id: featureId, - }, - }), - ) - } - />, - )} -
-
e.stopPropagation()} - > - {hasUserOverride && ( - <> - {Utils.renderWithPermission( - permission, - Constants.environmentPermissions( - Utils.getManageFeaturePermissionDescription( - false, - true, + + confirmToggle( + projectFlag, + actualFlags![name], + () => + toggleFlag({ + environmentFlag: + actualFlags![name], + environmentId, + identity: id, + identityFlag, + projectFlag: { + id: featureId, + }, + }), + ) + } + />, + )} +
+
e.stopPropagation()} + > + {hasUserOverride && ( + <> + {Utils.renderWithPermission( + permission, + Constants.environmentPermissions( + Utils.getManageFeaturePermissionDescription( + false, + true, + ), ), - ), - , - )} - - )} + , + )} + + )} +
+ ) + }} + + ) + }} + renderSearchWithNoResults + paging={FeatureListStore.paging} + search={search} + nextPage={() => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + FeatureListStore.paging.next, + getFilter(), + ) + } + prevPage={() => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + FeatureListStore.paging.previous, + getFilter(), + ) + } + goToPage={(pageNumber: number) => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + pageNumber, + getFilter(), + ) + } + /> + + {!preventAddTrait && ( + + + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+ } + header={ + + + Trait + + Value +
+ Remove +
+
+ } + renderRow={( + { id, trait_key, trait_value }: any, + i: number, + ) => ( + + editTrait({ + id, + trait_key, + trait_value, + }) + } + > + +
+ {trait_key}
- ) - }} - renderSearchWithNoResults - paging={FeatureListStore.paging} - search={search} - nextPage={() => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - FeatureListStore.paging.next, - getFilter(), - ) - } - prevPage={() => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - FeatureListStore.paging.previous, - getFilter(), - ) - } - goToPage={(pageNumber: number) => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - pageNumber, - getFilter(), - ) - } - /> - - {!preventAddTrait && ( - - + + + +
e.stopPropagation()} + > + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+
+ )} + renderNoResults={ + + className='no-pad' + action={ +
{Utils.renderWithPermission( manageUserPermission, Constants.environmentPermissions( @@ -933,6 +1033,7 @@ const UserPage: FC = (props) => { ),
} + > +
+ + This user has no traits. + +
+
+ } + filterRow={( + { trait_key }: any, + searchString: string, + ) => + trait_key + .toLowerCase() + .indexOf(searchString.toLowerCase()) > -1 + } + /> + + )} + + {({ segments }: any) => + !segments ? ( +
+ +
+ ) : ( + + - - Trait + + Name - Value + Description -
- Remove -
} + items={segments || []} renderRow={( - { id, trait_key, trait_value }: any, + { created_date, description, name }: any, i: number, ) => ( - editTrait({ - id, - trait_key, - trait_value, - }) - } + key={i} + onClick={() => editSegment(segments[i])} > - +
+ editSegment(segments[i]) + } > - {trait_key} + + {name} + +
+
+ Created{' '} + {moment(created_date).format( + 'DD/MMM/YYYY', + )}
- - - -
e.stopPropagation()} - > - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , + + {description && ( +
{description}
)} -
+
)} renderNoResults={ - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , - )} -
- } >
- This user has no traits. + This user is not a member of any + segments.
} filterRow={( - { trait_key }: any, + { name }: any, searchString: string, ) => - trait_key + name .toLowerCase() .indexOf(searchString.toLowerCase()) > -1 } /> - )} - - {({ segments }: any) => - !segments ? ( -
- -
- ) : ( - - - - Name - - - Description - - - } - items={segments || []} - renderRow={( - { - created_date, - description, - name, - }: any, - i: number, - ) => ( - - editSegment(segments[i]) - } - > - -
- editSegment(segments[i]) - } - > - - {name} - -
-
- Created{' '} - {moment(created_date).format( - 'DD/MMM/YYYY', - )} -
-
- - {description && ( -
{description}
- )} -
-
- )} - renderNoResults={ - -
- - This user is not a member of any - segments. - -
-
- } - filterRow={( - { name }: any, - searchString: string, - ) => - name - .toLowerCase() - .indexOf( - searchString.toLowerCase(), - ) > -1 - } - /> -
- ) - } -
- -
-
- - - - - - -
-
- - ) - } - -
- )} -
+ ) + } + +
+
+
+ + + + + + +
+ + + ) + } + + )}
diff --git a/frontend/web/components/pages/WidgetPage.tsx b/frontend/web/components/pages/WidgetPage.tsx index 0b572920dbdc..872f5917b4a3 100644 --- a/frontend/web/components/pages/WidgetPage.tsx +++ b/frontend/web/components/pages/WidgetPage.tsx @@ -228,201 +228,194 @@ const FeatureList = class extends Component { !!this.state.tags.length) && !isLoading) ? (
- - {({ permission }) => ( -
- { - this.setState( - { search: Utils.safeParseEventValue(e) }, - () => { - AppActions.searchFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - this.getFilter(), - this.props.pageSize, - ) - }, - ) - }} - nextPage={() => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).next || 1, - this.getFilter(), - this.props.pageSize, - ) - } - prevPage={() => - AppActions.getFeatures( +
+ { + this.setState( + { search: Utils.safeParseEventValue(e) }, + () => { + AppActions.searchFeatures( this.props.projectId, this.props.environmentId, true, this.state.search, this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).previous, this.getFilter(), this.props.pageSize, ) - } - goToPage={(page: number) => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - page, - this.getFilter(), - this.props.pageSize, - ) - } - onSortChange={(sort: string) => { - this.setState({ sort }, () => { - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - this.props.pageSize, - ) - }) - }} - sorting={[ - { - default: true, - label: 'Name', - order: 'asc', - value: 'name', - }, - { - label: 'Created Date', - order: 'asc', - value: 'created_date', - }, - ]} - items={projectFlags} - header={ - this.props.hideTags ? null : ( - - + }, + ) + }} + nextPage={() => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + ( + FeatureListStore.paging as PagedResponse + ).next || 1, + this.getFilter(), + this.props.pageSize, + ) + } + prevPage={() => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + ( + FeatureListStore.paging as PagedResponse + ).previous, + this.getFilter(), + this.props.pageSize, + ) + } + goToPage={(page: number) => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + page, + this.getFilter(), + this.props.pageSize, + ) + } + onSortChange={(sort: string) => { + this.setState({ sort }, () => { + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + 0, + this.getFilter(), + this.props.pageSize, + ) + }) + }} + sorting={[ + { + default: true, + label: 'Name', + order: 'asc', + value: 'name', + }, + { + label: 'Created Date', + order: 'asc', + value: 'created_date', + }, + ]} + items={projectFlags} + header={ + this.props.hideTags ? null : ( + + + this.setState( + { showArchived: false, tags: [] }, + this.filter, + ) + } + projectId={projectId} + value={this.state.tags} + tagStrategy={this.state.tag_strategy} + onChangeStrategy={(tag_strategy) => { + this.setState( + { tag_strategy }, + this.filter, + ) + }} + onChange={(tags) => { + FeatureListStore.isLoading = true + if ( + tags.includes('') && + tags.length > 1 + ) { + if (!this.state.tags.includes('')) { this.setState( - { showArchived: false, tags: [] }, + { tags: [''] }, this.filter, ) - } - projectId={projectId} - value={this.state.tags} - tagStrategy={this.state.tag_strategy} - onChangeStrategy={(tag_strategy) => { + } else { this.setState( - { tag_strategy }, + { + tags: tags.filter((v) => !!v), + }, this.filter, ) - }} - onChange={(tags) => { + } + } else { + this.setState({ tags }, this.filter) + } + AsyncStorage.setItem( + `${projectId}tags`, + JSON.stringify(tags), + ) + }} + > +
+ { FeatureListStore.isLoading = true - if ( - tags.includes('') && - tags.length > 1 - ) { - if ( - !this.state.tags.includes('') - ) { - this.setState( - { tags: [''] }, - this.filter, - ) - } else { - this.setState( - { - tags: tags.filter( - (v) => !!v, - ), - }, - this.filter, - ) - } - } else { - this.setState( - { tags }, - this.filter, - ) - } - AsyncStorage.setItem( - `${projectId}tags`, - JSON.stringify(tags), + this.setState( + { + showArchived: + !this.state.showArchived, + }, + this.filter, ) }} - > -
- { - FeatureListStore.isLoading = - true - this.setState( - { - showArchived: - !this.state.showArchived, - }, - this.filter, - ) - }} - className='px-2 py-2 ml-2 mr-2' - tag={{ - color: '#0AADDF', - label: 'Archived', - }} - /> -
- - - ) - } - renderRow={( - projectFlag: ProjectFlag, - i: number, - ) => ( + className='px-2 py-2 ml-2 mr-2' + tag={{ + color: '#0AADDF', + label: 'Archived', + }} + /> +
+
+
+ ) + } + renderRow={( + projectFlag: ProjectFlag, + i: number, + ) => ( + + {({ permission }) => ( { projectFlag={projectFlag} /> )} - filterRow={() => true} - /> -
- )} - + + )} + filterRow={() => true} + /> +
) : null}