0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 21:49:51 +01:00

feat(3000): help & support panel (#20710)

* html for new support pane

* add algolia search component

* polish

* docs are not old anymore

* ux improvements

* add filters

* reset active option

* styles

* email an engineer button

* fix panel title

* add checkmark to resolved questions

* add result count to tags / allow tabbing through tags

* polish

* docs links

* tooltips

* inline support form

* remove unnecessary effect

* use correct color in tooltip

* Revert "tooltips"

This reverts commit 5603a65080.

* hook it up

* fix lockfile

* use iconinfo

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* reup lockfile

* Update UI snapshots for `chromium` (1)

* fix

* use std colors (except for purple)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* Update unit.json

* Update docker-compose.dev-full.yml

* Update docker-compose.dev-full.yml

* upgrade @babel/runtime

* move to a regular dep?

* address pr feedback

* add stories

* handle arrowLeft and ArrowRight keydown

* Update UI snapshots for `chromium` (1)

---------

Co-authored-by: Eli Kinsey <eli@ekinsey.dev>
Co-authored-by: Raquel Smith <raquelmsmith@users.noreply.github.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Cory Watilo 2024-03-18 12:14:44 -04:00 committed by GitHub
parent 7435fe63d1
commit c8545b9692
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 956 additions and 151 deletions

View File

@ -103,7 +103,6 @@ services:
KAFKA_HOSTS: 'kafka:9092'
REDIS_URL: 'redis://redis:6379/'
plugins:
command: ./bin/plugin-server --no-restart-loop
restart: on-failure
@ -152,8 +151,6 @@ services:
volumes:
- /var/lib/elasticsearch/data
temporal:
environment:
- DB=postgresql
- DB_PORT=5432
@ -190,4 +187,3 @@ services:
restart: on-failure
environment:
TEMPORAL_HOST: temporal

View File

@ -182,4 +182,4 @@ services:
- clickhouse
- kafka
- object_storage
- temporal
- temporal

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -0,0 +1,268 @@
import { IconCheckCircle } from '@posthog/icons'
import { LemonButton, LemonInput, LemonTag } from '@posthog/lemon-ui'
import algoliasearch from 'algoliasearch/lite'
import { useActions } from 'kea'
import { useEffect, useRef, useState } from 'react'
import { InstantSearch, useHits, useRefinementList, useSearchBox } from 'react-instantsearch'
import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer'
import { List } from 'react-virtualized/dist/es/List'
import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic'
import { SidePanelTab } from '~/types'
const searchClient = algoliasearch('7VNQB5W0TX', '37f41fd37095bc85af76ed4edc85eb5a')
const rowRenderer = ({ key, index, style, hits, activeOption }: any): JSX.Element => {
const { slug, title, type, resolved } = hits[index]
return (
// eslint-disable-next-line react/forbid-dom-props
<li key={key} style={style} role="listitem" tabIndex={-1} className="p-1 border-b last:border-b-0">
<LemonButton
active={activeOption === index}
to={`https://posthog.com/${slug}`}
className="[&_>span>span]:flex-col [&_>span>span]:items-start [&_>span>span]:space-y-1"
>
<span>
<span className="flex space-x-2 items-center">
<p className="m-0 font-bold font-sans line-clamp-1">{title}</p>
{type === 'question' && resolved && (
<IconCheckCircle className="text-success size-4 flex-shrink-0" />
)}
</span>
<p className="text-xs m-0 opacity-80 font-normal font-sans line-clamp-1">/{slug}</p>
</span>
</LemonButton>
</li>
)
}
const Hits = ({ activeOption }: { activeOption?: number }): JSX.Element => {
const { hits } = useHits()
return (
<ol role="listbox" className="list-none m-0 p-0 h-[80vh]">
<AutoSizer>
{({ height, width }: { height: number; width: number }) => (
<List
scrollToIndex={activeOption}
width={width}
height={height}
rowCount={hits.length}
rowHeight={50}
rowRenderer={(options: any) => rowRenderer({ ...options, hits, activeOption })}
/>
)}
</AutoSizer>
</ol>
)
}
const SearchInput = ({
value,
setValue,
}: {
value: string
setValue: React.Dispatch<React.SetStateAction<string>>
}): JSX.Element => {
const { refine } = useSearchBox()
const handleChange = (value: string): void => {
setValue(value)
refine(value)
}
return <LemonInput onChange={handleChange} value={value} type="search" fullWidth placeholder="Search..." />
}
type Tag = {
type: string
label: string
}
const tags: Tag[] = [
{
type: 'all',
label: 'All',
},
{
type: 'docs',
label: 'Docs',
},
{
type: 'question',
label: 'Questions',
},
{
type: 'tutorial',
label: 'Tutorials',
},
]
type SearchTagProps = Tag & {
active?: boolean
onClick: (type: string) => void
}
const SearchTag = ({ type, label, active, onClick }: SearchTagProps): JSX.Element => {
const { refine, items } = useRefinementList({ attribute: 'type' })
const itemCount = type !== 'all' && items.find(({ value }) => value === type)?.count
const handleClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.stopPropagation()
onClick(type)
}
useEffect(() => {
refine(type)
}, [])
return (
<button className="p-0 cursor-pointer bg-bg-light" onClick={handleClick}>
<LemonTag size="medium" type={active ? 'primary' : 'option'}>
<span>{label}</span>
{type !== 'all' && <span>({itemCount ?? 0})</span>}
</LemonTag>
</button>
)
}
const Tags = ({
activeTag,
setActiveTag,
}: {
activeTag: string
setActiveTag: React.Dispatch<React.SetStateAction<string>>
}): JSX.Element => {
const handleClick = (type: string): void => {
setActiveTag(type)
}
return (
<ul className="list-none m-0 p-0 flex space-x-1 mt-1 mb-0.5 pb-1.5 border-b px-2">
{tags.map((tag) => {
const { type } = tag
return (
<li key={type}>
<SearchTag {...tag} active={activeTag === type} onClick={handleClick} />
</li>
)
})}
</ul>
)
}
const Search = (): JSX.Element => {
const { openSidePanel } = useActions(sidePanelStateLogic)
const { hits } = useHits()
const { items, refine } = useRefinementList({ attribute: 'type' })
const ref = useRef<HTMLDivElement>(null)
const [searchValue, setSearchValue] = useState<string>('')
const [activeOption, setActiveOption] = useState<undefined | number>()
const [activeTag, setActiveTag] = useState('all')
const [searchOpen, setSearchOpen] = useState(false)
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
switch (e.key) {
case 'Enter': {
if (activeOption !== undefined) {
openSidePanel(SidePanelTab.Docs, `https://posthog.com/${hits[activeOption].slug}`)
}
break
}
case 'Escape': {
setSearchOpen(false)
break
}
case 'ArrowDown': {
e.preventDefault()
setActiveOption((currOption) => {
if (currOption === undefined || currOption >= hits.length - 1) {
return 0
}
return currOption + 1
})
break
}
case 'ArrowUp': {
e.preventDefault()
setActiveOption((currOption) => {
if (currOption !== undefined) {
return currOption <= 0 ? hits.length - 1 : currOption - 1
}
})
break
}
case 'Tab':
case 'ArrowRight': {
e.preventDefault()
const currTagIndex = tags.findIndex(({ type }) => type === activeTag)
setActiveTag(tags[currTagIndex >= tags.length - 1 ? 0 : currTagIndex + 1].type)
break
}
case 'ArrowLeft': {
e.preventDefault()
const currTagIndex = tags.findIndex(({ type }) => type === activeTag)
setActiveTag(tags[currTagIndex <= 0 ? tags.length - 1 : currTagIndex - 1].type)
}
}
}
useEffect(() => {
setSearchOpen(!!searchValue)
setActiveOption(0)
}, [searchValue])
useEffect(() => {
setActiveOption(0)
if (activeTag === 'all') {
const filteredItems = items.filter(({ value }) => tags.some(({ type }) => type === value))
filteredItems.forEach(({ value, isRefined }) => {
if (!isRefined) {
refine(value)
}
})
} else {
items.forEach(({ value, isRefined }) => {
if (isRefined) {
refine(value)
}
})
refine(activeTag)
}
}, [activeTag])
useEffect(() => {
const handleClick = (e: any): void => {
if (!ref?.current?.contains(e.target)) {
setSearchOpen(false)
}
}
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<div className="relative" ref={ref} onKeyDown={handleKeyDown}>
<SearchInput value={searchValue} setValue={setSearchValue} />
{searchOpen && (
<div className="absolute w-full bg-bg-light z-50 border rounded-lg shadow-xl mt-0.5">
<Tags activeTag={activeTag} setActiveTag={setActiveTag} />
<Hits activeOption={activeOption} />
</div>
)}
</div>
)
}
export default function AlgoliaSearch(): JSX.Element {
return (
<InstantSearch searchClient={searchClient} indexName="prod_posthog_com">
<Search />
</InstantSearch>
)
}

