Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Identity alias #4620

Merged
merged 18 commits into from
Oct 2, 2024
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
Loading