Skip to content

Commit

Permalink
feat: Activity/notifications panel (#18936)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Nov 29, 2023
1 parent 5091469 commit 7ff2199
Show file tree
Hide file tree
Showing 24 changed files with 479 additions and 180 deletions.
Binary file modified frontend/__snapshots__/posthog-3000-navigation--navigation-3000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 24 additions & 6 deletions frontend/src/layout/navigation-3000/sidepanel/SidePanel.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.SidePanel3000 {
--side-panel-bar-width: 3rem;

position: relative;
z-index: var(--z-main-nav);
box-sizing: content-box;
Expand All @@ -19,7 +21,7 @@
position: fixed;
top: 0;
right: 0;
max-width: calc(100vw - 3rem);
max-width: calc(100vw - var(--side-panel-bar-width));
box-shadow: 0 0 30px rgb(0 0 0 / 20%);

[theme='dark'] & {
Expand All @@ -36,15 +38,31 @@
.SidePanel3000__bar {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 3rem;
align-items: center;
width: var(--side-panel-bar-width);
height: 100vh;
overflow-y: auto;
overflow: hidden;
user-select: none;
border-left-width: 1px;

.LemonButton {
min-height: 2.25rem !important; // Reduce minimum height
.SidePanel3000__tabs {
flex: 1;
width: var(--side-panel-bar-width);
overflow: hidden auto;

&::-webkit-scrollbar {
display: none;
}

.SidePanel3000__tabsrotation {
display: flex;
gap: 0.25rem;
align-items: center;
height: var(--side-panel-bar-width);
margin-top: calc(calc(var(--side-panel-bar-width) - 0.25rem) * -1);
transform: rotate(90deg);
transform-origin: bottom left;
}
}
}

Expand Down
84 changes: 62 additions & 22 deletions frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import './SidePanel.scss'

import { IconFeatures, IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'
import {
IconEllipsis,
IconFeatures,
IconGear,
IconInfo,
IconNotebook,
IconNotification,
IconSupport,
} from '@posthog/icons'
import { LemonButton, LemonMenu, LemonMenuItems } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { Resizer } from 'lib/components/Resizer/Resizer'
Expand All @@ -11,6 +19,7 @@ import { NotebookPanel } from 'scenes/notebooks/NotebookPanel/NotebookPanel'

import { SidePanelTab } from '~/types'

import { SidePanelActivity } from './panels/activity/SidePanelActivity'
import { SidePanelActivation, SidePanelActivationIcon } from './panels/SidePanelActivation'
import { SidePanelDocs } from './panels/SidePanelDocs'
import { SidePanelFeaturePreviews } from './panels/SidePanelFeaturePreviews'
Expand Down Expand Up @@ -51,10 +60,15 @@ export const SidePanelTabs: Record<SidePanelTab, { label: string; Icon: any; Con
Icon: IconFeatures,
Content: SidePanelFeaturePreviews,
},
[SidePanelTab.Activity]: {
label: 'Activity',
Icon: IconNotification,
Content: SidePanelActivity,
},
}

export function SidePanel(): JSX.Element | null {
const { visibleTabs } = useValues(sidePanelLogic)
const { visibleTabs, extraTabs } = useValues(sidePanelLogic)
const { selectedTab, sidePanelOpen } = useValues(sidePanelStateLogic)
const { openSidePanel, closeSidePanel, setSidePanelAvailable } = useActions(sidePanelStateLogic)

Expand Down Expand Up @@ -89,6 +103,23 @@ export function SidePanel(): JSX.Element | null {

const sidePanelOpenAndAvailable = selectedTab && sidePanelOpen && visibleTabs.includes(selectedTab)

const menuOptions: LemonMenuItems | undefined = extraTabs
? [
{
title: 'Open in side panel',
items: extraTabs.map((tab) => {
const { Icon, label } = SidePanelTabs[tab]

return {
label: label,
icon: <Icon />,
onClick: () => openSidePanel(tab),
}
}),
},
]
: undefined

return (
<div
className={clsx(
Expand All @@ -104,26 +135,35 @@ export function SidePanel(): JSX.Element | null {
>
<Resizer {...resizerLogicProps} />
<div className="SidePanel3000__bar">
<div className="rotate-90 flex items-center gap-1 px-2">
{visibleTabs.map((tab: SidePanelTab) => {
const { Icon, label } = SidePanelTabs[tab]
return (
<LemonButton
key={tab}
icon={<Icon className="rotate-270 w-6" />}
onClick={() =>
activeTab === tab ? closeSidePanel() : openSidePanel(tab as SidePanelTab)
}
data-attr={`sidepanel-tab-${tab}`}
active={activeTab === tab}
type="secondary"
stealth={true}
>
{label}
</LemonButton>
)
})}
<div className="SidePanel3000__tabs">
<div className="SidePanel3000__tabsrotation">
{visibleTabs.map((tab: SidePanelTab) => {
const { Icon, label } = SidePanelTabs[tab]
return (
<LemonButton
key={tab}
icon={<Icon className="rotate-270 w-6" />}
onClick={() =>
activeTab === tab ? closeSidePanel() : openSidePanel(tab as SidePanelTab)
}
data-attr={`sidepanel-tab-${tab}`}
active={activeTab === tab}
type="secondary"
stealth={true}
>
{label}
</LemonButton>
)
})}
</div>
</div>
{menuOptions ? (
<div className="shrink-0 flex items-center m-2">
<LemonMenu items={menuOptions}>
<LemonButton size="small" status="stealth" icon={<IconEllipsis />} />
</LemonMenu>
</div>
) : null}
</div>
<Resizer {...resizerLogicProps} offset={'3rem'} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import { IconClose } from 'lib/lemon-ui/icons'
import { sidePanelStateLogic } from '../sidePanelStateLogic'

export type SidePanelPaneHeaderProps = {
title?: string
title?: string | JSX.Element
children?: React.ReactNode
}

export function SidePanelPaneHeader({ children, title }: SidePanelPaneHeaderProps): JSX.Element {
const { closeSidePanel } = useActions(sidePanelStateLogic)

return (
<header className="border-b flex-0 p-1 flex items-center justify-end gap-1 h-10">
{title ? <h4 className="flex-1 font-semibold px-2 mb-0 truncate">{title}</h4> : null}
<header className="border-b shrink-0 p-1 flex items-center justify-end gap-1 h-10">
{title ? (
<h4 className="flex-1 flex items-center gap-1 font-semibold px-2 mb-0 truncate">{title}</h4>
) : null}
{children}
<Tooltip placement="bottomRight" title="Close this side panel">
<LemonButton size="small" sideIcon={<IconClose />} onClick={() => closeSidePanel()} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export const SidePanelActivationIcon = ({ className }: { className: LemonIconPro
const { activeTasks, completionPercent } = useValues(activationLogic)

return (
<LemonProgressCircle progress={completionPercent / 100} size={20} className={className}>
<span className="text-xs font-bold">{activeTasks.length}</span>
<LemonProgressCircle progress={completionPercent / 100} strokePercentage={0.15} size={20} className={className}>
<span className="text-xs font-semibold">{activeTasks.length}</span>
</LemonProgressCircle>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Link } from 'lib/lemon-ui/Link'
import { Popover } from 'lib/lemon-ui/Popover/Popover'
import { urls } from 'scenes/urls'

import { notificationsLogic } from '~/layout/navigation/TopBar/notificationsLogic'
import { notificationsLogic } from '~/layout/navigation-3000/sidepanel/panels/activity/notificationsLogic'

export function NotificationBell(): JSX.Element {
const { unreadCount, hasNotifications, notifications, isNotificationPopoverOpen, hasUnread } =
Expand All @@ -27,7 +27,7 @@ export function NotificationBell(): JSX.Element {
visible={isNotificationPopoverOpen}
onClickOutside={() => (isNotificationPopoverOpen ? toggleNotificationsPopover() : null)}
overlay={
<div className="activity-log notifications-menu">
<div className="ActivityLog notifications-menu">
<h5>
Notifications{' '}
<LemonTag type="warning" className="ml-1">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { LemonBanner, LemonButton, LemonSkeleton, LemonTabs, Link, Spinner } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { ActivityLogRow } from 'lib/components/ActivityLog/ActivityLog'
import { usePageVisibility } from 'lib/hooks/usePageVisibility'
import { useEffect, useRef } from 'react'
import { urls } from 'scenes/urls'

import {
notificationsLogic,
SidePanelActivityTab,
} from '~/layout/navigation-3000/sidepanel/panels/activity/notificationsLogic'

import { SidePanelPaneHeader } from '../../components/SidePanelPaneHeader'

const SCROLL_TRIGGER_OFFSET = 100

export const SidePanelActivity = (): JSX.Element => {
const {
hasNotifications,
notifications,
activeTab,
allActivity,
allActivityResponseLoading,
allActivityHasNext,
importantChangesLoading,
hasUnread,
} = useValues(notificationsLogic)
const { togglePolling, setActiveTab, maybeLoadOlderActivity, markAllAsRead, loadImportantChanges } =
useActions(notificationsLogic)

usePageVisibility((pageIsVisible) => {
togglePolling(pageIsVisible)
})

useEffect(() => {
loadImportantChanges(false)
return () => {
markAllAsRead()
togglePolling(false)
}
}, [])

const lastScrollPositionRef = useRef(0)
const contentRef = useRef<HTMLDivElement | null>(null)

const handleScroll = (e: React.UIEvent<HTMLDivElement>): void => {
// If we are scrolling down then check if we are at the bottom of the list
if (e.currentTarget.scrollTop > lastScrollPositionRef.current) {
const scrollPosition = e.currentTarget.scrollTop + e.currentTarget.clientHeight
if (e.currentTarget.scrollHeight - scrollPosition < SCROLL_TRIGGER_OFFSET) {
maybeLoadOlderActivity()
}
}

lastScrollPositionRef.current = e.currentTarget.scrollTop
}

return (
<div className="flex flex-col overflow-hidden">
<SidePanelPaneHeader title="Activity" />
<div className="flex flex-col overflow-hidden">
<div className="shrink-0 mx-2">
<LemonTabs
activeKey={activeTab as SidePanelActivityTab}
onChange={(key) => setActiveTab(key)}
tabs={[
{
key: SidePanelActivityTab.Unread,
label: 'My notifications',
},
{
key: SidePanelActivityTab.All,
label: 'All activity',
},
]}
/>
</div>

<div className="flex-1 overflow-y-auto px-2">
{activeTab === SidePanelActivityTab.Unread ? (
<div className="flex-1 overflow-y-auto space-y-px">
<LemonBanner type="info" className="mb-2">
Notifications shows you changes others make to{' '}
<Link to={urls.savedInsights('history')}>Insights</Link> and{' '}
<Link to={urls.featureFlags('history')}>Feature Flags</Link> that you created. Come join{' '}
<Link to={'https://posthog.com/community'}>our community forum</Link> and tell us what
else should be here!
</LemonBanner>

{hasUnread ? (
<div className="flex justify-end mb-2">
<LemonButton
type="secondary"
onClick={() => markAllAsRead()}
loading={importantChangesLoading}
>
Mark all as read
</LemonButton>
</div>
) : null}

{importantChangesLoading && !hasNotifications ? (
<LemonSkeleton className="my-2 h-12" repeat={10} fade />
) : hasNotifications ? (
notifications.map((logItem, index) => (
<ActivityLogRow logItem={logItem} key={index} showExtendedDescription={false} />
))
) : (
<p>You're all caught up!</p>
)}
</div>
) : (
<div className="flex-1 overflow-y-auto space-y-px" ref={contentRef} onScroll={handleScroll}>
{allActivityResponseLoading && !allActivity.length ? (
<LemonSkeleton className="my-2 h-12" repeat={10} fade />
) : allActivity.length ? (
<>
{allActivity.map((logItem, index) => (
<ActivityLogRow logItem={logItem} key={index} showExtendedDescription={false} />
))}

<div className="m-4 h-10 flex items-center justify-center gap-2 text-muted-alt">
{allActivityResponseLoading ? (
<>
<Spinner textColored /> Loading older activity
</>
) : allActivityHasNext ? (
<LemonButton
type="secondary"
fullWidth
center
onClick={() => maybeLoadOlderActivity()}
>
Load more
</LemonButton>
) : (
'No more results'
)}
</div>
</>
) : (
<p>You're all caught up!</p>
)}
</div>
)}
</div>
</div>
</div>
)
}
Loading

0 comments on commit 7ff2199

Please sign in to comment.