View File

@ -1,11 +1,13 @@
import { Meta, StoryFn } from '@storybook/react'
import { useActions } from 'kea'
import { router } from 'kea-router'
import { supportLogic } from 'lib/components/Support/supportLogic'
import { useEffect } from 'react'
import { App } from 'scenes/App'
import { urls } from 'scenes/urls'
import { mswDecorator } from '~/mocks/browser'
import { mswDecorator, useStorybookMocks } from '~/mocks/browser'
import organizationCurrent from '~/mocks/fixtures/api/organizations/@current/@current.json'
import { SidePanelTab } from '~/types'
import { sidePanelStateLogic } from './sidePanelStateLogic'
@ -59,3 +61,36 @@ export const SidePanelActivation: StoryFn = () => {
export const SidePanelNotebooks: StoryFn = () => {
return <BaseTemplate panel={SidePanelTab.Notebooks} />
}
export const SidePanelSupportNoEmail: StoryFn = () => {
return <BaseTemplate panel={SidePanelTab.Support} />
}
export const SidePanelSupportWithEmail: StoryFn = () => {
const { openEmailForm } = useActions(supportLogic)
useStorybookMocks({
get: {
// TODO: setting available featues should be a decorator to make this easy
'/api/users/@me': () => [
200,
{
email: 'test@posthog.com',
first_name: 'Test Hedgehog',
organization: {
...organizationCurrent,
available_product_features: [
{
key: 'email_support',
name: 'Email support',
},
],
},
},
],
},
})
useEffect(() => {
openEmailForm()
}, [])
return <BaseTemplate panel={SidePanelTab.Support} />
}

View File

@ -37,7 +37,7 @@ export const SIDE_PANEL_TABS: Record<
noModalSupport: true,
},
[SidePanelTab.Support]: {
label: 'Support',
label: 'Help',
Icon: IconSupport,
Content: SidePanelSupport,
},

