Skip to content

Commit

Permalink
feat: Reworked Playlist UI with Notebook support (#17802)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Oct 10, 2023
1 parent be56e9f commit 03093a8
Show file tree
Hide file tree
Showing 58 changed files with 2,204 additions and 2,426 deletions.
3 changes: 1 addition & 2 deletions cypress/fixtures/api/session-recordings/recording.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@
"created_at": "2023-07-11T14:21:33.883000Z",
"uuid": "01894554-925a-0000-11d9-a44d69b426d7"
},
"storage": "clickhouse",
"pinned_count": 0
"storage": "object_storage"
}
3 changes: 0 additions & 3 deletions ee/session_recordings/test/test_session_recording_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ def test_get_pinned_recordings_for_playlist(self):
).json()
assert len(result["results"]) == 2
assert {x["id"] for x in result["results"]} == {session_one, session_two}
assert {x["pinned_count"] for x in result["results"]} == {1, 1}

@patch("ee.session_recordings.session_recording_extensions.object_storage.list_objects")
@patch("ee.session_recordings.session_recording_extensions.object_storage.copy_objects")
Expand Down Expand Up @@ -313,11 +312,9 @@ def test_add_remove_static_playlist_items(self):

session_recording_obj_1 = SessionRecording.get_or_build(team=self.team, session_id=recording1_session_id)
assert session_recording_obj_1
assert session_recording_obj_1.pinned_count == 1

session_recording_obj_2 = SessionRecording.get_or_build(team=self.team, session_id=recording2_session_id)
assert session_recording_obj_2
assert session_recording_obj_2.pinned_count == 2

# Delete playlist items
result = self.client.delete(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 13 additions & 6 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1293,14 +1293,21 @@ const api = {
},

recordings: {
async list(params: string): Promise<SessionRecordingsResponse> {
return await new ApiRequest().recordings().withQueryString(params).get()
async list(params: Record<string, any>): Promise<SessionRecordingsResponse> {
return await new ApiRequest().recordings().withQueryString(toParams(params)).get()
},
async getMatchingEvents(params: string): Promise<{ results: string[] }> {
return await new ApiRequest().recordingMatchingEvents().withQueryString(params).get()
},
async get(recordingId: SessionRecordingType['id'], params: string): Promise<SessionRecordingType> {
return await new ApiRequest().recording(recordingId).withQueryString(params).get()
async get(
recordingId: SessionRecordingType['id'],
params: Record<string, any> = {}
): Promise<SessionRecordingType> {
return await new ApiRequest().recording(recordingId).withQueryString(toParams(params)).get()
},

async persist(recordingId: SessionRecordingType['id']): Promise<{ success: boolean }> {
return await new ApiRequest().recording(recordingId).withAction('persist').create()
},

async delete(recordingId: SessionRecordingType['id']): Promise<{ success: boolean }> {
Expand Down Expand Up @@ -1360,12 +1367,12 @@ const api = {

async listPlaylistRecordings(
playlistId: SessionRecordingPlaylistType['short_id'],
params: string
params: Record<string, any> = {}
): Promise<SessionRecordingsResponse> {
return await new ApiRequest()
.recordingPlaylist(playlistId)
.withAction('recordings')
.withQueryString(params)
.withQueryString(toParams(params))
.get()
},

Expand Down
14 changes: 3 additions & 11 deletions frontend/src/lib/components/PropertyIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import clsx from 'clsx'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { countryCodeToFlag } from 'scenes/insights/views/WorldMap'
import { ReactNode } from 'react'
import { HTMLAttributes, ReactNode } from 'react'

export const PROPERTIES_ICON_MAP = {
$browser: {
Expand Down Expand Up @@ -61,7 +61,7 @@ interface PropertyIconProps {
value?: string
className?: string
noTooltip?: boolean
onClick?: (property: string, value?: string) => void
onClick?: HTMLAttributes<HTMLDivElement>['onClick']
tooltipTitle?: (property: string, value?: string) => ReactNode // Tooltip title will default to `value`
}

Expand All @@ -87,15 +87,7 @@ export function PropertyIcon({
}

const content = (
<div
onClick={(e) => {
if (onClick) {
e.stopPropagation()
onClick(property, value)
}
}}
className={clsx('inline-flex items-center', className)}
>
<div onClick={onClick} className={clsx('inline-flex items-center', className)}>
{icon}
</div>
)
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/lib/components/TZLabel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { teamLogic } from '../../../scenes/teamLogic'
import { dayjs } from 'lib/dayjs'
import clsx from 'clsx'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { LemonButton, LemonDivider, LemonDropdown } from '@posthog/lemon-ui'
import { LemonButton, LemonDivider, LemonDropdown, LemonDropdownProps } from '@posthog/lemon-ui'
import { IconSettings } from 'lib/lemon-ui/icons'
import { urls } from 'scenes/urls'

const BASE_OUTPUT_FORMAT = 'ddd, MMM D, YYYY h:mm A'

interface TZLabelRawProps {
export type TZLabelProps = Omit<LemonDropdownProps, 'overlay' | 'trigger' | 'children'> & {
time: string | dayjs.Dayjs
showSeconds?: boolean
formatDate?: string
Expand All @@ -26,7 +26,7 @@ interface TZLabelRawProps {
const TZLabelPopoverContent = React.memo(function TZLabelPopoverContent({
showSeconds,
time,
}: Pick<TZLabelRawProps, 'showSeconds'> & { time: dayjs.Dayjs }): JSX.Element {
}: Pick<TZLabelProps, 'showSeconds'> & { time: dayjs.Dayjs }): JSX.Element {
const DATE_OUTPUT_FORMAT = !showSeconds ? BASE_OUTPUT_FORMAT : `${BASE_OUTPUT_FORMAT}:ss`
const { currentTeam } = useValues(teamLogic)
const { reportTimezoneComponentViewed } = useActions(eventUsageLogic)
Expand Down Expand Up @@ -86,7 +86,8 @@ function TZLabelRaw({
showPopover = true,
noStyles = false,
className,
}: TZLabelRawProps): JSX.Element {
...dropdownProps
}: TZLabelProps): JSX.Element {
const parsedTime = useMemo(() => (dayjs.isDayjs(time) ? time : dayjs(time)), [time])

const format = useCallback(() => {
Expand Down Expand Up @@ -120,9 +121,10 @@ function TZLabelRaw({
if (showPopover) {
return (
<LemonDropdown
trigger="hover"
placement="top"
showArrow
{...dropdownProps}
trigger="hover"
overlay={<TZLabelPopoverContent time={parsedTime} showSeconds={showSeconds} />}
>
{innerContent}
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ function NodeWrapper<T extends CustomNotebookNodeAttributes>({
}: NodeWrapperProps<T> & NotebookNodeViewProps<T>): JSX.Element {
const mountedNotebookLogic = useMountedLogic(notebookLogic)
const { isEditable, editingNodeId } = useValues(notebookLogic)
const { setEditingNodeId } = useActions(notebookLogic)

// nodeId can start null, but should then immediately be generated
const nodeId = attributes.nodeId
Expand All @@ -93,10 +92,11 @@ function NodeWrapper<T extends CustomNotebookNodeAttributes>({
resizeable: resizeableOrGenerator,
settings,
startExpanded,
defaultTitle,
}
const nodeLogic = useMountedLogic(notebookNodeLogic(nodeLogicProps))
const { resizeable, expanded, actions } = useValues(nodeLogic)
const { setExpanded, deleteNode } = useActions(nodeLogic)
const { setExpanded, deleteNode, toggleEditing } = useActions(nodeLogic)

const [ref, inView] = useInView({ triggerOnce: true })
const contentRef = useRef<HTMLDivElement | null>(null)
Expand Down Expand Up @@ -185,11 +185,7 @@ function NodeWrapper<T extends CustomNotebookNodeAttributes>({
<>
{settings ? (
<LemonButton
onClick={() =>
setEditingNodeId(
editingNodeId === nodeId ? null : nodeId
)
}
onClick={() => toggleEditing()}
size="small"
icon={<IconFilter />}
active={editingNodeId === nodeId}
Expand Down
82 changes: 37 additions & 45 deletions frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper'
import { FilterType, NotebookNodeType, RecordingFilters } from '~/types'
import {
RecordingsLists,
SessionRecordingsPlaylistProps,
} from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist'
import {
SessionRecordingPlaylistLogicProps,
addedAdvancedFilters,
getDefaultFilters,
sessionRecordingsListLogic,
} from 'scenes/session-recordings/playlist/sessionRecordingsListLogic'
sessionRecordingsPlaylistLogic,
} from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic'
import { useActions, useValues } from 'kea'
import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer'
import { useEffect, useMemo, useState } from 'react'
import { fromParamsGivenUrl } from 'lib/utils'
import { LemonButton } from '@posthog/lemon-ui'
import { IconChevronLeft } from 'lib/lemon-ui/icons'
import { urls } from 'scenes/urls'
import { notebookNodeLogic } from './notebookNodeLogic'
import { JSONContent, NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils'
import { SessionRecordingsFilters } from 'scenes/session-recordings/filters/SessionRecordingsFilters'
import { ErrorBoundary } from '@sentry/react'
import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist'
import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
import { IconComment } from 'lib/lemon-ui/icons'

const Component = (props: NotebookNodeViewProps<NotebookNodePlaylistAttributes>): JSX.Element => {
const { filters, nodeId } = props.attributes
const { filters, pinned, nodeId } = props.attributes
const playerKey = `notebook-${nodeId}`

const recordingPlaylistLogicProps: SessionRecordingsPlaylistProps = useMemo(
const recordingPlaylistLogicProps: SessionRecordingPlaylistLogicProps = useMemo(
() => ({
logicKey: playerKey,
filters,
Expand All @@ -37,24 +33,31 @@ const Component = (props: NotebookNodeViewProps<NotebookNodePlaylistAttributes>)
filters: newFilters,
})
},
pinnedRecordings: pinned,
onPinnedChange(recording, isPinned) {
props.updateAttributes({
pinned: isPinned
? [...(pinned || []), String(recording.id)]
: pinned?.filter((id) => id !== recording.id),
})
},
}),
[playerKey, filters]
[playerKey, filters, pinned]
)

const { expanded } = useValues(notebookNodeLogic)
const { setActions, insertAfter, setMessageListeners, scrollIntoView } = useActions(notebookNodeLogic)
const { setActions, insertAfter, insertReplayCommentByTimestamp, setMessageListeners, scrollIntoView } =
useActions(notebookNodeLogic)

const logic = sessionRecordingsListLogic(recordingPlaylistLogicProps)
const { activeSessionRecording, nextSessionRecording, matchingEventsMatchType, sessionRecordings } =
useValues(logic)
const logic = sessionRecordingsPlaylistLogic(recordingPlaylistLogicProps)
const { activeSessionRecording } = useValues(logic)
const { setSelectedRecordingId } = useActions(logic)

useEffect(() => {
setActions(
activeSessionRecording
? [
{
text: 'Pin replay',
text: 'View replay',
onClick: () => {
insertAfter({
type: NotebookNodeType.Recording,
Expand All @@ -64,6 +67,15 @@ const Component = (props: NotebookNodeViewProps<NotebookNodePlaylistAttributes>)
})
},
},
{
text: 'Comment',
icon: <IconComment />,
onClick: () => {
if (activeSessionRecording.id) {
insertReplayCommentByTimestamp(0, activeSessionRecording.id)
}
},
},
]
: []
)
Expand All @@ -72,6 +84,7 @@ const Component = (props: NotebookNodeViewProps<NotebookNodePlaylistAttributes>)
useEffect(() => {
setMessageListeners({
'play-replay': ({ sessionRecordingId, time }) => {
// IDEA: We could add the desired start time here as a param, which is picked up by the player...
setSelectedRecordingId(sessionRecordingId)
scrollIntoView()

Expand All @@ -83,32 +96,7 @@ const Component = (props: NotebookNodeViewProps<NotebookNodePlaylistAttributes>)
})
}, [])

if (!expanded) {
return <div className="p-4">{sessionRecordings.length}+ recordings </div>
}

const content = !activeSessionRecording?.id ? (
<RecordingsLists {...recordingPlaylistLogicProps} />
) : (
<>
<LemonButton
size="small"
type="secondary"
icon={<IconChevronLeft />}
onClick={() => setSelectedRecordingId(null)}
className="self-start"
/>
<SessionRecordingPlayer
playerKey={playerKey}
sessionRecordingId={activeSessionRecording.id}
recordingStartTime={activeSessionRecording ? activeSessionRecording.start_time : undefined}
nextSessionRecording={nextSessionRecording}
matchingEventsMatchType={matchingEventsMatchType}
/>
</>
)

return <div className="flex flex-row overflow-hidden gap-2 h-full">{content}</div>
return <SessionRecordingsPlaylist {...recordingPlaylistLogicProps} />
}

export const Settings = ({
Expand Down Expand Up @@ -141,6 +129,7 @@ export const Settings = ({

type NotebookNodePlaylistAttributes = {
filters: RecordingFilters
pinned?: string[]
}

export const NotebookNodePlaylist = createPostHogWidgetNode<NotebookNodePlaylistAttributes>({
Expand All @@ -153,11 +142,14 @@ export const NotebookNodePlaylist = createPostHogWidgetNode<NotebookNodePlaylist
return urls.replay(undefined, attrs.filters)
},
resizeable: true,
startExpanded: true,
expandable: false,
attributes: {
filters: {
default: undefined,
},
pinned: {
default: undefined,
},
},
pasteOptions: {
find: urls.replay() + '(.+)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const Component = (props: NotebookNodeViewProps<NotebookNodeRecordingAttributes>
return !expanded ? (
<div>
{sessionPlayerMetaData ? (
<SessionRecordingPreview recording={sessionPlayerMetaData} recordingPropertiesLoading={false} />
<SessionRecordingPreview recording={sessionPlayerMetaData} />
) : (
<SessionRecordingPreviewSkeleton />
)}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type NotebookNodeLogicProps = {
settings: NotebookNodeSettings
messageListeners?: NotebookNodeMessagesListeners
startExpanded: boolean
defaultTitle: string
} & NotebookNodeAttributeProperties<any>

const computeResizeable = (
Expand All @@ -65,6 +66,7 @@ export const notebookNodeLogic = kea<notebookNodeLogicType>([
setNextNode: (node: Node | null) => ({ node }),
deleteNode: true,
selectNode: true,
toggleEditing: true,
scrollIntoView: true,
setMessageListeners: (listeners: NotebookNodeMessagesListeners) => ({ listeners }),
}),
Expand Down Expand Up @@ -117,6 +119,7 @@ export const notebookNodeLogic = kea<notebookNodeLogicType>([
notebookLogic: [(_, p) => [p.notebookLogic], (notebookLogic) => notebookLogic],
nodeAttributes: [(_, p) => [p.attributes], (nodeAttributes) => nodeAttributes],
settings: [(_, p) => [p.settings], (settings) => settings],
defaultTitle: [(_, p) => [p.defaultTitle], (title) => title],

sendMessage: [
(s) => [s.messageListeners],
Expand Down Expand Up @@ -200,6 +203,11 @@ export const notebookNodeLogic = kea<notebookNodeLogicType>([
updateAttributes: ({ attributes }) => {
props.updateAttributes(attributes)
},
toggleEditing: () => {
props.notebookLogic.actions.setEditingNodeId(
props.notebookLogic.values.editingNodeId === props.nodeId ? null : props.nodeId
)
},
})),

afterMount(async (logic) => {
Expand Down
Loading

0 comments on commit 03093a8

Please sign in to comment.