-
+
+ {hours != '00' && {hours}:}
- {hours}:
-
- {minutes}:
-
- {seconds}
+ {minutes}:{seconds}
)
@@ -89,7 +81,6 @@ interface GatheredProperty {
property: string
value: string | undefined
label: string | undefined
- tooltipValue: string
}
const browserIconPropertyKeys = ['$geoip_country_code', '$browser', '$device_type', '$os']
@@ -107,118 +98,53 @@ export function gatherIconProperties(
const deviceType = iconProperties['$device_type'] || iconProperties['$initial_device_type']
const iconPropertyKeys = deviceType === 'Mobile' ? mobileIconPropertyKeys : browserIconPropertyKeys
- return iconPropertyKeys.flatMap((property) => {
- let value = iconProperties?.[property]
- let label = value
- if (property === '$device_type') {
- value = iconProperties?.['$device_type'] || iconProperties?.['$initial_device_type']
- }
-
- let tooltipValue = value
- if (property === '$geoip_country_code') {
- tooltipValue = `${iconProperties?.['$geoip_country_name']} (${value})`
- label = [iconProperties?.['$geoip_city_name'], iconProperties?.['$geoip_subdivision_1_code']]
- .filter(Boolean)
- .join(', ')
- }
- return { property, value, tooltipValue, label }
- })
+ return iconPropertyKeys
+ .flatMap((property) => {
+ let value = iconProperties?.[property]
+ const label = value
+ if (property === '$device_type') {
+ value = iconProperties?.['$device_type'] || iconProperties?.['$initial_device_type']
+ }
+
+ return { property, value, label }
+ })
+ .filter((property) => !!property.value)
}
export interface PropertyIconsProps {
recordingProperties: GatheredProperty[]
loading?: boolean
- onPropertyClick?: (property: string, value?: string) => void
- iconClassnames?: string
+ iconClassNames?: string
showTooltip?: boolean
showLabel?: (key: string) => boolean
}
-export function PropertyIcons({
- recordingProperties,
- loading,
- onPropertyClick,
- iconClassnames,
- showTooltip = true,
- showLabel = undefined,
-}: PropertyIconsProps): JSX.Element {
+export function PropertyIcons({ recordingProperties, loading, iconClassNames }: PropertyIconsProps): JSX.Element {
return (
-
+
{loading ? (
-
+
+
+
+
) : (
- recordingProperties.map(({ property, value, tooltipValue, label }) => {
- return (
-
- {
- if (e.altKey) {
- e.stopPropagation()
- posthog.capture('alt click property filter added', { property })
- onPropertyClick?.(property, value)
- }
- }}
- className={iconClassnames}
- property={property}
- value={value}
- noTooltip={!showTooltip}
- tooltipTitle={() => (
-
- Alt + Click
to filter for
-
- {tooltipValue ?? 'N/A'}
-
- )}
- />
- {showLabel?.(property) && {label || value}}
-
- )
- })
+ recordingProperties.map(({ property, value, label }) => (
+
+
+
+ {!value ? 'Not captured' : label || value}
+
+
+ ))
)}
)
}
-function ActivityIndicators({
- recording,
- ...props
-}: {
- recording: SessionRecordingType
- onPropertyClick?: (property: string, value?: string) => void
- iconClassnames: string
-}): JSX.Element {
- const { recordingPropertiesById, recordingPropertiesLoading } = useValues(sessionRecordingsListPropertiesLogic)
- const recordingProperties = recordingPropertiesById[recording.id]
- const loading = !recordingProperties && recordingPropertiesLoading
- const iconProperties = gatherIconProperties(recordingProperties, recording)
-
- return (
-
-
-
-
-
- {recording.click_count}
-
-
-
-
- {recording.keypress_count}
-
-
- )
-}
-
function FirstURL(props: { startUrl: string | undefined }): JSX.Element {
const firstPath = props.startUrl?.replace(/https?:\/\//g, '').split(/[?|#]/)[0]
return (
-
+
{firstPath}
@@ -239,12 +165,12 @@ function PinnedIndicator(): JSX.Element | null {
)
}
-function ViewedIndicator({ viewed }: { viewed: boolean }): JSX.Element | null {
- return !viewed ? (
+function ViewedIndicator(): JSX.Element {
+ return (
-
+
- ) : null
+ )
}
function durationToShow(recording: SessionRecordingType, durationType: DurationType | undefined): number | undefined {
@@ -259,108 +185,191 @@ export function SessionRecordingPreview({
recording,
isActive,
onClick,
- onPropertyClick,
pinned,
summariseFn,
sessionSummaryLoading,
}: SessionRecordingPreviewProps): JSX.Element {
const { orderBy } = useValues(sessionRecordingsPlaylistLogic)
- const { durationTypeToShow } = useValues(playerSettingsLogic)
-
- const iconClassnames = 'SessionRecordingPreview__property-icon text-base text-muted-alt'
+ const { durationTypeToShow, showRecordingListProperties } = useValues(playerSettingsLogic)
- const [summaryPopoverIsVisible, setSummaryPopoverIsVisible] = useState(false)
+ const nodeLogic = useNotebookNode()
+ const inNotebook = !!nodeLogic
- const [summaryButtonIsVisible, setSummaryButtonIsVisible] = useState(false)
+ const iconClassnames = 'text-base text-muted-alt'
- return (
-
- onClick?.()}
- onMouseEnter={() => setSummaryButtonIsVisible(true)}
- onMouseLeave={() => setSummaryButtonIsVisible(false)}
- >
-
- {summariseFn && (
- setSummaryPopoverIsVisible(false)}
- overlay={
- sessionSummaryLoading ? (
-
- ) : (
- {recording.summary}
- )
- }
- >
- }
- onClick={(e) => {
- e.preventDefault()
- e.stopPropagation()
- setSummaryPopoverIsVisible(!summaryPopoverIsVisible)
- if (!recording.summary) {
- summariseFn(recording)
- }
- }}
- />
-
- )}
-
-
-
-
-
- {asDisplay(recording.person)}
-
+ const innerContent = (
+
onClick?.()}
+ >
+
+
+
+
+ {asDisplay(recording.person)}
-
-
- {orderBy === 'console_error_count' ? (
-
- ) : (
-
- )}
-
+
+
+
+
+ {orderBy === 'console_error_count' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {!recording.viewed ?
: null}
+ {pinned ?
: null}
+
+
+ )
+
+ return (
+
+ {showRecordingListProperties && !inNotebook ? (
+
+ }
+ closeOnClickInside={false}
+ >
+ {innerContent}
+
+ ) : (
+ innerContent
+ )}
+
+ )
+}
+
+function SessionRecordingPreviewPopover({
+ recording,
+ summariseFn,
+ sessionSummaryLoading,
+}: {
+ recording: SessionRecordingType
+ summariseFn?: (recording: SessionRecordingType) => void
+ sessionSummaryLoading?: boolean
+}): JSX.Element {
+ const { recordingPropertiesById, recordingPropertiesLoading } = useValues(sessionRecordingsListPropertiesLogic)
+ const recordingProperties = recordingPropertiesById[recording.id]
+ const loading = !recordingProperties && recordingPropertiesLoading
+ const iconProperties = gatherIconProperties(recordingProperties, recording)
+
+ const iconClassNames = 'text-muted-alt mr-2 shrink-0'
+
+ return (
+
+
+
Session data
+
+
+
+
+
+
+
+ {recording.start_url}
+
+
+
+
+
-
-
+
+
+
+
+
+
Activity
-
-
- {pinned ?
: null}
+
+
+
+ {recording.click_count} clicks
+
+
+
+ {recording.keypress_count} key presses
+
+
+
+ {recording.console_error_count} console errors
+
-
+
+
+ {summariseFn && (
+ <>
+
+
+ {recording.summary ? (
+
{recording.summary}
+ ) : (
+
+ }
+ onClick={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (!recording.summary) {
+ summariseFn(recording)
+ }
+ }}
+ loading={sessionSummaryLoading}
+ >
+ Generate AI summary
+
+
+ )}
+
+ >
+ )}
+
+
)
}
diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss
index d2fcab212dbcb..ea0f5d0d4fc07 100644
--- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss
+++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.scss
@@ -59,13 +59,7 @@
}
.SessionRecordingPreview {
- position: relative;
- display: flex;
- padding: 0.5rem 0 0.5rem 0.5rem;
- overflow: hidden;
- cursor: pointer;
- border-left: 6px solid transparent;
- transition: background-color 200ms ease, border 200ms ease;
+ border-left: 3px solid transparent;
&--active {
border-left-color: var(--primary-3000);
@@ -74,9 +68,4 @@
&:hover {
background-color: var(--primary-3000-highlight);
}
-
- .SessionRecordingPreview__property-icon:hover {
- opacity: 1;
- transition: opacity 200ms;
- }
}
diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx
index 3c2a9842c0dbc..21eae025a4b42 100644
--- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx
+++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx
@@ -8,6 +8,7 @@ import { BindLogic, useActions, useValues } from 'kea'
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
import { FEATURE_FLAGS } from 'lib/constants'
+import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys'
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
import { IconWithCount } from 'lib/lemon-ui/icons'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
@@ -15,19 +16,20 @@ import { LemonTableLoader } from 'lib/lemon-ui/LemonTable/LemonTableLoader'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook'
import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext'
import { urls } from 'scenes/urls'
+import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
import { ReplayTabs, SessionRecordingType } from '~/types'
import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters'
+import { playerSettingsLogic } from '../player/playerSettingsLogic'
import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer'
import { SessionRecordingPreview, SessionRecordingPreviewSkeleton } from './SessionRecordingPreview'
import {
DEFAULT_RECORDING_FILTERS,
- defaultPageviewPropertyEntityFilter,
RECORDINGS_LIMIT,
SessionRecordingPlaylistLogicProps,
sessionRecordingsPlaylistLogic,
@@ -67,8 +69,8 @@ function UnusableEventsWarning(props: { unusableEventsInFilter: string[] }): JSX
}
function PinnedRecordingsList(): JSX.Element | null {
- const { setSelectedRecordingId, setAdvancedFilters } = useActions(sessionRecordingsPlaylistLogic)
- const { activeSessionRecordingId, filters, pinnedRecordings } = useValues(sessionRecordingsPlaylistLogic)
+ const { setSelectedRecordingId } = useActions(sessionRecordingsPlaylistLogic)
+ const { activeSessionRecordingId, pinnedRecordings } = useValues(sessionRecordingsPlaylistLogic)
const { featureFlags } = useValues(featureFlagLogic)
const isTestingSaved = featureFlags[FEATURE_FLAGS.SAVED_NOT_PINNED] === 'test'
@@ -89,9 +91,6 @@ function PinnedRecordingsList(): JSX.Element | null {
setSelectedRecordingId(rec.id)}
- onPropertyClick={(property, value) =>
- setAdvancedFilters(defaultPageviewPropertyEntityFilter(filters, property, value))
- }
isActive={activeSessionRecordingId === rec.id}
pinned={true}
/>
@@ -133,21 +132,30 @@ function RecordingsLists(): JSX.Element {
toggleShowOtherRecordings,
summarizeSession,
} = useActions(sessionRecordingsPlaylistLogic)
+ const { showRecordingListProperties } = useValues(playerSettingsLogic)
+ const { setShowRecordingListProperties } = useActions(playerSettingsLogic)
const onRecordingClick = (recording: SessionRecordingType): void => {
setSelectedRecordingId(recording.id)
}
- const onPropertyClick = (property: string, value?: string): void => {
- setAdvancedFilters(defaultPageviewPropertyEntityFilter(advancedFilters, property, value))
- }
-
const onSummarizeClick = (recording: SessionRecordingType): void => {
summarizeSession(recording.id)
}
const lastScrollPositionRef = useRef(0)
const contentRef = useRef(null)
+ const [isHovering, setIsHovering] = useState(null)
+
+ useKeyboardHotkeys(
+ {
+ p: {
+ action: () => setShowRecordingListProperties(!showRecordingListProperties),
+ disabled: !isHovering,
+ },
+ },
+ [isHovering]
+ )
const handleScroll = (e: React.UIEvent): void => {
// If we are scrolling down then check if we are at the bottom of the list
@@ -250,7 +258,11 @@ function RecordingsLists(): JSX.Element {
) : null}
{pinnedRecordings.length || otherRecordings.length ? (
-