Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reworked Playlist UI with Notebook support #17802

Merged
merged 37 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
523f98d
Started rework
benjackwhite Oct 5, 2023
cc0f979
Removed old params
benjackwhite Oct 5, 2023
23ce6b1
Tidying
benjackwhite Oct 5, 2023
6cd921b
Fixed styles
benjackwhite Oct 5, 2023
740e408
More tidying
benjackwhite Oct 5, 2023
d96130a
Fix
benjackwhite Oct 5, 2023
66e4e3c
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 5, 2023
cde535f
Fixed up movement within logic
benjackwhite Oct 5, 2023
ea92184
Merge branches 'feat/reworked-playlist-uo' and 'feat/reworked-playlis…
benjackwhite Oct 5, 2023
c88df18
Fixed up next session
benjackwhite Oct 5, 2023
1f5a7b7
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 5, 2023
ec0dbf1
Fix up editing
benjackwhite Oct 5, 2023
47fb9e6
Fix up logic
benjackwhite Oct 5, 2023
871cf17
Merge branch 'master' into feat/reworked-playlist-uo
benjackwhite Oct 5, 2023
d87d17d
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 5, 2023
f506857
More style changes
benjackwhite Oct 5, 2023
96d1bac
Merge branch 'feat/reworked-playlist-uo' of github.com:PostHog/postho…
benjackwhite Oct 5, 2023
1d5b75a
Simplify logics
benjackwhite Oct 5, 2023
b4dc199
Fix tests
benjackwhite Oct 5, 2023
36f1ba8
New and improved preview item
benjackwhite Oct 5, 2023
b190423
Fix up for loading IDs
benjackwhite Oct 6, 2023
4f2ac65
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 6, 2023
e1aa5ef
Fixed up loading logic
benjackwhite Oct 9, 2023
036b91c
Merge branch 'feat/reworked-playlist-uo' of github.com:PostHog/postho…
benjackwhite Oct 9, 2023
6f957f8
Added divider
benjackwhite Oct 9, 2023
64170d5
Merge branch 'master' into feat/reworked-playlist-uo
benjackwhite Oct 10, 2023
4fb1061
Updated position of tzlabel
benjackwhite Oct 10, 2023
5da360e
Fixed up editing title
benjackwhite Oct 10, 2023
d1e4f5f
Added tests for hook
benjackwhite Oct 10, 2023
ad420a7
Fixed up typing
benjackwhite Oct 10, 2023
052e9b9
Removed expanded state
benjackwhite Oct 10, 2023
73e0392
Removed optimisation step that was overall still really slow
benjackwhite Oct 10, 2023
88e0148
Removed todo
benjackwhite Oct 10, 2023
bac13cd
Remove comments
benjackwhite Oct 10, 2023
1e92096
feat: Persist Notebook pinned recordings (#17877)
benjackwhite Oct 10, 2023
142e310
Fixes
benjackwhite Oct 10, 2023
f7b527c
Fixes
benjackwhite Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading