From d18049bc3abd6ac49334fb041bc8fca4778ed198 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Wed, 2 Oct 2024 16:02:50 +0100 Subject: [PATCH] feat: Identity alias (#4620) --- frontend/.eslintrc.js | 1 - frontend/common/providers/IdentityProvider.js | 2 +- frontend/common/services/useIdentity.ts | 38 +- frontend/common/stores/feature-list-store.ts | 10 +- frontend/common/types/requests.ts | 7 +- frontend/common/useViewMode.ts | 2 +- frontend/env/project_dev.js | 2 +- frontend/env/project_prod.js | 2 +- frontend/web/components/CodeHelp.js | 2 +- frontend/web/components/EditIdentity.tsx | 127 ++ frontend/web/components/InfoMessage.tsx | 11 +- frontend/web/components/PanelSearch.js | 1 + frontend/web/components/TryIt.js | 4 +- frontend/web/components/base/forms/Button.tsx | 6 + frontend/web/components/modals/CreateFlag.js | 2 + frontend/web/components/modals/CreateTrait.js | 3 +- .../web/components/pages/SegmentsPage.tsx | 2 +- frontend/web/components/pages/UserPage.js | 1318 ----------------- frontend/web/components/pages/UserPage.tsx | 1203 +++++++++++++++ frontend/web/components/pages/UsersPage.tsx | 51 +- .../components/tables/TableValueFilter.tsx | 11 +- frontend/web/project/api.js | 2 +- frontend/web/project/project-components.js | 7 +- frontend/web/styles/components/_input.scss | 2 +- frontend/web/styles/project/_forms.scss | 4 + 25 files changed, 1443 insertions(+), 1377 deletions(-) create mode 100644 frontend/web/components/EditIdentity.tsx delete mode 100644 frontend/web/components/pages/UserPage.js create mode 100644 frontend/web/components/pages/UserPage.tsx diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index b8b72785806d..00de9863478a 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -39,7 +39,6 @@ module.exports = { 'Flex': true, 'FormGroup': true, 'Headway': true, - 'IdentityProvider': true, 'Input': true, 'InputGroup': true, 'Link': true, diff --git a/frontend/common/providers/IdentityProvider.js b/frontend/common/providers/IdentityProvider.js index 49f09ace3d53..9a0098bb379a 100644 --- a/frontend/common/providers/IdentityProvider.js +++ b/frontend/common/providers/IdentityProvider.js @@ -121,4 +121,4 @@ IdentityProvider.propTypes = { onSave: OptionalFunc, } -module.exports = IdentityProvider +export default IdentityProvider diff --git a/frontend/common/services/useIdentity.ts b/frontend/common/services/useIdentity.ts index d80cd2ae1b5d..a8f79f50334d 100644 --- a/frontend/common/services/useIdentity.ts +++ b/frontend/common/services/useIdentity.ts @@ -2,6 +2,7 @@ import { Res } from 'common/types/responses' import { Req } from 'common/types/requests' import { service } from 'common/service' import transformCorePaging from 'common/transformCorePaging' +import Utils from 'common/utils/utils' const getIdentityEndpoint = (environmentId: string, isEdge: boolean) => { const identityPart = isEdge ? 'edge-identities' : 'identities' @@ -52,6 +53,7 @@ export const identityService = service providesTags: [{ id: 'LIST', type: 'Identity' }], query: (baseQuery) => { const { + dashboard_alias, environmentId, isEdge, page, @@ -61,10 +63,11 @@ export const identityService = service q, search, } = baseQuery - let url = `${getIdentityEndpoint( - environmentId, - isEdge, - )}/?q=${encodeURIComponent(search || q || '')}&page_size=${page_size}` + let url = `${getIdentityEndpoint(environmentId, isEdge)}/?q=${ + dashboard_alias ? 'dashboard_alias:' : '' + }${encodeURIComponent( + dashboard_alias || search || q || '', + )}&page_size=${page_size}` let last_evaluated_key = null if (!isEdge) { url += `&page=${page}` @@ -127,6 +130,21 @@ export const identityService = service return transformCorePaging(req, baseQueryReturnValue) }, }), + updateIdentity: builder.mutation({ + invalidatesTags: (res) => [ + { id: 'LIST', type: 'Identity' }, + { id: res?.id, type: 'Identity' }, + ], + query: (query: Req['updateIdentity']) => ({ + body: query.data, + method: 'PUT', + url: `environments/${ + query.environmentId + }/${Utils.getIdentitiesEndpoint()}/${ + query.data.identity_uuid || query.data.id + }`, + }), + }), // END OF ENDPOINTS }), }) @@ -173,12 +191,24 @@ export async function getIdentities( store.dispatch(identityService.util.getRunningQueriesThunk()), ) } +export async function updateIdentity( + store: any, + data: Req['updateIdentity'], + options?: Parameters< + typeof identityService.endpoints.updateIdentity.initiate + >[1], +) { + return store.dispatch( + identityService.endpoints.updateIdentity.initiate(data, options), + ) +} // END OF FUNCTION_EXPORTS export const { useCreateIdentitiesMutation, useDeleteIdentityMutation, useGetIdentitiesQuery, + useUpdateIdentityMutation, // END OF EXPORTS } = identityService diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index 2939ffd91017..c906afa21fd5 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -473,9 +473,9 @@ const controller = { environment: environmentFlag.environment, feature: projectFlag.id, }, - { - forceRefetch: true - } + { + forceRefetch: true, + }, ) let segments = null if (mode === 'SEGMENT') { @@ -712,8 +712,8 @@ const controller = { return createAndSetFeatureVersion(getStore(), { environmentId: res, featureId: projectFlag.id, - projectId, featureStates, + projectId, }).then((version) => { if (version.error) { throw version.error @@ -768,10 +768,10 @@ const controller = { feature_state_value: flag.initial_value, }) return createAndSetFeatureVersion(getStore(), { - projectId, environmentId: res, featureId: projectFlag.id, featureStates: [data], + projectId, }).then((version) => { if (version.error) { throw version.error diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index e84f9b4a4300..55bca8eedc18 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -13,6 +13,7 @@ import { Environment, UserGroup, AttributeName, + Identity, } from './responses' export type PagedRequest = T & { @@ -97,7 +98,7 @@ export type Req = { getIdentities: PagedRequest<{ environmentId: string pageType?: 'NEXT' | 'PREVIOUS' - search?: string + dashboard_alias?: string pages?: (string | undefined)[] // this is needed for edge since it returns no paging info other than a key isEdge: boolean }> @@ -522,5 +523,9 @@ export type Req = { idp_attribute_name: string } } + updateIdentity: { + environmentId: string + data: Identity + } // END OF TYPES } diff --git a/frontend/common/useViewMode.ts b/frontend/common/useViewMode.ts index b0f5298830b4..2586020e56a2 100644 --- a/frontend/common/useViewMode.ts +++ b/frontend/common/useViewMode.ts @@ -1,6 +1,6 @@ import flagsmith from 'flagsmith' -export type ViewMode = 'compact' | 'normal' +export type ViewMode = 'compact' | 'default' export function getViewMode() { const viewMode = flagsmith.getTrait('view_mode') if (viewMode === 'compact') { diff --git a/frontend/env/project_dev.js b/frontend/env/project_dev.js index fee495078741..6c45318f17a1 100644 --- a/frontend/env/project_dev.js +++ b/frontend/env/project_dev.js @@ -16,10 +16,10 @@ module.exports = global.Project = { flagsmithClientEdgeAPI: 'https://edge.bullet-train-staging.win/api/v1/', // This is used for Sentry tracking maintenance: false, - useSecureCookies: true, plans: { scaleUp: { annual: 'scale-up-annual-v2', monthly: 'scale-up-v2' }, startup: { annual: 'startup-annual-v2', monthly: 'startup-v2' }, }, + useSecureCookies: true, ...(globalThis.projectOverrides || {}), } diff --git a/frontend/env/project_prod.js b/frontend/env/project_prod.js index d90a0c314444..24d0d388e496 100644 --- a/frontend/env/project_prod.js +++ b/frontend/env/project_prod.js @@ -22,10 +22,10 @@ module.exports = global.Project = { flagsmithClientEdgeAPI: 'https://edge.api.flagsmith.com/api/v1/', // This is used for Sentry tracking maintenance: false, - useSecureCookies: true, plans: { scaleUp: { annual: 'scale-up-12-months-v2', monthly: 'scale-up-v2' }, startup: { annual: 'start-up-12-months-v2', monthly: 'startup-v2' }, }, + useSecureCookies: true, ...(globalThis.projectOverrides || {}), } diff --git a/frontend/web/components/CodeHelp.js b/frontend/web/components/CodeHelp.js index f56a71a7c276..74bf803c50bc 100644 --- a/frontend/web/components/CodeHelp.js +++ b/frontend/web/components/CodeHelp.js @@ -249,4 +249,4 @@ const CodeHelp = class extends Component { CodeHelp.propTypes = {} -module.exports = ConfigProvider(CodeHelp) +export default ConfigProvider(CodeHelp) diff --git a/frontend/web/components/EditIdentity.tsx b/frontend/web/components/EditIdentity.tsx new file mode 100644 index 000000000000..db68c063e06b --- /dev/null +++ b/frontend/web/components/EditIdentity.tsx @@ -0,0 +1,127 @@ +import React, { FC, useEffect, useRef, useState } from 'react' +import { Identity } from 'common/types/responses' +import { useUpdateIdentityMutation } from 'common/services/useIdentity' +import Button from './base/forms/Button' +import ErrorMessage from './ErrorMessage' +import classNames from 'classnames'; + +type EditIdentityType = { + data: Identity + environmentId: string + onComplete?: () => void +} + +const EditIdentity: FC = ({ data, environmentId }) => { + const [alias, setAlias] = useState(data.dashboard_alias) + const aliasRef = useRef(null) + + useEffect(() => { + setAlias(data?.dashboard_alias) + }, [data]) + + const [updateIdentity, { error, isLoading }] = useUpdateIdentityMutation({}) + + const handleBlur = () => { + if (aliasRef.current) { + const updatedAlias = (aliasRef.current.textContent || '') + .replace(/\n/g, ' ') + .trim() + .toLowerCase() + + aliasRef.current.textContent = alias + setAlias(updatedAlias) + onSubmit(updatedAlias) + } + } + + const onSubmit = (updatedAlias: string) => { + if (!isLoading && updatedAlias) { + updateIdentity({ + data: { ...data, dashboard_alias: updatedAlias }, + environmentId, + }) + } + } + + const handleFocus = () => { + if (!alias) { + aliasRef.current.textContent = ''; // Clear the content + } + + // Ensure that aliasRef.current has at least one child node (a text node) + if (aliasRef.current && aliasRef.current.childNodes.length === 0) { + aliasRef.current.appendChild(document.createTextNode('')); + } + + if (aliasRef.current) { + const selection = window.getSelection(); + const range = document.createRange(); + + const textLength = aliasRef.current.textContent?.length || 0; + range.setStart(aliasRef.current.childNodes[0], textLength); + range.collapse(true); + + selection?.removeAllRanges(); + selection?.addRange(range); + } + }; + + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + aliasRef.current?.blur() + } + } + + const handleInput = () => { + if (aliasRef.current) { + const selection = window.getSelection() + const range = selection?.getRangeAt(0) + const cursorPosition = range?.startOffset || 0 + + const lowerCaseText = aliasRef.current.textContent?.toLowerCase() || '' + aliasRef.current.textContent = lowerCaseText + + // Restore cursor position + const newRange = document.createRange() + newRange.setStart(aliasRef.current.childNodes[0], Math.min(cursorPosition, lowerCaseText.length)) + newRange.collapse(true) + + selection?.removeAllRanges() + selection?.addRange(newRange) + } + } + + return ( + <> + + {alias || 'None'} + + + {error} + + ) +} + +export default EditIdentity diff --git a/frontend/web/components/InfoMessage.tsx b/frontend/web/components/InfoMessage.tsx index 5d8c305dc294..0d59637289fd 100644 --- a/frontend/web/components/InfoMessage.tsx +++ b/frontend/web/components/InfoMessage.tsx @@ -3,6 +3,7 @@ import Icon, { IconName } from './Icon' import { chevronForward, close as closeIcon, chevronDown } from 'ionicons/icons' import { IonIcon } from '@ionic/react' import { FC } from 'react' +import Button from 'components/base/forms/Button'; type InfoMessageType = { buttonText?: string @@ -79,14 +80,14 @@ const InfoMessage: FC = ({ {!isCollapsed && ( - <> -
{children}
+
+
{children}
{url && buttonText && ( - + )} - +
)} {isClosable && ( diff --git a/frontend/web/components/PanelSearch.js b/frontend/web/components/PanelSearch.js index d413ae78b8d4..8d5bd17747f1 100644 --- a/frontend/web/components/PanelSearch.js +++ b/frontend/web/components/PanelSearch.js @@ -229,6 +229,7 @@ const PanelSearch = class extends Component { placeholder='Search' search /> + {this.props.filterRowContent} )} diff --git a/frontend/web/components/TryIt.js b/frontend/web/components/TryIt.js index bbf72ba83428..cc7e4fc59cfc 100644 --- a/frontend/web/components/TryIt.js +++ b/frontend/web/components/TryIt.js @@ -103,6 +103,4 @@ const TryIt = class extends Component { } } -TryIt.propTypes = {} - -module.exports = ConfigProvider(TryIt) +export default ConfigProvider(TryIt) diff --git a/frontend/web/components/base/forms/Button.tsx b/frontend/web/components/base/forms/Button.tsx index b6d501566370..194268535c0f 100644 --- a/frontend/web/components/base/forms/Button.tsx +++ b/frontend/web/components/base/forms/Button.tsx @@ -33,6 +33,7 @@ export type ButtonType = ButtonHTMLAttributes & { target?: HTMLAttributeAnchorTarget theme?: keyof typeof themeClassNames size?: keyof typeof sizeClassNames + iconSize?: number } export const Button: FC = ({ @@ -44,6 +45,7 @@ export const Button: FC = ({ iconLeftColour, iconRight, iconRightColour, + iconSize = 24, onMouseUp, size = 'default', target, @@ -65,6 +67,7 @@ export const Button: FC = ({ fill={iconLeftColour ? Constants.colours[iconLeftColour] : undefined} className='me-2' name={iconLeft} + width={iconSize} /> )} {children} @@ -75,6 +78,7 @@ export const Button: FC = ({ } className='ml-2' name={iconRight} + width={iconSize} /> )} @@ -95,6 +99,7 @@ export const Button: FC = ({ fill={iconLeftColour ? Constants.colours[iconLeftColour] : undefined} className='mr-2' name={iconLeft} + width={iconSize} /> )} {children} @@ -105,6 +110,7 @@ export const Button: FC = ({ } className='ml-2' name={iconRight} + width={iconSize} /> )} diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 38c3b958dc22..6d968ea6b7b6 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -5,6 +5,8 @@ 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 { Bar, BarChart, diff --git a/frontend/web/components/modals/CreateTrait.js b/frontend/web/components/modals/CreateTrait.js index c7f4234d8971..19a542081d02 100644 --- a/frontend/web/components/modals/CreateTrait.js +++ b/frontend/web/components/modals/CreateTrait.js @@ -4,6 +4,7 @@ import Constants from 'common/constants' import Format from 'common/utils/format' import ErrorMessage from 'components/ErrorMessage' import ModalHR from './ModalHR' +import IdentityProvider from 'common/providers/IdentityProvider' const CreateTrait = class extends Component { static displayName = 'CreateTrait' @@ -171,4 +172,4 @@ const CreateTrait = class extends Component { CreateTrait.propTypes = {} -module.exports = CreateTrait +export default CreateTrait diff --git a/frontend/web/components/pages/SegmentsPage.tsx b/frontend/web/components/pages/SegmentsPage.tsx index 3e05f20897d6..89ece6ddbcf4 100644 --- a/frontend/web/components/pages/SegmentsPage.tsx +++ b/frontend/web/components/pages/SegmentsPage.tsx @@ -27,7 +27,7 @@ import classNames from 'classnames' import InfoMessage from 'components/InfoMessage' import { withRouter } from 'react-router-dom' -const CodeHelp = require('../../components/CodeHelp') +import CodeHelp from 'components/CodeHelp' type SegmentsPageType = { router: RouterChildContext['router'] match: { diff --git a/frontend/web/components/pages/UserPage.js b/frontend/web/components/pages/UserPage.js deleted file mode 100644 index c70853f7cf9b..000000000000 --- a/frontend/web/components/pages/UserPage.js +++ /dev/null @@ -1,1318 +0,0 @@ -import React, { Component } from 'react' -import ConfirmToggleFeature from 'components/modals/ConfirmToggleFeature' -import CreateFlagModal from 'components/modals/CreateFlag' -import CreateTraitModal from 'components/modals/CreateTrait' -import TryIt from 'components/TryIt' -import CreateSegmentModal from 'components/modals/CreateSegment' -import FeatureListStore from 'common/stores/feature-list-store' -import { getTags } from 'common/services/useTag' -import { getStore } from 'common/store' -import TagValues from 'components/tags/TagValues' -import _data from 'common/data/base/_data' -import JSONReference from 'components/JSONReference' -import Constants from 'common/constants' -import IdentitySegmentsProvider from 'common/providers/IdentitySegmentsProvider' -import ConfigProvider from 'common/providers/ConfigProvider' -import Permission from 'common/providers/Permission' -import Icon from 'components/Icon' -import FeatureValue from 'components/FeatureValue' -import PageTitle from 'components/PageTitle' -import TableTagFilter from 'components/tables/TableTagFilter' -import TableSearchFilter from 'components/tables/TableSearchFilter' -import TableFilterOptions from 'components/tables/TableFilterOptions' -import TableSortFilter from 'components/tables/TableSortFilter' -import { getViewMode, setViewMode } from 'common/useViewMode' -import classNames from 'classnames' -import IdentifierString from 'components/IdentifierString' -import Button from 'components/base/forms/Button' -import { removeUserOverride } from 'components/RemoveUserOverride' -import TableOwnerFilter from 'components/tables/TableOwnerFilter' -import TableGroupsFilter from 'components/tables/TableGroupsFilter' -import TableValueFilter from 'components/tables/TableValueFilter' -import Format from 'common/utils/format' -import InfoMessage from 'components/InfoMessage' -const width = [200, 48, 78] -const valuesEqual = (actualValue, flagValue) => { - const nullFalseyA = - actualValue == null || - actualValue === '' || - typeof actualValue === 'undefined' - const nullFalseyB = - flagValue == null || flagValue === '' || typeof flagValue === 'undefined' - if (nullFalseyA && nullFalseyB) { - return true - } - return actualValue === flagValue -} - -const UserPage = class extends Component { - static displayName = 'UserPage' - - constructor(props, context) { - super(props, context) - - const params = Utils.fromParam() - this.state = { - group_owners: - typeof params.group_owners === 'string' - ? params.group_owners.split(',').map((v) => parseInt(v)) - : [], - is_enabled: - params.is_enabled === 'true' - ? true - : params.is_enabled === 'false' - ? false - : null, - loadedOnce: false, - owners: - typeof params.owners === 'string' - ? params.owners.split(',').map((v) => parseInt(v)) - : [], - page: params.page ? parseInt(params.page) - 1 : 1, - preselect: Utils.fromParam().flag, - search: params.search || null, - showArchived: params.is_archived === 'true', - sort: { - label: Format.camelCase(params.sortBy || 'Name'), - sortBy: params.sortBy || 'name', - sortOrder: params.sortOrder || 'asc', - }, - tag_strategy: params.tag_strategy || 'INTERSECTION', - tags: - typeof params.tags === 'string' - ? params.tags.split(',').map((v) => parseInt(v)) - : [], - value_search: - typeof params.value_search === 'string' ? params.value_search : '', - } - } - - getFilter = () => ({ - group_owners: this.state.group_owners?.length - ? this.state.group_owners - : undefined, - is_archived: this.state.showArchived, - is_enabled: - this.state.is_enabled === null ? undefined : this.state.is_enabled, - owners: this.state.owners?.length ? this.state.owners : undefined, - tag_strategy: this.state.tag_strategy, - tags: - !this.state.tags || !this.state.tags.length - ? undefined - : this.state.tags.join(','), - value_search: this.state.value_search ? this.state.value_search : undefined, - }) - - componentDidMount() { - const { - match: { params }, - } = this.props - - AppActions.getIdentity( - this.props.match.params.environmentId, - this.props.match.params.id, - ) - AppActions.getIdentitySegments( - this.props.match.params.projectId, - this.props.match.params.id, - ) - AppActions.getFeatures( - this.props.match.params.projectId, - this.props.match.params.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - ) - getTags(getStore(), { - projectId: `${params.projectId}`, - }) - this.getActualFlags() - API.trackPage(Constants.pages.USER) - } - - onSave = () => { - this.getActualFlags() - } - - editSegment = (segment) => { - API.trackEvent(Constants.events.VIEW_SEGMENT) - openModal( - `Segment - ${segment.name}`, - , - 'side-modal create-segment-modal', - ) - } - - getActualFlags = () => { - const { environmentId, id } = this.props.match.params - const url = `${ - Project.api - }environments/${environmentId}/${Utils.getIdentitiesEndpoint()}/${id}/${Utils.getFeatureStatesEndpoint()}/all/` - _data - .get(url) - .then((res) => { - this.setState({ actualFlags: _.keyBy(res, (v) => v.feature.name) }) - }) - .catch(() => {}) - } - - onTraitSaved = () => { - AppActions.getIdentitySegments( - this.props.match.params.projectId, - this.props.match.params.id, - ) - } - - confirmToggle = (projectFlag, environmentFlag, cb) => { - openModal( - 'Toggle Feature', - , - 'p-0', - ) - } - editFeature = ( - projectFlag, - environmentFlag, - identityFlag, - multivariate_feature_state_values, - ) => { - history.replaceState( - {}, - null, - `${document.location.pathname}?flag=${projectFlag.name}`, - ) - API.trackEvent(Constants.events.VIEW_USER_FEATURE) - openModal( - - - Edit User Feature:{' '} - {projectFlag.name} - - - , - , - 'side-modal create-feature-modal overflow-y-auto', - () => { - history.replaceState({}, null, `${document.location.pathname}`) - }, - ) - } - - createTrait = () => { - API.trackEvent(Constants.events.VIEW_USER_FEATURE) - openModal( - 'Create User Trait', - , - 'p-0', - ) - } - - editTrait = (trait) => { - API.trackEvent(Constants.events.VIEW_USER_FEATURE) - openModal( - 'Edit User Trait', - , - 'p-0', - ) - } - - removeTrait = (id, trait_key) => { - openConfirm({ - body: ( -
- {'Are you sure you want to delete trait '} - {trait_key} - { - ' from this user? Traits can be re-added here or via one of our SDKs.' - } -
- ), - destructive: true, - onYes: () => - AppActions.deleteIdentityTrait( - this.props.match.params.environmentId, - this.props.match.params.id, - id || trait_key, - ), - title: 'Delete Trait', - yesText: 'Confirm', - }) - } - getURLParams = () => ({ - ...this.getFilter(), - group_owners: (this.state.group_owners || [])?.join(',') || undefined, - owners: (this.state.owners || [])?.join(',') || undefined, - page: this.state.page || 1, - search: this.state.search || '', - sortBy: this.state.sort.sortBy, - sortOrder: this.state.sort.sortOrder, - tags: (this.state.tags || [])?.join(',') || undefined, - }) - - filter = () => { - const currentParams = Utils.fromParam() - if (!currentParams.flag) { - // don't replace page if we are currently viewing a feature - this.props.router.history.replace( - `${document.location.pathname}?${Utils.toParam(this.getURLParams())}`, - ) - } - AppActions.searchFeatures( - this.props.match.params.projectId, - this.props.match.params.environmentId, - true, - this.state.search, - this.state.sort, - this.getFilter(), - ) - } - - render() { - const { actualFlags } = this.state - const { environmentId, projectId } = this.props.match.params - const preventAddTrait = !AccountStore.getOrganisation().persist_trait_data - return ( -
- - {({ permission: manageUserPermission }) => ( - - {({ permission }) => ( -
- - {( - { - environmentFlags, - identity, - identityFlags, - isLoading, - projectFlags, - traits, - }, - { removeFlag, toggleFlag }, - ) => - isLoading && - !this.state.tags.length && - !this.state.tags.length && - !this.state.showArchived && - typeof this.state.search !== 'string' && - (!identityFlags || !actualFlags || !projectFlags) ? ( -
- -
- ) : ( - <> - - } - > - 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 - this.setState( - { - search: - Utils.safeParseEventValue( - e, - ), - }, - this.filter, - ) - }} - value={this.state.search} - /> - - { - this.setState( - { - tag_strategy, - }, - this.filter, - ) - }} - isLoading={ - FeatureListStore.isLoading - } - 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) => { - 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( - { - 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, - ) - }} - /> - -
-
- } - isLoading={FeatureListStore.isLoading} - items={projectFlags} - renderRow={( - { description, id, name }, - i, - ) => { - const identityFlag = - identityFlags[id] || {} - const environmentFlag = - (environmentFlags && - environmentFlags[id]) || - {} - const hasUserOverride = - identityFlag.identity || - identityFlag.identity_uuid - const flagEnabled = hasUserOverride - ? identityFlag.enabled - : environmentFlag.enabled // show default value s - const flagValue = hasUserOverride - ? identityFlag.feature_state_value - : environmentFlag.feature_state_value - - const actualEnabled = - (actualFlags && - !!actualFlags && - actualFlags[name] && - actualFlags[name].enabled) || - false - const actualValue = - !!actualFlags && - actualFlags[name] && - actualFlags[name].feature_state_value - const flagEnabledDifferent = - hasUserOverride - ? false - : actualEnabled !== flagEnabled - const flagValueDifferent = hasUserOverride - ? false - : !valuesEqual(actualValue, flagValue) - const projectFlag = - projectFlags && - projectFlags.find( - (p) => - p.id === - (environmentFlag && - environmentFlag.feature), - ) - const isMultiVariateOverride = - flagValueDifferent && - projectFlag && - projectFlag.multivariate_options && - projectFlag.multivariate_options.find( - (v) => { - const value = - Utils.featureStateToValue(v) - return value === actualValue - }, - ) - const flagDifferent = - flagEnabledDifferent || - flagValueDifferent - const onClick = () => { - if (permission) { - this.editFeature( - _.find(projectFlags, { id }), - environmentFlags && - environmentFlags[id], - (identityFlags && - identityFlags[id]) || - actualFlags[name], - identityFlags && - identityFlags[id] && - identityFlags[id] - .multivariate_feature_state_values, - ) - } - } - const isCompact = - getViewMode() === 'compact' - if ( - name === this.state.preselect && - actualFlags - ) { - this.state.preselect = 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 - - ) : ( - - This flag is being - overridden by - segments and would - normally be{' '} - - {flagEnabled - ? 'on' - : 'off'} - {' '} - for this user - - )} - - -
- ) : flagValueDifferent ? ( - isMultiVariateOverride ? ( -
- - This feature is being - overriden by a % - variation in the - environment, the control - value of this feature is{' '} - - -
- ) : ( -
- - This feature is being - overriden 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, - ), - ), - - this.confirmToggle( - _.find(projectFlags, { - id, - }), - actualFlags[name], - () => { - toggleFlag({ - environmentFlag: - actualFlags[name], - environmentId: - this.props.match - .params - .environmentId, - identity: - this.props.match - .params.id, - identityFlag, - projectFlag: { id }, - }) - }, - ) - } - />, - )} -
-
{ - e.stopPropagation() - }} - > - {hasUserOverride && ( - <> - {Utils.renderWithPermission( - permission, - Constants.environmentPermissions( - Utils.getManageFeaturePermissionDescription( - false, - true, - ), - ), - , - )} - - )} -
-
- ) - }} - renderSearchWithNoResults - paging={FeatureListStore.paging} - search={this.state.search} - nextPage={() => - AppActions.getFeatures( - this.props.match.params.projectId, - this.props.match.params.environmentId, - true, - this.state.search, - this.state.sort, - FeatureListStore.paging.next, - this.getFilter(), - ) - } - prevPage={() => - AppActions.getFeatures( - this.props.match.params.projectId, - this.props.match.params.environmentId, - true, - this.state.search, - this.state.sort, - FeatureListStore.paging.previous, - this.getFilter(), - ) - } - goToPage={(page) => - AppActions.getFeatures( - this.props.match.params.projectId, - this.props.match.params.environmentId, - true, - this.state.search, - this.state.sort, - page, - this.getFilter(), - ) - } - /> - - {!preventAddTrait && ( - - - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , - )} -
- } - header={ - - - Trait - - - Value - -
- Remove -
-
- } - renderRow={( - { id, trait_key, trait_value }, - i, - ) => ( - - this.editTrait({ - id, - trait_key, - trait_value, - }) - } - > - -
- {trait_key} -
-
- - - -
e.stopPropagation()} - > - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , - )} -
-
- )} - renderNoResults={ - - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , - )} -
- } - > -
- - This user has no traits. - -
- - } - filterRow={({ trait_key }, search) => - trait_key - .toLowerCase() - .indexOf(search) > -1 - } - /> - - )} - - {({ segments }) => - !segments ? ( -
- -
- ) : ( - - - - Name - - - Description - - - } - items={segments || []} - renderRow={( - { created_date, description, name }, - i, - ) => ( - - this.editSegment(segments[i]) - } - className='list-item clickable' - space - key={i} - > - -
- this.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 }, search) => - name.toLowerCase().indexOf(search) > - -1 - } - /> -
- ) - } -
- -
-
- - - - - - -
- - - ) - } - - - )} - - )} - - - ) - } -} - -UserPage.propTypes = {} - -module.exports = ConfigProvider(UserPage) diff --git a/frontend/web/components/pages/UserPage.tsx b/frontend/web/components/pages/UserPage.tsx new file mode 100644 index 000000000000..f7bbbc300434 --- /dev/null +++ b/frontend/web/components/pages/UserPage.tsx @@ -0,0 +1,1203 @@ +import React, { FC, useCallback, useEffect, useState } from 'react' +import { RouterChildContext } from 'react-router' +import keyBy from 'lodash/keyBy' + +import { getStore } from 'common/store' +import { getTags } from 'common/services/useTag' +import { getViewMode, setViewMode } from 'common/useViewMode' +import { removeUserOverride } from 'components/RemoveUserOverride' +import { + FeatureState, + IdentityFeatureState, + ProjectFlag, +} from 'common/types/responses' +import API from 'project/api' +import AccountStore from 'common/stores/account-store' +import AppActions from 'common/dispatcher/app-actions' +import Button from 'components/base/forms/Button' +import CodeHelp from 'components/CodeHelp' +import ConfigProvider from 'common/providers/ConfigProvider' +import ConfirmToggleFeature from 'components/modals/ConfirmToggleFeature' +import Constants from 'common/constants' +import CreateFlagModal from 'components/modals/CreateFlag' +import CreateSegmentModal from 'components/modals/CreateSegment' +import CreateTraitModal from 'components/modals/CreateTrait' +import EditIdentity from 'components/EditIdentity' +import FeatureListStore from 'common/stores/feature-list-store' +import FeatureValue from 'components/FeatureValue' +import Format from 'common/utils/format' +import Icon from 'components/Icon' +import IdentifierString from 'components/IdentifierString' +import IdentityProvider from 'common/providers/IdentityProvider' +import IdentitySegmentsProvider from 'common/providers/IdentitySegmentsProvider' +import InfoMessage from 'components/InfoMessage' +import JSONReference from 'components/JSONReference' +import PageTitle from 'components/PageTitle' +import Panel from 'components/base/grid/Panel' +import PanelSearch from 'components/PanelSearch' +import Permission from 'common/providers/Permission' +import Project from 'common/project' +import Switch from 'components/Switch' +import TableFilterOptions from 'components/tables/TableFilterOptions' +import TableGroupsFilter from 'components/tables/TableGroupsFilter' +import TableOwnerFilter from 'components/tables/TableOwnerFilter' +import TableSearchFilter from 'components/tables/TableSearchFilter' +import TableSortFilter from 'components/tables/TableSortFilter' +import TableTagFilter from 'components/tables/TableTagFilter' +import TableValueFilter from 'components/tables/TableValueFilter' +import TagValues from 'components/tags/TagValues' +import TryIt from 'components/TryIt' +import Utils from 'common/utils/utils' +import _data from 'common/data/base/_data' +import classNames from 'classnames' +import moment from 'moment' + +const width = [200, 48, 78] + +const valuesEqual = (actualValue: any, flagValue: any) => { + const nullFalseyA = + actualValue == null || + actualValue === '' || + typeof actualValue === 'undefined' + const nullFalseyB = + flagValue == null || flagValue === '' || typeof flagValue === 'undefined' + return nullFalseyA && nullFalseyB ? true : actualValue === flagValue +} +type UserPageType = { + router: RouterChildContext['router'] + match: { + params: { + environmentId: string + projectId: string + id: string + identity: string + } + } +} +const UserPage: FC = (props) => { + const params = Utils.fromParam() + const { router } = props + const { environmentId, id, identity, projectId } = props.match.params + + // Separate state hooks + const [groupOwners, setGroupOwners] = useState( + typeof params.group_owners === 'string' + ? params.group_owners.split(',').map((v: string) => parseInt(v)) + : [], + ) + const [isEnabled, setIsEnabled] = useState( + params.is_enabled === 'true' + ? true + : params.is_enabled === 'false' + ? false + : null, + ) + const [owners, setOwners] = useState( + typeof params.owners === 'string' + ? params.owners.split(',').map((v: string) => parseInt(v)) + : [], + ) + const [preselect, setPreselect] = useState(Utils.fromParam().flag) + const [search, setSearch] = useState(params.search || null) + const [showArchived, setShowArchived] = useState( + params.is_archived === 'true', + ) + const [sort, setSort] = useState({ + label: Format.camelCase(params.sortBy || 'Name'), + sortBy: params.sortBy || 'name', + sortOrder: params.sortOrder || 'asc', + }) + const [tagStrategy, setTagStrategy] = useState( + params.tag_strategy || 'INTERSECTION', + ) + const [tags, setTags] = useState( + typeof params.tags === 'string' + ? params.tags.split(',').map((v: string) => parseInt(v)) + : [], + ) + const [valueSearch, setValueSearch] = useState(params.value_search || '') + const [actualFlags, setActualFlags] = + useState>() + + const getFilter = useCallback( + () => ({ + group_owners: groupOwners.length ? groupOwners : undefined, + is_archived: showArchived, + is_enabled: isEnabled === null ? undefined : isEnabled, + owners: owners.length ? owners : undefined, + tag_strategy: tagStrategy, + tags: tags.length ? tags.join(',') : undefined, + value_search: valueSearch ? valueSearch : undefined, + }), + [ + groupOwners, + showArchived, + isEnabled, + owners, + tagStrategy, + tags, + valueSearch, + ], + ) + useEffect(() => { + AppActions.searchFeatures( + projectId, + environmentId, + true, + search, + sort, + getFilter(), + ) + }, [search, sort, getFilter, environmentId, projectId]) + + useEffect(() => { + AppActions.getIdentity(environmentId, id) + AppActions.getIdentitySegments(projectId, id) + getTags(getStore(), { projectId: `${projectId}` }) + getActualFlags() + API.trackPage(Constants.pages.USER) + // eslint-disable-next-line + }, []) + + const getActualFlags = () => { + const url = `${ + Project.api + }environments/${environmentId}/${Utils.getIdentitiesEndpoint()}/${id}/${Utils.getFeatureStatesEndpoint()}/all/` + _data.get(url).then((res: IdentityFeatureState[]) => { + setActualFlags(keyBy(res, (v: IdentityFeatureState) => v.feature.name)) + }) + } + + const onSave = () => { + getActualFlags() + } + + const editSegment = (segment: any) => { + API.trackEvent(Constants.events.VIEW_SEGMENT) + openModal( + `Segment - ${segment.name}`, + , + 'side-modal create-segment-modal', + ) + } + + const confirmToggle = (projectFlag: any, environmentFlag: any, cb: any) => { + openModal( + 'Toggle Feature', + , + 'p-0', + ) + } + const editFeature = ( + projectFlag: ProjectFlag, + environmentFlag: FeatureState, + identityFlag: IdentityFeatureState, + multivariate_feature_state_values: IdentityFeatureState['multivariate_feature_state_values'], + ) => { + history.replaceState( + {}, + '', + `${document.location.pathname}?flag=${projectFlag.name}`, + ) + API.trackEvent(Constants.events.VIEW_USER_FEATURE) + openModal( + + + Edit User Feature:{' '} + {projectFlag.name} + + + , + , + 'side-modal create-feature-modal overflow-y-auto', + () => { + history.replaceState({}, '', `${document.location.pathname}`) + }, + ) + } + + const createTrait = () => { + API.trackEvent(Constants.events.VIEW_USER_FEATURE) + openModal( + 'Create User Trait', + , + 'p-0', + ) + } + + const filter = () => { + const currentParams = Utils.fromParam() + if (!currentParams.flag) { + props.router.history.replace( + `${document.location.pathname}?${Utils.toParam(getFilter())}`, + ) + } + AppActions.searchFeatures( + projectId, + environmentId, + true, + search, + sort, + getFilter(), + ) + } + const onTraitSaved = () => { + AppActions.getIdentitySegments(projectId, id) + } + + const editTrait = (trait: { + id: string + trait_key: string + trait_value: string + }) => { + openModal( + 'Edit User Trait', + , + 'p-0', + ) + } + + const removeTrait = (id: string, trait_key: string) => { + openConfirm({ + body: ( +
+ {'Are you sure you want to delete trait '} + {trait_key} + { + ' from this user? Traits can be re-added here or via one of our SDKs.' + } +
+ ), + destructive: true, + onYes: () => + AppActions.deleteIdentityTrait(environmentId, id, id || trait_key), + title: 'Delete Trait', + yesText: 'Confirm', + }) + } + + const preventAddTrait = !AccountStore.getOrganisation().persist_trait_data + const isEdge = Utils.getIsEdge() + const showAliases = isEdge && Utils.getFlagsmithHasFeature('identity_aliases') + + return ( +
+ + {({ permission: manageUserPermission }) => ( + + {({ permission }) => ( +
+ + {( + { + 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 + + +
+ + )} + 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) + }} + /> + +
+
+ } + 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, + ) + 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() + } + + 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 + + ) : ( + + This flag is being + overridden by segments + and would normally be{' '} + + {flagEnabled + ? 'on' + : 'off'} + {' '} + for this user + + )} + + +
+ ) : 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, + ), + ), + , + )} + + )} +
+
+ ) + }} + 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} +
+
+ + + +
e.stopPropagation()} + > + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+
+ )} + renderNoResults={ + + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+ } + > +
+ + This user has no traits. + +
+ + } + filterRow={( + { trait_key }: any, + searchString: string, + ) => + trait_key + .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 + } + /> +
+ ) + } +
+ +
+
+ + + + + + +
+ + + ) + } + + + )} + + )} + + + ) +} + +export default ConfigProvider(UserPage) diff --git a/frontend/web/components/pages/UsersPage.tsx b/frontend/web/components/pages/UsersPage.tsx index 8bf1cda3c76b..f74e51480181 100644 --- a/frontend/web/components/pages/UsersPage.tsx +++ b/frontend/web/components/pages/UsersPage.tsx @@ -19,10 +19,8 @@ import JSONReference from 'components/JSONReference' // we need this to make JSX import Utils from 'common/utils/utils' import Icon from 'components/Icon' import PageTitle from 'components/PageTitle' -import Format from 'common/utils/format' import IdentifierString from 'components/IdentifierString' - -const CodeHelp = require('../CodeHelp') +import CodeHelp from 'components/CodeHelp' type UsersPageType = { router: RouterChildContext['router'] @@ -33,6 +31,10 @@ type UsersPageType = { } } } +const searchTypes = [ + { label: 'ID', value: 'id' }, + { label: 'Alias', value: 'alias' }, +] const UsersPage: FC = (props) => { const [page, setPage] = useState<{ number: number @@ -52,15 +54,19 @@ const UsersPage: FC = (props) => { ) const [deleteIdentity] = useDeleteIdentityMutation({}) const isEdge = Utils.getIsEdge() + const [searchType, setSearchType] = useState<'id' | 'alias'>('id') + + const showAliases = isEdge && Utils.getFlagsmithHasFeature('identity_aliases') const { data: identities, isLoading } = useGetIdentitiesQuery({ + dashboard_alias: searchType === 'alias' ? search?.toLowerCase() : undefined, environmentId: props.match.params.environmentId, isEdge, page: page.number, pageType: page.pageType, page_size: 10, pages: page.pages, - q: search, + q: searchType === 'alias' ? undefined : search, }) const { environmentId } = props.match.params @@ -149,6 +155,19 @@ const UsersPage: FC = (props) => { +