diff --git a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx index 1fa999f94dfe7..6f11aadaa509c 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx +++ b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx @@ -14,7 +14,7 @@ type KeyboardShortcut = Array export interface LemonMenuItemBase extends Pick< LemonButtonProps, - 'icon' | 'sideIcon' | 'sideAction' | 'disabledReason' | 'tooltip' | 'active' | 'status' | 'data-attr' + 'icon' | 'sideIcon' | 'sideAction' | 'disabledReason' | 'tooltip' | 'active' | 'status' | 'data-attr' | 'size' > { label: string | JSX.Element key?: React.Key diff --git a/frontend/src/scenes/billing/billingLogic.tsx b/frontend/src/scenes/billing/billingLogic.tsx index d6f0fa86493a1..d3fb4cd7f3c1c 100644 --- a/frontend/src/scenes/billing/billingLogic.tsx +++ b/frontend/src/scenes/billing/billingLogic.tsx @@ -227,6 +227,7 @@ export const billingLogic = kea([ try { const response = await api.update('api/billing', { custom_limits_usd: limits }) lemonToast.success('Billing limits updated') + actions.loadBilling() return parseBillingResponse(response) } catch (error: any) { lemonToast.error( diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index 80cfc119dffb4..d483b0b3d7206 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -322,7 +322,6 @@ export const billingProductLogic = kea([ listeners(({ actions, values, props }) => ({ updateBillingLimitsSuccess: () => { actions.billingLoaded() - actions.loadBilling() }, billingLoaded: () => { function calculateDefaultBillingLimit(product: BillingProductV2Type | BillingProductV2AddonType): number { diff --git a/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx b/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx index bb7234e5a76bc..da8937897215a 100644 --- a/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx +++ b/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx @@ -436,11 +436,7 @@ const Content = ({ return responseLoading ? (
- +
) : !response ? (
diff --git a/frontend/src/scenes/session-recordings/components/PanelSettings.scss b/frontend/src/scenes/session-recordings/components/PanelSettings.scss new file mode 100644 index 0000000000000..9a200c008bcc4 --- /dev/null +++ b/frontend/src/scenes/session-recordings/components/PanelSettings.scss @@ -0,0 +1,11 @@ +.SettingsBar--button--square { + .LemonButtonWithSideAction__side-button { + top: 0; + bottom: 0; + border-radius: 0; + } + + .LemonButton { + border-radius: 0; + } +} diff --git a/frontend/src/scenes/session-recordings/components/PanelSettings.tsx b/frontend/src/scenes/session-recordings/components/PanelSettings.tsx index d00dc93692fe3..5187fe96b49c4 100644 --- a/frontend/src/scenes/session-recordings/components/PanelSettings.tsx +++ b/frontend/src/scenes/session-recordings/components/PanelSettings.tsx @@ -1,4 +1,7 @@ +import './PanelSettings.scss' + import clsx from 'clsx' +import { FloatingContainerContext } from 'lib/hooks/useFloatingContainerContext' import { LemonButton, LemonButtonWithoutSideActionProps, @@ -6,7 +9,7 @@ import { } from 'lib/lemon-ui/LemonButton' import { LemonMenu, LemonMenuItem, LemonMenuProps } from 'lib/lemon-ui/LemonMenu/LemonMenu' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { PropsWithChildren } from 'react' +import { PropsWithChildren, useRef } from 'react' /** * TODO the lemon button font only has 700 and 800 weights available. @@ -37,19 +40,23 @@ export function SettingsBar({ border: 'bottom' | 'top' | 'all' | 'none' className?: string }>): JSX.Element { + const containerRef = useRef(null) return ( -
- {children} -
+ +
+ {children} +
+
) } @@ -116,5 +123,9 @@ export function SettingsToggle({ title, icon, label, active, rounded, ...props } ) // otherwise the tooltip shows instead of the disabled reason - return props.disabledReason ? button : {button} + return ( +
+ {props.disabledReason ? button : {button}} +
+ ) } diff --git a/frontend/src/scenes/session-recordings/components/SimpleTimeLabel.tsx b/frontend/src/scenes/session-recordings/components/SimpleTimeLabel.tsx index eee0928044503..3028a67a8c80c 100644 --- a/frontend/src/scenes/session-recordings/components/SimpleTimeLabel.tsx +++ b/frontend/src/scenes/session-recordings/components/SimpleTimeLabel.tsx @@ -1,15 +1,17 @@ import clsx from 'clsx' import { Dayjs, dayjs } from 'lib/dayjs' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { shortTimeZone } from 'lib/utils' import { memo } from 'react' import { TimestampFormat } from 'scenes/session-recordings/player/playerSettingsLogic' function formattedReplayTime( time: string | number | Dayjs | null | undefined, - timestampFormat: TimestampFormat + timestampFormat: TimestampFormat, + timeOnly?: boolean ): string { if (time == null) { - return '--/--/----, 00:00:00' + return timeOnly ? '00:00:00' : '--/--/----, 00:00:00' } let d = dayjs(time) @@ -17,12 +19,16 @@ function formattedReplayTime( if (isUTC) { d = d.tz('UTC') } - const formatted = d.format(formatStringFor(d)) + const formatted = d.format(formatStringFor(d, timeOnly)) const timezone = isUTC ? 'UTC' : shortTimeZone(undefined, d.toDate()) return `${formatted} ${timezone}` } -function formatStringFor(d: Dayjs): string { +function formatStringFor(d: Dayjs, timeOnly?: boolean): string { + if (timeOnly) { + return 'HH:mm:ss' + } + const today = dayjs() if (d.isSame(today, 'year')) { return 'DD/MMM, HH:mm:ss' @@ -46,12 +52,15 @@ export function _SimpleTimeLabel({ timestampFormat, muted = true, size = 'xsmall', + containerSize, }: { startTime: string | number | Dayjs | undefined timestampFormat: TimestampFormat muted?: boolean size?: 'small' | 'xsmall' + containerSize?: 'small' | 'normal' }): JSX.Element { + const formattedTime = formattedReplayTime(startTime, timestampFormat, containerSize === 'small') return (
- {formattedReplayTime(startTime, timestampFormat)} + {containerSize === 'small' ? ( + {formattedTime} + ) : ( + formattedTime + )}
) } @@ -80,7 +93,8 @@ export const SimpleTimeLabel = memo( prevStartTimeTruncated === nextStartTimeTruncated && prevProps.timestampFormat === nextProps.timestampFormat && prevProps.muted === nextProps.muted && - prevProps.size === nextProps.size + prevProps.size === nextProps.size && + prevProps.containerSize === nextProps.containerSize ) } ) diff --git a/frontend/src/scenes/session-recordings/player/PlayerInspector.tsx b/frontend/src/scenes/session-recordings/player/PlayerInspector.tsx deleted file mode 100644 index 1a5c03548e1bb..0000000000000 --- a/frontend/src/scenes/session-recordings/player/PlayerInspector.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @fileoverview PlayerInspector component is a button that opens the inspector sidebar - */ -import { LemonButton } from '@posthog/lemon-ui' -import { useActions } from 'kea' -import { IconUnverifiedEvent } from 'lib/lemon-ui/icons' - -import { SessionRecordingSidebarTab } from '~/types' - -import { playerSettingsLogic } from './playerSettingsLogic' -import { playerSidebarLogic } from './sidebar/playerSidebarLogic' - -export function PlayerInspector(): JSX.Element { - const { setTab } = useActions(playerSidebarLogic) - const { setSidebarOpen } = useActions(playerSettingsLogic) - - const handleClick = (): void => { - setSidebarOpen(true) - setTab(SessionRecordingSidebarTab.INSPECTOR) - } - - return ( - } - onClick={handleClick} - > - Activity - - ) -} diff --git a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx index 4bd1a27e61660..63ae34a5cc4ce 100644 --- a/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx +++ b/frontend/src/scenes/session-recordings/player/SessionRecordingPlayer.tsx @@ -16,9 +16,9 @@ import { urls } from 'scenes/urls' import { NetworkView } from '../apm/NetworkView' import { PlayerController } from './controller/PlayerController' +import { PlayerMeta } from './player-meta/PlayerMeta' import { PlayerFrame } from './PlayerFrame' import { PlayerFrameOverlay } from './PlayerFrameOverlay' -import { PlayerMeta } from './PlayerMeta' import { PlaybackMode, playerSettingsLogic } from './playerSettingsLogic' import { PlayerSidebar } from './PlayerSidebar' import { sessionRecordingDataLogic } from './sessionRecordingDataLogic' @@ -146,15 +146,6 @@ export function SessionRecordingPlayer(props: SessionRecordingPlayerProps): JSX. ref: playerRef, } ) - const { size: playerMainSize } = useResizeBreakpoints( - { - 0: 'small', - 750: 'medium', - }, - { - ref: playerMainRef, - } - ) const { draggable, elementProps } = useNotebookDrag({ href: urls.replaySingle(sessionRecordingId) }) @@ -210,9 +201,7 @@ export function SessionRecordingPlayer(props: SessionRecordingPlayerProps): JSX.
{playbackMode === PlaybackMode.Recording ? ( <> - {!noMeta || isFullScreen ? ( - - ) : null} + {!noMeta || isFullScreen ? : null}
- ) : ( - - ) - } - data-attr="session-recording-speed-select" - items={PLAYBACK_SPEEDS.map((speedToggle) => ({ - label: ( -
- {speedToggle}x - ({humanFriendlyDuration(sessionPlayerData.durationMs / speedToggle / 1000)}) -
- ), - onClick: () => setSpeed(speedToggle), - active: speed === speedToggle && speedToggle !== 1, - status: speed === speedToggle ? 'danger' : 'default', - }))} - label={`Speed ${speed}x`} - /> - ) -} - function PlayPauseButton(): JSX.Element { const { playingState, endReached } = useValues(sessionRecordingPlayerLogic) const { togglePlayPause } = useActions(sessionRecordingPlayerLogic) @@ -95,109 +42,6 @@ function PlayPauseButton(): JSX.Element { ) } -function ShowMouseTail(): JSX.Element { - const { showMouseTail } = useValues(playerSettingsLogic) - const { setShowMouseTail } = useActions(playerSettingsLogic) - - return ( - setShowMouseTail(!showMouseTail)} - icon={} - /> - ) -} - -function SkipInactivity(): JSX.Element { - const { skipInactivitySetting } = useValues(playerSettingsLogic) - const { setSkipInactivitySetting } = useActions(playerSettingsLogic) - - return ( - setSkipInactivitySetting(!skipInactivitySetting)} - icon={} - /> - ) -} - -function SetTimeFormat(): JSX.Element { - const { timestampFormat } = useValues(playerSettingsLogic) - const { setTimestampFormat } = useActions(playerSettingsLogic) - - return ( - setTimestampFormat(TimestampFormat.UTC), - active: timestampFormat === TimestampFormat.UTC, - }, - { - label: 'Device', - onClick: () => setTimestampFormat(TimestampFormat.Device), - active: timestampFormat === TimestampFormat.Device, - }, - { - label: 'Relative', - onClick: () => setTimestampFormat(TimestampFormat.Relative), - active: timestampFormat === TimestampFormat.Relative, - }, - ]} - icon={} - label={TimestampFormatToLabel[timestampFormat]} - /> - ) -} - -function InspectDOM(): JSX.Element { - const { sessionPlayerMetaData } = useValues(sessionRecordingPlayerLogic) - const { openExplorer } = useActions(sessionRecordingPlayerLogic) - - return ( - openExplorer()} - disabledReason={ - sessionPlayerMetaData?.snapshot_source === 'web' ? undefined : 'Only available for web recordings' - } - icon={} - /> - ) -} - -export function PlayerBottomSettings(): JSX.Element { - const { - logicProps: { noInspector }, - } = useValues(sessionRecordingPlayerLogic) - - return ( - -
-
- - - - -
-
- {noInspector ? null : } - -
-
-
- ) -} - function FullScreen(): JSX.Element { const { isFullScreen } = useValues(sessionRecordingPlayerLogic) const { setIsFullScreen } = useActions(sessionRecordingPlayerLogic) @@ -216,49 +60,20 @@ function FullScreen(): JSX.Element { ) } -function Maximise(): JSX.Element { - const { sidebarOpen, playlistOpen } = useValues(playerSettingsLogic) - const { setSidebarOpen, setPlaylistOpen } = useActions(playerSettingsLogic) - - const isMaximised = !sidebarOpen && !playlistOpen - - function onChangeMaximise(): void { - setPlaylistOpen(isMaximised) - setSidebarOpen(isMaximised) - } - - useKeyboardHotkeys( - { - m: { - action: onChangeMaximise, - }, - }, - [] - ) - - return ( - - {isMaximised ? 'Open' : 'Close'} other panels - - } - icon={isMaximised ? : } - /> - ) -} - export function PlayerController(): JSX.Element { const { playlistLogic } = useValues(sessionRecordingPlayerLogic) + const { ref, size } = useResizeBreakpoints({ + 0: 'small', + 600: 'normal', + }) + return (
-
+
- +
@@ -267,7 +82,6 @@ export function PlayerController(): JSX.Element {
{playlistLogic ? : undefined} -
diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx index 13c5682ba50cf..72c700b3918c0 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerControllerTime.tsx @@ -13,7 +13,7 @@ import { HotKeyOrModifier } from '~/types' import { playerSettingsLogic, TimestampFormat } from '../playerSettingsLogic' import { seekbarLogic } from './seekbarLogic' -function RelativeTimestampLabel(): JSX.Element { +function RelativeTimestampLabel({ size }: { size: 'small' | 'normal' }): JSX.Element { const { logicProps, currentPlayerTime, sessionPlayerData } = useValues(sessionRecordingPlayerLogic) const { isScrubbing, scrubbingTime } = useValues(seekbarLogic(logicProps)) @@ -22,16 +22,25 @@ function RelativeTimestampLabel(): JSX.Element { const fixedUnits = endTimeSeconds > 3600 ? 3 : 2 - return ( + const current = colonDelimitedDuration(startTimeSeconds, fixedUnits) + const total = colonDelimitedDuration(endTimeSeconds, fixedUnits) + const fullDisplay = (
- {colonDelimitedDuration(startTimeSeconds, fixedUnits)} + {current} / - {colonDelimitedDuration(endTimeSeconds, fixedUnits)} + {total}
) + return size === 'small' ? ( + + {current} + + ) : ( + {fullDisplay} + ) } -export function Timestamp(): JSX.Element { +export function Timestamp({ size }: { size: 'small' | 'normal' }): JSX.Element { const { logicProps, currentTimestamp, sessionPlayerData } = useValues(sessionRecordingPlayerLogic) const { isScrubbing, scrubbingTime } = useValues(seekbarLogic(logicProps)) const { timestampFormat } = useValues(playerSettingsLogic) @@ -43,11 +52,12 @@ export function Timestamp(): JSX.Element { return (
{timestampFormat === TimestampFormat.Relative ? ( - + ) : ( )}
diff --git a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx index 2050b55080fff..28f97696c0543 100644 --- a/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx +++ b/frontend/src/scenes/session-recordings/player/modal/SessionPlayerModal.tsx @@ -2,7 +2,7 @@ import { LemonModal } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' -import { PlayerMeta } from '../PlayerMeta' +import { PlayerMeta } from '../player-meta/PlayerMeta' import { sessionRecordingPlayerLogic, SessionRecordingPlayerLogicProps } from '../sessionRecordingPlayerLogic' import { sessionPlayerModalLogic } from './sessionPlayerModalLogic' @@ -54,7 +54,7 @@ export function SessionPlayerModal(): JSX.Element | null {
{activeSessionRecording ? ( - + ) : null}
diff --git a/frontend/src/scenes/session-recordings/player/player-meta/PlayerInspectorButton.tsx b/frontend/src/scenes/session-recordings/player/player-meta/PlayerInspectorButton.tsx new file mode 100644 index 0000000000000..91f5408e6970f --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerInspectorButton.tsx @@ -0,0 +1,32 @@ +/** + * @fileoverview PlayerInspector component is a button that opens the inspector sidebar + */ +import { useActions, useValues } from 'kea' +import { IconUnverifiedEvent } from 'lib/lemon-ui/icons' +import { SettingsToggle } from 'scenes/session-recordings/components/PanelSettings' + +import { SessionRecordingSidebarTab } from '~/types' + +import { playerSettingsLogic } from '../playerSettingsLogic' +import { playerSidebarLogic } from '../sidebar/playerSidebarLogic' + +export function PlayerInspectorButton(): JSX.Element { + const { setTab } = useActions(playerSidebarLogic) + const { setSidebarOpen } = useActions(playerSettingsLogic) + const { sidebarOpen } = useValues(playerSettingsLogic) + + return ( + } + active={sidebarOpen} + onClick={(): void => { + setSidebarOpen(!sidebarOpen) + setTab(SessionRecordingSidebarTab.INSPECTOR) + }} + > + Activity + + ) +} diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.scss similarity index 95% rename from frontend/src/scenes/session-recordings/player/PlayerMeta.scss rename to frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.scss index d04d1d967706d..4b7040296f234 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.scss +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.scss @@ -44,10 +44,6 @@ } &--fullscreen { - flex-direction: row; - align-items: center; - justify-content: space-between; - .PlayerMetaPersonProperties { position: fixed; top: 48px; diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.tsx similarity index 79% rename from frontend/src/scenes/session-recordings/player/PlayerMeta.tsx rename to frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.tsx index 75cf80baf3549..c92d4dc832a46 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMeta.tsx @@ -10,16 +10,19 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' import { isObject } from 'lib/utils' import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' import { IconWindow } from 'scenes/session-recordings/player/icons' -import { PlayerMetaLinks } from 'scenes/session-recordings/player/PlayerMetaLinks' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' +import { PlayerMetaBottomSettings } from 'scenes/session-recordings/player/player-meta/PlayerMetaBottomSettings' +import { PlayerMetaLinks } from 'scenes/session-recordings/player/player-meta/PlayerMetaLinks' +import { + sessionRecordingPlayerLogic, + SessionRecordingPlayerMode, +} from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { urls } from 'scenes/urls' import { getCurrentExporterData } from '~/exporter/exporterViewLogic' import { Logo } from '~/toolbar/assets/Logo' -import { PlayerBottomSettings } from './controller/PlayerController' +import { playerMetaLogic } from './playerMetaLogic' import { PlayerPersonMeta } from './PlayerPersonMeta' -import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode } from './sessionRecordingPlayerLogic' function URLOrScreen({ lastUrl }: { lastUrl: string | undefined }): JSX.Element | null { if (isObject(lastUrl) && 'href' in lastUrl) { @@ -42,9 +45,9 @@ function URLOrScreen({ lastUrl }: { lastUrl: string | undefined }): JSX.Element } return ( - + · - + {isValidUrl ? ( @@ -67,7 +70,7 @@ function URLOrScreen({ lastUrl }: { lastUrl: string | undefined }): JSX.Element ) } -export function ResolutionView(): JSX.Element { +export function ResolutionView({ size }: { size?: PlayerMetaBreakpoints }): JSX.Element { const { logicProps } = useValues(sessionRecordingPlayerLogic) const { resolutionDisplay, scaleDisplay, loading } = useValues(playerMetaLogic(logicProps)) @@ -85,15 +88,17 @@ export function ResolutionView(): JSX.Element { } > - - {resolutionDisplay} + + {size === 'normal' && {resolutionDisplay}} ({scaleDisplay}) ) } -export function PlayerMeta({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { +export type PlayerMetaBreakpoints = 'small' | 'normal' + +export function PlayerMeta(): JSX.Element { const { logicProps, isFullScreen } = useValues(sessionRecordingPlayerLogic) const { windowIds, trackedWindow, lastPageviewEvent, lastUrl, currentWindowIndex, loading } = useValues( @@ -103,12 +108,10 @@ export function PlayerMeta({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { const { setTrackedWindow } = useActions(playerMetaLogic(logicProps)) const { ref, size } = useResizeBreakpoints({ - 0: 'compact', - 550: 'normal', + 0: 'small', + 600: 'normal', }) - const isSmallPlayer = size === 'compact' - const mode = logicProps.mode ?? SessionRecordingPlayerMode.Standard const whitelabel = getCurrentExporterData()?.whitelabel ?? false @@ -143,7 +146,7 @@ export function PlayerMeta({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { windowOptions.push({ label: , labelInMenu: ( -
+
Follow window:
), @@ -159,7 +162,7 @@ export function PlayerMeta({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { 'PlayerMeta--fullscreen': isFullScreen, })} > -
+
{loading ? ( ) : ( @@ -174,23 +177,21 @@ export function PlayerMeta({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { {lastPageviewEvent?.properties?.['$screen_name'] && ( - + · - + {lastPageviewEvent?.properties['$screen_name']} )} )} -
- - -
- -
+
+ + +
- +
) diff --git a/frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaBottomSettings.tsx b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaBottomSettings.tsx new file mode 100644 index 0000000000000..5f54990d9d6a0 --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaBottomSettings.tsx @@ -0,0 +1,193 @@ +import { IconClock, IconEllipsis, IconHourglass, IconMouse, IconRabbit, IconSearch, IconTortoise } from '@posthog/icons' +import { useActions, useValues } from 'kea' +import { LemonMenuItem } from 'lib/lemon-ui/LemonMenu' +import { humanFriendlyDuration } from 'lib/utils' +import { + SettingsBar, + SettingsButton, + SettingsMenu, + SettingsToggle, +} from 'scenes/session-recordings/components/PanelSettings' +import { PlayerInspectorButton } from 'scenes/session-recordings/player/player-meta/PlayerInspectorButton' +import { PlayerMetaBreakpoints } from 'scenes/session-recordings/player/player-meta/PlayerMeta' +import { playerSettingsLogic, TimestampFormat } from 'scenes/session-recordings/player/playerSettingsLogic' +import { + PLAYBACK_SPEEDS, + sessionRecordingPlayerLogic, +} from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' +import { TimestampFormatToLabel } from 'scenes/session-recordings/utils' + +function SetPlaybackSpeed(): JSX.Element { + const { speed, sessionPlayerData } = useValues(sessionRecordingPlayerLogic) + const { setSpeed } = useActions(sessionRecordingPlayerLogic) + return ( + + ) : ( + + ) + } + data-attr="session-recording-speed-select" + items={PLAYBACK_SPEEDS.map((speedToggle) => ({ + label: ( +
+ {speedToggle}x + ({humanFriendlyDuration(sessionPlayerData.durationMs / speedToggle / 1000)}) +
+ ), + onClick: () => setSpeed(speedToggle), + active: speed === speedToggle && speedToggle !== 1, + status: speed === speedToggle ? 'danger' : 'default', + }))} + label={`Speed ${speed}x`} + /> + ) +} + +function SkipInactivity(): JSX.Element { + const { skipInactivitySetting } = useValues(playerSettingsLogic) + const { setSkipInactivitySetting } = useActions(playerSettingsLogic) + + return ( + setSkipInactivitySetting(!skipInactivitySetting)} + icon={} + /> + ) +} + +function SetTimeFormat(): JSX.Element { + const { timestampFormat } = useValues(playerSettingsLogic) + const { setTimestampFormat } = useActions(playerSettingsLogic) + + return ( + setTimestampFormat(TimestampFormat.UTC), + active: timestampFormat === TimestampFormat.UTC, + }, + { + label: 'Device', + onClick: () => setTimestampFormat(TimestampFormat.Device), + active: timestampFormat === TimestampFormat.Device, + }, + { + label: 'Relative', + onClick: () => setTimestampFormat(TimestampFormat.Relative), + active: timestampFormat === TimestampFormat.Relative, + }, + ]} + icon={} + label={TimestampFormatToLabel[timestampFormat]} + /> + ) +} + +function InspectDOM(): JSX.Element { + const { sessionPlayerMetaData } = useValues(sessionRecordingPlayerLogic) + const { openExplorer } = useActions(sessionRecordingPlayerLogic) + + return ( + openExplorer()} + disabledReason={ + sessionPlayerMetaData?.snapshot_source === 'web' ? undefined : 'Only available for web recordings' + } + icon={} + /> + ) +} + +export function PlayerMetaBottomSettings({ size }: { size: PlayerMetaBreakpoints }): JSX.Element { + const { + logicProps: { noInspector }, + } = useValues(sessionRecordingPlayerLogic) + const { showMouseTail, skipInactivitySetting, timestampFormat } = useValues(playerSettingsLogic) + const { setShowMouseTail, setSkipInactivitySetting, setTimestampFormat } = useActions(playerSettingsLogic) + const isSmall = size === 'small' + + const menuItems: LemonMenuItem[] = [ + isSmall + ? { + label: TimestampFormatToLabel[timestampFormat], + icon: , + 'data-attr': 'time-format-in-menu', + matchWidth: true, + + items: [ + { + label: 'UTC', + onClick: () => setTimestampFormat(TimestampFormat.UTC), + active: timestampFormat === TimestampFormat.UTC, + size: 'xsmall', + }, + { + label: 'Device', + onClick: () => setTimestampFormat(TimestampFormat.Device), + active: timestampFormat === TimestampFormat.Device, + size: 'xsmall', + }, + { + label: 'Relative', + onClick: () => setTimestampFormat(TimestampFormat.Relative), + active: timestampFormat === TimestampFormat.Relative, + size: 'xsmall', + }, + ], + } + : undefined, + isSmall + ? { + label: 'Skip inactivity', + active: skipInactivitySetting, + 'data-attr': 'skip-inactivity-in-menu', + onClick: () => setSkipInactivitySetting(!skipInactivitySetting), + icon: , + } + : undefined, + { + // title: "Show a tail following the cursor to make it easier to see", + label: 'Show mouse tail', + active: showMouseTail, + 'data-attr': 'show-mouse-tail-in-menu', + onClick: () => setShowMouseTail(!showMouseTail), + icon: , + }, + ].filter(Boolean) as LemonMenuItem[] + + return ( + +
+
+ + {!isSmall && } + {!isSmall && } + + } + items={menuItems} + highlightWhenActive={false} + closeOnClickInside={false} + /> +
+
+ {noInspector ? null : } + +
+
+
+ ) +} diff --git a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaLinks.tsx similarity index 63% rename from frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx rename to frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaLinks.tsx index eaf380ff7eecd..e4033e6103df3 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerMetaLinks.tsx @@ -5,9 +5,11 @@ import { FEATURE_FLAGS } from 'lib/constants' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconComment } from 'lib/lemon-ui/icons' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { Fragment, useMemo } from 'react' +import { useMemo } from 'react' import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/sessionPlayerModalLogic' +import { PlaylistPopoverButton } from 'scenes/session-recordings/player/playlist-popover/PlaylistPopover' import { sessionRecordingPlayerLogic, SessionRecordingPlayerMode, @@ -17,16 +19,9 @@ import { personsModalLogic } from 'scenes/trends/persons-modal/personsModalLogic import { NotebookNodeType } from '~/types' -import { sessionPlayerModalLogic } from './modal/sessionPlayerModalLogic' -import { PlaylistPopoverButton } from './playlist-popover/PlaylistPopover' +import { PlayerMetaBreakpoints } from './PlayerMeta' -function PinToPlaylistButton({ - buttonContent, - ...buttonProps -}: { - buttonContent: (label: string) => JSX.Element - buttonProps?: Partial -}): JSX.Element { +function PinToPlaylistButton(): JSX.Element { const { logicProps } = useValues(sessionRecordingPlayerLogic) const { maybePersistRecording } = useActions(sessionRecordingPlayerLogic) const nodeLogic = useNotebookNode() @@ -42,7 +37,7 @@ function PinToPlaylistButton({ return logicProps.setPinned && !logicProps.pinned ? ( { if (nodeLogic) { // If we are in a node, then pinning should persist the recording @@ -60,35 +55,18 @@ function PinToPlaylistButton({ tooltip={tooltip} setPinnedInCurrentPlaylist={logicProps.setPinned} icon={logicProps.pinned ? : } - {...buttonProps} + size="xsmall" > - {buttonContent(description)} + {description} ) } -export function PlayerMetaLinks({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { +export function PlayerMetaLinks({ size }: { size: PlayerMetaBreakpoints }): JSX.Element { const { sessionRecordingId, logicProps } = useValues(sessionRecordingPlayerLogic) - const { setPause } = useActions(sessionRecordingPlayerLogic) + const mode = logicProps.mode ?? SessionRecordingPlayerMode.Standard const nodeLogic = useNotebookNode() - const { closeSessionPlayer } = useActions(sessionPlayerModalLogic()) - - const getCurrentPlayerTime = (): number => { - // NOTE: We pull this value at call time as otherwise it would trigger re-renders if pulled from the hook - const playerTime = sessionRecordingPlayerLogic.findMounted(logicProps)?.values.currentPlayerTime || 0 - return Math.floor(playerTime / 1000) - } - - const commonProps: Partial = { - size: 'xsmall', - } - - const buttonContent = (label: string): JSX.Element => { - return !iconsOnly ? {label} : - } - - const mode = logicProps.mode ?? SessionRecordingPlayerMode.Standard return (
@@ -96,42 +74,16 @@ export function PlayerMetaLinks({ iconsOnly }: { iconsOnly: boolean }): JSX.Elem <> {sessionRecordingId && (
- {mode === SessionRecordingPlayerMode.Standard && } +
)} - } - resource={{ - type: NotebookNodeType.Recording, - attrs: { id: sessionRecordingId, __init: { expanded: true } }, - }} - onClick={() => setPause()} - onNotebookOpened={(theNotebookLogic, theNodeLogic) => { - const time = getCurrentPlayerTime() * 1000 - - if (theNodeLogic) { - // Node already exists, we just add a comment - theNodeLogic.actions.insertReplayCommentByTimestamp(time, sessionRecordingId) - return - } - theNotebookLogic.actions.insertReplayCommentByTimestamp({ - timestamp: time, - sessionRecordingId, - }) - - closeSessionPlayer() - personsModalLogic.findMounted()?.actions.closeModal() - }} - > - {buttonContent('Comment')} - - - - - {nodeLogic?.props.nodeType === NotebookNodeType.RecordingPlaylist ? ( + {size === 'normal' && } + + + + {size === 'normal' && nodeLogic?.props.nodeType === NotebookNodeType.RecordingPlaylist ? ( } onClick={() => { nodeLogic.actions.insertAfter({ @@ -143,19 +95,65 @@ export function PlayerMetaLinks({ iconsOnly }: { iconsOnly: boolean }): JSX.Elem /> ) : null} - + ) : null}
) } -const MenuActions = (): JSX.Element => { +const AddToNotebookButton = ({ fullWidth = false }: Pick): JSX.Element => { + const { sessionRecordingId, logicProps } = useValues(sessionRecordingPlayerLogic) + const { setPause } = useActions(sessionRecordingPlayerLogic) + + const { closeSessionPlayer } = useActions(sessionPlayerModalLogic()) + + const getCurrentPlayerTime = (): number => { + // NOTE: We pull this value at call time as otherwise it would trigger re-renders if pulled from the hook + const playerTime = sessionRecordingPlayerLogic.findMounted(logicProps)?.values.currentPlayerTime || 0 + return Math.floor(playerTime / 1000) + } + + return ( + } + resource={{ + type: NotebookNodeType.Recording, + attrs: { id: sessionRecordingId, __init: { expanded: true } }, + }} + onClick={() => setPause()} + onNotebookOpened={(theNotebookLogic, theNodeLogic) => { + const time = getCurrentPlayerTime() * 1000 + + if (theNodeLogic) { + // Node already exists, we just add a comment + theNodeLogic.actions.insertReplayCommentByTimestamp(time, sessionRecordingId) + return + } + theNotebookLogic.actions.insertReplayCommentByTimestamp({ + timestamp: time, + sessionRecordingId, + }) + + closeSessionPlayer() + personsModalLogic.findMounted()?.actions.closeModal() + }} + > + Comment + + ) +} + +const MenuActions = ({ size }: { size: PlayerMetaBreakpoints }): JSX.Element => { const { logicProps } = useValues(sessionRecordingPlayerLogic) const { deleteRecording, setIsFullScreen, exportRecordingToFile } = useActions(sessionRecordingPlayerLogic) const hasMobileExportFlag = useFeatureFlag('SESSION_REPLAY_EXPORT_MOBILE_DATA') const hasMobileExport = window.IMPERSONATED_SESSION || hasMobileExportFlag + const isStandardMode = + (logicProps.mode ?? SessionRecordingPlayerMode.Standard) === SessionRecordingPlayerMode.Standard const onDelete = useMemo( () => () => { @@ -178,7 +176,7 @@ const MenuActions = (): JSX.Element => { const items: LemonMenuItems = useMemo(() => { const itemsArray: LemonMenuItems = [ - { + isStandardMode && { label: '.json', status: 'default', icon: , @@ -186,26 +184,33 @@ const MenuActions = (): JSX.Element => { tooltip: 'Export recording to a JSON file. This can be loaded later into PostHog for playback.', }, ] - if (hasMobileExport) { - itemsArray.push({ - label: 'DEBUG - mobile.json', - status: 'default', - icon: , - onClick: () => exportRecordingToFile(true), - tooltip: - 'DEBUG - ONLY VISIBLE TO POSTHOG STAFF - Export untransformed recording to a file. This can be loaded later into PostHog for playback.', + if (size === 'small') { + itemsArray.unshift({ + label: () => , }) } + if (hasMobileExport) { + isStandardMode && + itemsArray.push({ + label: 'DEBUG - mobile.json', + status: 'default', + icon: , + onClick: () => exportRecordingToFile(true), + tooltip: + 'DEBUG - ONLY VISIBLE TO POSTHOG STAFF - Export untransformed recording to a file. This can be loaded later into PostHog for playback.', + }) + } if (logicProps.playerKey !== 'modal') { - itemsArray.push({ - label: 'Delete recording', - status: 'danger', - onClick: onDelete, - icon: , - }) + isStandardMode && + itemsArray.push({ + label: 'Delete recording', + status: 'danger', + onClick: onDelete, + icon: , + }) } return itemsArray - }, [logicProps.playerKey, onDelete, exportRecordingToFile, hasMobileExport]) + }, [logicProps.playerKey, onDelete, exportRecordingToFile, hasMobileExport, size]) return ( diff --git a/frontend/src/scenes/session-recordings/player/PlayerPersonMeta.tsx b/frontend/src/scenes/session-recordings/player/player-meta/PlayerPersonMeta.tsx similarity index 82% rename from frontend/src/scenes/session-recordings/player/PlayerPersonMeta.tsx rename to frontend/src/scenes/session-recordings/player/player-meta/PlayerPersonMeta.tsx index 15e359885697a..76195f5520183 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerPersonMeta.tsx +++ b/frontend/src/scenes/session-recordings/player/player-meta/PlayerPersonMeta.tsx @@ -3,13 +3,13 @@ import './PlayerMeta.scss' import { useActions, useValues } from 'kea' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { PersonIcon } from 'scenes/persons/PersonDisplay' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' +import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic' import { SessionRecordingSidebarTab } from '~/types' -import { playerSettingsLogic } from './playerSettingsLogic' -import { sessionRecordingPlayerLogic } from './sessionRecordingPlayerLogic' -import { playerSidebarLogic } from './sidebar/playerSidebarLogic' +import { playerSettingsLogic } from '../playerSettingsLogic' +import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' +import { playerSidebarLogic } from '../sidebar/playerSidebarLogic' export function PlayerPersonMeta(): JSX.Element { const { logicProps } = useValues(sessionRecordingPlayerLogic) diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts b/frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.test.ts similarity index 89% rename from frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts rename to frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.test.ts index 4fccafcfb856a..8b4e5147fc8dc 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.test.ts @@ -1,15 +1,15 @@ import { expectLogic } from 'kea-test-utils' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' +import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic' import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic' import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import recordingEventsJson from '../__mocks__/recording_events_query' -import recordingMetaJson from '../__mocks__/recording_meta.json' -import { snapshotsAsJSONLines } from '../__mocks__/recording_snapshots' +import recordingEventsJson from '../../__mocks__/recording_events_query' +import recordingMetaJson from '../../__mocks__/recording_meta.json' +import { snapshotsAsJSONLines } from '../../__mocks__/recording_snapshots' const playerProps = { sessionRecordingId: '1', playerKey: 'playlist' } diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx b/frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.tsx similarity index 98% rename from frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx rename to frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.tsx index 9dd87bae2a567..414a66939712a 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.tsx +++ b/frontend/src/scenes/session-recordings/player/player-meta/playerMetaLogic.tsx @@ -20,8 +20,8 @@ import { import { PersonType, PropertyFilterType } from '~/types' -import { SimpleTimeLabel } from '../components/SimpleTimeLabel' -import { sessionRecordingsListPropertiesLogic } from '../playlist/sessionRecordingsListPropertiesLogic' +import { SimpleTimeLabel } from '../../components/SimpleTimeLabel' +import { sessionRecordingsListPropertiesLogic } from '../../playlist/sessionRecordingsListPropertiesLogic' import type { playerMetaLogicType } from './playerMetaLogicType' const recordingPropertyKeys = ['click_count', 'keypress_count', 'console_error_count'] as const diff --git a/frontend/src/scenes/session-recordings/player/share/PlayerShareMenu.tsx b/frontend/src/scenes/session-recordings/player/share/PlayerShareMenu.tsx index bce905e8ce187..19ec656a39f29 100644 --- a/frontend/src/scenes/session-recordings/player/share/PlayerShareMenu.tsx +++ b/frontend/src/scenes/session-recordings/player/share/PlayerShareMenu.tsx @@ -5,7 +5,7 @@ import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/se import { openPlayerShareDialog } from 'scenes/session-recordings/player/share/PlayerShare' import { PlayerShareLogicProps } from 'scenes/session-recordings/player/share/playerShareLogic' -export function PlayerShareMenu({ iconsOnly }: { iconsOnly: boolean }): JSX.Element { +export function PlayerShareMenu(): JSX.Element { const { sessionRecordingId, logicProps } = useValues(sessionRecordingPlayerLogic) const { setPause, setIsFullScreen } = useActions(sessionRecordingPlayerLogic) @@ -47,7 +47,7 @@ export function PlayerShareMenu({ iconsOnly }: { iconsOnly: boolean }): JSX.Elem buttonSize="xsmall" > }> - {iconsOnly ? '' : 'Share'} + Share ) diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewGrid.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewGrid.tsx index a60be050a0b2b..94d93a5d97181 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewGrid.tsx +++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewGrid.tsx @@ -1,9 +1,9 @@ import { useValues } from 'kea' import { PropertyIcon } from 'lib/components/PropertyIcon' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic' import { OverviewGrid, OverviewGridItem } from '../../components/OverviewGrid' -import { playerMetaLogic } from '../playerMetaLogic' import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' export function PlayerSidebarOverviewGrid(): JSX.Element { diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewOtherWatchers.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewOtherWatchers.tsx index 534f69a371646..dad34940d9bb3 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewOtherWatchers.tsx +++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewOtherWatchers.tsx @@ -49,7 +49,7 @@ export function PlayerSidebarOverviewOtherWatchers(): JSX.Element {
{sessionPlayerMetaDataLoading ? ( - ) : sessionPlayerMetaData?.viewers ? ( + ) : sessionPlayerMetaData?.viewers?.length ? ( ) : ( diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewTab.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewTab.tsx index 1f4c3c77c1f2b..419780780618d 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewTab.tsx +++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarOverviewTab.tsx @@ -2,7 +2,7 @@ import { useValues } from 'kea' import { PersonDisplay } from 'scenes/persons/PersonDisplay' import { PlayerSidebarSessionSummary } from 'scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary' -import { playerMetaLogic } from '../playerMetaLogic' +import { playerMetaLogic } from '../player-meta/playerMetaLogic' import { sessionRecordingPlayerLogic } from '../sessionRecordingPlayerLogic' import { PlayerSidebarOverviewGrid } from './PlayerSidebarOverviewGrid' import { PlayerSidebarOverviewOtherWatchers } from './PlayerSidebarOverviewOtherWatchers' diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx index aacab996a7c06..2a9cf45ad326a 100644 --- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx +++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx @@ -5,7 +5,7 @@ import { FEATURE_FLAGS } from 'lib/constants' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { Spinner } from 'lib/lemon-ui/Spinner' -import { playerMetaLogic } from 'scenes/session-recordings/player/playerMetaLogic' +import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic' import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic' function SessionSummary(): JSX.Element { diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx index e9d4e3a1c4839..c3d3d3c6fc6aa 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingPreview.tsx @@ -13,7 +13,7 @@ import { colonDelimitedDuration } from 'lib/utils' import { DraggableToNotebook } from 'scenes/notebooks/AddToNotebook/DraggableToNotebook' import { asDisplay } from 'scenes/persons/person-utils' import { SimpleTimeLabel } from 'scenes/session-recordings/components/SimpleTimeLabel' -import { countryTitleFrom } from 'scenes/session-recordings/player/playerMetaLogic' +import { countryTitleFrom } from 'scenes/session-recordings/player/player-meta/playerMetaLogic' import { playerSettingsLogic, TimestampFormat } from 'scenes/session-recordings/player/playerSettingsLogic' import { urls } from 'scenes/urls' diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 495cd04c11536..0dad6019dd1b7 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -1259,7 +1259,7 @@ export const surveyLogic = kea([ const questionResults = surveyRatingResults[questionIdx] // If we don't have any results, return 'No data available' instead of NaN. - if (questionResults.total === 0 || !questionResults) { + if (!questionResults || questionResults.total === 0) { return 'No data available' } diff --git a/plugin-server/tsconfig.json b/plugin-server/tsconfig.json index a478ce98097a9..4a001e56233be 100644 --- a/plugin-server/tsconfig.json +++ b/plugin-server/tsconfig.json @@ -24,6 +24,6 @@ }, "skipLibCheck": true }, - "include": ["src"], + "include": ["src", "tests"], "exclude": ["node_modules", "dist", "bin"] } diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index a090b5585db45..46d4b85dd8991 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1055,7 +1055,9 @@ def visit_call(self, node: ast.Call): ) # check that we're not running inside another aggregate - for stack_node in self.stack: + for stack_node in reversed(self.stack): + if isinstance(stack_node, ast.SelectQuery): + break if stack_node != node and isinstance(stack_node, ast.Call) and find_hogql_aggregation(stack_node.name): raise QueryError( f"Aggregation '{node.name}' cannot be nested inside another aggregation '{stack_node.name}'." diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index d479d57a78603..8f2dd5264dc1a 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -213,15 +213,7 @@ def test_intersect_and_union_parens(self): response = to_printed_hogql(expr, self.team) self.assertEqual( response, - "SELECT\n" - " 1 AS id\n" - "LIMIT 50000\n" - "INTERSECT\n" - "(SELECT\n" - " 2 AS id\n" - "UNION ALL\n" - "SELECT\n" - " 3 AS id)", + "SELECT\n 1 AS id\nLIMIT 50000\nINTERSECT\n(SELECT\n 2 AS id\nUNION ALL\nSELECT\n 3 AS id)", ) # INTERSECT has higher priority than union @@ -893,6 +885,10 @@ def test_expr_parse_errors(self): "avg(avg(properties.bla))", "Aggregation 'avg' cannot be nested inside another aggregation 'avg'.", ) + self.assertEqual( # does not error through subqueries + "avg((select avg(properties.bla) from events))", + "avg((select avg(properties.bla) from events))", + ) self._assert_expr_error("person.chipotle", "Field not found: chipotle") self._assert_expr_error("properties.0", "SQL indexes start from one, not from zero. E.g: array.1") self._assert_expr_error( diff --git a/posthog/warehouse/api/table.py b/posthog/warehouse/api/table.py index c55cb4fc77438..932270193c6ed 100644 --- a/posthog/warehouse/api/table.py +++ b/posthog/warehouse/api/table.py @@ -11,7 +11,6 @@ from posthog.tasks.warehouse import validate_data_warehouse_table_columns from posthog.warehouse.models import ( DataWarehouseCredential, - DataWarehouseSavedQuery, DataWarehouseTable, ) from posthog.warehouse.api.external_data_source import SimpleExternalDataSourceSerializers @@ -186,8 +185,6 @@ def safely_get_queryset(self, queryset): def destroy(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: instance: DataWarehouseTable = self.get_object() - DataWarehouseSavedQuery.objects.filter(external_tables__icontains=instance.name).delete() - instance.soft_delete() return response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/posthog/warehouse/api/test/test_view.py b/posthog/warehouse/api/test/test_view.py index 558cb1ce75135..1ae8ee3024fb4 100644 --- a/posthog/warehouse/api/test/test_view.py +++ b/posthog/warehouse/api/test/test_view.py @@ -132,4 +132,4 @@ def test_view_with_external_table(self, patch_get_columns_1, patch_get_columns_2 response = self.client.delete(f"/api/projects/{self.team.id}/warehouse_tables/{response['id']}") - self.assertEqual(DataWarehouseSavedQuery.objects.all().count(), 0) + self.assertEqual(DataWarehouseSavedQuery.objects.all().count(), 1)