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 4d5f9882073b5..85ee5c764549d 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__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png index 7fdf9df817e35..a59b99eb0c6bc 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png differ diff --git a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png index 6b12e17b93cb0..2d8c89860f650 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png differ diff --git a/frontend/src/lib/components/DateFilter/DateFilter.tsx b/frontend/src/lib/components/DateFilter/DateFilter.tsx index c0ae259baddb3..3649ef0b1ab66 100644 --- a/frontend/src/lib/components/DateFilter/DateFilter.tsx +++ b/frontend/src/lib/components/DateFilter/DateFilter.tsx @@ -35,6 +35,7 @@ export interface DateFilterProps { dateOptions?: DateMappingOption[] isDateFormatted?: boolean size?: LemonButtonProps['size'] + type?: LemonButtonProps['type'] dropdownPlacement?: Placement /* True when we're not dealing with ranges, but a single date / relative date */ isFixedDateMode?: boolean @@ -60,6 +61,7 @@ export function DateFilter({ dateOptions = dateMapping, isDateFormatted = true, size, + type, dropdownPlacement = 'bottom-start', max, isFixedDateMode = false, @@ -242,7 +244,7 @@ export function DateFilter({ } diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 7dcec3058115a..312f461aba011 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -238,6 +238,7 @@ export const FEATURE_FLAGS = { CDP_ACTIVITY_LOG_NOTIFICATIONS: 'cdp-activity-log-notifications', // owner: #team-cdp COOKIELESS_SERVER_HASH_MODE_SETTING: 'cookieless-server-hash-mode-setting', // owner: @robbie-c #team-web-analytics INSIGHT_COLORS: 'insight-colors', // owner @thmsobrmlr #team-product-analytics + SESSION_REPLAY_PANELS_UI: 'session-replay-panels-ui', // owner: @pauldambra #team-replay } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx index 00f8b0f1327ed..3972b6bd65864 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx @@ -2,7 +2,6 @@ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' import { FilterType, NotebookNodeType, RecordingUniversalFilters, ReplayTabs } from '~/types' import { DEFAULT_RECORDING_FILTERS, - SessionRecordingPlaylistLogicProps, convertLegacyFiltersToUniversalFilters, sessionRecordingsPlaylistLogic, } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' @@ -17,6 +16,7 @@ import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/se import { IconComment } from 'lib/lemon-ui/icons' import { sessionRecordingPlayerLogicType } from 'scenes/session-recordings/player/sessionRecordingPlayerLogicType' import { RecordingsUniversalFilters } from 'scenes/session-recordings/filters/RecordingsUniversalFilters' +import { SessionRecordingPlaylistLogicProps } from 'scenes/session-recordings/types' const Component = ({ attributes, diff --git a/frontend/src/scenes/session-recordings/PanelUI.tsx b/frontend/src/scenes/session-recordings/PanelUI.tsx new file mode 100644 index 0000000000000..3fce936054ad8 --- /dev/null +++ b/frontend/src/scenes/session-recordings/PanelUI.tsx @@ -0,0 +1,78 @@ +import './SessionReplay.scss' + +import clsx from 'clsx' +import { useValues } from 'kea' +import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' + +import { PanelFilters } from './panels/Filters' +import { PanelOverview } from './panels/Overview' +import { PanelPlayback } from './panels/Playback' +import { PanelPlaylist } from './panels/Playlist' +import { PlayerInspector } from './player/inspector/PlayerInspector' + +export function PanelsUI(): JSX.Element { + const workspaceConfig = { overview: false, inspector: true } + + const { activeSessionRecordingId } = useValues(sessionRecordingsPlaylistLogic({ updateSearchParams: true })) + + return ( + + + + + + + + + + + + {workspaceConfig.overview && ( + + + + + + )} + + + + + {workspaceConfig.inspector && ( + + {/*TODO: this only works because we're not using a playerkey yet*/} + + + )} + + + + ) +} + +function PanelLayout(props: Omit): JSX.Element { + return +} + +type PanelContainerProps = { + children: React.ReactNode + primary: boolean + className?: string +} + +function PanelContainer({ children, primary, className }: PanelContainerProps): JSX.Element { + return
{children}
+} + +function Panel({ + className, + primary, + children, +}: { + className?: string + primary: boolean + collapsed?: boolean + children: JSX.Element +}): JSX.Element { + return
{children}
+} diff --git a/frontend/src/scenes/session-recordings/SessionRecordings.tsx b/frontend/src/scenes/session-recordings/SessionRecordings.tsx index d06f0a27c5cbd..dd31960ba3a72 100644 --- a/frontend/src/scenes/session-recordings/SessionRecordings.tsx +++ b/frontend/src/scenes/session-recordings/SessionRecordings.tsx @@ -7,6 +7,7 @@ import { AuthorizedUrlListType, defaultAuthorizedUrlProperties, } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { PageHeader } from 'lib/components/PageHeader' import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' @@ -26,6 +27,7 @@ import { urls } from 'scenes/urls' import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' import { AvailableFeature, NotebookNodeType, ReplayTabs } from '~/types' +import { PanelsUI } from './PanelUI' import { createPlaylist } from './playlist/playlistUtils' import { SessionRecordingsPlaylist } from './playlist/SessionRecordingsPlaylist' import { SavedSessionRecordingPlaylists } from './saved-playlists/SavedSessionRecordingPlaylists' @@ -195,9 +197,16 @@ function MainPanel(): JSX.Element { {!tab ? ( ) : tab === ReplayTabs.Home ? ( -
- -
+ <> + + + + +
+ +
+
+ ) : tab === ReplayTabs.Playlists ? ( ) : tab === ReplayTabs.Templates ? ( diff --git a/frontend/src/scenes/session-recordings/SessionReplay.scss b/frontend/src/scenes/session-recordings/SessionReplay.scss new file mode 100644 index 0000000000000..a395f04a182c5 --- /dev/null +++ b/frontend/src/scenes/session-recordings/SessionReplay.scss @@ -0,0 +1,101 @@ +.SessionReplay__layout { + --session-recordings-tabs-height: 62px; + --session-recordings-panel-height: calc( + 100vh - var(--breadcrumbs-height-full) - var(--scene-padding) - var(--scene-padding-bottom) - + var(--session-recordings-tabs-height) + ); + --panel-gap: 0.5rem; + --panel-playlist-min-width: 350px; + --panel-filters-min-width: 350px; + --panel-playback-min-width: 300px; + --panel-inspector-min-width: 200px; + --container-primary-min-width: max(var(--panel-playback-min-width), var(--panel-inspector-min-width)); + --container-secondary-min-width: max(var(--panel-playlist-min-width), var(--panel-filters-min-width)); + --container-secondary-max-width: 200px; + + height: var(--session-recordings-panel-height); + container: replay-panel-layout / inline-size; + + .PanelLayout__primary { + container: replay-primary-panels / inline-size; + flex: 1 1 0% !important; + min-width: var(--container-primary-min-width); + } + + .PanelLayout__playlist { + flex: 1 1 0% !important; + min-width: var(--panel-playlist-min-width); + } + + .PanelLayout__playback { + flex: 1 1 0% !important; + min-width: var(--panel-playback-min-width); + min-height: 300px; + } + + .PanelLayout__inspector { + min-width: var(--panel-inspector-min-width); + } + + .UniversalFilterButton { + height: 1.75rem; + font-size: smaller; + } + + .PanelLayout__Panel { + &--collapsed { + width: 40px; + min-width: unset; + height: 40px; + } + } + + // All primary panels horizontally stacked + // var(--panel-playback-min-width) + var(--panel-inspector-min-width) + var(--panel-gap) + var(--panel-gap) + @container replay-panel-layout (width >= 1040px) { + .PanelLayout__inspector { + height: 100%; + overflow-y: auto; + } + + .PanelLayout__primary { + flex-direction: column; + } + } + + // Primary panels vertically stacked + // var(--panel-playback-min-width) + var(--panel-inspector-min-width) + var(--panel-gap) + var(--panel-gap) + @container replay-panel-layout (width < 1040px) { + .PanelLayout__inspector { + width: 100%; + } + } + + // Layout vertically stacked + @container replay-panel-layout (width < 508px) { + // var(--container-primary-min-width) + var(--container-secondary-min-width) + var(--panel-gap) + .PanelLayout__secondary { + width: 100%; + } + + .PanelLayout__playlist { + max-height: 150px; + } + } + + @container replay-panel-layout (width > 508px) { + // var(--container-primary-min-width) + var(--container-secondary-min-width) + var(--panel-gap) + .PanelLayout__secondary { + min-width: var(--container-secondary-min-width); + max-width: var(--container-secondary-max-width); + height: 100%; + } + } + + @container replay-panel-layout (508px < width < 1040px) { + .PanelLayout__primary { + height: 100%; + overflow-y: auto; + } + } +} diff --git a/frontend/src/scenes/session-recordings/apm/networkViewLogic.ts b/frontend/src/scenes/session-recordings/apm/networkViewLogic.ts index c16cb605c6b46..8adeed54efb17 100644 --- a/frontend/src/scenes/session-recordings/apm/networkViewLogic.ts +++ b/frontend/src/scenes/session-recordings/apm/networkViewLogic.ts @@ -2,17 +2,13 @@ import { actions, afterMount, connect, kea, key, path, props, reducers, selector import { humanFriendlyMilliseconds } from 'lib/utils' import { performanceEventDataLogic } from 'scenes/session-recordings/apm/performanceEventDataLogic' import { percentagesWithinEventRange } from 'scenes/session-recordings/apm/waterfall/TimingBar' -import { - sessionRecordingDataLogic, - SessionRecordingDataLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { NetworkViewLogicProps } from 'scenes/session-recordings/types' import { PerformanceEvent } from '~/types' import type { networkViewLogicType } from './networkViewLogicType' -export interface NetworkViewLogicProps extends SessionRecordingDataLogicProps {} - export const networkViewLogic = kea([ path(['scenes', 'session-recordings', 'apm', 'networkViewLogic']), key((props) => `network-view-${props.sessionRecordingId}`), @@ -21,7 +17,7 @@ export const networkViewLogic = kea([ values: [ sessionRecordingDataLogic(props), ['sessionPlayerData', 'sessionPlayerMetaData', 'snapshotsLoading', 'sessionPlayerMetaDataLoading'], - performanceEventDataLogic({ key: props.sessionRecordingId, sessionRecordingId: props.sessionRecordingId }), + performanceEventDataLogic(props), ['allPerformanceEvents', 'sizeBreakdown'], ], actions: [sessionRecordingDataLogic(props), ['loadSnapshots', 'maybeLoadRecordingMeta']], diff --git a/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts b/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts index 2e145aabd578e..03c67cecd38e2 100644 --- a/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts +++ b/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts @@ -5,10 +5,8 @@ import { itemSizeInfo, } from 'scenes/session-recordings/apm/performance-event-utils' import { InspectorListItemBase } from 'scenes/session-recordings/player/inspector/playerInspectorLogic' -import { - sessionRecordingDataLogic, - SessionRecordingDataLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { PerformanceEventDataLogicProps } from 'scenes/session-recordings/types' import { FilterableInspectorListItemTypes, PerformanceEvent, RecordingEventType } from '~/types' @@ -19,10 +17,6 @@ export type InspectorListItemPerformance = InspectorListItemBase & { data: PerformanceEvent } -export interface PerformanceEventDataLogicProps extends SessionRecordingDataLogicProps { - key?: string -} - /** it's pretty quick to sort an already sorted list */ function sortPerformanceEvents(events: PerformanceEvent[]): PerformanceEvent[] { return events.sort((a, b) => (a.timestamp.valueOf() > b.timestamp.valueOf() ? 1 : -1)) @@ -101,7 +95,7 @@ function matchWebVitalsEvents( export const performanceEventDataLogic = kea([ path(['scenes', 'session-recordings', 'apm', 'performanceEventDataLogic']), props({} as PerformanceEventDataLogicProps), - key((props: PerformanceEventDataLogicProps) => `${props.key}-${props.sessionRecordingId}`), + key((props: PerformanceEventDataLogicProps) => props.sessionRecordingId), connect((props: PerformanceEventDataLogicProps) => ({ actions: [], values: [sessionRecordingDataLogic(props), ['sessionPlayerData', 'webVitalsEvents']], diff --git a/frontend/src/scenes/session-recordings/components/PanelSettings.tsx b/frontend/src/scenes/session-recordings/components/PanelSettings.tsx index c31972e82cd49..b3f51fda7c56d 100644 --- a/frontend/src/scenes/session-recordings/components/PanelSettings.tsx +++ b/frontend/src/scenes/session-recordings/components/PanelSettings.tsx @@ -34,7 +34,7 @@ export function SettingsBar({ className={clsx( border === 'bottom' && 'border-b', border === 'top' && 'border-t', - 'flex flex-row w-full overflow-hidden font-light text-xs bg-bg-3000 items-center', + 'flex flex-row w-full overflow-hidden font-light text-xs bg-bg-3000 items-center overflow-x-auto', className )} > diff --git a/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx b/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx index d0f036983a9ac..32776bff25673 100644 --- a/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx +++ b/frontend/src/scenes/session-recordings/filters/DurationFilter.tsx @@ -1,4 +1,4 @@ -import { LemonButton } from '@posthog/lemon-ui' +import { LemonButton, LemonButtonProps } from '@posthog/lemon-ui' import { convertSecondsToDuration, DurationPicker } from 'lib/components/DurationPicker/DurationPicker' import { OperatorSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect' import { Popover } from 'lib/lemon-ui/Popover/Popover' @@ -12,6 +12,8 @@ interface Props { durationTypeFilter: DurationType onChange: (recordingDurationFilter: RecordingDurationFilter, durationType: DurationType) => void pageKey: string + size?: LemonButtonProps['size'] + type?: LemonButtonProps['type'] } const durationTypeMapping: Record = { @@ -31,7 +33,13 @@ export const humanFriendlyDurationFilter = ( return `${operator} ${duration.timeValue || 0} ${durationDescription}${unit}` } -export function DurationFilter({ recordingDurationFilter, durationTypeFilter, onChange }: Props): JSX.Element { +export function DurationFilter({ + recordingDurationFilter, + durationTypeFilter, + onChange, + size, + type, +}: Props): JSX.Element { const [isOpen, setIsOpen] = useState(false) const durationString = useMemo( () => humanFriendlyDurationFilter(recordingDurationFilter, durationTypeFilter), @@ -69,8 +77,8 @@ export function DurationFilter({ recordingDurationFilter, durationTypeFilter, on } > { setIsOpen(true) }} diff --git a/frontend/src/scenes/session-recordings/panels/Filters.tsx b/frontend/src/scenes/session-recordings/panels/Filters.tsx new file mode 100644 index 0000000000000..dd7059617e3ba --- /dev/null +++ b/frontend/src/scenes/session-recordings/panels/Filters.tsx @@ -0,0 +1,161 @@ +import { IconFilter, IconPeopleFilled } from '@posthog/icons' +import { useActions, useValues } from 'kea' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters' +import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { isUniversalGroupFilterLike } from 'lib/components/UniversalFilters/utils' +import { useEffect, useState } from 'react' + +import { NodeKind } from '~/queries/schema' +import { FilterLogicalOperator, UniversalFiltersGroup } from '~/types' + +import { SettingsBar, SettingsMenu, SettingsToggle } from '../components/PanelSettings' +import { DurationFilter } from '../filters/DurationFilter' +import { sessionRecordingsPlaylistLogic } from '../playlist/sessionRecordingsPlaylistLogic' + +export const PanelFilters = (): JSX.Element => { + const { filters } = useValues(sessionRecordingsPlaylistLogic({ updateSearchParams: true })) + const { setFilters } = useActions(sessionRecordingsPlaylistLogic({ updateSearchParams: true })) + + const durationFilter = filters.duration[0] + + const taxonomicGroupTypes = [ + TaxonomicFilterGroupType.Replay, + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Actions, + TaxonomicFilterGroupType.Cohorts, + TaxonomicFilterGroupType.PersonProperties, + TaxonomicFilterGroupType.SessionProperties, + TaxonomicFilterGroupType.HogQLExpression, + ] + + return ( + <> +
+ { + setFilters({ + date_from: changedDateFrom, + date_to: changedDateTo, + }) + }} + dateOptions={[ + { key: 'Custom', values: [] }, + { key: 'Last 24 hours', values: ['-24h'] }, + { key: 'Last 3 days', values: ['-3d'] }, + { key: 'Last 7 days', values: ['-7d'] }, + { key: 'Last 30 days', values: ['-30d'] }, + { key: 'All time', values: ['-90d'] }, + ]} + dropdownPlacement="bottom-start" + /> + { + setFilters({ + duration: [{ ...newRecordingDurationFilter, key: newDurationType }], + }) + }} + recordingDurationFilter={durationFilter} + durationTypeFilter={durationFilter.key} + pageKey="session-recordings" + size="xsmall" + type="tertiary" + /> + setFilters({ filter_group: filterGroup })} + > + + +
+ + + ) +} + +const RecordingsUniversalFilterGroup = (): JSX.Element => { + const { filterGroup } = useValues(universalFiltersLogic) + const { replaceGroupValue, removeGroupValue } = useActions(universalFiltersLogic) + const [allowInitiallyOpen, setAllowInitiallyOpen] = useState(false) + + useEffect(() => { + setAllowInitiallyOpen(true) + }, []) + + return ( + <> + {filterGroup.values.map((filterOrGroup, index) => { + return isUniversalGroupFilterLike(filterOrGroup) ? ( + + + + + ) : ( + removeGroupValue(index)} + onChange={(value) => replaceGroupValue(index, value)} + initiallyOpen={allowInitiallyOpen} + metadataSource={{ kind: NodeKind.RecordingsQuery }} + /> + ) + })} + + ) +} + +function BottomSettings(): JSX.Element { + const { filters } = useValues(sessionRecordingsPlaylistLogic) + const { setFilters } = useActions(sessionRecordingsPlaylistLogic) + + const onChangeOperator = (type: FilterLogicalOperator): void => { + let values = filters.filter_group.values + + // set the type on the nested child when only using a single filter group + const hasSingleGroup = values.length === 1 + if (hasSingleGroup) { + const group = values[0] as UniversalFiltersGroup + values = [{ ...group, type }] + } + + setFilters({ filter_group: { type: type, values: values } }) + } + + return ( + + onChangeOperator(FilterLogicalOperator.Or), + active: filters.filter_group.type === FilterLogicalOperator.Or, + }, + { + label: 'All', + onClick: () => onChangeOperator(FilterLogicalOperator.And), + active: filters.filter_group.type === FilterLogicalOperator.And, + }, + ]} + icon={} + label={`Match ${filters.filter_group.type === FilterLogicalOperator.And ? 'all' : 'any'}...`} + /> + } + label="Show internal users" + active={filters.filter_test_accounts || false} + onClick={() => setFilters({ filter_test_accounts: !filters.filter_test_accounts })} + /> + + ) +} diff --git a/frontend/src/scenes/session-recordings/panels/Overview.tsx b/frontend/src/scenes/session-recordings/panels/Overview.tsx new file mode 100644 index 0000000000000..0a999bec03e24 --- /dev/null +++ b/frontend/src/scenes/session-recordings/panels/Overview.tsx @@ -0,0 +1,11 @@ +import { PersonDisplay } from 'scenes/persons/PersonDisplay' +import { PlayerSidebarOverviewGrid } from 'scenes/session-recordings/player/sidebar/PlayerSidebarOverviewGrid' + +export const PanelOverview = (): JSX.Element => { + return ( + <> + + + + ) +} diff --git a/frontend/src/scenes/session-recordings/panels/Playback.tsx b/frontend/src/scenes/session-recordings/panels/Playback.tsx new file mode 100644 index 0000000000000..483fd0844a868 --- /dev/null +++ b/frontend/src/scenes/session-recordings/panels/Playback.tsx @@ -0,0 +1,50 @@ +import { useValues } from 'kea' +import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' + +import { SessionRecordingType } from '~/types' + +import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' +import { sessionRecordingsPlaylistLogic } from '../playlist/sessionRecordingsPlaylistLogic' + +export const PanelPlayback = ({ + logicKey, + onPinnedChange, +}: { + logicKey?: string + onPinnedChange?: (recording: SessionRecordingType, pinned: boolean) => void +}): JSX.Element => { + const { pinnedRecordings, matchingEventsMatchType, activeSessionRecordingId, activeSessionRecording } = useValues( + sessionRecordingsPlaylistLogic({ updateSearchParams: true }) + ) + + return activeSessionRecordingId ? ( + x.id === activeSessionRecordingId)} + setPinned={ + onPinnedChange + ? (pinned) => { + if (!activeSessionRecording) { + return + } + onPinnedChange?.(activeSessionRecording, pinned) + } + : undefined + } + /> + ) : ( +
+ +
+ ) +} diff --git a/frontend/src/scenes/session-recordings/panels/Playlist.tsx b/frontend/src/scenes/session-recordings/panels/Playlist.tsx new file mode 100644 index 0000000000000..75c51026879a7 --- /dev/null +++ b/frontend/src/scenes/session-recordings/panels/Playlist.tsx @@ -0,0 +1,299 @@ +import { LemonBanner, LemonButton, LemonCollapse, LemonSkeleton, Link, Spinner } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { PlaylistProps, PlaylistSection } from 'lib/components/Playlist/Playlist' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { FEATURE_FLAGS } from 'lib/constants' +import { LemonTableLoader } from 'lib/lemon-ui/LemonTable/LemonTableLoader' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { range } from 'lib/utils' +import { useRef } from 'react' +import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' +import { urls } from 'scenes/urls' + +import { ReplayTabs, SessionRecordingType } from '~/types' + +import { SessionRecordingPreview } from '../playlist/SessionRecordingPreview' +import { DEFAULT_RECORDING_FILTERS, sessionRecordingsPlaylistLogic } from '../playlist/sessionRecordingsPlaylistLogic' +import { + SessionRecordingPlaylistBottomSettings, + SessionRecordingsPlaylistTopSettings, +} from '../playlist/SessionRecordingsPlaylistSettings' +import { SessionRecordingsPlaylistTroubleshooting } from '../playlist/SessionRecordingsPlaylistTroubleshooting' + +const SCROLL_TRIGGER_OFFSET = 100 + +export const PanelPlaylist = (): JSX.Element => { + const { + filters, + pinnedRecordings, + sessionRecordingsResponseLoading, + otherRecordings, + activeSessionRecordingId, + hasNext, + } = useValues(sessionRecordingsPlaylistLogic({ updateSearchParams: true })) + const { maybeLoadSessionRecordings, setSelectedRecordingId, setFilters, setShowOtherRecordings } = useActions( + sessionRecordingsPlaylistLogic({ updateSearchParams: true }) + ) + + const { featureFlags } = useValues(featureFlagLogic) + const isTestingSaved = featureFlags[FEATURE_FLAGS.SAVED_NOT_PINNED] === 'test' + + const pinnedDescription = isTestingSaved ? 'Saved' : 'Pinned' + + const sections: PlaylistSection[] = [] + + if (pinnedRecordings.length) { + sections.push({ + key: 'pinned', + title: `${pinnedDescription} recordings`, + items: pinnedRecordings, + render: ({ item, isActive }) => ( + + ), + initiallyOpen: true, + }) + } + + sections.push({ + key: 'other', + title: 'Other recordings', + items: otherRecordings, + render: ({ item, isActive }) => , + footer: ( +
+
+ {sessionRecordingsResponseLoading ? ( + <> + Loading older recordings + + ) : hasNext ? ( + maybeLoadSessionRecordings('older')}>Load more + ) : ( + 'No more results' + )} +
+
+ ), + }) + + return ( + } + footerActions={} + onScrollListEdge={(edge) => { + if (edge === 'top') { + maybeLoadSessionRecordings('newer') + } else { + maybeLoadSessionRecordings('older') + } + }} + activeItemId={activeSessionRecordingId ?? null} + setActiveItemId={(item) => setSelectedRecordingId(item.id)} + onChangeSections={(activeSections) => setShowOtherRecordings(activeSections.includes('other'))} + emptyState={} + /> + ) +} + +const ListEmptyState = (): JSX.Element => { + const { filters, sessionRecordingsAPIErrored, unusableEventsInFilter } = useValues(sessionRecordingsPlaylistLogic) + const { setFilters } = useActions(sessionRecordingsPlaylistLogic) + + return ( +
+ {sessionRecordingsAPIErrored ? ( + Error while trying to load recordings. + ) : unusableEventsInFilter.length ? ( + + ) : ( +
+ {filters.date_from === DEFAULT_RECORDING_FILTERS.date_from ? ( + <> + No matching recordings found + setFilters({ date_from: '-30d' })} + > + Search over the last 30 days + + + ) : ( + + )} +
+ )} +
+ ) +} + +function UnusableEventsWarning(props: { unusableEventsInFilter: string[] }): JSX.Element { + // TODO add docs on how to enrich custom events with session_id and link to it from here + return ( + +

Cannot use these events to filter for session recordings:

+
  • + {props.unusableEventsInFilter.map((event) => ( + "{event}" + ))} +
  • +

    + Events have to have a to be used to filter recordings. This is + added automatically by{' '} + + the Web SDK + + ,{' '} + + and the Mobile SDKs (Android, iOS, React Native and Flutter) + +

    +
    + ) +} + +function List< + T extends { + id: string | number + [key: string]: any + } +>({ + notebooksHref, + setActiveItemId, + headerActions, + footerActions, + sections, + onChangeSections, + activeItemId, + onScrollListEdge, + loading, + emptyState, +}: { + title?: string + notebooksHref: PlaylistProps['notebooksHref'] + activeItemId: T['id'] | null + setActiveItemId: (item: T) => void + headerActions: PlaylistProps['headerActions'] + footerActions: PlaylistProps['footerActions'] + sections: PlaylistProps['sections'] + onChangeSections?: (activeKeys: string[]) => void + onScrollListEdge: PlaylistProps['onScrollListEdge'] + loading: PlaylistProps['loading'] + emptyState: PlaylistProps['listEmptyState'] +}): JSX.Element { + const lastScrollPositionRef = useRef(0) + const contentRef = useRef(null) + + const handleScroll = (e: React.UIEvent): void => { + // If we are scrolling down then check if we are at the bottom of the list + if (e.currentTarget.scrollTop > lastScrollPositionRef.current) { + const scrollPosition = e.currentTarget.scrollTop + e.currentTarget.clientHeight + if (e.currentTarget.scrollHeight - scrollPosition < SCROLL_TRIGGER_OFFSET) { + onScrollListEdge?.('bottom') + } + } + + // Same again but if scrolling to the top + if (e.currentTarget.scrollTop < lastScrollPositionRef.current) { + if (e.currentTarget.scrollTop < SCROLL_TRIGGER_OFFSET) { + onScrollListEdge?.('top') + } + } + + lastScrollPositionRef.current = e.currentTarget.scrollTop + } + + const initiallyOpenSections = sections.filter((s) => s.initiallyOpen).map((s) => s.key) + + return ( +
    + +
    +
    + {headerActions} +
    + +
    +
    + +
    + {sections.flatMap((s) => s.items).length ? ( + <> + {sections.length > 1 ? ( + ({ + key: s.key, + header: s.title, + content: ( + + ), + className: 'p-0', + }))} + onChange={onChangeSections} + multiple + embedded + size="small" + /> + ) : ( + + )} + + ) : loading ? ( + + ) : ( + emptyState + )} +
    +
    + {footerActions} +
    +
    + ) +} + +export function ListSection< + T extends { + id: string | number + [key: string]: any + } +>({ + items, + render, + footer, + onClick, + activeItemId, +}: PlaylistSection & { + onClick: (item: T) => void + activeItemId: T['id'] | null +}): JSX.Element { + return ( + <> + {items.length > 0 + ? items.map((item) => ( +
    onClick(item)}> + {render({ item, isActive: item.id === activeItemId })} +
    + )) + : null} + {footer} + + ) +} + +const LoadingState = (): JSX.Element => { + return ( + <> + {range(20).map((i) => ( +
    + + +
    + ))} + + ) +} diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx index d70a3267a2532..f4f37bec89878 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx @@ -11,7 +11,7 @@ import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { useMemo, useRef } from 'react' import { useNotebookDrag } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' import { RecordingNotFound } from 'scenes/session-recordings/player/RecordingNotFound' -import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import { MatchingEventsMatchType, SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { urls } from 'scenes/urls' import { NetworkView } from '../apm/NetworkView' @@ -26,7 +26,6 @@ import { ONE_FRAME_MS, PLAYBACK_SPEEDS, sessionRecordingPlayerLogic, - SessionRecordingPlayerLogicProps, SessionRecordingPlayerMode, } from './sessionRecordingPlayerLogic' import { SessionRecordingPlayerExplorer } from './view-explorer/SessionRecordingPlayerExplorer' diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx index 80f17633c1fc3..e58e8fa85a57d 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerSeekbarPreview.tsx @@ -3,15 +3,12 @@ import { Dayjs } from 'lib/dayjs' import useIsHovering from 'lib/hooks/useIsHovering' import { colonDelimitedDuration } from 'lib/utils' import { memo, MutableRefObject, useEffect, useRef, useState } from 'react' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { useDebouncedCallback } from 'use-debounce' import { PlayerFrame } from '../PlayerFrame' import { TimestampFormat } from '../playerSettingsLogic' -import { - sessionRecordingPlayerLogic, - SessionRecordingPlayerLogicProps, - SessionRecordingPlayerMode, -} from '../sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from '../sessionRecordingPlayerLogic' const TWENTY_MINUTES_IN_MS = 20 * 60 * 1000 diff --git a/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts b/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts index a5c337c8fc76a..15e04298be42b 100644 --- a/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts +++ b/frontend/src/scenes/session-recordings/player/controller/seekbarLogic.ts @@ -1,10 +1,8 @@ import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { clamp } from 'lib/utils' import { MutableRefObject } from 'react' -import { - sessionRecordingPlayerLogic, - SessionRecordingPlayerLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { getXPos, InteractEvent, ReactInteractEvent, THUMB_OFFSET, THUMB_SIZE } from '../utils/playerUtils' import type { seekbarLogicType } from './seekbarLogicType' diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.stories.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.stories.tsx index 3b98f13ba2392..1a0db0c34ed20 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.stories.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { BindLogic, useActions, useValues } from 'kea' +import { useActions, useValues } from 'kea' import { useEffect } from 'react' import { largeRecordingJSONL } from 'scenes/session-recordings/__mocks__/large_recording_blob_one' import largeRecordingEventsJson from 'scenes/session-recordings/__mocks__/large_recording_load_events_one.json' @@ -7,7 +7,6 @@ import largeRecordingMetaJson from 'scenes/session-recordings/__mocks__/large_re import largeRecordingWebVitalsEventsPropertiesJson from 'scenes/session-recordings/__mocks__/large_recording_web_vitals_props.json' import { PlayerInspector } from 'scenes/session-recordings/player/inspector/PlayerInspector' import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { mswDecorator } from '~/mocks/browser' @@ -76,15 +75,12 @@ const BasicTemplate: StoryFn = () => { return (
    - - - + />
    ) } diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx index ec00c7285f6b8..65e0e300d390a 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspector.tsx @@ -1,13 +1,18 @@ +import { BindLogic } from 'kea' import { PlayerInspectorBottomSettings } from 'scenes/session-recordings/player/inspector/PlayerInspectorBottomSettings' import { PlayerInspectorControls } from 'scenes/session-recordings/player/inspector/PlayerInspectorControls' import { PlayerInspectorList } from 'scenes/session-recordings/player/inspector/PlayerInspectorList' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { SessionRecordingPlayerLogicKey } from 'scenes/session-recordings/types' -export function PlayerInspector(): JSX.Element { - return ( - <> +export function PlayerInspector(props: SessionRecordingPlayerLogicKey): JSX.Element { + return props.sessionRecordingId ? ( + - + + ) : ( + <>some empty message ) } diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts index 5fe1914f28852..4ace8aafeec88 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts @@ -19,10 +19,8 @@ import { itemToMiniFilter, } from 'scenes/session-recordings/player/inspector/inspectorListFiltering' import { MiniFilterKey, miniFiltersLogic } from 'scenes/session-recordings/player/inspector/miniFiltersLogic' -import { - convertUniversalFiltersToRecordingsQuery, - MatchingEventsMatchType, -} from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import { convertUniversalFiltersToRecordingsQuery } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import { PlayerInspectorLogicProps } from 'scenes/session-recordings/types' import { RecordingsQuery } from '~/queries/schema' import { @@ -35,7 +33,7 @@ import { } from '~/types' import { sessionRecordingDataLogic } from '../sessionRecordingDataLogic' -import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import type { playerInspectorLogicType } from './playerInspectorLogicType' const CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1' @@ -134,10 +132,6 @@ export type InspectorListItem = | InspectorListItemSummary | InspectorListItemInactivity -export interface PlayerInspectorLogicProps extends SessionRecordingPlayerLogicProps { - matchingEventsMatchType?: MatchingEventsMatchType -} - function _isCustomSnapshot(x: unknown): x is customEvent { return (x as customEvent).type === 5 } @@ -247,7 +241,7 @@ export const playerInspectorLogic = kea([ ], sessionRecordingPlayerLogic(props), ['currentPlayerTime'], - performanceEventDataLogic({ key: props.playerKey, sessionRecordingId: props.sessionRecordingId }), + performanceEventDataLogic(props), ['allPerformanceEvents'], sessionRecordingDataLogic(props), ['trackedWindow'], diff --git a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx index 825f5223e0a51..2ca2527f184ac 100644 --- a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx +++ b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx @@ -1,9 +1,10 @@ import { LemonModal } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { PlayerMeta } from '../PlayerMeta' -import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import { sessionPlayerModalLogic } from './sessionPlayerModalLogic' /** diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx b/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx index 55147ee1790b0..88e035c804842 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx @@ -11,10 +11,8 @@ import posthog from 'posthog-js' import { countryCodeToName } from 'scenes/insights/views/WorldMap' import { OverviewItem } from 'scenes/session-recordings/components/OverviewGrid' import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { - sessionRecordingPlayerLogic, - SessionRecordingPlayerLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { PersonType } from '~/types' diff --git a/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts b/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts index e4db7ee981bfa..b5ac28aafeed7 100644 --- a/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playlist-popover/playlistPopoverLogic.ts @@ -4,12 +4,10 @@ import { loaders } from 'kea-loaders' import api from 'lib/api' import { toParams } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { - sessionRecordingPlayerLogic, - SessionRecordingPlayerLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { addRecordingToPlaylist, removeRecordingFromPlaylist } from 'scenes/session-recordings/player/utils/playerUtils' import { createPlaylist } from 'scenes/session-recordings/playlist/playlistUtils' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { SessionRecordingPlaylistType } from '~/types' diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index ff63bd4b1f397..35bb217ea1f1f 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -28,6 +28,7 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { compressedEventWithTime } from 'posthog-js/lib/src/extensions/replay/sessionrecording' import { RecordingComment } from 'scenes/session-recordings/player/inspector/playerInspectorLogic' +import { SessionRecordingDataLogicProps } from 'scenes/session-recordings/types' import { teamLogic } from 'scenes/teamLogic' import { HogQLQuery, NodeKind } from '~/queries/schema' @@ -44,7 +45,6 @@ import { RecordingSegment, RecordingSnapshot, SessionPlayerData, - SessionRecordingId, SessionRecordingSnapshotParams, SessionRecordingSnapshotSource, SessionRecordingSnapshotSourceResponse, @@ -351,11 +351,6 @@ const resetTimingsCache = (cache: Record): void => { cache.firstPaintDuration = null } -export interface SessionRecordingDataLogicProps { - sessionRecordingId: SessionRecordingId - realTimePollingIntervalMilliseconds?: number -} - function makeEventsQuery( person: PersonType | null, distinctId: string | null, diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index 5e7e7955f4602..a037a2699ea74 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -1,20 +1,7 @@ import { lemonToast } from '@posthog/lemon-ui' import { EventType, eventWithTime, IncrementalSource } from '@rrweb/types' import { captureException } from '@sentry/react' -import { - actions, - afterMount, - beforeUnmount, - BuiltLogic, - connect, - kea, - key, - listeners, - path, - props, - reducers, - selectors, -} from 'kea' +import { actions, afterMount, beforeUnmount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { router } from 'kea-router' import { subscriptions } from 'kea-subscriptions' import { delay } from 'kea-test-utils' @@ -24,22 +11,17 @@ import { clamp, downloadFile, objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { wrapConsole } from 'lib/utils/wrapConsole' import posthog from 'posthog-js' -import { RefObject } from 'react' import { Replayer } from 'rrweb' import { playerConfig, ReplayPlugin } from 'rrweb/typings/types' import { openBillingPopupModal } from 'scenes/billing/BillingPopup' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { - sessionRecordingDataLogic, - SessionRecordingDataLogicProps, -} from 'scenes/session-recordings/player/sessionRecordingDataLogic' -import { MatchingEventsMatchType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { AvailableFeature, RecordingSegment, SessionPlayerData, SessionPlayerState } from '~/types' -import type { sessionRecordingsPlaylistLogicType } from '../playlist/sessionRecordingsPlaylistLogicType' import { playerSettingsLogic } from './playerSettingsLogic' import { COMMON_REPLAYER_CONFIG, CorsPlugin, HLSPlayerPlugin } from './rrweb' import { CanvasReplayerPlugin } from './rrweb/canvas/canvas-plugin' @@ -80,18 +62,6 @@ export enum SessionRecordingPlayerMode { Preview = 'preview', } -export interface SessionRecordingPlayerLogicProps extends SessionRecordingDataLogicProps { - playerKey: string - sessionRecordingData?: SessionPlayerData - matchingEventsMatchType?: MatchingEventsMatchType - playlistLogic?: BuiltLogic - autoPlay?: boolean - mode?: SessionRecordingPlayerMode - playerRef?: RefObject - pinned?: boolean - setPinned?: (pinned: boolean) => void -} - const isMediaElementPlaying = (element: HTMLMediaElement): boolean => !!(element.currentTime > 0 && !element.paused && !element.ended && element.readyState > 2) diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarTab.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarTab.tsx index 9c69a46274d11..707c0f155c17d 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarTab.tsx +++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarTab.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { PlayerInspector } from 'scenes/session-recordings/player/inspector/PlayerInspector' +import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { SessionRecordingSidebarTab } from '~/types' @@ -9,12 +10,15 @@ import { PlayerSidebarOverviewTab } from './PlayerSidebarOverviewTab' export function PlayerSidebarTab(): JSX.Element | null { const { activeTab } = useValues(playerSidebarLogic) + // this tab is mounted within a component that has bound this logic + // the inspector is not always, and we pass the values in + const { logicProps } = useValues(sessionRecordingPlayerLogic) switch (activeTab) { case SessionRecordingSidebarTab.OVERVIEW: return case SessionRecordingSidebarTab.INSPECTOR: - return + return case SessionRecordingSidebarTab.DEBUGGER: return default: diff --git a/frontend/src/scenes/session-recordings/player/sidebar/playerSidebarLogic.ts b/frontend/src/scenes/session-recordings/player/sidebar/playerSidebarLogic.ts index 24b630be4b4a7..0bb48ab7cb9a0 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/playerSidebarLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sidebar/playerSidebarLogic.ts @@ -1,8 +1,8 @@ import { actions, kea, path, props, reducers } from 'kea' +import { SessionRecordingPlayerLogicProps } from 'scenes/session-recordings/types' import { SessionRecordingSidebarTab } from '~/types' -import { SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' import type { playerSidebarLogicType } from './playerSidebarLogicType' export const playerSidebarLogic = kea([ diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index 023b395b067cc..32e81b54936a8 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -8,6 +8,7 @@ import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic' +import { SessionRecordingPlaylistLogicProps } from 'scenes/session-recordings/types' import { urls } from 'scenes/urls' import { ReplayTabs, SessionRecordingType } from '~/types' @@ -15,11 +16,7 @@ import { ReplayTabs, SessionRecordingType } from '~/types' import { RecordingsUniversalFilters } from '../filters/RecordingsUniversalFilters' import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' import { SessionRecordingPreview } from './SessionRecordingPreview' -import { - DEFAULT_RECORDING_FILTERS, - SessionRecordingPlaylistLogicProps, - sessionRecordingsPlaylistLogic, -} from './sessionRecordingsPlaylistLogic' +import { DEFAULT_RECORDING_FILTERS, sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic' import { SessionRecordingPlaylistBottomSettings, SessionRecordingsPlaylistTopSettings, diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index c550d14f3bc8c..23335f0b043a3 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -16,6 +16,7 @@ import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { objectClean, objectsEqual } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { MatchingEventsMatchType, SessionRecordingPlaylistLogicProps } from 'scenes/session-recordings/types' import { NodeKind, RecordingOrder, RecordingsQuery, RecordingsQueryResponse } from '~/queries/schema' import { @@ -47,27 +48,6 @@ interface Params { order?: RecordingsQuery['order'] } -interface NoEventsToMatch { - matchType: 'none' -} - -interface EventNamesMatching { - matchType: 'name' - eventNames: string[] -} - -interface EventUUIDsMatching { - matchType: 'uuid' - eventUUIDs: string[] -} - -interface BackendEventsMatching { - matchType: 'backend' - filters: RecordingUniversalFilters -} - -export type MatchingEventsMatchType = NoEventsToMatch | EventNamesMatching | EventUUIDsMatching | BackendEventsMatching - export const RECORDINGS_LIMIT = 20 export const PINNED_RECORDINGS_LIMIT = 100 // NOTE: This is high but avoids the need for pagination for now... @@ -242,17 +222,6 @@ function sortRecordings( }) } -export interface SessionRecordingPlaylistLogicProps { - logicKey?: string - personUUID?: PersonUUID - updateSearchParams?: boolean - autoPlay?: boolean - filters?: RecordingUniversalFilters - onFiltersChange?: (filters: RecordingUniversalFilters) => void - pinnedRecordings?: (SessionRecordingType | string)[] - onPinnedChange?: (recording: SessionRecordingType, pinned: boolean) => void -} - export const sessionRecordingsPlaylistLogic = kea([ path((key) => ['scenes', 'session-recordings', 'playlist', 'sessionRecordingsPlaylistLogic', key]), props({} as SessionRecordingPlaylistLogicProps), diff --git a/frontend/src/scenes/session-recordings/types.ts b/frontend/src/scenes/session-recordings/types.ts new file mode 100644 index 0000000000000..40ceab8d56d94 --- /dev/null +++ b/frontend/src/scenes/session-recordings/types.ts @@ -0,0 +1,78 @@ +import { BuiltLogic } from 'kea' +import { RefObject } from 'react' +import { SessionRecordingPlayerMode } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { PersonUUID } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import type { sessionRecordingsPlaylistLogicType } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogicType' + +import { RecordingUniversalFilters, SessionPlayerData, SessionRecordingId, SessionRecordingType } from '~/types' + +interface NoEventsToMatch { + matchType: 'none' +} + +interface EventNamesMatching { + matchType: 'name' + eventNames: string[] +} + +interface EventUUIDsMatching { + matchType: 'uuid' + eventUUIDs: string[] +} + +interface BackendEventsMatching { + matchType: 'backend' + filters: RecordingUniversalFilters +} + +export type MatchingEventsMatchType = NoEventsToMatch | EventNamesMatching | EventUUIDsMatching | BackendEventsMatching + +// the logics that use the props below mount each other +// the complexity of the interfaces below +// is to keep track of how they mount each other more clearly + +interface SessionRecordingDataKey { + sessionRecordingId: SessionRecordingId +} + +export interface SessionRecordingDataLogicProps extends SessionRecordingDataKey { + realTimePollingIntervalMilliseconds?: number +} + +export interface SessionRecordingPlayerLogicKey { + playerKey: string + sessionRecordingId: SessionRecordingId +} + +export interface SessionRecordingPlayerLogicProps extends SessionRecordingPlayerLogicKey, SessionRecordingDataKey { + sessionRecordingData?: SessionPlayerData + matchingEventsMatchType?: MatchingEventsMatchType + playlistLogic?: BuiltLogic + autoPlay?: boolean + mode?: SessionRecordingPlayerMode + playerRef?: RefObject + pinned?: boolean + setPinned?: (pinned: boolean) => void +} + +export interface PlayerInspectorLogicProps extends SessionRecordingPlayerLogicKey { + matchingEventsMatchType?: MatchingEventsMatchType +} + +export interface NetworkViewLogicProps extends SessionRecordingDataKey {} + +export interface PerformanceEventDataLogicProps extends SessionRecordingDataKey {} + +interface SessionRecordingPlaylistLogicKey { + logicKey?: string + personUUID?: PersonUUID + updateSearchParams?: boolean +} + +export interface SessionRecordingPlaylistLogicProps extends SessionRecordingPlaylistLogicKey { + autoPlay?: boolean + filters?: RecordingUniversalFilters + onFiltersChange?: (filters: RecordingUniversalFilters) => void + pinnedRecordings?: (SessionRecordingType | string)[] + onPinnedChange?: (recording: SessionRecordingType, pinned: boolean) => void +}