Skip to content
Merged
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
22 changes: 4 additions & 18 deletions frontend/src/scenes/session-recordings/playlist/ItemCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import { useActions, useValues } from 'kea'

import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox'

import { SessionRecordingType } from '~/types'

import { MAX_SELECTED_RECORDINGS, sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic'
import { useRecordingSelection } from './useRecordingSelection'

export function ItemCheckbox({ recording }: { recording: SessionRecordingType }): JSX.Element {
const { selectedRecordingsIds } = useValues(sessionRecordingsPlaylistLogic)
const { setSelectedRecordingsIds } = useActions(sessionRecordingsPlaylistLogic)
const { checked, disabledReason, toggle } = useRecordingSelection(recording)

const checked = selectedRecordingsIds.some((s) => s === recording.id)
return (
<LemonCheckbox
checked={checked}
data-attr="select-recording"
aria-label="Select recording"
disabledReason={
!checked && selectedRecordingsIds.length >= MAX_SELECTED_RECORDINGS
? `Cannot select more than ${MAX_SELECTED_RECORDINGS} recordings at once`
: undefined
}
onChange={() => {
if (checked) {
setSelectedRecordingsIds(selectedRecordingsIds.filter((r) => r !== recording.id))
} else {
setSelectedRecordingsIds([...selectedRecordingsIds, recording.id])
}
}}
disabledReason={disabledReason}
onChange={toggle}
stopPropagation
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Checkbox } from '@posthog/quill'

import { SessionRecordingType } from '~/types'

import { useRecordingSelection } from './useRecordingSelection'

export function QuillItemCheckbox({ recording }: { recording: SessionRecordingType }): JSX.Element {
const { checked, disabledReason, toggle } = useRecordingSelection(recording)

return (
// The row behind the checkbox opens the recording on click, so the click must stop here.
<span className="inline-flex" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={checked}
data-attr="select-recording"
aria-label="Select recording"
disabledReason={disabledReason}
onCheckedChange={toggle}
/>
</span>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useValues } from 'kea'

import { Dot, Tooltip, TooltipContent, TooltipTrigger } from '@posthog/quill'

import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'

import { unwatchedIndicatorAriaLabel, unwatchedIndicatorTooltip } from './unwatchedIndicatorTooltip'

export function QuillUnwatchedIndicator({ otherViewersCount }: { otherViewersCount: number }): JSX.Element {
const { featureFlags } = useValues(featureFlagLogic)
const isExcludedFromHideRecordingsMenu = !!featureFlags[FEATURE_FLAGS.REPLAY_EXCLUDE_FROM_HIDE_RECORDINGS_MENU]
const watchedByOthers = !isExcludedFromHideRecordingsMenu && otherViewersCount > 0

return (
<Tooltip>
<TooltipTrigger
render={
<span
className="inline-flex"
aria-label={unwatchedIndicatorAriaLabel(otherViewersCount, isExcludedFromHideRecordingsMenu)}
/>
}
>
<Dot variant={watchedByOthers ? 'info' : 'destructive'} />
</TooltipTrigger>
<TooltipContent>
{unwatchedIndicatorTooltip(otherViewersCount, isExcludedFromHideRecordingsMenu)}
</TooltipContent>
</Tooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import clsx from 'clsx'
import { ReactNode } from 'react'

import { Tooltip, TooltipContent, TooltipTrigger } from '@posthog/quill'

export function RecordingRowStat({
icon,
value,
label,
destructive = false,
}: {
icon: ReactNode
value: ReactNode
label: string
destructive?: boolean
}): JSX.Element {
return (
<Tooltip>
<TooltipTrigger
render={
<span
className={clsx(
'flex items-center gap-x-0.5 shrink-0',
destructive && 'text-destructive-foreground'
)}
/>
}
>
{icon}
{value}
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import { memo } from 'react'

import { IconBug, IconCursorClick, IconKeyboard, IconLive } from '@posthog/icons'
import { LemonTag } from '@posthog/lemon-ui'
import { Badge, Skeleton, Tooltip, TooltipContent, TooltipTrigger } from '@posthog/quill'

import { PropertyIcon } from 'lib/components/PropertyIcon/PropertyIcon'
import { TZLabel } from 'lib/components/TZLabel'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook'
import { SimpleTimeLabel } from 'scenes/session-recordings/components/SimpleTimeLabel'
import { TimestampFormat, playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic'
Expand All @@ -19,11 +17,12 @@
import { asDisplay } from 'products/persons/frontend/person-utils'

import { gatherIconProperties } from './gatherIconProperties'
import { ItemCheckbox } from './ItemCheckbox'
import { QuillItemCheckbox } from './QuillItemCheckbox'
import { QuillUnwatchedIndicator } from './QuillUnwatchedIndicator'
import { RecordingRowStat } from './RecordingRowStat'
import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPropertiesLogic'
import { DEFAULT_RECORDING_FILTERS_ORDER_BY, sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic'
import { SortMetric } from './SortMetric'
import { UnwatchedIndicator } from './UnwatchedIndicator'

export interface SessionRecordingPreviewCompactProps {
recording: SessionRecordingType
Expand All @@ -32,7 +31,7 @@
}

export const SessionRecordingPreviewCompact = memo(
function SessionRecordingPreviewCompact({

Check warning on line 34 in frontend/src/scenes/session-recordings/playlist/SessionRecordingPreviewCompact.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`SessionRecordingPreviewCompact` has cyclomatic complexity 17 (warn >10)
recording,
isActive,
selectable = false,
Expand All @@ -46,6 +45,7 @@
const loading = !recordingProperties && recordingPropertiesLoading
const iconProperties = gatherIconProperties(recordingProperties, recording)
const errorCount = recording.console_error_count ?? 0

const tileDetails: { label: string; value: string | undefined }[] = [
{
label: 'Location',
Expand All @@ -69,45 +69,47 @@
return (
<DraggableToNotebook href={urls.replaySingle(recording.id)}>
<div
data-quill
className={clsx(
'SessionRecordingPreview SessionRecordingPreview--compact group flex items-center gap-x-2 overflow-hidden cursor-pointer py-1.5 px-2 text-[11px]',
'SessionRecordingPreview SessionRecordingPreview--compact group flex items-center gap-x-2 overflow-hidden cursor-pointer py-1.5 px-2 text-[11px] text-foreground',
isActive && 'SessionRecordingPreview--active'
)}
>
{selectable && (
<span className="shrink-0 flex items-center [&_label]:min-h-0">
<ItemCheckbox recording={recording} />
<span className="shrink-0 flex items-center">
<QuillItemCheckbox recording={recording} />
</span>
)}
{loading ? (
<LemonSkeleton className="w-8 h-8 shrink-0 rounded" />
<Skeleton className="w-8 h-8 shrink-0 rounded" />
) : (
<Tooltip
placement="right"
title={
<div className="flex flex-col gap-0.5 text-xs">
{tileDetails.map(({ label, value }) => (
<span key={label} className="break-all">
<span className="text-secondary">{label}: </span>
{value}
</span>
))}
</div>
}
>
<div className="grid grid-cols-2 gap-0.5 p-0.5 w-8 h-8 shrink-0 rounded border bg-surface-secondary ph-no-capture">
<Tooltip>
<TooltipTrigger
render={
<div className="grid grid-cols-2 gap-0.5 p-0.5 w-8 h-8 shrink-0 rounded border border-border bg-muted ph-no-capture" />
}
>
{iconProperties.map(({ property, value }) => (
<span
key={property}
className="flex items-center justify-center rounded-sm bg-surface-primary leading-none text-[11px]"
className="flex items-center justify-center rounded-sm bg-card leading-none text-[11px]"
>
<PropertyIcon property={property} value={value} />
</span>
))}
</div>
</TooltipTrigger>
<TooltipContent side="right">
<div className="flex flex-col gap-0.5 text-xs">
{tileDetails.map(({ label, value }) => (
<span key={label} className="break-all">
<span className="text-muted-foreground">{label}: </span>
{value}
</span>
))}
</div>
</TooltipContent>
</Tooltip>
)}

<div className="grow min-w-0 flex flex-col gap-y-0.5">
<div className="flex items-center gap-x-1.5">
<span
Expand All @@ -119,15 +121,19 @@
{asDisplay(recording.person)}
</span>
{recording.matches_filters === false && (
<Tooltip title="This recording is listed only because it's open. It doesn't match the current filters.">
<LemonTag type="warning" size="small" className="shrink-0">
<Tooltip>
<TooltipTrigger render={<Badge variant="warning" className="shrink-0" />}>
Doesn't match filters
</LemonTag>
</TooltipTrigger>
<TooltipContent>
This recording is listed only because it's open. It doesn't match the current
filters.
</TooltipContent>
</Tooltip>
)}
{playlistTimestampFormat === TimestampFormat.Relative ? (
<TZLabel
className="text-secondary shrink-0"
className="text-muted-foreground shrink-0"
time={recording.start_time}
placement="right"
/>
Expand All @@ -138,49 +144,46 @@
/>
)}
</div>

<div className="flex items-center gap-x-2 text-secondary">
<Tooltip title="Clicks">
<span className="flex items-center gap-x-0.5 shrink-0">
<IconCursorClick />
{recording.click_count}
</span>
</Tooltip>
<Tooltip title="Key presses">
<span className="flex items-center gap-x-0.5 shrink-0">
<IconKeyboard />
{recording.keypress_count}
</span>
</Tooltip>
<div className="flex items-center gap-x-2 text-muted-foreground">
<RecordingRowStat icon={<IconCursorClick />} value={recording.click_count} label="Clicks" />
<RecordingRowStat
icon={<IconKeyboard />}
value={recording.keypress_count}
label="Key presses"
/>
{errorCount > 0 && order !== 'console_error_count' && (
<Tooltip title="Console errors">
<span className="flex items-center gap-x-0.5 shrink-0 text-danger">
<IconBug />
{errorCount}
</span>
</Tooltip>
<RecordingRowStat
icon={<IconBug />}
value={errorCount}
label="Console errors"
destructive
/>
)}
<span className="grow" />
<SortMetric recording={recording} order={order} />
</div>
{isActive && recording.start_url && (
<span
title={`First URL: ${recording.start_url}`}
className="truncate text-secondary ph-no-capture"
className="truncate text-muted-foreground ph-no-capture"
>
{recording.start_url.replace(/https?:\/\//g, '').split(/[?|#]/)[0]}
</span>
)}
</div>

<div className="min-w-4 flex flex-col items-center gap-y-1">
{recording.ongoing ? (
<Tooltip title="This recording is still ongoing - we received data within the last 5 minutes.">
<IconLive className="animate-[pulse_1s_ease-out_infinite] text-primary-3000" />
<Tooltip>
<TooltipTrigger render={<span className="inline-flex" />}>
<IconLive className="animate-[pulse_1s_ease-out_infinite] text-primary-3000" />
</TooltipTrigger>
<TooltipContent>
This recording is still ongoing - we received data within the last 5 minutes.
</TooltipContent>
</Tooltip>
) : null}
{!recording.viewed ? (
<UnwatchedIndicator otherViewersCount={recording.viewers?.length || 0} />
<QuillUnwatchedIndicator otherViewersCount={recording.viewers?.length || 0} />
) : null}
</div>
</div>
Expand Down
Loading
Loading