diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index b3edece5bd24..ef606bdb1875 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -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[] @@ -169,6 +174,7 @@ export type Segment = { project: string | number feature?: number metadata: Metadata[] | [] + memberships?: SegmentMembership[] } export type ProjectChangeRequest = Omit< ChangeRequest, diff --git a/frontend/web/components/EnvironmentSelect.tsx b/frontend/web/components/EnvironmentSelect.tsx index 966129dabe0a..8636953ed48c 100644 --- a/frontend/web/components/EnvironmentSelect.tsx +++ b/frontend/web/components/EnvironmentSelect.tsx @@ -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> & { projectId: number value?: string @@ -73,11 +79,9 @@ const EnvironmentSelect: FC = ({ ? [{ 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) + } /> ) diff --git a/frontend/web/components/Tooltip.tsx b/frontend/web/components/Tooltip.tsx index 7057d37110ea..c0be5e78b7f5 100644 --- a/frontend/web/components/Tooltip.tsx +++ b/frontend/web/components/Tooltip.tsx @@ -14,11 +14,13 @@ export type TooltipProps = { effect?: 'float' | 'solid' afterShow?: () => void renderInPortal?: boolean + delayShow?: number } const Tooltip: FC = ({ afterShow, children, + delayShow = 500, effect, place, plainText, @@ -53,7 +55,7 @@ const Tooltip: FC = ({ place={place || 'top'} float={effect === 'float'} afterShow={afterShow} - delayShow={500} + delayShow={delayShow} style={{ wordBreak: 'break-word' }} /> diff --git a/frontend/web/components/modals/CreateSegment.tsx b/frontend/web/components/modals/CreateSegment.tsx index a4e4510a4698..db77d1063ac2 100644 --- a/frontend/web/components/modals/CreateSegment.tsx +++ b/frontend/web/components/modals/CreateSegment.tsx @@ -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' @@ -582,7 +583,17 @@ const CreateSegment: FC = ({ /> - + + Identities + + + } + >
= ({ name={name} searchInput={searchInput} setSearchInput={setSearchInput} + memberships={segment.memberships} />
diff --git a/frontend/web/components/modals/CreateSegmentUsersTabContent.tsx b/frontend/web/components/modals/CreateSegmentUsersTabContent.tsx index 69b8c0e87248..8025cd85caef 100644 --- a/frontend/web/components/modals/CreateSegmentUsersTabContent.tsx +++ b/frontend/web/components/modals/CreateSegmentUsersTabContent.tsx @@ -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() + ;(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 ( + + {label} + {environment && membership && ( + + )} + + ) + } + + 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 ( <> @@ -100,13 +137,24 @@ const CreateSegmentUsersTabContent: React.FC< title='Environment' className='col-4' component={ - { - setEnvironmentId(environmentId) - }} - /> + <> + { + setEnvironmentId(environmentId) + }} + formatOptionLabel={renderEnvOption} + /> +
+ Last synced:{' '} + {selectedMembership + ? moment(selectedMembership.last_synced_at).format( + 'Do MMM YYYY HH:mm:ss', + ) + : '—'} +
+ } /> { + 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 = ({ count, dataTest, tooltip }) => ( + + + {count} + + } + > + {tooltip} + +) + +type TotalProps = { + memberships: SegmentMembership[] | undefined +} + +export const SegmentMembershipTotalBadge: FC = ({ 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 ( + + ) +} + +type EnvProps = { + membership: SegmentMembership + environment: Environment +} + +export const SegmentMembershipEnvBadge: FC = ({ + environment, + membership, +}) => ( + +) diff --git a/frontend/web/components/segments/SegmentRow/SegmentRow.tsx b/frontend/web/components/segments/SegmentRow/SegmentRow.tsx index a965bc95ebf0..fbcae5e35c14 100644 --- a/frontend/web/components/segments/SegmentRow/SegmentRow.tsx +++ b/frontend/web/components/segments/SegmentRow/SegmentRow.tsx @@ -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' @@ -81,6 +82,7 @@ const SegmentRow: FC = ({ index, projectId, segment }) => { {feature && (
Feature-Specific
)} +
{description || 'No description'} diff --git a/frontend/web/project/project-components.js b/frontend/web/project/project-components.js index 696c7e0e0a9d..0dcef7ea6bb5 100644 --- a/frontend/web/project/project-components.js +++ b/frontend/web/project/project-components.js @@ -92,6 +92,15 @@ 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} +
{props.data.description}
+ + ) return (
{ props.data.isDisabled ? 'text-muted cursor-not-allowed' : '' }`} > -
- {props.data.label} -
{props.data.description}
-
+
{labelContent}
{props.isSelected && ( )} diff --git a/frontend/web/styles/components/_chip.scss b/frontend/web/styles/components/_chip.scss index d4e0313bb5c1..0f3e07a99d85 100644 --- a/frontend/web/styles/components/_chip.scss +++ b/frontend/web/styles/components/_chip.scss @@ -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;