diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 8d7ef2d216a..10b89d5b5be 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -30,7 +30,17 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 - cache: pnpm + + - name: Get pnpm cache directory path + id: pnpm-cache-dir + run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: pnpm-cache + with: + path: ${{ steps.pnpm-cache-dir.outputs.PNPM_STORE_PATH }} + key: ${{ runner.os }}-pnpm-cypress-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-cypress- - name: Install package.json dependencies with pnpm run: pnpm install --frozen-lockfile diff --git a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png index 0204ecc04cd..8f354b12f2b 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png and b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png b/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png index fe295d1272f..184490f81c8 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png and b/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png differ diff --git a/frontend/src/layout/navigation/SideBar/SideBar.tsx b/frontend/src/layout/navigation/SideBar/SideBar.tsx index 5ec1c5285fc..c3bf0ac1ce5 100644 --- a/frontend/src/layout/navigation/SideBar/SideBar.tsx +++ b/frontend/src/layout/navigation/SideBar/SideBar.tsx @@ -282,7 +282,7 @@ export function SideBar({ children }: { children: React.ReactNode }): JSX.Elemen function AppUrls({ setIsToolbarLaunchShown }: { setIsToolbarLaunchShown: (state: boolean) => void }): JSX.Element { const { authorizedUrls, launchUrl, suggestionsLoading } = useValues( - authorizedUrlListLogic({ type: AuthorizedUrlListType.TOOLBAR_URLS }) + authorizedUrlListLogic({ type: AuthorizedUrlListType.TOOLBAR_URLS, actionId: null }) ) return (
diff --git a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx index f48a1d86753..e227641bc69 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx +++ b/frontend/src/lib/components/AuthorizedUrlList/AuthorizedUrlList.tsx @@ -2,11 +2,7 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { - AuthorizedUrlListType as AuthorizedUrlListType, - authorizedUrlListLogic, - AuthorizedUrlListProps, -} from './authorizedUrlListLogic' +import { AuthorizedUrlListType as AuthorizedUrlListType, authorizedUrlListLogic } from './authorizedUrlListLogic' import { IconDelete, IconEdit, IconOpenInApp, IconPlus } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { Form } from 'kea-forms' @@ -48,7 +44,7 @@ function EmptyState({ } function AuthorizedUrlForm({ actionId, type }: AuthorizedUrlListProps): JSX.Element { - const logic = authorizedUrlListLogic({ actionId, type }) + const logic = authorizedUrlListLogic({ actionId: actionId ?? null, type }) const { isProposedUrlSubmitting } = useValues(logic) const { cancelProposingUrl } = useActions(logic) return ( @@ -78,12 +74,17 @@ function AuthorizedUrlForm({ actionId, type }: AuthorizedUrlListProps): JSX.Elem ) } +export interface AuthorizedUrlListProps { + actionId?: number + type: AuthorizedUrlListType +} + export function AuthorizedUrlList({ actionId, type, addText = 'Add', }: AuthorizedUrlListProps & { addText?: string }): JSX.Element { - const logic = authorizedUrlListLogic({ actionId, type }) + const logic = authorizedUrlListLogic({ actionId: actionId ?? null, type }) const { urlsKeyed, suggestionsLoading, diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts index bfef90a9d4f..1d35529e7ae 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.test.ts @@ -28,6 +28,7 @@ describe('the authorized urls list logic', () => { initKeaTests() logic = authorizedUrlListLogic({ type: AuthorizedUrlListType.TOOLBAR_URLS, + actionId: null, }) logic.mount() }) @@ -106,6 +107,7 @@ describe('the authorized urls list logic', () => { beforeEach(() => { logic = authorizedUrlListLogic({ type: AuthorizedUrlListType.RECORDING_DOMAINS, + actionId: null, }) logic.mount() }) diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts index 034d822f678..a9bd8d0fb24 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts @@ -62,19 +62,18 @@ export const validateProposedURL = ( } /** defaultIntent: whether to launch with empty intent (i.e. toolbar mode is default) */ -export function appEditorUrl(appUrl?: string, actionId?: number, defaultIntent?: boolean): string { - // See - // https://github.com/PostHog/posthog-js/blob/f7119c7542c940354719a9ba8120a08ba25b5ae8/src/extensions/toolbar.ts#L52 - // for where these params are passed. - const params: ToolbarParams = { +export function appEditorUrl(appUrl: string, actionId?: number | null, defaultIntent?: boolean): string { + // See https://github.com/PostHog/posthog-js/blob/f7119c/src/extensions/toolbar.ts#L52 for where these params + // are passed. `appUrl` is an extra `redirect_to_site` param. + const params: ToolbarParams & { appUrl: string } = { userIntent: defaultIntent ? undefined : actionId ? 'edit-action' : 'add-action', // Make sure to pass the app url, otherwise the api_host will be used by // the toolbar, which isn't correct when used behind a reverse proxy as // we require e.g. SSO login to the app, which will not work when placed // behind a proxy unless we register each domain with the OAuth2 client. apiURL: window.location.origin, + appUrl, ...(actionId ? { actionId } : {}), - ...(appUrl ? { appUrl } : {}), } return '/api/user/redirect_to_site/' + encodeParams(params, '?') } @@ -87,14 +86,14 @@ export interface KeyedAppUrl { originalIndex: number } -export interface AuthorizedUrlListProps { - actionId?: number +export interface AuthorizedUrlListLogicProps { + actionId: number | null type: AuthorizedUrlListType } export const authorizedUrlListLogic = kea([ path((key) => ['lib', 'components', 'AuthorizedUrlList', 'authorizedUrlListLogic', key]), key((props) => `${props.type}-${props.actionId}`), - props({} as AuthorizedUrlListProps), + props({} as AuthorizedUrlListLogicProps), connect({ values: [teamLogic, ['currentTeam', 'currentTeamId']], actions: [teamLogic, ['updateCurrentTeam']], @@ -280,7 +279,7 @@ export const authorizedUrlListLogic = kea([ actions.resetProposedUrl() }, })), - selectors(({ props }) => ({ + selectors({ urlToEdit: [ (s) => [s.authorizedUrls, s.editUrlIndex], (authorizedUrls, editUrlIndex) => { @@ -319,10 +318,10 @@ export const authorizedUrlListLogic = kea([ .map((result) => result.item) }, ], - launchUrl: [() => [], () => (url: string) => appEditorUrl(url, props.actionId, !props.actionId)], + launchUrl: [(_, p) => [p.actionId], (actionId) => (url: string) => appEditorUrl(url, actionId, !actionId)], isAddUrlFormVisible: [(s) => [s.editUrlIndex], (editUrlIndex) => editUrlIndex === -1], - onlyAllowDomains: [() => [], () => props.type === AuthorizedUrlListType.RECORDING_DOMAINS], - })), + onlyAllowDomains: [(_, p) => [p.type], (type) => type === AuthorizedUrlListType.RECORDING_DOMAINS], + }), urlToAction(({ actions }) => ({ [urls.toolbarLaunch()]: (_, searchParams) => { if (searchParams.addNew) { diff --git a/frontend/src/lib/components/HelpButton/HelpButton.tsx b/frontend/src/lib/components/HelpButton/HelpButton.tsx index f036e31955f..0d19537ad86 100644 --- a/frontend/src/lib/components/HelpButton/HelpButton.tsx +++ b/frontend/src/lib/components/HelpButton/HelpButton.tsx @@ -3,27 +3,26 @@ import { kea, useActions, useValues } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { HelpType } from '~/types' import type { helpButtonLogicType } from './HelpButtonType' -import { Popover } from 'lib/lemon-ui/Popover/Popover' -import { LemonButton } from 'lib/lemon-ui/LemonButton' import { IconArrowDropDown, IconArticle, IconHelpOutline, - IconMail, IconQuestionAnswer, IconMessages, IconFlare, - IconTrendingUp, IconLive, + IconSupport, + IconFeedback, + IconBugReport, } from 'lib/lemon-ui/icons' import clsx from 'clsx' import { Placement } from '@floating-ui/react' import { DefaultAction, inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' import { hedgehogbuddyLogic } from '../HedgehogBuddy/hedgehogbuddyLogic' import { HedgehogBuddyWithLogic } from '../HedgehogBuddy/HedgehogBuddy' -import { navigationLogic } from '~/layout/navigation/navigationLogic' import { supportLogic } from '../Support/supportLogic' -import SupportForm from '../Support/SupportForm' +import { SupportModal } from '../Support/SupportModal' +import { LemonMenu } from 'lib/lemon-ui/LemonMenu' const HELP_UTM_TAGS = '?utm_medium=in-product&utm_campaign=help-button-top' @@ -88,111 +87,103 @@ export function HelpButton({ const { isPromptVisible } = useValues(inAppPromptLogic) const { hedgehogModeEnabled } = useValues(hedgehogbuddyLogic) const { setHedgehogModeEnabled } = useActions(hedgehogbuddyLogic) - const { toggleActivationSideBar } = useActions(navigationLogic) const { openSupportForm } = useActions(supportLogic) return ( <> - - {!contactOnly && ( - } - status="stealth" - fullWidth - onClick={() => { + , + label: "What's new?", + onClick: () => { reportHelpButtonUsed(HelpType.Updates) hideHelp() - }} - to={`https://posthog.com/blog/categories/posthog-news`} - targetBlank - > - What's new? - - )} - } - status="stealth" - fullWidth - onClick={() => { - reportHelpButtonUsed(HelpType.SupportForm) - openSupportForm() - hideHelp() - }} - > - Report bug / get support - - {!contactOnly && ( - } - status="stealth" - fullWidth - onClick={() => { + }, + to: 'https://posthog.com/blog/categories/posthog-news', + targetBlank: true, + }, + ], + }, + { + items: [ + { + label: 'Ask on the forum', + icon: , + onClick: () => { + reportHelpButtonUsed(HelpType.Slack) + hideHelp() + }, + to: `https://posthog.com/questions${HELP_UTM_TAGS}`, + targetBlank: true, + }, + { + label: 'Report a bug', + icon: , + onClick: () => { + reportHelpButtonUsed(HelpType.SupportForm) + openSupportForm('bug') + hideHelp() + }, + }, + { + label: 'Give feedback', + icon: , + onClick: () => { + reportHelpButtonUsed(HelpType.SupportForm) + openSupportForm('feedback') + hideHelp() + }, + }, + { + label: 'Get support', + icon: , + onClick: () => { + reportHelpButtonUsed(HelpType.SupportForm) + openSupportForm('support') + hideHelp() + }, + }, + ], + }, + !contactOnly && { + items: [ + { + label: 'Read the docs', + icon: , + onClick: () => { reportHelpButtonUsed(HelpType.Docs) hideHelp() - }} - to={`https://posthog.com/docs${HELP_UTM_TAGS}`} - targetBlank - > - Read the docs - - )} - } - status="stealth" - fullWidth - onClick={() => { - reportHelpButtonUsed(HelpType.Slack) - hideHelp() - }} - to={`https://posthog.com/questions${HELP_UTM_TAGS}`} - targetBlank - > - Ask a question on our forum - - } - status="stealth" - fullWidth - onClick={() => { - toggleActivationSideBar() - hideHelp() - }} - > - Open Quick Start - - {validProductTourSequences.length > 0 && ( - } - status="stealth" - fullWidth - onClick={() => { + }, + to: `https://posthog.com/docs${HELP_UTM_TAGS}`, + targetBlank: true, + }, + validProductTourSequences.length > 0 && { + label: isPromptVisible ? 'Stop tutorial' : 'Explain this page', + icon: , + onClick: () => { if (isPromptVisible) { promptAction(DefaultAction.SKIP) } else { runFirstValidSequence({ runDismissedOrCompleted: true }) } hideHelp() - }} - > - {isPromptVisible ? 'Stop tutorial' : 'Explain this page'} - - )} - } - status="stealth" - fullWidth - onClick={() => { - setHedgehogModeEnabled(!hedgehogModeEnabled) - hideHelp() - }} - > - {hedgehogModeEnabled ? 'Disable' : 'Enable'} Hedgehog Mode - - - } - onClickOutside={hideHelp} + }, + }, + { + label: `${hedgehogModeEnabled ? 'Disable' : 'Enable'} hedgehog mode`, + icon: , + onClick: () => { + setHedgehogModeEnabled(!hedgehogModeEnabled) + hideHelp() + }, + }, + ], + }, + ]} + onVisibilityChange={(visible) => !visible && hideHelp()} visible={isHelpVisible} placement={placement} actionable @@ -205,9 +196,9 @@ export function HelpButton({ )}
- + - + ) } diff --git a/frontend/src/lib/components/Support/SupportForm.tsx b/frontend/src/lib/components/Support/SupportForm.tsx deleted file mode 100644 index 2e6886403ea..00000000000 --- a/frontend/src/lib/components/Support/SupportForm.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useActions, useValues } from 'kea' -import { TargetAreaToName, supportLogic } from './supportLogic' -import { Form } from 'kea-forms' -import { LemonButton } from 'lib/lemon-ui/LemonButton/LemonButton' -import { LemonModal } from 'lib/lemon-ui/LemonModal/LemonModal' -import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect/LemonSelect' -import { Field } from 'lib/forms/Field' -import { capitalizeFirstLetter } from 'lib/utils' - -export default function SupportForm(): JSX.Element { - const { isSupportFormOpen } = useValues(supportLogic) - const { closeSupportForm } = useActions(supportLogic) - - return ( - -
- - Cancel - - - Submit - -
- - } - > -
- - ({ - value: key, - label: capitalizeFirstLetter(key), - }))} - /> - - - ({ - label: value, - value: key, - }))} - /> - - - - -
-
- ) -} diff --git a/frontend/src/lib/components/Support/SupportModal.tsx b/frontend/src/lib/components/Support/SupportModal.tsx new file mode 100644 index 00000000000..bda8633fc16 --- /dev/null +++ b/frontend/src/lib/components/Support/SupportModal.tsx @@ -0,0 +1,125 @@ +import { useActions, useValues } from 'kea' +import { SupportTicketKind, TARGET_AREA_TO_NAME, supportLogic } from './supportLogic' +import { Form } from 'kea-forms' +import { LemonButton } from 'lib/lemon-ui/LemonButton/LemonButton' +import { LemonModal } from 'lib/lemon-ui/LemonModal/LemonModal' +import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { LemonSelect, LemonSelectOptions } from 'lib/lemon-ui/LemonSelect/LemonSelect' +import { Field } from 'lib/forms/Field' +import { IconBugReport, IconFeedback, IconSupport } from 'lib/lemon-ui/icons' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { LemonFileInput, useUploadFiles } from 'lib/lemon-ui/LemonFileInput/LemonFileInput' +import { useRef } from 'react' +import { lemonToast } from '@posthog/lemon-ui' + +const SUPPORT_TICKET_OPTIONS: LemonSelectOptions = [ + { + value: 'bug', + label: 'Bug', + icon: , + }, + { + value: 'feedback', + label: 'Feedback', + icon: , + }, + { + value: 'support', + label: 'Support', + icon: , + }, +] +const SUPPORT_TICKET_KIND_TO_TITLE: Record = { + bug: 'Report a bug', + feedback: 'Give feedback', + support: 'Get support', +} +const SUPPORT_TICKET_KIND_TO_PROMPT: Record = { + bug: "What's the bug?", + feedback: 'What feedback do you have?', + support: 'What can we help you with?', +} + +export function SupportModal(): JSX.Element { + const { sendSupportRequest, isSupportFormOpen } = useValues(supportLogic) + const { setSendSupportRequestValue, closeSupportForm } = useActions(supportLogic) + const { objectStorageAvailable } = useValues(preflightLogic) + + const dropRef = useRef(null) + + const { setFilesToUpload, filesToUpload, uploading } = useUploadFiles({ + onUpload: (url, fileName) => { + setSendSupportRequestValue('message', sendSupportRequest.message + `\n\n![${fileName}](${url})`) + }, + onError: (detail) => { + lemonToast.error(`Error uploading image: ${detail}`) + }, + }) + + return ( + + + Cancel + + + Submit + + + } + > +
+ + + + + ({ + label: value, + value: key, + }))} + /> + + + {(props) => ( +
+ + {objectStorageAvailable && ( + + )} +
+ )} +
+
+
+ ) +} diff --git a/frontend/src/lib/components/Support/supportLogic.ts b/frontend/src/lib/components/Support/supportLogic.ts index c6d8cc8d7fa..e184123ec4e 100644 --- a/frontend/src/lib/components/Support/supportLogic.ts +++ b/frontend/src/lib/components/Support/supportLogic.ts @@ -8,6 +8,7 @@ import { forms } from 'kea-forms' import { UserType } from '~/types' import { lemonToast } from 'lib/lemon-ui/lemonToast' import { actionToUrl, router, urlToAction } from 'kea-router' +import { captureException } from '@sentry/react' function getSessionReplayLink(): string { const LOOK_BACK = 30 @@ -28,24 +29,24 @@ function getDjangoAdminLink(user: UserType | null): string { return `\nAdmin link: ${link} (Organization: '${user.organization?.name}'; Project: '${user.team?.name}')` } -export const TargetAreaToName = { - analytics: 'Analytics', +export const TARGET_AREA_TO_NAME = { app_performance: 'App Performance', apps: 'Apps', + login: 'Authentication (Login / Sign-up / Invites)', billing: 'Billing', cohorts: 'Cohorts', - data_management: 'Data Management', data_integrity: 'Data Integrity', - ingestion: 'Events Ingestion', + data_management: 'Data Management', + ingestion: 'Event Ingestion', experiments: 'Experiments', feature_flags: 'Feature Flags', - login: 'Login / Sign up / Invites', - session_replay: 'Session Replay', + analytics: 'Product Analytics (Insights, Dashboards, Annotations)', + session_replay: 'Session Replay (Recordings)', } -export type supportTicketTargetArea = keyof typeof TargetAreaToName | null -export type supportTicketKind = 'bug' | 'feedback' | null +export type SupportTicketTargetArea = keyof typeof TARGET_AREA_TO_NAME +export type SupportTicketKind = 'bug' | 'feedback' | 'support' -export const URLPathToTargetArea: Record = { +export const URL_PATH_TO_TARGET_AREA: Record = { insights: 'analytics', recordings: 'session_replay', replay: 'session_replay', @@ -63,9 +64,9 @@ export const URLPathToTargetArea: Record = { toolbar: 'analytics', } -export function getURLPathToTargetArea(pathname: string): supportTicketTargetArea | null { +export function getURLPathToTargetArea(pathname: string): SupportTicketTargetArea | null { const first_part = pathname.split('/')[1] - return URLPathToTargetArea[first_part] ?? null + return URL_PATH_TO_TARGET_AREA[first_part] ?? null } export const supportLogic = kea([ @@ -75,11 +76,18 @@ export const supportLogic = kea([ })), actions(() => ({ closeSupportForm: () => true, - openSupportForm: (kind: supportTicketKind = null, target_area: supportTicketTargetArea = null) => ({ + openSupportForm: ( + kind: SupportTicketKind | null = null, + target_area: SupportTicketTargetArea | null = null + ) => ({ kind, target_area, }), - submitZendeskTicket: (kind: supportTicketKind, target_area: supportTicketTargetArea, message: string) => ({ + submitZendeskTicket: ( + kind: SupportTicketKind | null, + target_area: SupportTicketTargetArea | null, + message: string + ) => ({ kind, target_area, message, @@ -97,8 +105,8 @@ export const supportLogic = kea([ forms(({ actions }) => ({ sendSupportRequest: { defaults: {} as unknown as { - kind: supportTicketKind - target_area: supportTicketTargetArea + kind: SupportTicketKind | null + target_area: SupportTicketTargetArea | null message: string }, errors: ({ message, kind, target_area }) => { @@ -162,12 +170,13 @@ export const supportLogic = kea([ } posthog.capture('support_ticket', properties) lemonToast.success( - 'Got it! The relevant team will check it out and aim to respond via email if necessary.' + "Got the message! If we have follow-up information for you, we'll reply via email." ) }) .catch((err) => { + captureException(err) console.log(err) - lemonToast.error('Failed to submit form.') + lemonToast.error(`There was an error sending the message.`) }) }, })), @@ -179,7 +188,7 @@ export const supportLogic = kea([ actions.openSupportForm( ['bug', 'feedback'].includes(kind) ? kind : null, - Object.keys(TargetAreaToName).includes(area) ? area : null + Object.keys(TARGET_AREA_TO_NAME).includes(area) ? area : null ) } }, diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx index 26a323fd7e3..348d8f5dae8 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx @@ -179,6 +179,7 @@ export const LemonFileInput = ({ onChange={onInputChange} /> Click or drag and drop to upload + {accept ? ` ${acceptToDisplayName(accept)}` : ''}
{files.map((x, i) => ( @@ -196,3 +197,14 @@ const lazyImageBlobReducer = async (blob: Blob): Promise => { const blobReducer = (await import('image-blob-reduce')).default() return blobReducer.toBlob(blob, { max: 2000 }) } + +function acceptToDisplayName(accept: string): string { + const match = accept.match(/(\w+)\/\*/) + if (match) { + return `${match[1]}s` + } + if (accept.startsWith('.')) { + return `${accept.slice(1).toUpperCase()} files` + } + return `files` +} diff --git a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx index 209fca84ca8..86db7571430 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx +++ b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx @@ -17,7 +17,7 @@ export interface LemonMenuItemBase label: string | JSX.Element } export interface LemonMenuItemNode extends LemonMenuItemBase { - items: LemonMenuItemLeaf[] + items: (LemonMenuItemLeaf | false | null)[] } export type LemonMenuItemLeaf = | (LemonMenuItemBase & { @@ -25,20 +25,22 @@ export type LemonMenuItemLeaf = }) | (LemonMenuItemBase & { to: string + targetBlank?: boolean }) | (LemonMenuItemBase & { onClick: () => void to: string + targetBlank?: boolean }) export type LemonMenuItem = LemonMenuItemLeaf | LemonMenuItemNode export interface LemonMenuSection { title?: string | React.ReactNode - items: LemonMenuItem[] + items: (LemonMenuItem | false | null)[] footer?: string | React.ReactNode } -export type LemonMenuItems = (LemonMenuItem | LemonMenuSection)[] +export type LemonMenuItems = (LemonMenuItem | LemonMenuSection | false | null)[] export interface LemonMenuProps extends Pick< @@ -56,13 +58,19 @@ export interface LemonMenuProps LemonMenuOverlayProps { /** Must support `ref` and `onKeyDown` for keyboard navigation. */ children: React.ReactElement - /** Optional index of the active (e.g. selected) item. This improves the keyboard navigation experience. */ + /** Index of the active (e.g. selected) item, if there is a specific one. */ activeItemIndex?: number } -export function LemonMenu({ items, activeItemIndex, tooltipPlacement, ...dropdownProps }: LemonMenuProps): JSX.Element { +export function LemonMenu({ + items, + activeItemIndex, + tooltipPlacement, + onVisibilityChange, + ...dropdownProps +}: LemonMenuProps): JSX.Element { const { referenceRef, itemsRef } = useKeyboardNavigation( - items.flatMap((item) => (isLemonMenuSection(item) ? item.items : item)).length, + items.flatMap((item) => (item && isLemonMenuSection(item) ? item.items : item)).length, activeItemIndex ) @@ -71,6 +79,16 @@ export function LemonMenu({ items, activeItemIndex, tooltipPlacement, ...dropdow overlay={} closeOnClickInside referenceRef={referenceRef} + onVisibilityChange={(visible) => { + onVisibilityChange?.(visible) + if (visible && activeItemIndex && activeItemIndex > -1) { + // Scroll the active item into view once the menu is open (i.e. in the next tick) + setTimeout( + () => itemsRef?.current?.[activeItemIndex]?.current?.scrollIntoView({ block: 'center' }), + 0 + ) + } + }} {...dropdownProps} /> ) @@ -135,7 +153,7 @@ export function LemonMenuSectionList({ ) ) : null} 0) { sections.push(implicitSection) @@ -239,7 +260,7 @@ function normalizeItems(sectionsAndItems: (LemonMenuItem | LemonMenuSection)[]): } if (sections.length === 1 && !sections[0].title && !sections[0].footer) { - return sections[0].items + return sections[0].items.filter(Boolean) as LemonMenuItem[] } return sections } diff --git a/frontend/src/lib/lemon-ui/LemonMenu/useKeyboardNavigation.ts b/frontend/src/lib/lemon-ui/LemonMenu/useKeyboardNavigation.ts index 99cee3b6d90..41744607d9b 100644 --- a/frontend/src/lib/lemon-ui/LemonMenu/useKeyboardNavigation.ts +++ b/frontend/src/lib/lemon-ui/LemonMenu/useKeyboardNavigation.ts @@ -50,7 +50,7 @@ export function useKeyboardNavigation({ actionable className={menu?.className} maxContentWidth={dropdownMaxContentWidth} - activeItemIndex={items.flatMap((i) => (isLemonMenuSection(i) ? i.items : i)).findIndex((i) => i.active)} + activeItemIndex={items + .flatMap((i) => (isLemonMenuSection(i) ? i.items.filter(Boolean) : i)) + .findIndex((i) => (i as LemonMenuItem).active)} closeParentPopoverOnClickInside={menu?.closeParentPopoverOnClickInside} > ( options: LemonSelectOptions, activeValue: T | null, onSelect: OnSelect -): [LemonMenuItems, LemonSelectOptionLeaf[]] { +): [(LemonMenuItem | LemonMenuSection)[], LemonSelectOptionLeaf[]] { const leafOptionsAccumulator: LemonSelectOptionLeaf[] = [] - const items: LemonMenuItems = options.map((option) => + const items: (LemonMenuItem | LemonMenuSection)[] = options.map((option) => convertToMenuSingle(option, activeValue, onSelect, leafOptionsAccumulator) ) return [items, leafOptionsAccumulator] diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx index 86503e9f218..b1e2f3edcce 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx @@ -62,9 +62,10 @@ export const LemonTextArea = React.forwardRef void + placeholder?: string + 'data-attr'?: string } export function LemonTextMarkdown({ value, onChange, ...editAreaProps }: LemonTextMarkdownProps): JSX.Element { diff --git a/frontend/src/lib/lemon-ui/icons/icons.tsx b/frontend/src/lib/lemon-ui/icons/icons.tsx index d621d7070ca..71358e76361 100644 --- a/frontend/src/lib/lemon-ui/icons/icons.tsx +++ b/frontend/src/lib/lemon-ui/icons/icons.tsx @@ -235,8 +235,8 @@ export function IconTrendUp(props: LemonIconProps): JSX.Element { ) } -/** Material Design Announcement icon. */ -export function IconFeedbackWarning(props: LemonIconProps): JSX.Element { +/** Material Design Feedback / Announcement icon. */ +export function IconFeedback(props: LemonIconProps): JSX.Element { return ( + + + ) +} + /** Material Design Push Pin icon, outlined. */ export function IconPinOutline(props: LemonIconProps): JSX.Element { return ( @@ -2362,6 +2374,16 @@ export function IconDragHandle(props: LemonIconProps): JSX.Element { ) } +export function IconBugReport(props: LemonIconProps): JSX.Element { + return ( + + + + ) +} /** https://pictogrammers.com/library/mdi/icon/shield-bug-outline/ */ export function IconBugShield(props: LemonIconProps): JSX.Element { diff --git a/frontend/src/scenes/apps/appMetricsSceneLogic.ts b/frontend/src/scenes/apps/appMetricsSceneLogic.ts index f95d555172b..cb3cdd918f5 100644 --- a/frontend/src/scenes/apps/appMetricsSceneLogic.ts +++ b/frontend/src/scenes/apps/appMetricsSceneLogic.ts @@ -212,17 +212,15 @@ export const appMetricsSceneLogic = kea([ () => INITIAL_TABS.filter((tab) => values.showTab(tab))[0] ?? AppMetricsTab.History, ], - currentTime: [() => [], () => Date.now()], - defaultDateFrom: [ - (s) => [s.pluginConfig, s.currentTime], - (pluginConfig, currentTime) => { + (s) => [s.pluginConfig], + (pluginConfig) => { if (!pluginConfig?.created_at) { return DEFAULT_DATE_FROM } const installedAt = dayjs.utc(pluginConfig.created_at) - const daysSinceInstall = dayjs(currentTime).diff(installedAt, 'days', true) + const daysSinceInstall = dayjs().diff(installedAt, 'days', true) if (daysSinceInstall <= 1) { return '-24h' } else if (daysSinceInstall <= 7) { diff --git a/frontend/src/scenes/experiments/Experiments.tsx b/frontend/src/scenes/experiments/Experiments.tsx index 379fccb3088..9ea4745ebb7 100644 --- a/frontend/src/scenes/experiments/Experiments.tsx +++ b/frontend/src/scenes/experiments/Experiments.tsx @@ -1,6 +1,6 @@ import { PageHeader } from 'lib/components/PageHeader' import { SceneExport } from 'scenes/sceneTypes' -import { experimentsLogic } from './experimentsLogic' +import { experimentsLogic, getExperimentStatus } from './experimentsLogic' import { useActions, useValues } from 'kea' import { LemonTable, LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' @@ -25,8 +25,7 @@ export const scene: SceneExport = { } export function Experiments(): JSX.Element { - const { filteredExperiments, experimentsLoading, tab, getExperimentStatus, searchTerm } = - useValues(experimentsLogic) + const { filteredExperiments, experimentsLoading, tab, searchTerm } = useValues(experimentsLogic) const { setExperimentsTab, deleteExperiment, setSearchStatus, setSearchTerm } = useActions(experimentsLogic) const { hasAvailableFeature } = useValues(userLogic) diff --git a/frontend/src/scenes/experiments/experimentsLogic.ts b/frontend/src/scenes/experiments/experimentsLogic.ts index 511d7cc7df7..d51d0f72902 100644 --- a/frontend/src/scenes/experiments/experimentsLogic.ts +++ b/frontend/src/scenes/experiments/experimentsLogic.ts @@ -9,6 +9,15 @@ import { userLogic } from 'scenes/userLogic' import { subscriptions } from 'kea-subscriptions' import { loaders } from 'kea-loaders' +export function getExperimentStatus(experiment: Experiment): ExperimentStatus { + if (!experiment.start_date) { + return ExperimentStatus.Draft + } else if (!experiment.end_date) { + return ExperimentStatus.Running + } + return ExperimentStatus.Complete +} + export const experimentsLogic = kea([ path(['scenes', 'experiments', 'experimentsLogic']), connect({ values: [teamLogic, ['currentTeamId'], userLogic, ['user', 'hasAvailableFeature']] }), @@ -57,27 +66,9 @@ export const experimentsLogic = kea([ ], })), selectors(({ values }) => ({ - getExperimentStatus: [ - (s) => [s.experiments], - () => - (experiment: Experiment): ExperimentStatus => { - if (!experiment.start_date) { - return ExperimentStatus.Draft - } else if (!experiment.end_date) { - return ExperimentStatus.Running - } - return ExperimentStatus.Complete - }, - ], filteredExperiments: [ - (selectors) => [ - selectors.experiments, - selectors.searchTerm, - selectors.searchStatus, - selectors.tab, - selectors.getExperimentStatus, - ], - (experiments, searchTerm, searchStatus, tab, getExperimentStatus) => { + (selectors) => [selectors.experiments, selectors.searchTerm, selectors.searchStatus, selectors.tab], + (experiments, searchTerm, searchStatus, tab) => { let filteredExperiments: Experiment[] = experiments if (tab === ExperimentsTabs.Archived) { @@ -108,8 +99,8 @@ export const experimentsLogic = kea([ }, ], hasExperimentAvailableFeature: [ - () => [], - (): boolean => values.hasAvailableFeature(AvailableFeature.EXPERIMENTATION), + (s) => [s.hasAvailableFeature], + (hasAvailableFeature): boolean => hasAvailableFeature(AvailableFeature.EXPERIMENTATION), ], })), events(({ actions }) => ({ diff --git a/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx b/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx index 9d2a7ffe3a4..2df30f62bf6 100644 --- a/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/SamplingFilter.tsx @@ -1,5 +1,5 @@ import { FilterType, InsightLogicProps, QueryEditorFilterProps } from '~/types' -import { LemonButton, LemonLabel, LemonSwitch } from '@posthog/lemon-ui' +import { LemonButton, LemonLabel, LemonSwitch, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { AVAILABLE_SAMPLING_PERCENTAGES, samplingFilterLogic } from './samplingFilterLogic' import posthog from 'posthog-js' @@ -50,7 +50,7 @@ export function SamplingFilter({ info={infoTooltipContent || DEFAULT_SAMPLING_INFO_TOOLTIP_CONTENT} infoLink="https://posthog.com/manual/sampling" > - Sampling (Beta) + SamplingBETA

- Adjust your - funnel definition to improve correlation analysis + Adjust your funnel + definition to improve correlation analysis

diff --git a/package.json b/package.json index b5dee92af7a..1a853cf4789 100644 --- a/package.json +++ b/package.json @@ -240,7 +240,7 @@ "jest-canvas-mock": "^2.4.0", "jest-environment-jsdom": "^29.3.1", "jest-image-snapshot": "^6.1.0", - "kea-typegen": "^3.1.5", + "kea-typegen": "^3.1.6", "less": "^3.12.2", "less-loader": "^7.0.2", "lint-staged": "~10.2.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57c19518561..b4dbd62fe70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,8 +522,8 @@ devDependencies: specifier: ^6.1.0 version: 6.1.0(jest@29.3.1) kea-typegen: - specifier: ^3.1.5 - version: 3.1.5(typescript@4.9.5) + specifier: ^3.1.6 + version: 3.1.6(typescript@4.9.5) less: specifier: ^3.12.2 version: 3.13.1 @@ -13394,8 +13394,8 @@ packages: kea-waitfor: 0.2.1(kea@3.1.5) dev: false - /kea-typegen@3.1.5(typescript@4.9.5): - resolution: {integrity: sha512-ocQUbGcUoAc3C23wgSYjMkwXkBvD3IKwbTgtBfP4K3iz8R02BGxwbiqpxIrtCGVDImx0p+L2UDz8YX0ADkolAQ==} + /kea-typegen@3.1.6(typescript@4.9.5): + resolution: {integrity: sha512-Rbdr2+oW4N7S4WsTpMv8DdazpPOCSeKa4xbpMfkA2Qy6lNmr1SLMFgx43Ei/lDVLO52H04C5/qISIdqjQ6hpvQ==} hasBin: true peerDependencies: typescript: '>=4.5.3'