From eaa210a6bbb2c38dcff424b998e02cf0af6f8c39 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 5 Jul 2023 15:10:24 +0200 Subject: [PATCH 1/7] feat(3000): Add sites sidebar for Toolbar launching --- .../layout/navigation-3000/Navigation.scss | 15 ++- .../src/layout/navigation-3000/Navigation.tsx | 12 +- .../components/SidebarList.tsx | 13 +- .../layout/navigation-3000/navbarItems.tsx | 8 ++ .../sidebars/dataManagement.ts | 4 +- .../navigation-3000/sidebars/insights.ts | 2 +- .../sidebars/personsAndGroups.ts | 2 +- .../navigation-3000/sidebars/toolbar.ts | 118 ++++++++++++++++++ frontend/src/layout/navigation-3000/types.ts | 9 +- frontend/src/layout/navigation/Navigation.tsx | 4 +- .../src/layout/navigation/navigationLogic.ts | 5 +- .../src/lib/lemon-ui/LemonTag/LemonTag.scss | 9 +- .../src/lib/lemon-ui/LemonTag/LemonTag.tsx | 7 +- frontend/src/scenes/appScenes.ts | 1 + frontend/src/scenes/sceneTypes.ts | 11 +- frontend/src/scenes/scenes.ts | 12 +- frontend/src/scenes/sites/Site.scss | 4 + frontend/src/scenes/sites/Site.tsx | 30 +++++ frontend/src/scenes/sites/siteLogic.ts | 26 ++++ frontend/src/scenes/urls.ts | 1 + frontend/src/styles/global.scss | 4 + posthog/settings/web.py | 1 + 22 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 frontend/src/layout/navigation-3000/sidebars/toolbar.ts create mode 100644 frontend/src/scenes/sites/Site.scss create mode 100644 frontend/src/scenes/sites/Site.tsx create mode 100644 frontend/src/scenes/sites/siteLogic.ts diff --git a/frontend/src/layout/navigation-3000/Navigation.scss b/frontend/src/layout/navigation-3000/Navigation.scss index baf6c6bd6bb28..3fb606ac62cff 100644 --- a/frontend/src/layout/navigation-3000/Navigation.scss +++ b/frontend/src/layout/navigation-3000/Navigation.scss @@ -17,10 +17,16 @@ } .Navigation3000__scene { - margin: var(--scene-padding-y) var(--scene-padding-x); - // The below is for positioning of the scene-level spinner + // `relative` is for positioning of the scene-level spinner position: relative; + margin: var(--scene-padding-y) var(--scene-padding-x); min-height: calc(100vh - var(--breadcrumbs-height) - var(--scene-padding-y) * 2); + overflow-x: hidden; + &.Navigation3000__scene--plain { + --scene-padding-y: 0px; + --scene-padding-x: 0px; + display: flex; + } } // Navbar @@ -299,7 +305,7 @@ border-bottom-width: 1px; list-style: none; - &:hover, + &:hover:not([aria-disabled='true']), &[aria-current='page'] { opacity: 1; --sidebar-list-item-background: var(--border-3000); @@ -404,6 +410,9 @@ row-gap: 1px; padding: 0 var(--sidebar-horizontal-padding) 0 var(--sidebar-list-item-inset); color: inherit !important; // Disable link color + .SidebarListItem[aria-disabled='true'] & { + cursor: default; + } } .SidebarListItem__rename { diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index 7f94bdd4971cf..a004f2bccfa5a 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -8,9 +8,12 @@ import { Sidebar } from './components/Sidebar' import './Navigation.scss' import { themeLogic } from './themeLogic' import { navigation3000Logic } from './navigationLogic' +import clsx from 'clsx' +import { sceneLogic } from 'scenes/sceneLogic' export function Navigation({ children }: { children: ReactNode }): JSX.Element { useMountedLogic(themeLogic) + const { sceneConfig } = useValues(sceneLogic) const { activeNavbarItem } = useValues(navigation3000Logic) useEffect(() => { @@ -25,8 +28,13 @@ export function Navigation({ children }: { children: ReactNode }): JSX.Element {
-
-
{children}
+
+ {children}
diff --git a/frontend/src/layout/navigation-3000/components/SidebarList.tsx b/frontend/src/layout/navigation-3000/components/SidebarList.tsx index 83a750ddad8b5..2a5508bc35f6d 100644 --- a/frontend/src/layout/navigation-3000/components/SidebarList.tsx +++ b/frontend/src/layout/navigation-3000/components/SidebarList.tsx @@ -5,7 +5,7 @@ import { IconCheckmark, IconClose, IconEllipsis } from 'lib/lemon-ui/icons' import { BasicListItem, ExtendedListItem, ExtraListItemContext, SidebarCategory } from '../types' import React, { useEffect, useMemo, useRef, useState } from 'react' import { LemonMenu } from 'lib/lemon-ui/LemonMenu' -import { LemonButton, lemonToast } from '@posthog/lemon-ui' +import { LemonButton, LemonTag, lemonToast } from '@posthog/lemon-ui' import { navigation3000Logic } from '../navigationLogic' import { captureException } from '@sentry/react' import { KeyboardShortcut } from './KeyboardShortcut' @@ -104,6 +104,16 @@ function SidebarListItem({ if (!item.url || item.isNamePlaceholder) { formattedName = {formattedName} } + if (item.tag) { + formattedName = ( + <> + {formattedName} + + {item.tag.text} + + + ) + } const { onRename } = item const menuItems = useMemo(() => { @@ -264,6 +274,7 @@ function SidebarListItem({ !!item.marker?.status && `SidebarListItem--marker-status-${item.marker.status}`, 'summary' in item && 'SidebarListItem--extended' )} + aria-disabled={!item.url} aria-current={active ? 'page' : undefined} style={style} // eslint-disable-line react/forbid-dom-props > diff --git a/frontend/src/layout/navigation-3000/navbarItems.tsx b/frontend/src/layout/navigation-3000/navbarItems.tsx index 2abbbfea67c3e..6628c1bfed15e 100644 --- a/frontend/src/layout/navigation-3000/navbarItems.tsx +++ b/frontend/src/layout/navigation-3000/navbarItems.tsx @@ -10,6 +10,7 @@ import { IconLive, IconPerson, IconRecording, + IconTools, IconUnverifiedEvent, } from 'lib/lemon-ui/icons' import { Scene } from 'scenes/sceneTypes' @@ -23,6 +24,7 @@ import { insightsSidebarLogic } from './sidebars/insights' import { dataManagementSidebarLogic } from './sidebars/dataManagement' import { annotationsSidebarLogic } from './sidebars/annotations' import { experimentsSidebarLogic } from './sidebars/experiments' +import { toolbarSidebarLogic } from './sidebars/toolbar' /** A list of navbar sections with items. */ export const NAVBAR_ITEMS: NavbarItem[][] = [ @@ -93,6 +95,12 @@ export const NAVBAR_ITEMS: NavbarItem[][] = [ icon: , logic: experimentsSidebarLogic, }, + { + identifier: Scene.ToolbarLaunch, + label: 'Toolbar', + icon: , + logic: toolbarSidebarLogic, + }, ], [ { diff --git a/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts b/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts index 186d88235c13e..1f38cfe0a7084 100644 --- a/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts +++ b/frontend/src/layout/navigation-3000/sidebars/dataManagement.ts @@ -36,7 +36,7 @@ export const dataManagementSidebarLogic = kea([ { loadEventDefinitions: async ({ startIndex, stopIndex }) => { if (!startIndex) { - cache.requestedEventDefinitions.length = 0 // Clear cache + cache.requestedEventDefinitions = [] } for (let i = startIndex; i < stopIndex; i++) { cache.requestedEventDefinitions[i] = true @@ -59,7 +59,7 @@ export const dataManagementSidebarLogic = kea([ { loadPropertyDefinitions: async ({ startIndex, stopIndex }) => { if (!startIndex) { - cache.requestedPropertyDefinitions.length = 0 // Clear cache + cache.requestedPropertyDefinitions = [] } for (let i = startIndex; i < stopIndex; i++) { cache.requestedPropertyDefinitions[i] = true diff --git a/frontend/src/layout/navigation-3000/sidebars/insights.ts b/frontend/src/layout/navigation-3000/sidebars/insights.ts index e3bb0be379c28..da3baf796dc1e 100644 --- a/frontend/src/layout/navigation-3000/sidebars/insights.ts +++ b/frontend/src/layout/navigation-3000/sidebars/insights.ts @@ -139,7 +139,7 @@ export const insightsSidebarLogic = kea([ listeners(({ values, cache }) => ({ loadInsights: () => { if (!values.paramsFromFilters.offset) { - cache.requestedInsights.length = 0 + cache.requestedInsights = [] } }, })), diff --git a/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts b/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts index 4337d81b6c071..0514a2ab24f57 100644 --- a/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts +++ b/frontend/src/layout/navigation-3000/sidebars/personsAndGroups.ts @@ -173,7 +173,7 @@ export const personsAndGroupsSidebarLogic = kea { const offset = url ? parseInt(new URL(url).searchParams.get('offset') || '0') : 0 if (offset === 0) { - cache.requestedPersons.length = 0 // Clear cache + cache.requestedPersons = [] } }, })), diff --git a/frontend/src/layout/navigation-3000/sidebars/toolbar.ts b/frontend/src/layout/navigation-3000/sidebars/toolbar.ts new file mode 100644 index 0000000000000..f012dc19c6f14 --- /dev/null +++ b/frontend/src/layout/navigation-3000/sidebars/toolbar.ts @@ -0,0 +1,118 @@ +import { connect, kea, path, selectors } from 'kea' +import { sceneLogic } from 'scenes/sceneLogic' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' +import { SidebarCategory, BasicListItem } from '../types' +import Fuse from 'fuse.js' +import { subscriptions } from 'kea-subscriptions' +import { navigation3000Logic } from '~/layout/navigation-3000/navigationLogic' +import { FuseSearchMatch } from './utils' +import { + AuthorizedUrlListType, + KeyedAppUrl, + authorizedUrlListLogic, +} from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' + +import type { toolbarSidebarLogicType } from './toolbarType' + +const fuse = new Fuse([], { + keys: ['url'], + threshold: 0.3, + ignoreLocation: true, + includeMatches: true, +}) + +export const toolbarSidebarLogic = kea([ + path(['layout', 'navigation-3000', 'sidebars', 'toolbarSidebarLogic']), + connect(() => ({ + values: [ + authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }), + ['urlsKeyed', 'suggestionsLoading'], + sceneLogic, + ['activeScene', 'sceneParams'], + ], + actions: [ + authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }), + ['addUrl', 'removeUrl', 'updateUrl'], + ], + })), + selectors(({ actions }) => ({ + contents: [ + (s) => [s.relevantUrls, s.suggestionsLoading], + (relevantUrls, suggestionsLoading) => [ + { + key: 'sites', + title: 'Sites', + loading: suggestionsLoading, + items: relevantUrls.map( + ([url, matches]) => + ({ + key: url.url, + name: url.url, + url: url.type !== 'suggestion' ? urls.site(url.url) : null, + tag: url.type === 'suggestion' ? { status: 'warning', text: 'SUGGESTION' } : undefined, + searchMatch: matches + ? { + matchingFields: matches.map((match) => match.key), + nameHighlightRanges: matches.find((match) => match.key === 'url')?.indices, + } + : null, + onRename: + url.type !== 'suggestion' + ? (newUrl) => actions.updateUrl(url.originalIndex, newUrl) + : undefined, + menuItems: + url.type !== 'suggestion' + ? (initiateRename) => [ + { + items: [ + { + to: urls.site(url.url), + targetBlank: true, + label: 'Open in new tab', + }, + ], + }, + { + items: [ + { + onClick: initiateRename, + label: 'Rename', + keyboardShortcut: ['enter'], + }, + { + onClick: () => actions.removeUrl(url.originalIndex), + status: 'danger', + label: 'Delete site', + }, + ], + }, + ] + : [{ onClick: () => actions.addUrl(url.url), label: 'Apply suggestion' }], + } as BasicListItem) + ), + } as SidebarCategory, + ], + ], + activeListItemKey: [ + (s) => [s.activeScene, s.sceneParams], + (activeScene, sceneParams) => { + return activeScene === Scene.Site ? decodeURIComponent(sceneParams.params.url) : null + }, + ], + relevantUrls: [ + (s) => [s.urlsKeyed, navigation3000Logic.selectors.searchTerm], + (urlsKeyed, searchTerm): [KeyedAppUrl, FuseSearchMatch[] | null][] => { + if (searchTerm) { + return fuse.search(searchTerm).map((result) => [result.item, result.matches as FuseSearchMatch[]]) + } + return urlsKeyed.map((url) => [url, null]) + }, + ], + })), + subscriptions({ + urlsKeyed: (urlsKeyed) => { + fuse.setCollection(urlsKeyed) + }, + }), +]) diff --git a/frontend/src/layout/navigation-3000/types.ts b/frontend/src/layout/navigation-3000/types.ts index 4c78df9bc73cd..3be340919aa16 100644 --- a/frontend/src/layout/navigation-3000/types.ts +++ b/frontend/src/layout/navigation-3000/types.ts @@ -1,3 +1,4 @@ +import { LemonTagType } from '@posthog/lemon-ui' import { Logic, LogicWrapper } from 'kea' import { Dayjs } from 'lib/dayjs' import { LemonMenuItems } from 'lib/lemon-ui/LemonMenu' @@ -76,8 +77,7 @@ export interface BasicListItem { /** Whether the name is a placeholder (e.g. an insight derived name), in which case it'll be italicized. */ isNamePlaceholder?: boolean /** - * URL within the app. - * In rare cases this can be explicitly null (e.g. the "Load more" item). Such items are italicized. + * URL within the app. In specific cases this can be null - such items are italicized. */ url: string | null /** An optional marker to highlight item state. */ @@ -90,6 +90,11 @@ export interface BasicListItem { */ status?: 'muted' | 'success' | 'warning' | 'danger' | 'completion' } + /** An optional tag shown as a suffix of the name. */ + tag?: { + status: LemonTagType + text: string + } /** If search is on, this should be present to convey why this item is included in results. */ searchMatch?: SearchMatch | null menuItems?: LemonMenuItems | ((initiateRename?: () => void) => LemonMenuItems) diff --git a/frontend/src/layout/navigation/Navigation.tsx b/frontend/src/layout/navigation/Navigation.tsx index 21042046af8ec..6c6c80a9b9fb3 100644 --- a/frontend/src/layout/navigation/Navigation.tsx +++ b/frontend/src/layout/navigation/Navigation.tsx @@ -15,8 +15,8 @@ export function Navigation({ children }: { children: any }): JSX.Element {
{activeScene !== Scene.Ingestion && } -
- {!sceneConfig?.plain && ( +
+ {sceneConfig?.plain !== 2 && ( <> {!sceneConfig?.hideProjectNotice && } diff --git a/frontend/src/layout/navigation/navigationLogic.ts b/frontend/src/layout/navigation/navigationLogic.ts index e58f6d306d980..dc17eadb1bb53 100644 --- a/frontend/src/layout/navigation/navigationLogic.ts +++ b/frontend/src/layout/navigation/navigationLogic.ts @@ -120,7 +120,10 @@ export const navigationLogic = kea({ }), selectors: { /** `bareNav` whether the current scene should display a sidebar at all */ - bareNav: [(s) => [s.fullscreen, s.sceneConfig], (fullscreen, sceneConfig) => fullscreen || sceneConfig?.plain], + bareNav: [ + (s) => [s.fullscreen, s.sceneConfig], + (fullscreen, sceneConfig) => fullscreen || sceneConfig?.plain === 2, + ], isSideBarShown: [ (s) => [s.mobileLayout, s.isSideBarShownBase, s.isSideBarShownMobile, s.bareNav], (mobileLayout, isSideBarShownBase, isSideBarShownMobile, bareNav) => diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss index 451315918260b..7c188c95ee201 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.scss @@ -49,6 +49,11 @@ background: none; } + &.LemonTag--size-small { + font-size: 0.625rem; + padding: 0.0625rem 0.1875rem; + } + .LemonTag__icon { font-size: 0.875rem; margin-right: 0.125rem; @@ -60,8 +65,4 @@ min-height: 1.5rem; padding: 0.125rem 0.125rem; } - - &.clickable { - cursor: pointer; - } } diff --git a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx index c73af1ea2c014..404eb1835b88c 100644 --- a/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx +++ b/frontend/src/lib/lemon-ui/LemonTag/LemonTag.tsx @@ -18,6 +18,7 @@ export type LemonTagType = interface LemonTagProps extends React.HTMLAttributes { type?: LemonTagType children: React.ReactNode + size?: 'small' | 'medium' icon?: JSX.Element closable?: boolean onClose?: () => void @@ -28,6 +29,7 @@ export function LemonTag({ type = 'default', children, className, + size = 'medium', icon, closable, onClose, @@ -35,7 +37,10 @@ export function LemonTag({ ...props }: LemonTagProps): JSX.Element { return ( -
+
{icon && {icon}} {children} {popover?.overlay && ( diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index e4ac0ceb77d77..7d5efa5012ccd 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -49,6 +49,7 @@ export const appScenes: Record any> = { [Scene.ProjectCreateFirst]: () => import('./project/Create'), [Scene.SystemStatus]: () => import('./instance/SystemStatus'), [Scene.ToolbarLaunch]: () => import('./toolbar-launch/ToolbarLaunch'), + [Scene.Site]: () => import('./sites/Site'), [Scene.AsyncMigrations]: () => import('./instance/AsyncMigrations/AsyncMigrations'), [Scene.DeadLetterQueue]: () => import('./instance/DeadLetterQueue/DeadLetterQueue'), [Scene.MySettings]: () => import('./me/Settings'), diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 8a971f82a819d..b47dba7790f9d 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -58,6 +58,7 @@ export enum Scene { AppMetrics = 'AppMetrics', SavedInsights = 'SavedInsights', ToolbarLaunch = 'ToolbarLaunch', + Site = 'Site', WebPerformance = 'WebPerformance', IntegrationsRedirect = 'IntegrationsRedirect', // Authentication, onboarding & initialization routes @@ -114,8 +115,12 @@ export interface SceneConfig { onlyUnauthenticated?: boolean /** Route **can** be accessed when logged out (i.e. can be accessed when logged in too; should be added to posthog/urls.py too) */ allowUnauthenticated?: boolean - /** Hides most navigation UI, like the sidebar and breadcrumbs. */ - plain?: boolean + /** + * If 1, the scene is rendered with no padding. + * If 2, most navigation UI (sidebar, breadcrumbs) is also hidden. + * Otherwise full navigation is shown. + */ + plain?: 0 | 1 | 2 /** Hides project notice (ProjectNotice.tsx). */ hideProjectNotice?: boolean /** Personal account management (used e.g. by breadcrumbs) */ @@ -124,6 +129,6 @@ export interface SceneConfig { instanceLevel?: boolean /** Route requires organization access (used e.g. by breadcrumbs) */ organizationBased?: boolean - /** Route requires project access (used e.g. by breadcrumbs). `true` implies `organizationBased` */ + /** Route requires project access (used e.g. by breadcrumbs). `true` implies also `organizationBased` */ projectBased?: boolean } diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 41525bd566a84..049826bf0f1e4 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -210,12 +210,17 @@ export const sceneConfigurations: Partial> = { }, [Scene.Ingestion]: { projectBased: true, - plain: true, + plain: 2, }, [Scene.ToolbarLaunch]: { projectBased: true, name: 'Launch Toolbar', }, + [Scene.Site]: { + projectBased: true, + hideProjectNotice: true, + plain: 1, + }, // Organization-based routes [Scene.OrganizationCreateFirst]: { name: 'Organization creation', @@ -252,7 +257,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.InviteSignup]: { allowUnauthenticated: true, - plain: true, + plain: 2, }, // Instance management routes [Scene.SystemStatus]: { @@ -282,7 +287,7 @@ export const sceneConfigurations: Partial> = { }, [Scene.VerifyEmail]: { allowUnauthenticated: true, - plain: true, + plain: 2, }, [Scene.Feedback]: { projectBased: true, @@ -433,6 +438,7 @@ export const routes: Record = { [urls.deadLetterQueue()]: Scene.DeadLetterQueue, [urls.mySettings()]: Scene.MySettings, [urls.toolbarLaunch()]: Scene.ToolbarLaunch, + [urls.site(':url')]: Scene.Site, // Onboarding / setup routes [urls.login()]: Scene.Login, [urls.login2FA()]: Scene.Login2FA, diff --git a/frontend/src/scenes/sites/Site.scss b/frontend/src/scenes/sites/Site.scss new file mode 100644 index 0000000000000..dfdf92f7044a5 --- /dev/null +++ b/frontend/src/scenes/sites/Site.scss @@ -0,0 +1,4 @@ +.Site { + width: 100%; + border: none; +} diff --git a/frontend/src/scenes/sites/Site.tsx b/frontend/src/scenes/sites/Site.tsx new file mode 100644 index 0000000000000..ad487e77da371 --- /dev/null +++ b/frontend/src/scenes/sites/Site.tsx @@ -0,0 +1,30 @@ +import { SceneExport } from 'scenes/sceneTypes' +import './Site.scss' +import { SiteLogicProps, siteLogic } from './siteLogic' +import { AuthorizedUrlListType, authorizedUrlListLogic } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { useValues } from 'kea' + +export const scene: SceneExport = { + component: Site, + paramsToProps: ({ params: { url } }): SiteLogicProps => ({ url: decodeURIComponent(url) }), + logic: siteLogic, +} + +export function Site({ url }: { url?: string } = {}): JSX.Element { + const { launchUrl } = useValues( + authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS }) + ) + + const decodedUrl = decodeURIComponent(url || '') + + return ( +