Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ export type SegmentRule = {
conditions: SegmentCondition[]
version_of: number | undefined
}
export type SegmentMembership = {
environment: number
count: number
last_synced_at: string
}
export type Segment = {
id: number
rules: SegmentRule[]
Expand All @@ -169,6 +174,7 @@ export type Segment = {
project: string | number
feature?: number
metadata: Metadata[] | []
memberships?: SegmentMembership[]
}
export type ProjectChangeRequest = Omit<
ChangeRequest,
Expand Down
14 changes: 9 additions & 5 deletions frontend/web/components/EnvironmentSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
import { Props } from 'react-select'
import { Environment } from 'common/types/responses'

export type EnvironmentSelectOption = {
value: string
label: string
environment: Environment | null
}

type EnvironmentSelectType = Partial<Omit<Props, 'value'>> & {
projectId: number
value?: string
Expand Down Expand Up @@ -73,11 +79,9 @@ const EnvironmentSelect: FC<EnvironmentSelectType> = ({
? [{ environment: null, label: 'All Environments', value: '' }]
: []
).concat(environments)}
onChange={(value: {
value: string
label: string
environment: Environment
}) => onChange(value?.value || '', value?.environment)}
onChange={(value: EnvironmentSelectOption) =>
onChange(value?.value || '', value?.environment)
}
/>
</div>
)
Expand Down
4 changes: 3 additions & 1 deletion frontend/web/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export type TooltipProps = {
effect?: 'float' | 'solid'
afterShow?: () => void
renderInPortal?: boolean
delayShow?: number
}

const Tooltip: FC<TooltipProps> = ({
afterShow,
children,
delayShow = 500,
effect,
place,
plainText,
Expand Down Expand Up @@ -53,7 +55,7 @@ const Tooltip: FC<TooltipProps> = ({
place={place || 'top'}
float={effect === 'float'}
afterShow={afterShow}
delayShow={500}
delayShow={delayShow}
style={{ wordBreak: 'break-word' }}
/>
</>
Expand Down
14 changes: 13 additions & 1 deletion frontend/web/components/modals/CreateSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from 'common/services/useSegment'
import Utils from 'common/utils/utils'
import AssociatedSegmentOverrides from 'components/segments/AssociatedSegmentOverrides'
import { SegmentMembershipTotalBadge } from 'components/segments/SegmentMembershipBadge'
import Button from 'components/base/forms/Button'
import InfoMessage from 'components/InfoMessage'
import InputGroup from 'components/base/forms/InputGroup'
Expand Down Expand Up @@ -582,7 +583,17 @@ const CreateSegment: FC<CreateSegmentType> = ({
/>
</div>
</TabItem>
<TabItem tabLabel='Identities'>
<TabItem
tabLabelString='Identities'
tabLabel={
<>
Identities
<SegmentMembershipTotalBadge
memberships={segment.memberships}
/>
</>
}
>
<div className='my-4'>
<CreateSegmentUsersTabContent
projectId={projectId}
Expand All @@ -595,6 +606,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
name={name}
searchInput={searchInput}
setSearchInput={setSearchInput}
memberships={segment.memberships}
/>
</div>
</TabItem>
Expand Down
66 changes: 57 additions & 9 deletions frontend/web/components/modals/CreateSegmentUsersTabContent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React, { FC } from 'react'
import EnvironmentSelect from 'components/EnvironmentSelect'
import moment from 'moment'
import EnvironmentSelect, {
EnvironmentSelectOption,
} from 'components/EnvironmentSelect'
import PanelSearch from 'components/PanelSearch'
import InfoMessage from 'components/InfoMessage'
import InputGroup from 'components/base/forms/InputGroup'
import Utils from 'common/utils/utils'
import { Res } from 'common/types/responses'
import { Environment, Res, SegmentMembership } from 'common/types/responses'
import Icon from 'components/icons/Icon'
import {
identitySegmentService,
useGetIdentitySegmentsQuery,
} from 'common/services/useIdentitySegment'
import { getStore } from 'common/store'
import ProjectStore from 'common/stores/project-store'
import { SegmentMembershipEnvBadge } from 'components/segments/SegmentMembershipBadge'

interface CreateSegmentUsersTabContentProps {
projectId: string | number
Expand All @@ -23,6 +28,7 @@ interface CreateSegmentUsersTabContentProps {
name: string
searchInput: string
setSearchInput: (input: string) => void
memberships?: SegmentMembership[]
}

type UserRowType = {
Expand Down Expand Up @@ -80,6 +86,7 @@ const CreateSegmentUsersTabContent: React.FC<
environmentId,
identities,
identitiesLoading,
memberships,
name,
page,
projectId,
Expand All @@ -88,6 +95,36 @@ const CreateSegmentUsersTabContent: React.FC<
setPage,
setSearchInput,
}) => {
const membershipByEnvId = React.useMemo(() => {
const map = new Map<number, SegmentMembership>()
;(memberships ?? []).forEach((m) => map.set(m.environment, m))
return map
}, [memberships])

const renderEnvOption = ({ environment, label }: EnvironmentSelectOption) => {
const membership = environment
? membershipByEnvId.get(environment.id)
: undefined
return (
<span className='d-flex align-items-center'>
<span>{label}</span>
{environment && membership && (
<SegmentMembershipEnvBadge
membership={membership}
environment={environment}
/>
)}
</span>
)
}

const selectedMembership = React.useMemo(() => {
if (!environmentId) return null
const envs = (ProjectStore.getEnvs() as Environment[] | null) || []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replace this with a RTK Query ?

  const { data: environmentsData } = useGetEnvironmentsQuery(
    { projectId: Number(projectId) },
    { skip: !projectId },
  )
  const envs = environmentsData?.results ?? []

  const selectedMembership = React.useMemo(() => {
    if (!environmentId) return null
    const env = envs.find((e) => e.api_key === environmentId)
    return env ? membershipByEnvId.get(env.id) ?? null : null
  }, [environmentId, membershipByEnvId, envs])

const env = envs.find((e) => e.api_key === environmentId)
return env ? membershipByEnvId.get(env.id) ?? null : null
}, [environmentId, membershipByEnvId])

return (
<>
<InfoMessage collapseId={'random-identity-sample'}>
Expand All @@ -100,13 +137,24 @@ const CreateSegmentUsersTabContent: React.FC<
title='Environment'
className='col-4'
component={
<EnvironmentSelect
projectId={`${projectId}`}
value={environmentId}
onChange={(environmentId: string) => {
setEnvironmentId(environmentId)
}}
/>
<>
<EnvironmentSelect
projectId={`${projectId}`}
value={environmentId}
onChange={(environmentId: string) => {
setEnvironmentId(environmentId)
}}
formatOptionLabel={renderEnvOption}
/>
<div className='text-muted fs-small mt-2'>
Last synced:{' '}
{selectedMembership
? moment(selectedMembership.last_synced_at).format(
'Do MMM YYYY HH:mm:ss',
)
: '—'}
</div>
Comment on lines +149 to +156
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The "Last synced: —" line renders unconditionally before the user picks an environment. What if we only render the line once selectedMembership exists ?

  {selectedMembership && (
    <div className='text-muted fs-small mt-2'>
      Last synced: {moment(selectedMembership.last_synced_at).format(
        'Do MMM YYYY HH:mm:ss',
      )}    
    </div>
  )}

</>
}
/>
<PanelSearch
Expand Down
84 changes: 84 additions & 0 deletions frontend/web/components/segments/SegmentMembershipBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { FC } from 'react'

import { Environment, SegmentMembership } from 'common/types/responses'
import Tooltip from 'components/Tooltip'
import UsersIcon from 'components/icons/UsersIcon'

const shortAgo = (iso: string): string => {
const diffSec = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000))
if (diffSec < 60) return `${diffSec}s ago`
const diffMin = Math.round(diffSec / 60)
if (diffMin < 60) return `${diffMin}m ago`
const diffHr = Math.round(diffMin / 60)
if (diffHr < 24) return `${diffHr}h ago`
return `${Math.round(diffHr / 24)}d ago`
}

const formatTooltip = (count: number, lastSyncedAt: string | undefined): string => {
const noun = count === 1 ? 'identity' : 'identities'
const base = `${count} ${noun}`
return lastSyncedAt ? `${base} — last synced ~${shortAgo(lastSyncedAt)}` : base
}

type ChipProps = {
count: number
dataTest?: string
tooltip: string
}

const Chip: FC<ChipProps> = ({ count, dataTest, tooltip }) => (
<Tooltip
plainText
delayShow={100}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind giving a context why do we need this specific delay time ?

title={
<span
className='chip chip--xs bg-primary text-white ms-3'
style={{ border: 'none', alignSelf: 'center', verticalAlign: 'middle' }}
data-test={dataTest}
>
<UsersIcon className='chip-svg-icon' />
<span>{count}</span>
</span>
}
>
{tooltip}
</Tooltip>
)

type TotalProps = {
memberships: SegmentMembership[] | undefined
}

export const SegmentMembershipTotalBadge: FC<TotalProps> = ({ memberships }) => {
if (!memberships?.length) {
return null
}
const total = memberships.reduce((sum, m) => sum + m.count, 0)
const latest = memberships.reduce(
(acc, m) => (!acc || m.last_synced_at > acc ? m.last_synced_at : acc),
'',
)
return (
<Chip
count={total}
tooltip={formatTooltip(total, latest || undefined)}
dataTest='segment-membership-total'
/>
)
}

type EnvProps = {
membership: SegmentMembership
environment: Environment
}

export const SegmentMembershipEnvBadge: FC<EnvProps> = ({
environment,
membership,
}) => (
<Chip
count={membership.count}
tooltip={formatTooltip(membership.count, membership.last_synced_at)}
dataTest={`segment-membership-${environment.api_key}`}
/>
)
2 changes: 2 additions & 0 deletions frontend/web/components/segments/SegmentRow/SegmentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useHasPermission } from 'common/providers/Permission'

import { Segment } from 'common/types/responses'
import SegmentAction from './components/SegmentAction'
import { SegmentMembershipTotalBadge } from 'components/segments/SegmentMembershipBadge'
import ConfirmCloneSegment from 'components/modals/ConfirmCloneSegment'
import { useCloneSegmentMutation } from 'common/services/useSegment'
import { handleRemoveSegment } from 'components/modals/ConfirmRemoveSegment'
Expand Down Expand Up @@ -81,6 +82,7 @@ const SegmentRow: FC<SegmentRowProps> = ({ index, projectId, segment }) => {
{feature && (
<div className='chip chip--xs ml-2'>Feature-Specific</div>
)}
<SegmentMembershipTotalBadge memberships={segment.memberships} />
</Row>
<div className='list-item-subtitle mt-1'>
{description || 'No description'}
Expand Down
14 changes: 10 additions & 4 deletions frontend/web/project/project-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,23 @@ global.ToggleChip = ToggleChip

// Custom Option component to show the tick mark next to selected option in the dropdown
const Option = (props) => {
const { formatOptionLabel } = props.selectProps
const labelContent = formatOptionLabel
? formatOptionLabel(props.data, { context: 'menu' })
: (
<>
{props.data.label}
<div className='text-small'>{props.data.description}</div>
</>
)
return (
<components.Option {...props}>
<div
className={`d-flex justify-content-between align-items-center ${
props.data.isDisabled ? 'text-muted cursor-not-allowed' : ''
}`}
>
<div>
{props.data.label}
<div className='text-small'>{props.data.description}</div>
</div>
<div>{labelContent}</div>
{props.isSelected && (
<IonIcon icon={checkmarkCircle} className='text-primary' />
)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/styles/components/_chip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
.chip {
display: flex;
align-items: center;
align-self: flex-start !important;
align-self: flex-start;
background-color: $primary-alfa-8;
border: 1px solid $primary-alfa-24;
padding: 5px 12px;
Expand Down
Loading