-
Notifications
You must be signed in to change notification settings - Fork 522
feat(Segment membership inspection PoC): Surface identity counts in segments UI #7467
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
base: main
Are you sure you want to change the base?
Changes from all commits
4a4cffd
f87b4eb
8e80b52
edfb511
52bf43f
3b6c7d5
2dfbb7b
a4f7dff
a9f61b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
@@ -23,6 +28,7 @@ interface CreateSegmentUsersTabContentProps { | |
| name: string | ||
| searchInput: string | ||
| setSearchInput: (input: string) => void | ||
| memberships?: SegmentMembership[] | ||
| } | ||
|
|
||
| type UserRowType = { | ||
|
|
@@ -80,6 +86,7 @@ const CreateSegmentUsersTabContent: React.FC< | |
| environmentId, | ||
| identities, | ||
| identitiesLoading, | ||
| memberships, | ||
| name, | ||
| page, | ||
| projectId, | ||
|
|
@@ -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) || [] | ||
| 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'}> | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
{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 | ||
|
|
||
| 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} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`} | ||
| /> | ||
| ) | ||
There was a problem hiding this comment.
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 ?