View File

@ -1,51 +1,305 @@
import { LemonButton } from '@posthog/lemon-ui'
import {
IconBug,
IconChevronDown,
IconFeatures,
IconFlask,
IconHelmet,
IconMap,
IconMessage,
IconRewindPlay,
IconStack,
IconToggle,
IconTrends,
} from '@posthog/icons'
import { LemonButton, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { SupportForm } from 'lib/components/Support/SupportForm'
import { supportLogic } from 'lib/components/Support/supportLogic'
import React from 'react'
import { billingLogic } from 'scenes/billing/billingLogic'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'
import { SidePanelTab } from '~/types'
import { AvailableFeature, ProductKey, SidePanelTab } from '~/types'
import AlgoliaSearch from '../../components/AlgoliaSearch'
import { SidePanelPaneHeader } from '../components/SidePanelPaneHeader'
import { SIDE_PANEL_TABS } from '../SidePanel'
import { sidePanelStateLogic } from '../sidePanelStateLogic'
const PRODUCTS = [
{
name: 'Product OS',
slug: 'product-os',
icon: <IconStack className="text-danger h-5 w-5" />,
},
{
name: 'Product analytics',
slug: 'product-analytics',
icon: <IconTrends className="text-brand-blue h-5 w-5" />,
},
{
name: 'Session replay',
slug: 'session-replay',
icon: <IconRewindPlay className="text-warning h-5 w-5" />,
},
{
name: 'Feature flags',
slug: 'feature-flags',
icon: <IconToggle className="text-success h-5 w-5" />,
},
{
name: 'A/B testing',
slug: 'ab-testing',
icon: <IconFlask className="text-purple h-5 w-5" />,
},
{
name: 'Surveys',
slug: 'surveys',
icon: <IconMessage className="text-danger h-5 w-5" />,
},
]
const Section = ({ title, children }: { title: string; children: React.ReactNode }): React.ReactElement => {
return (
<section className="mb-6">
<h3>{title}</h3>
{children}
</section>
)
}
const SupportFormBlock = ({ onCancel }: { onCancel: () => void }): JSX.Element => {
const { billing } = useValues(billingLogic)
const supportResponseTimes = {
[AvailableFeature.EMAIL_SUPPORT]: '2-3 days',
[AvailableFeature.PRIORITY_SUPPORT]: '4-6 hours',
}
return (
<Section title="Email an engineer">
<div className="grid grid-cols-2 border rounded [&_>*]:px-2 [&_>*]:py-0.5 mb-4 bg-bg-light">
<div className="col-span-full flex justify-between border-b bg-bg-white py-1">
<div>
<strong>Avg support response times</strong>
</div>
<div>
<Link to={urls.organizationBilling([ProductKey.PLATFORM_AND_SUPPORT])}>Explore options</Link>
</div>
</div>
{billing?.products
?.find((product) => product.type == ProductKey.PLATFORM_AND_SUPPORT)
?.plans?.map((plan, i) => (
<React.Fragment key={`support-panel-${plan.plan_key}`}>
<div className={plan.current_plan ? 'font-bold' : undefined}>
{i == 1 ? 'Pay-per-use' : plan.name}
{plan.current_plan && (
<>
{' '}
<span className="font-normal opacity-60 text-sm">(your plan)</span>
</>
)}
</div>
<div className={plan.current_plan ? 'font-bold' : undefined}>
{plan.features.some((f) => f.key == AvailableFeature.PRIORITY_SUPPORT)
? supportResponseTimes[AvailableFeature.PRIORITY_SUPPORT]
: plan.features.some((f) => f.key == AvailableFeature.EMAIL_SUPPORT)
? supportResponseTimes[AvailableFeature.EMAIL_SUPPORT]
: 'Community support only'}
</div>
</React.Fragment>
))}
</div>
<SupportForm />
<LemonButton
form="support-modal-form"
htmlType="submit"
type="primary"
data-attr="submit"
fullWidth
center
className="mt-4"
>
Submit
</LemonButton>
<LemonButton
form="support-modal-form"
type="secondary"
onClick={onCancel}
fullWidth
center
className="mt-2"
>
Cancel
</LemonButton>
</Section>
)
}
export const SidePanelSupport = (): JSX.Element => {
const { closeSidePanel } = useActions(sidePanelStateLogic)
const { hasAvailableFeature } = useValues(userLogic)
const { openEmailForm, closeEmailForm } = useActions(supportLogic)
const { isEmailFormOpen } = useValues(supportLogic)
const theLogic = supportLogic({ onClose: () => closeSidePanel(SidePanelTab.Support) })
const { title } = useValues(theLogic)
const { closeSupportForm } = useActions(theLogic)
return (
<>
<SidePanelPaneHeader title={title} />
<SidePanelPaneHeader title={isEmailFormOpen ? title : SIDE_PANEL_TABS[SidePanelTab.Support].label} />
<div className="overflow-y-auto" data-attr="side-panel-support-container">
<div className="p-3 max-w-160 w-full mx-auto">
<SupportForm />
<Section title="Search docs & community questions">
<AlgoliaSearch />
</Section>
<footer>
<Section title="Explore the docs">
<ul className="border rounded divide-y bg-bg-light dark:bg-transparent font-title font-medium">
{PRODUCTS.map((product, index) => (
<li key={index}>
<Link
to={`https://posthog.com/docs/${product.slug}`}
className="group flex items-center justify-between px-2 py-1.5"
>
<div className="flex items-center gap-1.5">
{product.icon}
<span className="text-default opacity-75 group-hover:opacity-100">
{product.name}
</span>
</div>
<div>
<IconChevronDown className="text-default h-6 w-6 opacity-60 -rotate-90 group-hover:opacity-90" />
</div>
</Link>
</li>
))}
</ul>
</Section>
<Section title="Ask the community">
<p>
Questions about features, how to's, or use cases? There are thousands of discussions in our
community forums.
</p>
<LemonButton
form="support-modal-form"
htmlType="submit"
type="primary"
data-attr="submit"
fullWidth
center
className="mt-4"
>
Submit
</LemonButton>
<LemonButton
form="support-modal-form"
type="secondary"
onClick={closeSupportForm}
fullWidth
center
to="https://posthog.com/questions"
targetBlank
className="mt-2"
>
Cancel
Ask a question
</LemonButton>
</footer>
</Section>
<Section title="Share feedback">
<ul>
<li>
<LemonButton
type="secondary"
status="alt"
to="https://github.com/posthog/posthog/issues"
icon={<IconBug />}
targetBlank
>
Report a bug
</LemonButton>
</li>
<li>
<LemonButton
type="secondary"
status="alt"
to="https://posthog.com/wip"
icon={<IconHelmet />}
targetBlank
>
See what we're building
</LemonButton>
</li>
<li>
<LemonButton
type="secondary"
status="alt"
to="https://posthog.com/roadmap"
icon={<IconMap />}
targetBlank
>
Vote on our roadmap
</LemonButton>
</li>
<li>
<LemonButton
type="secondary"
status="alt"
to="https://github.com/posthog/posthog/issues"
icon={<IconFeatures />}
targetBlank
>
Request a feature
</LemonButton>
</li>
</ul>
</Section>
{hasAvailableFeature(AvailableFeature.EMAIL_SUPPORT) ? (
<Section title="More options">
{isEmailFormOpen ? (
<SupportFormBlock onCancel={() => closeEmailForm()} />
) : (
<p>
Can't find what you need in the docs?{' '}
<Link onClick={() => openEmailForm()}>Email an engineer</Link>
</p>
)}
</Section>
) : (
<Section title="Contact support">
<p>
Due to our large userbase, we're unable to offer email support to organizations on the
free plan. But we still want to help!
</p>
<ol className="pl-5">
<li>
<strong className="block">Search our docs</strong>
<p>
We're constantly updating our docs and tutorials to provide the latest
information about installing, using, and troubleshooting.
</p>
</li>
<li>
<strong className="block">Ask a community question</strong>
<p>
Many common (and niche) questions have already been resolved by users just like
you. (Our own engineers also keep an eye on the questions as they have time!){' '}
<Link to="https://posthog.com/question" className="block">
Search community questions or ask your own.
</Link>
</p>
</li>
<li>
<strong className="block">
Explore <Link to="https://posthog.com/partners">PostHog partners</Link>
</strong>
<p>
Third-party providers can help with installation and debugging of data issues.
</p>
</li>
<li>
<strong className="block">Upgrade to a paid plan</strong>
<p>
Our paid plans offer email support.{' '}
<Link to={urls.organizationBilling([ProductKey.PLATFORM_AND_SUPPORT])}>
Explore options.
</Link>
</p>
</li>
</ol>
</Section>
)}
</div>
</div>
</>

View File

@ -1,4 +1,4 @@
import { IconBug, IconQuestion } from '@posthog/icons'
import { IconBug, IconInfo, IconQuestion } from '@posthog/icons'
import {
LemonBanner,
LemonInput,
@ -6,6 +6,7 @@ import {
LemonSegmentedButtonOption,
lemonToast,
Link,
Tooltip,
} from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
@ -90,9 +91,12 @@ export function SupportForm(): JSX.Element | null {
</LemonField>
</>
)}
<LemonField name="kind" label="What type of message is this?">
<LemonField name="kind" label="Message type">
<LemonSegmentedButton fullWidth options={SUPPORT_TICKET_OPTIONS} />
</LemonField>
<LemonField name="target_area" label="Topic">
<LemonSelect fullWidth options={TARGET_AREA_TO_NAME} />
</LemonField>
{posthog.getFeatureFlag('show-troubleshooting-docs-in-support-form') === 'test-replay-banner' &&
sendSupportRequest.target_area === 'session_replay' && (
<LemonBanner type="info">
@ -127,18 +131,6 @@ export function SupportForm(): JSX.Element | null {
</>
</LemonBanner>
)}
<LemonField name="target_area" label="What area does this best relate to?">
<LemonSelect fullWidth type="secondary" options={TARGET_AREA_TO_NAME} />
</LemonField>
<LemonField name="severity_level" label="What is the severity of this issue?">
<LemonSelect
fullWidth
options={Object.entries(SEVERITY_LEVEL_TO_NAME).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</LemonField>
<LemonField
name="message"
label={sendSupportRequest.kind ? SUPPORT_TICKET_KIND_TO_PROMPT[sendSupportRequest.kind] : 'Content'}
@ -163,6 +155,30 @@ export function SupportForm(): JSX.Element | null {
</div>
)}
</LemonField>
<LemonField name="severity_level">
<>
<div className="flex justify-between items-center">
<label className="LemonLabel">
Severity level
<Tooltip title="Severity levels help us prioritize your request.">
<span>
<IconInfo className="opacity-75" />
</span>
</Tooltip>
</label>
<Link target="_blank" to="https://posthog.com/docs/support-options#severity-levels">
Definitions
</Link>
</div>
<LemonSelect
fullWidth
options={Object.entries(SEVERITY_LEVEL_TO_NAME).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</>
</LemonField>
</Form>
)
}

View File

@ -45,7 +45,7 @@ function getSentryLink(user: UserType | null, cloudRegion: Region | null | undef
}
const SUPPORT_TICKET_KIND_TO_TITLE: Record<SupportTicketKind, string> = {
support: 'Ask a question',
support: 'Contact support',
feedback: 'Give feedback',
bug: 'Report a bug',
}
@ -237,6 +237,8 @@ export const supportLogic = kea<supportLogicType>([
openSupportForm: (values: Partial<SupportFormFields>) => values,
submitZendeskTicket: (form: SupportFormFields) => form,
updateUrlParams: true,
openEmailForm: true,
closeEmailForm: true,
})),
reducers(() => ({
isSupportFormOpen: [
@ -246,6 +248,13 @@ export const supportLogic = kea<supportLogicType>([
closeSupportForm: () => false,
},
],
isEmailFormOpen: [
false,
{
openEmailForm: () => true,
closeEmailForm: () => false,
},
],
})),
forms(({ actions, values }) => ({
sendSupportRequest: {

View File

@ -43,6 +43,7 @@ import { NodeKind } from './queries/schema'
export type Optional<T, K extends string | number | symbol> = Omit<T, K> & { [K in keyof T]?: T[K] }
// Keep this in sync with backend constants/features/{product_name}.yml
export enum AvailableFeature {
APPS = 'apps',
SLACK_INTEGRATION = 'slack_integration',
@ -143,6 +144,7 @@ export enum AvailableFeature {
PRODUCT_ANALYTICS_SQL_QUERIES = 'product_analytics_sql_queries',
TWOFA_ENFORCEMENT = '2fa_enforcement',
AUDIT_LOGS = 'audit_logs',
PRIORITY_SUPPORT = 'priority_support',
}
type AvailableFeatureUnion = `${AvailableFeature}`

View File

@ -66,6 +66,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@babel/runtime": "^7.24.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
@ -96,6 +97,7 @@
"@types/react-transition-group": "^4.4.5",
"@types/react-virtualized": "^9.21.23",
"ajv": "^8.12.0",
"algoliasearch": "^4.22.1",
"antd": "^4.17.1",
"antd-dayjs-webpack-plugin": "^1.0.6",
"autoprefixer": "^10.4.13",
@ -154,6 +156,7 @@
"react-dom": "^18.2.0",
"react-draggable": "^4.2.0",
"react-grid-layout": "^1.3.0",
"react-instantsearch": "^7.6.0",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^5.0.3",
"react-modal": "^3.15.1",
@ -182,7 +185,6 @@
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.10",
"@cypress/webpack-preprocessor": "^5.17.1",
"@playwright/test": "1.41.2",
"@sentry/types": "7.22.0",

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,8 @@ const config = {
// TODO: Move all colors over to Tailwind
// Currently color utility classes are still generated with SCSS in colors.scss due to relying on our color
// CSS vars in lots of stylesheets
purple: '#B62AD9',
},
fontFamily: {
sans: [