feat(3000): Show the 3000 index-less navbar with labels (#18263)

* feat(3000): Show the 3000 index-less navbar with labels

* Use new icons and move "Event explorer"

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* feat: Allow new nav to be collapsed (#18343)

* Add alpha/beta tags, move "Toolbar", rename "Persons"

* Fix tooltips not showing when navbar collapsed

* Align "Session replay" in using sentence case

* Update UI snapshots for `chromium` (1)

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben White <ben@posthog.com>
This commit is contained in:
Michael Matloka
2023-11-02 22:38:00 +01:00
committed by GitHub
parent f8b5af1768
commit 243cf56766
16 changed files with 207 additions and 98 deletions

View File

@@ -125,7 +125,7 @@ jobs:
- name: Check for syntax errors, import sort, and code style violations
run: |
ruff check .
ruff check .
- name: Check formatting
run: |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -33,6 +33,7 @@
// Navbar
.Navbar3000 {
position: relative;
flex: 0 0 3rem;
border-right: 1px solid transparent; // This is just for sizing, the visible border is on the content
box-sizing: content-box;
@@ -63,12 +64,35 @@
left: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100vh;
padding: 0 0.375rem;
border-right-width: 1px;
background: var(--accent-3000);
overflow-y: auto;
.Navbar3000__content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 0.375rem;
background: var(--accent-3000);
overflow-y: auto;
z-index: var(--z-main-nav);
.LemonButton {
min-height: 2.25rem !important; // Reduce minimum height
padding: 0.375rem !important; // Use a custom padding for the navbar only
}
ul {
padding: 0.5rem 0;
}
ul + ul {
border-top-width: 1px;
}
li + li {
margin-top: 1px;
}
}
}
.NavbarButton {
@@ -93,20 +117,6 @@
transform: translateY(-0.25rem);
}
}
&.NavbarButton--popover {
&::before {
content: '';
position: absolute;
top: 0.1875rem;
right: 0.1875rem;
width: 0;
height: 0;
border-radius: 1px;
border: 0.1875rem solid transparent;
border-top-color: currentColor;
border-right-color: currentColor;
}
}
}
// Sidebar

View File

