mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 18:07:17 +01:00
chore(support): Improve bug/feedback/support modal experience (#15312)
* Use proper beta tag for sampling * Auto scroll to selected item in menu * Revamp support modal * Add file uploads * Fix modal footer alignment * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) * Update toast message * Update kea-typegen to fix logic type * Update frontend/src/lib/components/Support/supportLogic.ts Co-authored-by: Tiina Turban <tiina303@gmail.com> * Try to fix pnpm cache in CI * Fix typing * Refactor away nonsense selectors in `experimentsLogic` * Refactor away more selectors with empty deps --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tiina Turban <tiina303@gmail.com>
This commit is contained in:
parent
a5544cf7e4
commit
4f9c9e7a72
12
.github/workflows/ci-frontend.yml
vendored
12
.github/workflows/ci-frontend.yml
vendored
@ -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
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.5 KiB |
@ -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 (
|
||||
<div className="SideBar__side-actions" data-attr="sidebar-launch-toolbar">
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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<authorizedUrlListLogicType>([
|
||||
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<authorizedUrlListLogicType>([
|
||||
actions.resetProposedUrl()
|
||||
},
|
||||
})),
|
||||
selectors(({ props }) => ({
|
||||
selectors({
|
||||
urlToEdit: [
|
||||
(s) => [s.authorizedUrls, s.editUrlIndex],
|
||||
(authorizedUrls, editUrlIndex) => {
|
||||
@ -319,10 +318,10 @@ export const authorizedUrlListLogic = kea<authorizedUrlListLogicType>([
|
||||
.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) {
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Popover
|
||||
overlay={
|
||||
<>
|
||||
{!contactOnly && (
|
||||
<LemonButton
|
||||
icon={<IconLive />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
<LemonMenu
|
||||
items={[
|
||||
!contactOnly && {
|
||||
items: [
|
||||
{
|
||||
icon: <IconLive />,
|
||||
label: "What's new?",
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.Updates)
|
||||
hideHelp()
|
||||
}}
|
||||
to={`https://posthog.com/blog/categories/posthog-news`}
|
||||
targetBlank
|
||||
>
|
||||
What's new?
|
||||
</LemonButton>
|
||||
)}
|
||||
<LemonButton
|
||||
icon={<IconMail />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
reportHelpButtonUsed(HelpType.SupportForm)
|
||||
openSupportForm()
|
||||
hideHelp()
|
||||
}}
|
||||
>
|
||||
Report bug / get support
|
||||
</LemonButton>
|
||||
{!contactOnly && (
|
||||
<LemonButton
|
||||
icon={<IconArticle />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
},
|
||||
to: 'https://posthog.com/blog/categories/posthog-news',
|
||||
targetBlank: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: 'Ask on the forum',
|
||||
icon: <IconQuestionAnswer />,
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.Slack)
|
||||
hideHelp()
|
||||
},
|
||||
to: `https://posthog.com/questions${HELP_UTM_TAGS}`,
|
||||
targetBlank: true,
|
||||
},
|
||||
{
|
||||
label: 'Report a bug',
|
||||
icon: <IconBugReport />,
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.SupportForm)
|
||||
openSupportForm('bug')
|
||||
hideHelp()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Give feedback',
|
||||
icon: <IconFeedback />,
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.SupportForm)
|
||||
openSupportForm('feedback')
|
||||
hideHelp()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Get support',
|
||||
icon: <IconSupport />,
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.SupportForm)
|
||||
openSupportForm('support')
|
||||
hideHelp()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
!contactOnly && {
|
||||
items: [
|
||||
{
|
||||
label: 'Read the docs',
|
||||
icon: <IconArticle />,
|
||||
onClick: () => {
|
||||
reportHelpButtonUsed(HelpType.Docs)
|
||||
hideHelp()
|
||||
}}
|
||||
to={`https://posthog.com/docs${HELP_UTM_TAGS}`}
|
||||
targetBlank
|
||||
>
|
||||
Read the docs
|
||||
</LemonButton>
|
||||
)}
|
||||
<LemonButton
|
||||
icon={<IconQuestionAnswer />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
reportHelpButtonUsed(HelpType.Slack)
|
||||
hideHelp()
|
||||
}}
|
||||
to={`https://posthog.com/questions${HELP_UTM_TAGS}`}
|
||||
targetBlank
|
||||
>
|
||||
Ask a question on our forum
|
||||
</LemonButton>
|
||||
<LemonButton
|
||||
icon={<IconTrendingUp />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
toggleActivationSideBar()
|
||||
hideHelp()
|
||||
}}
|
||||
>
|
||||
Open Quick Start
|
||||
</LemonButton>
|
||||
{validProductTourSequences.length > 0 && (
|
||||
<LemonButton
|
||||
icon={<IconMessages />}
|
||||
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: <IconMessages />,
|
||||
onClick: () => {
|
||||
if (isPromptVisible) {
|
||||
promptAction(DefaultAction.SKIP)
|
||||
} else {
|
||||
runFirstValidSequence({ runDismissedOrCompleted: true })
|
||||
}
|
||||
hideHelp()
|
||||
}}
|
||||
>
|
||||
{isPromptVisible ? 'Stop tutorial' : 'Explain this page'}
|
||||
</LemonButton>
|
||||
)}
|
||||
<LemonButton
|
||||
icon={<IconFlare />}
|
||||
status="stealth"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
setHedgehogModeEnabled(!hedgehogModeEnabled)
|
||||
hideHelp()
|
||||
}}
|
||||
>
|
||||
{hedgehogModeEnabled ? 'Disable' : 'Enable'} Hedgehog Mode
|
||||
</LemonButton>
|
||||
</>
|
||||
}
|
||||
onClickOutside={hideHelp}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `${hedgehogModeEnabled ? 'Disable' : 'Enable'} hedgehog mode`,
|
||||
icon: <IconFlare />,
|
||||
onClick: () => {
|
||||
setHedgehogModeEnabled(!hedgehogModeEnabled)
|
||||
hideHelp()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
onVisibilityChange={(visible) => !visible && hideHelp()}
|
||||
visible={isHelpVisible}
|
||||
placement={placement}
|
||||
actionable
|
||||
@ -205,9 +196,9 @@ export function HelpButton({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
</LemonMenu>
|
||||
<HedgehogBuddyWithLogic />
|
||||
<SupportForm />
|
||||
<SupportModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<LemonModal
|
||||
isOpen={isSupportFormOpen}
|
||||
onClose={closeSupportForm}
|
||||
title={'Bug / Feedback'}
|
||||
description="We're using aggregate bug reports for prioritization and won't get back to everyone individually."
|
||||
footer={
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<LemonButton form="support-modal-form" type="secondary" onClick={closeSupportForm}>
|
||||
Cancel
|
||||
</LemonButton>
|
||||
<LemonButton form="support-modal-form" htmlType="submit" type="primary" data-attr="submit">
|
||||
Submit
|
||||
</LemonButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
logic={supportLogic}
|
||||
formKey="sendSupportRequest"
|
||||
id="support-modal-form"
|
||||
enableFormOnSubmit
|
||||
className="space-y-4"
|
||||
>
|
||||
<Field name="kind" label="What kind of request is this?">
|
||||
<LemonSelect
|
||||
fullWidth
|
||||
options={['bug', 'feedback'].map((key) => ({
|
||||
value: key,
|
||||
label: capitalizeFirstLetter(key),
|
||||
}))}
|
||||
/>
|
||||
</Field>
|
||||
<Field name="target_area" label="What area does it best relate to?">
|
||||
<LemonSelect
|
||||
fullWidth
|
||||
options={Object.entries(TargetAreaToName).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
</Field>
|
||||
<Field name="message" label="Content">
|
||||
<LemonTextArea placeholder="Type your message here" data-attr="support-form-content-input" />
|
||||
</Field>
|
||||
</Form>
|
||||
</LemonModal>
|
||||
)
|
||||
}
|
125
frontend/src/lib/components/Support/SupportModal.tsx
Normal file
125
frontend/src/lib/components/Support/SupportModal.tsx
Normal file
@ -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<SupportTicketKind> = [
|
||||
{
|
||||
value: 'bug',
|
||||
label: 'Bug',
|
||||
icon: <IconBugReport />,
|
||||
},
|
||||
{
|
||||
value: 'feedback',
|
||||
label: 'Feedback',
|
||||
icon: <IconFeedback />,
|
||||
},
|
||||
{
|
||||
value: 'support',
|
||||
label: 'Support',
|
||||
icon: <IconSupport />,
|
||||
},
|
||||
]
|
||||
const SUPPORT_TICKET_KIND_TO_TITLE: Record<SupportTicketKind, string> = {
|
||||
bug: 'Report a bug',
|
||||
feedback: 'Give feedback',
|
||||
support: 'Get support',
|
||||
}
|
||||
const SUPPORT_TICKET_KIND_TO_PROMPT: Record<SupportTicketKind, string> = {
|
||||
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<HTMLDivElement>(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 (
|
||||
<LemonModal
|
||||
isOpen={isSupportFormOpen}
|
||||
onClose={closeSupportForm}
|
||||
title={
|
||||
sendSupportRequest.kind
|
||||
? SUPPORT_TICKET_KIND_TO_TITLE[sendSupportRequest.kind]
|
||||
: 'Leave a message with PostHog'
|
||||
}
|
||||
footer={
|
||||
<div className="flex items-center gap-2">
|
||||
<LemonButton form="support-modal-form" type="secondary" onClick={closeSupportForm}>
|
||||
Cancel
|
||||
</LemonButton>
|
||||
<LemonButton form="support-modal-form" htmlType="submit" type="primary" data-attr="submit">
|
||||
Submit
|
||||
</LemonButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
logic={supportLogic}
|
||||
formKey="sendSupportRequest"
|
||||
id="support-modal-form"
|
||||
enableFormOnSubmit
|
||||
className="space-y-4"
|
||||
>
|
||||
<Field name="kind" label="What type of message is this?">
|
||||
<LemonSelect fullWidth options={SUPPORT_TICKET_OPTIONS} />
|
||||
</Field>
|
||||
<Field name="target_area" label="What area does this best relate to?">
|
||||
<LemonSelect
|
||||
fullWidth
|
||||
options={Object.entries(TARGET_AREA_TO_NAME).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name="message"
|
||||
label={sendSupportRequest.kind ? SUPPORT_TICKET_KIND_TO_PROMPT[sendSupportRequest.kind] : 'Content'}
|
||||
>
|
||||
{(props) => (
|
||||
<div ref={dropRef} className="flex flex-col gap-2">
|
||||
<LemonTextArea
|
||||
placeholder="Type your message here"
|
||||
data-attr="support-form-content-input"
|
||||
{...props}
|
||||
/>
|
||||
{objectStorageAvailable && (
|
||||
<LemonFileInput
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
alternativeDropTargetRef={dropRef}
|
||||
onChange={setFilesToUpload}
|
||||
loading={uploading}
|
||||
value={filesToUpload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</Form>
|
||||
</LemonModal>
|
||||
)
|
||||
}
|
@ -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<string, supportTicketTargetArea> = {
|
||||
export const URL_PATH_TO_TARGET_AREA: Record<string, SupportTicketTargetArea> = {
|
||||
insights: 'analytics',
|
||||
recordings: 'session_replay',
|
||||
replay: 'session_replay',
|
||||
@ -63,9 +64,9 @@ export const URLPathToTargetArea: Record<string, supportTicketTargetArea> = {
|
||||
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<supportLogicType>([
|
||||
@ -75,11 +76,18 @@ export const supportLogic = kea<supportLogicType>([
|
||||
})),
|
||||
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<supportLogicType>([
|
||||
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<supportLogicType>([
|
||||
}
|
||||
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<supportLogicType>([
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -179,6 +179,7 @@ export const LemonFileInput = ({
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<IconUploadFile className={'text-2xl'} /> Click or drag and drop to upload
|
||||
{accept ? ` ${acceptToDisplayName(accept)}` : ''}
|
||||
</label>
|
||||
<div className={'flex flex-row gap-2'}>
|
||||
{files.map((x, i) => (
|
||||
@ -196,3 +197,14 @@ const lazyImageBlobReducer = async (blob: Blob): Promise<Blob> => {
|
||||
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`
|
||||
}
|
||||
|
@ -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<HTMLElement, HTMLButtonElement>(
|
||||
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={<LemonMenuOverlay items={items} tooltipPlacement={tooltipPlacement} itemsRef={itemsRef} />}
|
||||
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}
|
||||
<LemonMenuItemList
|
||||
items={section.items}
|
||||
items={section.items.filter(Boolean) as LemonMenuItem[]}
|
||||
buttonSize={buttonSize}
|
||||
tooltipPlacement={tooltipPlacement}
|
||||
itemsRef={itemsRef}
|
||||
@ -220,10 +238,13 @@ const LemonMenuItemButton: FunctionComponent<LemonMenuItemButtonProps & React.Re
|
||||
})
|
||||
LemonMenuItemButton.displayName = 'LemonMenuItemButton'
|
||||
|
||||
function normalizeItems(sectionsAndItems: (LemonMenuItem | LemonMenuSection)[]): LemonMenuItem[] | LemonMenuSection[] {
|
||||
function normalizeItems(sectionsAndItems: LemonMenuItems): LemonMenuItem[] | LemonMenuSection[] {
|
||||
const sections: LemonMenuSection[] = []
|
||||
let implicitSection: LemonMenuSection = { items: [] }
|
||||
for (const sectionOrItem of sectionsAndItems) {
|
||||
if (!sectionOrItem) {
|
||||
continue // Ignore falsy items
|
||||
}
|
||||
if (isLemonMenuSection(sectionOrItem)) {
|
||||
if (implicitSection.items.length > 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
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export function useKeyboardNavigation<R extends HTMLElement = HTMLElement, I ext
|
||||
item.current?.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [focusedItemIndex, itemCount])
|
||||
|
||||
return { referenceRef, itemsRef }
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
LemonMenuItemBase,
|
||||
LemonMenuItemLeaf,
|
||||
LemonMenuItemNode,
|
||||
LemonMenuItems,
|
||||
LemonMenuProps,
|
||||
LemonMenuSection,
|
||||
isLemonMenuSection,
|
||||
@ -119,7 +118,9 @@ export function LemonSelect<T>({
|
||||
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}
|
||||
>
|
||||
<LemonButton
|
||||
@ -165,9 +166,9 @@ function convertSelectOptionsToMenuItems<T>(
|
||||
options: LemonSelectOptions<T>,
|
||||
activeValue: T | null,
|
||||
onSelect: OnSelect<T>
|
||||
): [LemonMenuItems, LemonSelectOptionLeaf<T>[]] {
|
||||
): [(LemonMenuItem | LemonMenuSection)[], LemonSelectOptionLeaf<T>[]] {
|
||||
const leafOptionsAccumulator: LemonSelectOptionLeaf<T>[] = []
|
||||
const items: LemonMenuItems = options.map((option) =>
|
||||
const items: (LemonMenuItem | LemonMenuSection)[] = options.map((option) =>
|
||||
convertToMenuSingle(option, activeValue, onSelect, leafOptionsAccumulator)
|
||||
)
|
||||
return [items, leafOptionsAccumulator]
|
||||
|
@ -62,9 +62,10 @@ export const LemonTextArea = React.forwardRef<HTMLTextAreaElement, LemonTextArea
|
||||
})
|
||||
|
||||
interface LemonTextMarkdownProps {
|
||||
'data-attr'?: string
|
||||
value?: string
|
||||
onChange?: (s: string) => void
|
||||
placeholder?: string
|
||||
'data-attr'?: string
|
||||
}
|
||||
|
||||
export function LemonTextMarkdown({ value, onChange, ...editAreaProps }: LemonTextMarkdownProps): JSX.Element {
|
||||
|
@ -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 (
|
||||
<LemonIconBase viewBox="0 0 32 32" {...props}>
|
||||
<path
|
||||
@ -787,6 +787,18 @@ export function IconExclamation(props: LemonIconProps): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
/** Material Design Support icon. */
|
||||
export function IconSupport(props: LemonIconProps): JSX.Element {
|
||||
return (
|
||||
<LemonIconBase {...props}>
|
||||
<path
|
||||
d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M19.46,9.12l-2.78,1.15 c-0.51-1.36-1.58-2.44-2.95-2.94l1.15-2.78C16.98,5.35,18.65,7.02,19.46,9.12z M12,15c-1.66,0-3-1.34-3-3s1.34-3,3-3s3,1.34,3,3 S13.66,15,12,15z M9.13,4.54l1.17,2.78c-1.38,0.5-2.47,1.59-2.98,2.97L4.54,9.13C5.35,7.02,7.02,5.35,9.13,4.54z M4.54,14.87 l2.78-1.15c0.51,1.38,1.59,2.46,2.97,2.96l-1.17,2.78C7.02,18.65,5.35,16.98,4.54,14.87z M14.88,19.46l-1.15-2.78 c1.37-0.51,2.45-1.59,2.95-2.97l2.78,1.17C18.65,16.98,16.98,18.65,14.88,19.46z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</LemonIconBase>
|
||||
)
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
</LemonIconBase>
|
||||
)
|
||||
}
|
||||
export function IconBugReport(props: LemonIconProps): JSX.Element {
|
||||
return (
|
||||
<LemonIconBase {...props}>
|
||||
<path
|
||||
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-4 4v3c0 .22-.03.47-.07.7l-.1.65-.37.65c-.72 1.24-2.04 2-3.46 2s-2.74-.77-3.46-2l-.37-.64-.1-.65C8.03 15.48 8 15.23 8 15v-4c0-.23.03-.48.07-.7l.1-.65.37-.65c.3-.52.72-.97 1.21-1.31l.57-.39.74-.18c.31-.08.63-.12.94-.12.32 0 .63.04.95.12l.68.16.61.42c.5.34.91.78 1.21 1.31l.38.65.1.65c.04.22.07.47.07.69v1zm-6 2h4v2h-4zm0-4h4v2h-4z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</LemonIconBase>
|
||||
)
|
||||
}
|
||||
|
||||
/** https://pictogrammers.com/library/mdi/icon/shield-bug-outline/ */
|
||||
export function IconBugShield(props: LemonIconProps): JSX.Element {
|
||||
|
@ -212,17 +212,15 @@ export const appMetricsSceneLogic = kea<appMetricsSceneLogicType>([
|
||||
() => 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) {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<experimentsLogicType>([
|
||||
path(['scenes', 'experiments', 'experimentsLogic']),
|
||||
connect({ values: [teamLogic, ['currentTeamId'], userLogic, ['user', 'hasAvailableFeature']] }),
|
||||
@ -57,27 +66,9 @@ export const experimentsLogic = kea<experimentsLogicType>([
|
||||
],
|
||||
})),
|
||||
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<experimentsLogicType>([
|
||||
},
|
||||
],
|
||||
hasExperimentAvailableFeature: [
|
||||
() => [],
|
||||
(): boolean => values.hasAvailableFeature(AvailableFeature.EXPERIMENTATION),
|
||||
(s) => [s.hasAvailableFeature],
|
||||
(hasAvailableFeature): boolean => hasAvailableFeature(AvailableFeature.EXPERIMENTATION),
|
||||
],
|
||||
})),
|
||||
events(({ actions }) => ({
|
||||
|
@ -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)
|
||||
Sampling<LemonTag type="warning">BETA</LemonTag>
|
||||
</LemonLabel>
|
||||
<LemonSwitch
|
||||
className="m-2"
|
||||
|
@ -4,7 +4,7 @@ import { Card } from 'antd'
|
||||
import { insightLogic } from 'scenes/insights/insightLogic'
|
||||
import { funnelLogic } from 'scenes/funnels/funnelLogic'
|
||||
|
||||
import { IconFeedbackWarning } from 'lib/lemon-ui/icons'
|
||||
import { IconFeedback } from 'lib/lemon-ui/icons'
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic'
|
||||
|
||||
@ -40,8 +40,8 @@ const FunnelCorrelationSkewWarningComponent = ({
|
||||
return (
|
||||
<Card className="skew-warning">
|
||||
<h4>
|
||||
<IconFeedbackWarning style={{ fontSize: 24, marginRight: 4, color: 'var(--warning)' }} /> Adjust your
|
||||
funnel definition to improve correlation analysis
|
||||
<IconFeedback style={{ fontSize: 24, marginRight: 4, color: 'var(--warning)' }} /> Adjust your funnel
|
||||
definition to improve correlation analysis
|
||||
<CloseOutlined className="close-button" onClick={hideSkewWarning} />
|
||||
</h4>
|
||||
<div>
|
||||
|
@ -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",
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user