mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 13:39:22 +01:00
feat: Remove flag for infinite scroller and fix issue with reloading (#16148)
This commit is contained in:
parent
2a6f2a98a4
commit
687190fa89
Binary file not shown.
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
@ -145,7 +145,6 @@ export const FEATURE_FLAGS = {
|
||||
RECORDINGS_DOM_EXPLORER: 'recordings-dom-explorer', // owner: #team-session-recordings
|
||||
AUTO_REDIRECT: 'auto-redirect', // owner: @lharries
|
||||
SESSION_RECORDING_BLOB_REPLAY: 'session-recording-blob-replay', // owner: #team-monitoring
|
||||
SESSION_RECORDING_INFINITE_LIST: 'session-recording-infinite-list', // owner: #team-monitoring
|
||||
SESSION_RECORDING_SUMMARY_LISTING: 'session-recording-summary-listing', // owner: #team-monitoring
|
||||
SURVEYS: 'surveys', // owner: @liyiy
|
||||
NEW_EMPTY_STATES: 'new-empty-states', // experiment, owner: @raquelmsmith
|
||||
|
@ -55,6 +55,7 @@ export const personsLogic = kea<personsLogicType>({
|
||||
deleteProperty: (key: string) => ({ key }),
|
||||
navigateToCohort: (cohort: CohortType) => ({ cohort }),
|
||||
navigateToTab: (tab: PersonsTabType) => ({ tab }),
|
||||
setActiveTab: (tab: PersonsTabType) => ({ tab }),
|
||||
setSplitMergeModalShown: (shown: boolean) => ({ shown }),
|
||||
setDistinctId: (distinctId: string) => ({ distinctId }),
|
||||
},
|
||||
@ -94,6 +95,7 @@ export const personsLogic = kea<personsLogicType>({
|
||||
null as PersonsTabType | null,
|
||||
{
|
||||
navigateToTab: (_, { tab }) => tab,
|
||||
setActiveTab: (_, { tab }) => tab,
|
||||
},
|
||||
],
|
||||
splitMergeModalShown: [
|
||||
@ -305,11 +307,7 @@ export const personsLogic = kea<personsLogicType>({
|
||||
},
|
||||
navigateToTab: () => {
|
||||
if (props.syncWithUrl && router.values.location.pathname.indexOf('/person') > -1) {
|
||||
const searchParams = { ...router.values.searchParams }
|
||||
|
||||
if (values.activeTab !== PersonsTabType.HISTORY) {
|
||||
delete searchParams['page']
|
||||
}
|
||||
const searchParams = {}
|
||||
|
||||
return [
|
||||
router.values.location.pathname,
|
||||
@ -333,16 +331,14 @@ export const personsLogic = kea<personsLogicType>({
|
||||
}
|
||||
}
|
||||
},
|
||||
'/person/*': ({ _: rawPersonDistinctId }, { sessionRecordingId }, { activeTab }) => {
|
||||
'/person/*': ({ _: rawPersonDistinctId }, {}, { activeTab }) => {
|
||||
if (props.syncWithUrl) {
|
||||
if (sessionRecordingId) {
|
||||
actions.navigateToTab(PersonsTabType.SESSION_RECORDINGS)
|
||||
} else if (activeTab && values.activeTab !== activeTab) {
|
||||
actions.navigateToTab(activeTab as PersonsTabType)
|
||||
if (activeTab && values.activeTab !== activeTab) {
|
||||
actions.setActiveTab(activeTab as PersonsTabType)
|
||||
}
|
||||
|
||||
if (!activeTab && values.activeTab && values.activeTab !== PersonsTabType.PROPERTIES) {
|
||||
actions.navigateToTab(PersonsTabType.PROPERTIES)
|
||||
if (!activeTab) {
|
||||
actions.setActiveTab(PersonsTabType.PROPERTIES)
|
||||
}
|
||||
|
||||
if (rawPersonDistinctId) {
|
||||
|
@ -211,19 +211,24 @@ export function SessionRecordingsFilters({
|
||||
}}
|
||||
/>
|
||||
|
||||
<LemonLabel info="Show recordings by persons who match the set criteria">
|
||||
Filter by persons and cohorts
|
||||
</LemonLabel>
|
||||
|
||||
{showPropertyFilters && (
|
||||
<PropertyFilters
|
||||
pageKey={'session-recordings'}
|
||||
taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts]}
|
||||
propertyFilters={filters.properties}
|
||||
onChange={(properties) => {
|
||||
setFilters({ properties })
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<LemonLabel info="Show recordings by persons who match the set criteria">
|
||||
Filter by persons and cohorts
|
||||
</LemonLabel>
|
||||
|
||||
<PropertyFilters
|
||||
pageKey={'session-recordings'}
|
||||
taxonomicGroupTypes={[
|
||||
TaxonomicFilterGroupType.PersonProperties,
|
||||
TaxonomicFilterGroupType.Cohorts,
|
||||
]}
|
||||
propertyFilters={filters.properties}
|
||||
onChange={(properties) => {
|
||||
setFilters({ properties })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FlaggedFeature flag={FEATURE_FLAGS.SESSION_RECORDING_SHOW_CONSOLE_LOGS_FILTER} match={true}>
|
||||
|
@ -29,7 +29,7 @@ describe('sessionPlayerModalLogic', () => {
|
||||
})
|
||||
it('is set by openSessionPlayer and cleared by closeSessionPlayer', async () => {
|
||||
expectLogic(logic, () => logic.actions.openSessionPlayer({ id: 'abc' }))
|
||||
.toDispatchActions(['getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
selectedSessionRecording: { id: 'abc' },
|
||||
activeSessionRecording: listOfSessionRecordings[0],
|
||||
|
@ -12,12 +12,10 @@ import './SessionRecordingsPlaylist.scss'
|
||||
import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer'
|
||||
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
|
||||
import { LemonButton, LemonDivider, LemonSwitch } from '@posthog/lemon-ui'
|
||||
import { IconChevronLeft, IconChevronRight, IconFilter, IconPause, IconPlay, IconWithCount } from 'lib/lemon-ui/icons'
|
||||
import { IconFilter, IconPause, IconPlay, IconWithCount } from 'lib/lemon-ui/icons'
|
||||
import { SessionRecordingsList } from './SessionRecordingsList'
|
||||
import clsx from 'clsx'
|
||||
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { Spinner } from 'lib/lemon-ui/Spinner'
|
||||
import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters'
|
||||
@ -45,7 +43,6 @@ export function RecordingsLists({
|
||||
const {
|
||||
filters,
|
||||
hasNext,
|
||||
hasPrev,
|
||||
sessionRecordings,
|
||||
sessionRecordingsResponseLoading,
|
||||
activeSessionRecording,
|
||||
@ -55,23 +52,11 @@ export function RecordingsLists({
|
||||
totalFiltersCount,
|
||||
listingVersion,
|
||||
} = useValues(logic)
|
||||
const {
|
||||
setSelectedRecordingId,
|
||||
loadNext,
|
||||
loadPrev,
|
||||
setFilters,
|
||||
maybeLoadSessionRecordings,
|
||||
setShowFilters,
|
||||
resetFilters,
|
||||
} = useActions(logic)
|
||||
const { setSelectedRecordingId, setFilters, maybeLoadSessionRecordings, setShowFilters, resetFilters } =
|
||||
useActions(logic)
|
||||
const { autoplayDirection } = useValues(playerSettingsLogic)
|
||||
const { toggleAutoplayDirection } = useActions(playerSettingsLogic)
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
const infiniteScrollerEnabled = featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]
|
||||
|
||||
const [collapsed, setCollapsed] = useState({ pinned: false, other: false })
|
||||
const offset = filters.offset ?? 0
|
||||
const nextLength = offset + (sessionRecordingsResponseLoading ? RECORDINGS_LIMIT : sessionRecordings.length)
|
||||
|
||||
const onRecordingClick = (recording: SessionRecordingType): void => {
|
||||
setSelectedRecordingId(recording.id)
|
||||
@ -81,26 +66,6 @@ export function RecordingsLists({
|
||||
setFilters(defaultPageviewPropertyEntityFilter(filters, property, value))
|
||||
}
|
||||
|
||||
const paginationControls = nextLength ? (
|
||||
<div className="flex items-center gap-1 mx-2">
|
||||
<span>{`${offset + 1} - ${nextLength}`}</span>
|
||||
<LemonButton
|
||||
icon={<IconChevronLeft />}
|
||||
status="stealth"
|
||||
size="small"
|
||||
disabled={!hasPrev}
|
||||
onClick={() => loadPrev()}
|
||||
/>
|
||||
<LemonButton
|
||||
icon={<IconChevronRight />}
|
||||
status="stealth"
|
||||
disabled={!hasNext}
|
||||
size="small"
|
||||
onClick={() => loadNext()}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="SessionRecordingsPlaylist__lists">
|
||||
@ -144,27 +109,23 @@ export function RecordingsLists({
|
||||
title={!playlistShortId ? 'Recordings' : 'Other recordings'}
|
||||
titleRight={
|
||||
<>
|
||||
{infiniteScrollerEnabled ? (
|
||||
sessionRecordings.length ? (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
title={
|
||||
<>
|
||||
Showing {sessionRecordings.length} results.
|
||||
<br />
|
||||
Scrolling to the bottom or the top of the list will load older or newer
|
||||
recordings respectively.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<CounterBadge>{Math.min(999, sessionRecordings.length)}+</CounterBadge>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null
|
||||
) : (
|
||||
paginationControls
|
||||
)}
|
||||
{sessionRecordings.length ? (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
title={
|
||||
<>
|
||||
Showing {sessionRecordings.length} results.
|
||||
<br />
|
||||
Scrolling to the bottom or the top of the list will load older or newer
|
||||
recordings respectively.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<CounterBadge>{Math.min(999, sessionRecordings.length)}+</CounterBadge>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
<LemonButton
|
||||
noPadding
|
||||
@ -249,30 +210,25 @@ export function RecordingsLists({
|
||||
</div>
|
||||
}
|
||||
activeRecordingId={activeSessionRecording?.id}
|
||||
onScrollToEnd={infiniteScrollerEnabled ? () => maybeLoadSessionRecordings('older') : undefined}
|
||||
onScrollToStart={infiniteScrollerEnabled ? () => maybeLoadSessionRecordings('newer') : undefined}
|
||||
onScrollToEnd={() => maybeLoadSessionRecordings('older')}
|
||||
onScrollToStart={() => maybeLoadSessionRecordings('newer')}
|
||||
footer={
|
||||
infiniteScrollerEnabled ? (
|
||||
<>
|
||||
<LemonDivider />
|
||||
<div className="m-4 h-10 flex items-center justify-center gap-2 text-muted-alt">
|
||||
{sessionRecordingsResponseLoading ? (
|
||||
<>
|
||||
<Spinner monocolor /> Loading older recordings
|
||||
</>
|
||||
) : hasNext ? (
|
||||
<LemonButton
|
||||
status="primary"
|
||||
onClick={() => maybeLoadSessionRecordings('older')}
|
||||
>
|
||||
Load more
|
||||
</LemonButton>
|
||||
) : (
|
||||
'No more results'
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : null
|
||||
<>
|
||||
<LemonDivider />
|
||||
<div className="m-4 h-10 flex items-center justify-center gap-2 text-muted-alt">
|
||||
{sessionRecordingsResponseLoading ? (
|
||||
<>
|
||||
<Spinner monocolor /> Loading older recordings
|
||||
</>
|
||||
) : hasNext ? (
|
||||
<LemonButton status="primary" onClick={() => maybeLoadSessionRecordings('older')}>
|
||||
Load more
|
||||
</LemonButton>
|
||||
) : (
|
||||
'No more results'
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
draggableHref={urls.replay(ReplayTabs.Recent, filters)}
|
||||
/>
|
||||
|
@ -15,6 +15,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
let logic: ReturnType<typeof sessionRecordingsListLogic.build>
|
||||
const aRecording = { id: 'abc', viewed: false, recording_duration: 10 }
|
||||
const listOfSessionRecordings = [aRecording]
|
||||
jest.setTimeout(500)
|
||||
|
||||
beforeEach(() => {
|
||||
useMocks({
|
||||
@ -103,7 +104,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
describe('core assumptions', () => {
|
||||
it('loads recent recordings and pinned recordings after mounting', async () => {
|
||||
await expectLogic(logic)
|
||||
.toDispatchActionsInAnyOrder(['getSessionRecordingsSuccess', 'loadPinnedRecordingsSuccess'])
|
||||
.toDispatchActionsInAnyOrder(['loadSessionRecordingsSuccess', 'loadPinnedRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
sessionRecordings: listOfSessionRecordings,
|
||||
pinnedRecordingsResponse: {
|
||||
@ -119,30 +120,30 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
it('is set by setSessionRecordingId', async () => {
|
||||
expectLogic(logic, () => logic.actions.setSelectedRecordingId('abc'))
|
||||
.toDispatchActions(['getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
selectedRecordingId: 'abc',
|
||||
activeSessionRecording: listOfSessionRecordings[0],
|
||||
})
|
||||
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
})
|
||||
|
||||
it('is partial if sessionRecordingId not in list', async () => {
|
||||
expectLogic(logic, () => logic.actions.setSelectedRecordingId('not-in-list'))
|
||||
.toDispatchActions(['getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
selectedRecordingId: 'not-in-list',
|
||||
activeSessionRecording: { id: 'not-in-list' },
|
||||
})
|
||||
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'not-in-list')
|
||||
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'not-in-list')
|
||||
})
|
||||
|
||||
it('is read from the URL on the session recording page', async () => {
|
||||
router.actions.push('/replay', {}, { sessionRecordingId: 'abc' })
|
||||
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
|
||||
await expectLogic(logic)
|
||||
.toDispatchActionsInAnyOrder(['setSelectedRecordingId', 'getSessionRecordingsSuccess'])
|
||||
.toDispatchActionsInAnyOrder(['setSelectedRecordingId', 'loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
selectedRecordingId: 'abc',
|
||||
activeSessionRecording: listOfSessionRecordings[0],
|
||||
@ -156,11 +157,11 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
|
||||
it('returns the first session recording if none selected', () => {
|
||||
expectLogic(logic).toDispatchActions(['getSessionRecordingsSuccess']).toMatchValues({
|
||||
expectLogic(logic).toDispatchActions(['loadSessionRecordingsSuccess']).toMatchValues({
|
||||
selectedRecordingId: undefined,
|
||||
activeSessionRecording: listOfSessionRecordings[0],
|
||||
})
|
||||
expect(router.values.hashParams).not.toHaveProperty('sessionRecordingId', 'not-in-list')
|
||||
expect(router.values.searchParams).not.toHaveProperty('sessionRecordingId', 'not-in-list')
|
||||
})
|
||||
})
|
||||
|
||||
@ -175,7 +176,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
|
||||
})
|
||||
})
|
||||
.toDispatchActions(['setFilters', 'getSessionRecordings', 'getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['setFilters', 'loadSessionRecordings', 'loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
sessionRecordings: ['List of recordings filtered by events'],
|
||||
})
|
||||
@ -185,26 +186,6 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('limit and offset', () => {
|
||||
it('is set by loadNext and loadPrev and gets the right results and sets the url', async () => {
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadNext()
|
||||
})
|
||||
.toMatchValues({ filters: expect.objectContaining({ offset: RECORDINGS_LIMIT }) })
|
||||
.toDispatchActions(['loadNext', 'getSessionRecordingsSuccess'])
|
||||
.toMatchValues({ sessionRecordings: [`List of recordings offset by ${RECORDINGS_LIMIT}`] })
|
||||
expect(router.values.searchParams.filters).toHaveProperty('offset', RECORDINGS_LIMIT)
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadPrev()
|
||||
})
|
||||
.toMatchValues({ filters: expect.objectContaining({ offset: 0 }) })
|
||||
.toDispatchActions(['loadPrev', 'getSessionRecordingsSuccess'])
|
||||
.toMatchValues({ sessionRecordings: listOfSessionRecordings })
|
||||
expect(router.values.searchParams.filters).toHaveProperty('offset', 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('date range', () => {
|
||||
it('is set by setFilters and fetches results from server and sets the url', async () => {
|
||||
await expectLogic(logic, () => {
|
||||
@ -219,7 +200,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
date_to: '2021-10-20',
|
||||
}),
|
||||
})
|
||||
.toDispatchActions(['setFilters', 'getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['setFilters', 'loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({ sessionRecordings: ['Recordings filtered by date'] })
|
||||
|
||||
expect(router.values.searchParams.filters).toHaveProperty('date_from', '2021-10-05')
|
||||
@ -248,7 +229,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
},
|
||||
}),
|
||||
})
|
||||
.toDispatchActions(['setFilters', 'getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['setFilters', 'loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({ sessionRecordings: ['Recordings filtered by duration'] })
|
||||
|
||||
expect(router.values.searchParams.filters).toHaveProperty('session_recording_duration', {
|
||||
@ -289,7 +270,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
logic.mount()
|
||||
|
||||
await expectLogic(logic).toDispatchActions(['getSessionRecordingsSuccess']).toMatchValues({
|
||||
await expectLogic(logic).toDispatchActions(['loadSessionRecordingsSuccess']).toMatchValues({
|
||||
selectedRecordingId: 'abc',
|
||||
})
|
||||
|
||||
@ -340,7 +321,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
|
||||
})
|
||||
})
|
||||
.toDispatchActions(['setFilters', 'getSessionRecordings', 'getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['setFilters', 'loadSessionRecordings', 'loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({
|
||||
sessionRecordings: ['List of recordings filtered by events'],
|
||||
})
|
||||
@ -365,7 +346,7 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
|
||||
await expectLogic(logic)
|
||||
.toDispatchActions(['replaceFilters'])
|
||||
.toDispatchActions(['setFilters'])
|
||||
.toMatchValues({
|
||||
filters: {
|
||||
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
|
||||
@ -373,6 +354,8 @@ describe('sessionRecordingsListLogic', () => {
|
||||
date_from: '2021-10-01',
|
||||
date_to: '2021-10-10',
|
||||
offset: 50,
|
||||
console_logs: [],
|
||||
properties: [],
|
||||
session_recording_duration: {
|
||||
type: PropertyFilterType.Recording,
|
||||
key: 'duration',
|
||||
@ -391,11 +374,19 @@ describe('sessionRecordingsListLogic', () => {
|
||||
})
|
||||
|
||||
await expectLogic(logic)
|
||||
.toDispatchActions(['replaceFilters'])
|
||||
.toDispatchActions(['setFilters'])
|
||||
.toMatchValues({
|
||||
customFilters: {
|
||||
actions: [{ id: '1', type: 'actions', order: 0, name: 'View Recording' }],
|
||||
},
|
||||
filters: {
|
||||
actions: [{ id: '1', type: 'actions', order: 0, name: 'View Recording' }],
|
||||
session_recording_duration: defaultRecordingDurationFilter,
|
||||
console_logs: [],
|
||||
date_from: '-7d',
|
||||
date_to: null,
|
||||
events: [],
|
||||
properties: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -413,13 +404,13 @@ describe('sessionRecordingsListLogic', () => {
|
||||
|
||||
it('loads session recordings for a specific user', async () => {
|
||||
await expectLogic(logic)
|
||||
.toDispatchActions(['getSessionRecordingsSuccess'])
|
||||
.toDispatchActions(['loadSessionRecordingsSuccess'])
|
||||
.toMatchValues({ sessionRecordings: ["List of specific user's recordings from server"] })
|
||||
})
|
||||
|
||||
it('reads sessionRecordingId from the URL on the person page', async () => {
|
||||
router.actions.push('/person/123', {}, { sessionRecordingId: 'abc' })
|
||||
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
|
||||
|
||||
await expectLogic(logic).toDispatchActions([logic.actionCreators.setSelectedRecordingId('abc')])
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { toParams } from 'lib/utils'
|
||||
import { objectClean, toParams } from 'lib/utils'
|
||||
import {
|
||||
AnyPropertyFilter,
|
||||
PropertyFilterType,
|
||||
@ -22,11 +22,9 @@ import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPro
|
||||
import { playerSettingsLogic } from '../player/playerSettingsLogic'
|
||||
|
||||
export type PersonUUID = string
|
||||
|
||||
interface Params {
|
||||
filters?: RecordingFilters
|
||||
}
|
||||
|
||||
interface HashParams {
|
||||
sessionRecordingId?: SessionRecordingId
|
||||
}
|
||||
|
||||
@ -146,7 +144,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
}),
|
||||
actions({
|
||||
setFilters: (filters: Partial<RecordingFilters>) => ({ filters }),
|
||||
replaceFilters: (filters: RecordingFilters) => ({ filters }),
|
||||
setShowFilters: (showFilters: boolean) => ({ showFilters }),
|
||||
resetFilters: true,
|
||||
setSelectedRecordingId: (id: SessionRecordingType['id'] | null) => ({
|
||||
@ -154,7 +151,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
}),
|
||||
loadAllRecordings: true,
|
||||
loadPinnedRecordings: true,
|
||||
getSessionRecordings: true,
|
||||
loadSessionRecordings: (direction?: 'newer' | 'older') => ({ direction }),
|
||||
maybeLoadSessionRecordings: (direction?: 'newer' | 'older') => ({ direction }),
|
||||
loadNext: true,
|
||||
@ -167,27 +163,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
has_next: false,
|
||||
} as SessionRecordingsResponse,
|
||||
{
|
||||
getSessionRecordings: async (_, breakpoint) => {
|
||||
const paramsDict = {
|
||||
...values.filters,
|
||||
person_uuid: props.personUUID ?? '',
|
||||
limit: RECORDINGS_LIMIT,
|
||||
version: values.listingVersion,
|
||||
}
|
||||
|
||||
const params = toParams(paramsDict)
|
||||
await breakpoint(100) // Debounce for lots of quick filter changes
|
||||
|
||||
const startTime = performance.now()
|
||||
const response = await api.recordings.list(params)
|
||||
const loadTimeMs = performance.now() - startTime
|
||||
|
||||
actions.reportRecordingsListFetched(loadTimeMs, values.listingVersion)
|
||||
|
||||
breakpoint()
|
||||
return response
|
||||
},
|
||||
|
||||
loadSessionRecordings: async ({ direction }, breakpoint) => {
|
||||
const currentResults = direction ? values.sessionRecordingsResponse?.results ?? [] : []
|
||||
|
||||
@ -278,20 +253,14 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
],
|
||||
})),
|
||||
reducers(({ props }) => ({
|
||||
filters: [
|
||||
props.filters || getDefaultFilters(props.personUUID),
|
||||
customFilters: [
|
||||
(props.filters ?? null) as RecordingFilters | null,
|
||||
{
|
||||
replaceFilters: (_, { filters }) => {
|
||||
return {
|
||||
...filters,
|
||||
session_recording_duration:
|
||||
filters.session_recording_duration || defaultRecordingDurationFilter,
|
||||
}
|
||||
},
|
||||
setFilters: (state, { filters }) => ({
|
||||
...state,
|
||||
...filters,
|
||||
}),
|
||||
resetFilters: () => null,
|
||||
},
|
||||
],
|
||||
showFilters: [
|
||||
@ -303,9 +272,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
sessionRecordings: [
|
||||
[] as SessionRecordingType[],
|
||||
{
|
||||
getSessionRecordingsSuccess: (_, { sessionRecordingsResponse }) => {
|
||||
return [...(sessionRecordingsResponse?.results ?? [])]
|
||||
},
|
||||
loadSessionRecordingsSuccess: (_, { sessionRecordingsResponse }) => {
|
||||
return [...(sessionRecordingsResponse?.results ?? [])]
|
||||
},
|
||||
@ -331,43 +297,13 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
})),
|
||||
listeners(({ props, actions, values }) => ({
|
||||
loadAllRecordings: () => {
|
||||
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
|
||||
actions.loadSessionRecordings()
|
||||
} else {
|
||||
actions.getSessionRecordings()
|
||||
}
|
||||
actions.loadSessionRecordings()
|
||||
actions.loadPinnedRecordings()
|
||||
},
|
||||
setFilters: () => {
|
||||
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
|
||||
actions.loadSessionRecordings()
|
||||
} else {
|
||||
actions.getSessionRecordings()
|
||||
}
|
||||
|
||||
actions.loadSessionRecordings()
|
||||
props.onFiltersChange?.(values.filters)
|
||||
},
|
||||
replaceFilters: () => {
|
||||
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
|
||||
actions.loadSessionRecordings()
|
||||
} else {
|
||||
actions.getSessionRecordings()
|
||||
}
|
||||
},
|
||||
resetFilters: () => {
|
||||
actions.setFilters(getDefaultFilters(props.personUUID))
|
||||
},
|
||||
|
||||
loadNext: () => {
|
||||
actions.setFilters({
|
||||
offset: (values.filters?.offset || 0) + RECORDINGS_LIMIT,
|
||||
})
|
||||
},
|
||||
loadPrev: () => {
|
||||
actions.setFilters({
|
||||
offset: Math.max((values.filters?.offset || 0) - RECORDINGS_LIMIT, 0),
|
||||
})
|
||||
},
|
||||
|
||||
maybeLoadSessionRecordings: ({ direction }) => {
|
||||
if (direction === 'older' && !values.hasNext) {
|
||||
@ -383,20 +319,26 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
actions.maybeLoadPropertiesForSessions(values.sessionRecordings.map((s) => s.id))
|
||||
},
|
||||
|
||||
getSessionRecordingsSuccess: () => {
|
||||
actions.maybeLoadPropertiesForSessions(values.sessionRecordings.map((s) => s.id))
|
||||
},
|
||||
setSelectedRecordingId: () => {
|
||||
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
|
||||
// If we are at the end of the list then try to load more
|
||||
const recordingIndex = values.sessionRecordings.findIndex((s) => s.id === values.selectedRecordingId)
|
||||
if (recordingIndex === values.sessionRecordings.length - 1) {
|
||||
actions.maybeLoadSessionRecordings('older')
|
||||
}
|
||||
// If we are at the end of the list then try to load more
|
||||
const recordingIndex = values.sessionRecordings.findIndex((s) => s.id === values.selectedRecordingId)
|
||||
if (recordingIndex === values.sessionRecordings.length - 1) {
|
||||
actions.maybeLoadSessionRecordings('older')
|
||||
}
|
||||
},
|
||||
})),
|
||||
selectors({
|
||||
filters: [
|
||||
(s) => [s.customFilters, (_, props) => props.personUUID],
|
||||
(customFilters, personUUID): RecordingFilters => {
|
||||
const defaultFilters = getDefaultFilters(personUUID)
|
||||
return {
|
||||
...defaultFilters,
|
||||
...customFilters,
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
listingVersion: [
|
||||
(s) => [s.featureFlags],
|
||||
(featureFlags): '1' | '2' | '3' => {
|
||||
@ -437,8 +379,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
: sessionRecordings[activeSessionRecordingIndex - 1]
|
||||
},
|
||||
],
|
||||
|
||||
hasPrev: [(s) => [s.filters], (filters) => (filters.offset || 0) > 0],
|
||||
hasNext: [
|
||||
(s) => [s.sessionRecordingsResponse],
|
||||
(sessionRecordingsResponse) => sessionRecordingsResponse.has_next,
|
||||
@ -447,6 +387,7 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
(s) => [s.filters, (_, props) => props.personUUID],
|
||||
(filters, personUUID) => {
|
||||
const defaultFilters = getDefaultFilters(personUUID)
|
||||
|
||||
return (
|
||||
(filters?.actions?.length || 0) +
|
||||
(filters?.events?.length || 0) +
|
||||
@ -475,42 +416,41 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
replace: boolean
|
||||
}
|
||||
] => {
|
||||
const params: Params = {
|
||||
filters: values.filters,
|
||||
}
|
||||
const hashParams: HashParams = {
|
||||
...router.values.hashParams,
|
||||
}
|
||||
if (!values.selectedRecordingId) {
|
||||
delete hashParams.sessionRecordingId
|
||||
} else {
|
||||
hashParams.sessionRecordingId = values.selectedRecordingId
|
||||
const params: Params = objectClean({
|
||||
filters: values.customFilters ?? undefined,
|
||||
sessionRecordingId: values.selectedRecordingId ?? undefined,
|
||||
})
|
||||
|
||||
// We used to have sessionRecordingId in the hash, so we keep it there for backwards compatibility
|
||||
if (router.values.hashParams.sessionRecordingId) {
|
||||
delete router.values.hashParams.sessionRecordingId
|
||||
}
|
||||
|
||||
return [router.values.location.pathname, params, hashParams, { replace }]
|
||||
return [router.values.location.pathname, params, router.values.hashParams, { replace }]
|
||||
}
|
||||
|
||||
return {
|
||||
getSessionRecordings: () => buildURL(true),
|
||||
setSelectedRecordingId: () => buildURL(false),
|
||||
setFilters: () => buildURL(true),
|
||||
resetFilters: () => buildURL(true),
|
||||
}
|
||||
}),
|
||||
|
||||
urlToAction(({ actions, values, props }) => {
|
||||
const urlToAction = (_: any, params: Params, hashParams: HashParams): void => {
|
||||
const urlToAction = (_: any, params: Params, hashParams: Params): void => {
|
||||
if (!props.updateSearchParams) {
|
||||
return
|
||||
}
|
||||
|
||||
const nulledSessionRecordingId = hashParams.sessionRecordingId ?? null
|
||||
// We changed to have the sessionRecordingId in the query params, but it used to be in the hash so backwards compatibility
|
||||
const nulledSessionRecordingId = params.sessionRecordingId ?? hashParams.sessionRecordingId ?? null
|
||||
if (nulledSessionRecordingId !== values.selectedRecordingId) {
|
||||
actions.setSelectedRecordingId(nulledSessionRecordingId)
|
||||
}
|
||||
|
||||
if (params.filters) {
|
||||
if (!equal(params.filters, values.filters)) {
|
||||
actions.replaceFilters(params.filters)
|
||||
if (!equal(params.filters, values.customFilters)) {
|
||||
actions.setFilters(params.filters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -520,12 +460,8 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
|
||||
}),
|
||||
|
||||
// NOTE: It is important this comes after urlToAction, as it will override the default behavior
|
||||
afterMount(({ actions, values }) => {
|
||||
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
|
||||
actions.loadSessionRecordings()
|
||||
} else {
|
||||
actions.getSessionRecordings()
|
||||
}
|
||||
afterMount(({ actions }) => {
|
||||
actions.loadSessionRecordings()
|
||||
actions.loadPinnedRecordings()
|
||||
}),
|
||||
])
|
||||
|
@ -694,7 +694,6 @@ export interface RecordingFilters {
|
||||
events?: FilterType['events']
|
||||
actions?: FilterType['actions']
|
||||
properties?: AnyPropertyFilter[]
|
||||
offset?: number
|
||||
session_recording_duration?: RecordingDurationFilter
|
||||
duration_type_filter?: DurationTypeFilter
|
||||
console_logs?: FilterableLogLevel[]
|
||||
|
@ -20,6 +20,7 @@
|
||||
"copy-scripts": "mkdir -p frontend/dist/ && ./bin/copy-posthog-js",
|
||||
"test": "pnpm test:unit && pnpm test:visual-regression",
|
||||
"test:unit": "jest --testPathPattern=frontend/",
|
||||
"jest": "jest",
|
||||
"test:visual-regression": "docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:legacy:docker && pnpm test:visual-regression:stories:docker",
|
||||
"test:visual-regression:legacy": "docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:legacy:docker",
|
||||
"test:visual-regression:legacy:docker": "STORYBOOK_URL=http://host.docker.internal:6006 playwright test -u",
|
||||
|
Loading…
Reference in New Issue
Block a user