@@ -12,46 +12,55 @@ import { navigation3000Logic } from '../navigationLogic'
import { themeLogic } from '../themeLogic'
import { NavbarButton } from './NavbarButton'
import { urls } from 'scenes/urls'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { Resizer } from 'lib/components/Resizer/Resizer'
import { useRef } from 'react'
export function Navbar(): JSX.Element {
const { user } = useValues(userLogic)
const { isSitePopoverOpen } = useValues(navigationLogic)
const { closeSitePopover, toggleSitePopover } = useActions(navigationLogic)
const { isSidebarShown, activeNavbarItemId, navbarItems } = useValues(navigation3000Logic)
const { showSidebar, hideSidebar } = useActions(navigation3000Logic)
const { showSidebar, hideSidebar, toggleNavCollapsed } = useActions(navigation3000Logic)
const { isDarkModeOn, darkModeSavedPreference, darkModeSystemPreference, isThemeSyncedWithSystem } =
useValues(themeLogic)
const { toggleTheme } = useActions(themeLogic)
const { featureFlags } = useValues(featureFlagLogic)
const activeThemeIcon = isDarkModeOn ? <IconNight /> : <IconDay />
const containerRef = useRef<HTMLDivElement | null>(null)
return (
<nav className="Navbar3000">
<nav className="Navbar3000" ref={containerRef}>
<div className="Navbar3000__content">
<div className="Navbar3000__top">
{navbarItems.map((section, index) => (
<ul key={index}>
{section.map((item) => (
<NavbarButton
key={item.identifier}
title={item.label}
identifier={item.identifier}
icon={item.icon}
to={'to' in item ? item.to : undefined}
onClick={
'logic' in item
? () => {
if (activeNavbarItemId === item.identifier && isSidebarShown) {
hideSidebar()
} else {
showSidebar(item.identifier)
{section.map((item) =>
item.featureFlag && !featureFlags[item.featureFlag] ? null : (
<NavbarButton
key={item.identifier}
title={item.label}
identifier={item.identifier}
icon={item.icon}
tag={item.tag}
to={'to' in item ? item.to : undefined}
onClick={
'logic' in item
? () => {
if (activeNavbarItemId === item.identifier && isSidebarShown) {
hideSidebar()
} else {
showSidebar(item.identifier)
}
}
}
: undefined
}
active={activeNavbarItemId === item.identifier && isSidebarShown}
/>
))}
: undefined
}
active={activeNavbarItemId === item.identifier && isSidebarShown}
/>
)
)}
</ul>
))}
</div>
@@ -78,6 +87,7 @@ export function Navbar(): JSX.Element {
? 'Switch to light mode'
: 'Switch to dark mode'
}
shortTitle="Toggle theme"
onClick={() => toggleTheme()}
persistentTooltip
/>
@@ -87,7 +97,7 @@ export function Navbar(): JSX.Element {
icon={<IconQuestion />}
identifier="help-button"
title="Need any help?"
popoverMarker
shortTitle="Help"
/>
}
placement="right-end"
@@ -95,6 +105,7 @@ export function Navbar(): JSX.Element {
<NavbarButton
icon={<IconGear />}
identifier={Scene.ProjectSettings}
title="Project settings"
to={urls.projectSettings()}
/>
<Popover
@@ -107,13 +118,20 @@ export function Navbar(): JSX.Element {
icon={<ProfilePicture name={user?.first_name} email={user?.email} size="md" />}
identifier="me"
title={`Hi${user?.first_name ? `, ${user?.first_name}` : ''}!`}
shortTitle={user?.first_name || user?.email}
onClick={toggleSitePopover}
popoverMarker
/>
</Popover>
</ul>
</div>
</div>
<Resizer
placement={'right'}
containerRef={containerRef}
closeThreshold={100}
onToggleClosed={(shouldBeClosed) => toggleNavCollapsed(shouldBeClosed)}
onDoubleClick={() => toggleNavCollapsed()}
/>
</nav>
)
}

View File

@@ -4,35 +4,69 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
import clsx from 'clsx'
import { useValues } from 'kea'
import { sceneLogic } from 'scenes/sceneLogic'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { navigation3000Logic } from '../navigationLogic'
import { LemonTag } from '@posthog/lemon-ui'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
export interface NavbarButtonProps {
identifier: string
icon: ReactElement
title?: string
shortTitle?: string
tag?: 'alpha' | 'beta'
onClick?: () => void
to?: string
persistentTooltip?: boolean
active?: boolean
popoverMarker?: boolean
}
export const NavbarButton: FunctionComponent<NavbarButtonProps> = React.forwardRef<
HTMLButtonElement,
NavbarButtonProps
>(({ identifier, title, onClick, persistentTooltip, popoverMarker, ...buttonProps }, ref): JSX.Element => {
>(({ identifier, shortTitle, title, tag, onClick, persistentTooltip, ...buttonProps }, ref): JSX.Element => {
const { aliasedActiveScene } = useValues(sceneLogic)
const { featureFlags } = useValues(featureFlagLogic)
const { isNavCollapsed } = useValues(navigation3000Logic)
const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV')
const [hasBeenClicked, setHasBeenClicked] = useState(false)
const here = featureFlags[FEATURE_FLAGS.POSTHOG_3000_NAV] ? aliasedActiveScene === identifier : false
const here = aliasedActiveScene === identifier
const isNavCollapsedActually = isNavCollapsed || isUsingNewNav
if (!isUsingNewNav) {
buttonProps.active = here
}
let content: JSX.Element | string | undefined
if (!isNavCollapsedActually) {
content = shortTitle || title
if (tag) {
if (tag === 'alpha') {
content = (
<>
{content}
<LemonTag type="completion" size="small" className="ml-2">
ALPHA
</LemonTag>
</>
)
} else if (tag === 'beta') {
content = (
<>
{content}
<LemonTag type="warning" size="small" className="ml-2">
BETA
</LemonTag>
</>
)
}
}
}
return (
<li>
<li className="w-full">
<Tooltip
title={here ? `${title} (you are here)` : title}
title={isNavCollapsedActually ? (here ? `${title} (you are here)` : title) : null}
placement="right"
delayMs={0}
visible={!persistentTooltip && hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
@@ -45,13 +79,12 @@ export const NavbarButton: FunctionComponent<NavbarButtonProps> = React.forwardR
setHasBeenClicked(true)
onClick?.()
}}
className={clsx(
'NavbarButton',
here && 'NavbarButton--here',
popoverMarker && 'NavbarButton--popover'
)}
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
fullWidth
{...buttonProps}
/>
>
{content}
</LemonButton>
</Tooltip>
</li>
)

View File

@@ -26,6 +26,10 @@ import {
IconTestTube,
IconToggle,
IconToolbar,
IconNotebook,
IconRocket,
IconServer,
IconChat,
} from '@posthog/icons'
import { urls } from 'scenes/urls'
import { annotationsSidebarLogic } from './sidebars/annotations'
@@ -53,6 +57,7 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
actions({
hideSidebar: true,
showSidebar: (newNavbarItemId?: string) => ({ newNavbarItemId }),
toggleNavCollapsed: (override?: boolean) => ({ override }),
toggleSidebar: true,
setSidebarWidth: (width: number) => ({ width }),
setSidebarOverslide: (overslide: number) => ({ overslide }),
@@ -86,6 +91,13 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
toggleSidebar: (isSidebarShown) => !isSidebarShown,
},
],
isNavCollapsed: [
false,
{ persist: true },
{
toggleNavCollapsed: (state, { override }) => override ?? !state,
},
],
sidebarWidth: [
DEFAULT_SIDEBAR_WIDTH_PX,
{ persist: true },
@@ -289,6 +301,19 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
logic: isUsingSidebar ? dashboardsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.dashboards(),
},
{
identifier: Scene.Notebooks,
label: 'Notebooks',
icon: <IconNotebook />,
to: urls.notebooks(),
featureFlag: FEATURE_FLAGS.NOTEBOOKS,
},
{
identifier: Scene.Events,
label: 'Event explorer',
icon: <IconLive />,
to: urls.events(),
},
{
identifier: Scene.DataManagement,
label: 'Data management',
@@ -298,7 +323,7 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
},
{
identifier: Scene.Persons,
label: 'Persons and groups',
label: 'People and groups',
icon: <IconPerson />,
logic: isUsingSidebar ? personsAndGroupsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.persons(),
@@ -317,17 +342,18 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
logic: isUsingSidebar ? annotationsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.annotations(),
},
{
identifier: Scene.ToolbarLaunch,
label: 'Toolbar',
icon: <IconToolbar />,
logic: isUsingSidebar ? toolbarSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.toolbarLaunch(),
},
],
[
{
identifier: Scene.Events,
label: 'Events',
icon: <IconLive />,
to: urls.events(),
},
{
identifier: Scene.SavedInsights,
label: 'Product Analytics',
label: 'Product analytics',
icon: <IconGraph />,
logic: isUsingSidebar ? insightsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.savedInsights(),
@@ -335,37 +361,51 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
featureFlags[FEATURE_FLAGS.WEB_ANALYTICS]
? {
identifier: Scene.WebAnalytics,
label: 'Web Analytics',
label: 'Web analytics',
icon: <IconPieChart />,
to: isUsingSidebar ? undefined : urls.webAnalytics(),
tag: 'alpha' as const,
}
: null,
{
identifier: Scene.DataWarehouse,
label: 'Data warehouse',
icon: <IconServer />,
to: urls.dataWarehouse(),
featureFlag: FEATURE_FLAGS.DATA_WAREHOUSE,
tag: 'beta' as const,
},
{
identifier: Scene.Replay,
label: 'Session Replay',
label: 'Session replay',
icon: <IconRewindPlay />,
to: urls.replay(),
},
{
identifier: Scene.Surveys,
label: 'Surveys',
icon: <IconChat />,
to: urls.surveys(),
},
{
identifier: Scene.FeatureFlags,
label: 'Feature Flags',
label: 'Feature flags',
icon: <IconToggle />,
logic: isUsingSidebar ? featureFlagsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.featureFlags(),
},
{
identifier: Scene.Experiments,
label: 'A/B Testing',
label: 'A/B testing',
icon: <IconTestTube />,
logic: isUsingSidebar ? experimentsSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.experiments(),
},
{
identifier: Scene.ToolbarLaunch,
label: 'Toolbar',
icon: <IconToolbar />,
logic: isUsingSidebar ? toolbarSidebarLogic : undefined,
to: isUsingSidebar ? undefined : urls.toolbarLaunch(),
identifier: Scene.EarlyAccessFeatures,
label: 'Early access features',
icon: <IconRocket />,
to: urls.earlyAccessFeatures(),
},
].filter(isNotNil),
[

View File

@@ -1,5 +1,6 @@
import { LemonTagType } from '@posthog/lemon-ui'
import { Logic, LogicWrapper } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { Dayjs } from 'lib/dayjs'
import { LemonMenuItems } from 'lib/lemon-ui/LemonMenu'
import React from 'react'
@@ -27,6 +28,8 @@ interface NavbarItemBase {
identifier: string
label: string
icon: JSX.Element
featureFlag?: (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
tag?: 'alpha' | 'beta'
}
export interface SceneNavbarItem extends NavbarItemBase {
to: string

View File

@@ -22,6 +22,7 @@ export type ResizerLogicProps = {
closeThreshold?: number
/** Fired when the "closeThreshold" is crossed */
onToggleClosed?: (closed: boolean) => void
onDoubleClick?: () => void
}
const removeAllListeners = (cache: Record<string, any>): void => {
@@ -83,7 +84,7 @@ export const resizerLogic = kea<resizerLogicType>([
return
}
const isDoubleClick = cache.firstClickTimestamp && Date.now() - cache.firstClickTimestamp < 200
let isDoubleClick = cache.firstClickTimestamp && Date.now() - cache.firstClickTimestamp < 500
cache.firstClickTimestamp = Date.now()
const originContainerBounds = props.containerRef.current.getBoundingClientRect()
@@ -115,6 +116,7 @@ export const resizerLogic = kea<resizerLogicType>([
const event = calculateEvent(e, false)
props.onResize?.(event)
actions.setResizingWidth(event.desiredWidth)
isDoubleClick = false
const newIsClosed = props.closeThreshold ? event.desiredWidth < props.closeThreshold : false
@@ -128,7 +130,13 @@ export const resizerLogic = kea<resizerLogicType>([
if (e.button === 0) {
const event = calculateEvent(e, false)
if (event.desiredWidth !== values.width) {
if (isDoubleClick) {
// Double click - reset to original width
actions.resetDesiredWidth()
cache.firstClickTimestamp = null
props.onDoubleClick?.()
} else if (event.desiredWidth !== values.width) {
if (!isClosed) {
// We only want to persist the value if it is open
actions.setDesiredWidth(event.desiredWidth)
@@ -142,10 +150,6 @@ export const resizerLogic = kea<resizerLogicType>([
originalWidth: originContainerBounds.width,
isClosed,
})
} else if (isDoubleClick) {
// Double click - reset to original width
actions.resetDesiredWidth()
cache.firstClickTimestamp = null
}
actions.endResize()

View File

@@ -68,7 +68,7 @@ export const Actions: StoryFn<typeof TaxonomicFilter> = (args) => {
setIndex(0)
}, [])
return (
<div className="w-fit border rounded p-2 bg-white">
<div className="w-fit border rounded p-2">
<TaxonomicFilter {...args} />
</div>
)

View File

@@ -243,19 +243,6 @@
color: var(--muted);
}
&:not([aria-disabled='true']):hover,
&.LemonButton--active {
color: var(--default);
background: var(--border);
.LemonButton__icon {
color: var(--default);
}
}
&:not([aria-disabled='true']):active {
transform: scale(0.96875); // 31/32 (0.5px less on both sides, assuming a 32px tall button)
}
&.LemonButton--status-primary {
color: var(--muted);
}
@@ -289,6 +276,19 @@
color: var(--default);
}
}
&:not([aria-disabled='true']):hover,
&.LemonButton--active {
color: var(--default);
background: var(--border);
.LemonButton__icon {
color: var(--default);
}
}
&:not([aria-disabled='true']):active {
transform: scale(calc(35 / 36));
}
}
}

View File

@@ -13,7 +13,7 @@ export function LemonCard({ hoverEffect = true, className, children, onClick, fo
<div
className={`LemonCard ${hoverEffect && 'LemonCard--hoverEffect'} border ${
focused ? 'border-2 border-primary' : 'border-border'
} rounded-lg p-6 bg-white ${className}`}
} rounded-lg p-6 bg-bg-light ${className}`}
onClick={onClick}
>
{children}

View File

@@ -35,7 +35,7 @@ export const InlineMenu = ({ editor }: { editor: Editor }): JSX.Element => {
>
<div
ref={menuRef}
className="NotebookInlineMenu flex bg-white rounded border items-center text-muted-alt p-1 space-x-0.5"
className="NotebookInlineMenu flex bg-bg-light rounded border items-center text-muted-alt p-1 space-x-0.5"
>
{editor.isActive('link') ? (
<>

View File

@@ -42,6 +42,7 @@ const sceneNavAlias: Partial<Record<Scene, Scene>> = {
[Scene.AppMetrics]: Scene.Apps,
[Scene.ReplaySingle]: Scene.Replay,
[Scene.ReplayPlaylist]: Scene.ReplayPlaylist,
[Scene.Site]: Scene.ToolbarLaunch,
}
export const sceneLogic = kea<sceneLogicType>([

View File

@@ -14,7 +14,7 @@ const Filters = (): JSX.Element => {
const { webAnalyticsFilters, dateTo, dateFrom } = useValues(webAnalyticsLogic)
const { setWebAnalyticsFilters, setDates } = useActions(webAnalyticsLogic)
return (
<div className="sticky top-0 bg-white z-20 pt-2">
<div className="sticky top-0 z-20 pt-2">
<div className="flex flex-row flex-wrap gap-2">
<DateFilter dateFrom={dateFrom} dateTo={dateTo} onChange={setDates} />
<PropertyFilters