Skip to content

Commit

Permalink
feat: Identity alias (#4620)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-ssg authored Oct 2, 2024
1 parent 22a3083 commit d18049b
Show file tree
Hide file tree
Showing 25 changed files with 1,443 additions and 1,377 deletions.
1 change: 0 additions & 1 deletion frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ module.exports = {
'Flex': true,
'FormGroup': true,
'Headway': true,
'IdentityProvider': true,
'Input': true,
'InputGroup': true,
'Link': true,
Expand Down
2 changes: 1 addition & 1 deletion frontend/common/providers/IdentityProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,4 @@ IdentityProvider.propTypes = {
onSave: OptionalFunc,
}

module.exports = IdentityProvider
export default IdentityProvider
38 changes: 34 additions & 4 deletions frontend/common/services/useIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -52,6 +53,7 @@ export const identityService = service
providesTags: [{ id: 'LIST', type: 'Identity' }],
query: (baseQuery) => {
const {
dashboard_alias,
environmentId,
isEdge,
page,
Expand All @@ -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}`
Expand Down Expand Up @@ -127,6 +130,21 @@ export const identityService = service
return transformCorePaging(req, baseQueryReturnValue)
},
}),
updateIdentity: builder.mutation<Res['identity'], Req['updateIdentity']>({
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
}),
})
Expand Down Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions frontend/common/stores/feature-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,9 @@ const controller = {
environment: environmentFlag.environment,
feature: projectFlag.id,
},
{
forceRefetch: true
}
{
forceRefetch: true,
},
)
let segments = null
if (mode === 'SEGMENT') {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Environment,
UserGroup,
AttributeName,
Identity,
} from './responses'

export type PagedRequest<T> = T & {
Expand Down Expand Up @@ -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
}>
Expand Down Expand Up @@ -522,5 +523,9 @@ export type Req = {
idp_attribute_name: string
}
}
updateIdentity: {
environmentId: string
data: Identity
}
// END OF TYPES
}
2 changes: 1 addition & 1 deletion frontend/common/useViewMode.ts
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion frontend/env/project_dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}),
}
2 changes: 1 addition & 1 deletion frontend/env/project_prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}),
}
2 changes: 1 addition & 1 deletion frontend/web/components/CodeHelp.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,4 @@ const CodeHelp = class extends Component {

CodeHelp.propTypes = {}

module.exports = ConfigProvider(CodeHelp)
export default ConfigProvider(CodeHelp)
127 changes: 127 additions & 0 deletions frontend/web/components/EditIdentity.tsx
Original file line number Diff line number Diff line change
@@ -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<EditIdentityType> = ({ data, environmentId }) => {
const [alias, setAlias] = useState(data.dashboard_alias)
const aliasRef = useRef<HTMLSpanElement>(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<HTMLSpanElement>) => {
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 (
<>
<span
ref={aliasRef}
className={classNames('fw-normal',{'text-muted':!alias})}
contentEditable={true}
suppressContentEditableWarning={true}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onInput={handleInput}
role='textbox'
aria-label='Alias'
>
{alias || 'None'}
</span>
<Button
disabled={!data}
iconSize={18}
theme='text'
className='ms-2 text-primary'
iconRightColour='primary'
iconRight={'edit'}
onClick={handleFocus}
>
Edit
</Button>
<ErrorMessage>{error}</ErrorMessage>
</>
)
}

export default EditIdentity
11 changes: 6 additions & 5 deletions frontend/web/components/InfoMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,14 +80,14 @@ const InfoMessage: FC<InfoMessageType> = ({
</div>
</div>
{!isCollapsed && (
<>
<div className='flex-fill mt-1'>{children}</div>
<div className="flex-row">
<div className='flex-fill'>{children}</div>
{url && buttonText && (
<button className='btn my-2 ml-2' onClick={handleOpenNewWindow}>
<Button onClick={handleOpenNewWindow}>
{buttonText}
</button>
</Button>
)}
</>
</div>
)}
</div>
{isClosable && (
Expand Down
1 change: 1 addition & 0 deletions frontend/web/components/PanelSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ const PanelSearch = class extends Component {
placeholder='Search'
search
/>
{this.props.filterRowContent}
</Row>
</Row>
)}
Expand Down
4 changes: 1 addition & 3 deletions frontend/web/components/TryIt.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,4 @@ const TryIt = class extends Component {
}
}

TryIt.propTypes = {}

module.exports = ConfigProvider(TryIt)
export default ConfigProvider(TryIt)
Loading

0 comments on commit d18049b

Please sign in to comment.