diff --git a/frontend/__snapshots__/replay-components-propertyicons--android-recording--dark.png b/frontend/__snapshots__/replay-components-propertyicons--android-recording--dark.png index da6e3a8932a98..1e89f87950222 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--android-recording--dark.png and b/frontend/__snapshots__/replay-components-propertyicons--android-recording--dark.png differ diff --git a/frontend/__snapshots__/replay-components-propertyicons--android-recording--light.png b/frontend/__snapshots__/replay-components-propertyicons--android-recording--light.png index 5bd336a5b19b4..e714c6f371d7f 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--android-recording--light.png and b/frontend/__snapshots__/replay-components-propertyicons--android-recording--light.png differ diff --git a/frontend/__snapshots__/replay-components-propertyicons--loading--dark.png b/frontend/__snapshots__/replay-components-propertyicons--loading--dark.png index d245ee9a2da8c..8a404e2a4678f 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--loading--dark.png and b/frontend/__snapshots__/replay-components-propertyicons--loading--dark.png differ diff --git a/frontend/__snapshots__/replay-components-propertyicons--loading--light.png b/frontend/__snapshots__/replay-components-propertyicons--loading--light.png index 705a5a44c2846..272b11050ae7b 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--loading--light.png and b/frontend/__snapshots__/replay-components-propertyicons--loading--light.png differ diff --git a/frontend/__snapshots__/replay-components-propertyicons--web-recording--dark.png b/frontend/__snapshots__/replay-components-propertyicons--web-recording--dark.png index 6fb7947a1bfae..16805c539b8be 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--web-recording--dark.png and b/frontend/__snapshots__/replay-components-propertyicons--web-recording--dark.png differ diff --git a/frontend/__snapshots__/replay-components-propertyicons--web-recording--light.png b/frontend/__snapshots__/replay-components-propertyicons--web-recording--light.png index 1e85fe4f89708..f84bfb91ffc82 100644 Binary files a/frontend/__snapshots__/replay-components-propertyicons--web-recording--light.png and b/frontend/__snapshots__/replay-components-propertyicons--web-recording--light.png differ diff --git a/frontend/__snapshots__/replay-player-failure--recent-recordings-404--dark.png b/frontend/__snapshots__/replay-player-failure--recent-recordings-404--dark.png index 7a6c231f00fa8..47bf7ab05f463 100644 Binary files a/frontend/__snapshots__/replay-player-failure--recent-recordings-404--dark.png and b/frontend/__snapshots__/replay-player-failure--recent-recordings-404--dark.png differ diff --git a/frontend/__snapshots__/replay-player-failure--recent-recordings-404--light.png b/frontend/__snapshots__/replay-player-failure--recent-recordings-404--light.png index b883eb06df047..b8636e803a367 100644 Binary files a/frontend/__snapshots__/replay-player-failure--recent-recordings-404--light.png and b/frontend/__snapshots__/replay-player-failure--recent-recordings-404--light.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png index 198133fababd3..be16d2121c557 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--light.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--light.png index 489687d6b6c30..95c52c2f024ca 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--light.png and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--light.png differ diff --git a/frontend/src/lib/components/PropertyIcon.tsx b/frontend/src/lib/components/PropertyIcon.tsx index c6dc47a36660b..8d4d9577fe1c0 100644 --- a/frontend/src/lib/components/PropertyIcon.tsx +++ b/frontend/src/lib/components/PropertyIcon.tsx @@ -71,14 +71,7 @@ interface PropertyIconProps { tooltipTitle?: (property: string, value?: string) => ReactNode // Tooltip title will default to `value` } -export function PropertyIcon({ - property, - value, - className, - noTooltip, - tooltipTitle, - onClick, -}: PropertyIconProps): JSX.Element { +export function PropertyIcon({ property, value, className, noTooltip, tooltipTitle }: PropertyIconProps): JSX.Element { if (!property || !(property in PROPERTIES_ICON_MAP)) { return <> } @@ -93,11 +86,7 @@ export function PropertyIcon({ icon = countryCodeToFlag(value) } - const content = ( -
- {icon} -
- ) + const content =
{icon}
return noTooltip ? content : {content} } diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx index bd87a15c6b11e..70be8a2abb3c7 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx @@ -19,7 +19,6 @@ import { asDisplay } from 'scenes/persons/person-utils' import { PersonDisplay } from 'scenes/persons/PersonDisplay' import { IconWindow } from 'scenes/session-recordings/player/icons' import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' -import { gatherIconProperties, PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordingPreview' import { urls } from 'scenes/urls' import { getCurrentExporterData } from '~/exporter/exporterViewLogic' @@ -29,23 +28,6 @@ import { PlayerMetaLinks } from './PlayerMetaLinks' import { sessionRecordingDataLogic } from './sessionRecordingDataLogic' import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from './sessionRecordingPlayerLogic' -function SessionPropertyMeta(props: { - fullScreen: boolean - iconProperties: Record - predicate: (x: string) => boolean -}): JSX.Element { - const gatheredProperties = gatherIconProperties(props.iconProperties) - - return ( - (props.fullScreen ? key === '$geoip_country_code' : key !== '$geoip_country_code')} - /> - ) -} - function URLOrScreen({ lastUrl }: { lastUrl: string | undefined }): JSX.Element | null { if (!lastUrl) { return null @@ -98,7 +80,6 @@ export function PlayerMeta(): JSX.Element { currentWindowIndex, startTime, sessionPlayerMetaDataLoading, - sessionProperties, } = useValues(playerMetaLogic(logicProps)) const { ref, size } = useResizeBreakpoints({ @@ -197,17 +178,6 @@ export function PlayerMeta(): JSX.Element { )} -
- {sessionPlayerMetaDataLoading ? ( - - ) : sessionProperties ? ( - !!x} - /> - ) : null} -
{sessionRecordingId && ( diff --git a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts index 15fa227082e51..b88bec2df3f5e 100644 --- a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts @@ -190,6 +190,7 @@ export const playerSettingsLogic = kea([ setShowFilters: (showFilters: boolean) => ({ showFilters }), setPrefersAdvancedFilters: (prefersAdvancedFilters: boolean) => ({ prefersAdvancedFilters }), setQuickFilterProperties: (properties: string[]) => ({ properties }), + setShowRecordingListProperties: (enabled: boolean) => ({ enabled }), setTimestampFormat: (format: TimestampFormat) => ({ format }), }), reducers(() => ({ @@ -234,6 +235,13 @@ export const playerSettingsLogic = kea([ setSpeed: (_, { speed }) => speed, }, ], + showRecordingListProperties: [ + false, + { persist: true }, + { + setShowRecordingListProperties: (_, { enabled }) => enabled, + }, + ], timestampFormat: [ TimestampFormat.Relative as TimestampFormat, { persist: true }, diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.stories.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.stories.tsx index bd6ab57e1e3d1..79c2f32276089 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.stories.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.stories.tsx @@ -23,45 +23,37 @@ const Template: StoryFn = (args: PropertyIconsProps) => { export const WebRecording = Template.bind({}) WebRecording.args = { - iconClassnames: 'wat', loading: false, - onPropertyClick: () => {}, recordingProperties: [ - { label: 'Mac OS X', value: 'Mac OS X', property: '$os', tooltipValue: 'Mac OS X' }, - { label: 'Chrome', value: 'Chrome', property: '$browser', tooltipValue: 'Chrome' }, + { label: 'Mac OS X', value: 'Mac OS X', property: '$os' }, + { label: 'Chrome', value: 'Chrome', property: '$browser' }, { label: 'United States', value: 'United States', property: '$geoip_country_code', - tooltipValue: 'United States', }, - { label: 'Desktop', value: 'Desktop', property: '$device_type', tooltipValue: 'Desktop' }, + { label: 'Desktop', value: 'Desktop', property: '$device_type' }, ], } export const AndroidRecording = Template.bind({}) AndroidRecording.args = { - iconClassnames: 'wat', loading: false, - onPropertyClick: () => {}, recordingProperties: [ - { label: 'Android', value: 'Android', property: '$os_name', tooltipValue: 'Android' }, - { label: 'Awesome Fun App', value: 'Awesome Fun App', property: '$app_name', tooltipValue: 'Awesome Fun App' }, + { label: 'Android', value: 'Android', property: '$os_name' }, + { label: 'Awesome Fun App', value: 'Awesome Fun App', property: '$app_name' }, { label: 'United States', value: 'United States', property: '$geoip_country_code', - tooltipValue: 'United States', }, - { label: 'Mobile', value: 'Mobile', property: '$device_type', tooltipValue: 'Mobile' }, + { label: 'Mobile', value: 'Mobile', property: '$device_type' }, ], } export const Loading = Template.bind({}) Loading.args = { - iconClassnames: 'wat', loading: true, - onPropertyClick: () => {}, recordingProperties: [], } Loading.parameters = { diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx index 10e31ca899d00..19350d0ab4b8d 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx @@ -1,20 +1,27 @@ -import { IconBug, IconClock, IconCursorClick, IconKeyboard, IconMagicWand, IconPinFilled } from '@posthog/icons' +import { + IconBug, + IconCalendar, + IconCursorClick, + IconKeyboard, + IconMagicWand, + IconPinFilled, + IconTerminal, +} from '@posthog/icons' +import { LemonDivider, LemonDropdown, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useValues } from 'kea' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { PropertyIcon } from 'lib/components/PropertyIcon' import { TZLabel } from 'lib/components/TZLabel' import { FEATURE_FLAGS } from 'lib/constants' +import { IconLink } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { Popover } from 'lib/lemon-ui/Popover' -import { Spinner } from 'lib/lemon-ui/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { colonDelimitedDuration } from 'lib/utils' -import posthog from 'posthog-js' -import { Fragment, useState } from 'react' import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' +import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { asDisplay } from 'scenes/persons/person-utils' import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' import { urls } from 'scenes/urls' @@ -26,7 +33,6 @@ import { sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic export interface SessionRecordingPreviewProps { recording: SessionRecordingType - onPropertyClick?: (property: string, value?: string) => void isActive?: boolean onClick?: () => void pinned?: boolean @@ -34,33 +40,19 @@ export interface SessionRecordingPreviewProps { sessionSummaryLoading?: boolean } -function RecordingDuration({ - iconClassNames, - recordingDuration, -}: { - iconClassNames: string - recordingDuration: number | undefined -}): JSX.Element { +function RecordingDuration({ recordingDuration }: { recordingDuration: number | undefined }): JSX.Element { if (recordingDuration === undefined) { - return
-
+ return
-
} const formattedDuration = colonDelimitedDuration(recordingDuration) const [hours, minutes, seconds] = formattedDuration.split(':') return ( -
- +
+ {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 ? ( -
    +
      setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}> +
      + Hint: Hover list and press to preview +
      + {pinnedRecordings.length ? ( @@ -269,7 +281,6 @@ function RecordingsLists(): JSX.Element { onRecordingClick(rec)} - onPropertyClick={onPropertyClick} isActive={activeSessionRecordingId === rec.id} pinned={false} summariseFn={onSummarizeClick} diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index e46c2acfc6040..5a12f9018eba7 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -10,7 +10,6 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { - AnyPropertyFilter, DurationType, PropertyFilterType, PropertyOperator, @@ -92,59 +91,6 @@ export const getDefaultFilters = (personUUID?: PersonUUID): RecordingFilters => return personUUID ? DEFAULT_PERSON_RECORDING_FILTERS : DEFAULT_RECORDING_FILTERS } -export const defaultPageviewPropertyEntityFilter = ( - filters: RecordingFilters, - property: string, - value?: string -): Partial => { - const existingPageview = filters.events?.find(({ name }) => name === '$pageview') - const eventEntityFilters = filters.events ?? [] - const propToAdd = value - ? { - key: property, - value: [value], - operator: PropertyOperator.Exact, - type: 'event', - } - : { - key: property, - value: PropertyOperator.IsNotSet, - operator: PropertyOperator.IsNotSet, - type: 'event', - } - - // If pageview exists, add property to the first pageview event - if (existingPageview) { - return { - events: eventEntityFilters.map((eventFilter) => - eventFilter.order === existingPageview.order - ? { - ...eventFilter, - properties: [ - ...(eventFilter.properties?.filter(({ key }: AnyPropertyFilter) => key !== property) ?? - []), - propToAdd, - ], - } - : eventFilter - ), - } - } else { - return { - events: [ - ...eventEntityFilters, - { - id: '$pageview', - name: '$pageview', - type: 'events', - order: eventEntityFilters.length, - properties: [propToAdd], - }, - ], - } - } -} - const capturePartialFilters = (filters: Partial): void => { // capture only the partial filters applied (not the full filters object) // take each key from the filter and change it to `partial_filter_chosen_${key}`