Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdp): Native slack integration #23103

Merged
merged 48 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e59b277
Enable re ordering of inputs
benjackwhite Jun 20, 2024
63f5eb5
Added slack integration stuff
benjackwhite Jun 20, 2024
7338dd1
Add vars
benjackwhite Jun 20, 2024
69c0aba
Merge branch 'feat/cdp-input-ux' into feat/cdp-native-slack
benjackwhite Jun 20, 2024
7abbd11
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
0325b52
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
2f31a29
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
7f21022
Fix up template
benjackwhite Jun 20, 2024
cfbc749
Fix template
benjackwhite Jun 20, 2024
0a7c7b6
Fixes
benjackwhite Jun 20, 2024
5de86fe
Merge branch 'feat/cdp-native-slack' of github.com:PostHog/posthog in…
benjackwhite Jun 20, 2024
5615c5d
Fixes
benjackwhite Jun 20, 2024
5337d3d
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
757a471
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
1a9cb1d
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
643993f
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
6a04e4a
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
3232a7d
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
7c7370b
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
6756bb5
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
bb5cc51
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
c69e34c
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
b64d137
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
a9a5f82
Big refactor work
benjackwhite Jun 20, 2024
774d7bd
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
8f6529c
Merge branch 'feat/cdp-native-slack' of github.com:PostHog/posthog in…
benjackwhite Jun 20, 2024
4227e5a
Fix up
benjackwhite Jun 20, 2024
1969fd3
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
3738396
Move things around
benjackwhite Jun 20, 2024
c2226f7
Merge branch 'feat/cdp-native-slack' of github.com:PostHog/posthog in…
benjackwhite Jun 20, 2024
cf19519
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
7dfbfb5
fix
benjackwhite Jun 20, 2024
2b2cb8b
Fix
benjackwhite Jun 20, 2024
3b5100d
Merge branch 'feat/cdp-native-slack' of github.com:PostHog/posthog in…
benjackwhite Jun 20, 2024
2d9dd4c
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
33159d9
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
7527e9c
Merge branch 'master' into feat/cdp-input-ux
benjackwhite Jun 20, 2024
152d4ce
Merge branch 'feat/cdp-input-ux' into feat/cdp-native-slack
benjackwhite Jun 20, 2024
c0ecce6
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
76246f4
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
bdfd62c
Update UI snapshots for `chromium` (1)
github-actions[bot] Jun 20, 2024
97a382d
Added integration enrichment
benjackwhite Jun 20, 2024
39e016a
Fixes
benjackwhite Jun 20, 2024
9187289
Merge branch 'feat/cdp-native-slack' of github.com:PostHog/posthog in…
benjackwhite Jun 20, 2024
95ff939
Fixes
benjackwhite Jun 20, 2024
d3fd986
Merge branch 'master' into feat/cdp-native-slack
benjackwhite Jun 20, 2024
c037e72
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
22461e0
Update UI snapshots for `chromium` (2)
github-actions[bot] Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 1 addition & 9 deletions frontend/src/lib/components/Subscriptions/subscriptionLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { dayjs } from 'lib/dayjs'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
import { isEmail, isURL } from 'lib/utils'
import { getInsightId } from 'scenes/insights/utils'
import { integrationsLogic } from 'scenes/settings/project/integrationsLogic'

import { SubscriptionType } from '~/types'

Expand All @@ -33,7 +32,6 @@ export const subscriptionLogic = kea<subscriptionLogicType>([
key(({ id, insightShortId, dashboardId }) => `${insightShortId || dashboardId}-${id ?? 'new'}`),
connect(({ insightShortId, dashboardId }: SubscriptionsLogicProps) => ({
actions: [subscriptionsLogic({ insightShortId, dashboardId }), ['loadSubscriptions']],
values: [integrationsLogic, ['isMemberOfSlackChannel']],
})),

loaders(({ props }) => ({
Expand All @@ -48,7 +46,7 @@ export const subscriptionLogic = kea<subscriptionLogicType>([
},
})),

forms(({ props, actions, values }) => ({
forms(({ props, actions }) => ({
subscription: {
defaults: {} as unknown as SubscriptionType,
errors: ({ frequency, interval, target_value, target_type, title, start_date }) => ({
Expand Down Expand Up @@ -76,12 +74,6 @@ export const subscriptionLogic = kea<subscriptionLogicType>([
? 'Must be a valid URL'
: undefined
: undefined,
memberOfSlackChannel:
target_type == 'slack'
? target_value && !values.isMemberOfSlackChannel(target_value)
? 'Please add the PostHog Slack App to the selected channel'
: undefined
: undefined,
}),
submit: async (subscription, breakpoint) => {
const insightId = props.insightShortId ? await getInsightId(props.insightShortId) : undefined
Expand Down
30 changes: 2 additions & 28 deletions frontend/src/lib/components/Subscriptions/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { IconLetter } from '@posthog/icons'
import { LemonSelectOptions } from '@posthog/lemon-ui'
import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons'
import { LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { IconSlack } from 'lib/lemon-ui/icons'
import { range } from 'lib/utils'
import { urls } from 'scenes/urls'

import { InsightShortId, SlackChannelType } from '~/types'
import { InsightShortId } from '~/types'

export interface SubscriptionBaseProps {
dashboardId?: number
Expand Down Expand Up @@ -80,28 +79,3 @@ export const timeOptions: LemonSelectOptions<string> = range(0, 24).map((x) => (
value: String(x),
label: `${String(x).padStart(2, '0')}:00`,
}))

export const getSlackChannelOptions = (
value: string,
slackChannels?: SlackChannelType[] | null
): LemonInputSelectOption[] => {
return slackChannels
? slackChannels.map((x) => ({
key: `${x.id}|#${x.name}`,
labelComponent: (
<span className="flex items-center">
{x.is_private ? `🔒${x.name}` : `#${x.name}`}
{x.is_ext_shared ? <IconSlackExternal className="ml-2" /> : null}
</span>
),
label: `${x.id} #${x.name}`,
}))
: value
? [
{
key: value,
label: value?.split('|')?.pop() || value,
},
]
: []
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import { Form } from 'kea-forms'
import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator'
import { usersLemonSelectOptions } from 'lib/components/UserSelectItem'
import { dayjs } from 'lib/dayjs'
import { integrationsLogic } from 'lib/integrations/integrationsLogic'
import { SlackChannelPicker } from 'lib/integrations/SlackIntegrationHelpers'
import { IconChevronLeft } from 'lib/lemon-ui/icons'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonInputSelect, LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
import { LemonModal } from 'lib/lemon-ui/LemonModal'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { useEffect, useMemo } from 'react'
import { membersLogic } from 'scenes/organization/membersLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { integrationsLogic } from 'scenes/settings/project/integrationsLogic'
import { urls } from 'scenes/urls'

import { subscriptionLogic } from '../subscriptionLogic'
Expand All @@ -25,7 +25,6 @@ import {
bysetposOptions,
frequencyOptionsPlural,
frequencyOptionsSingular,
getSlackChannelOptions,
intervalOptions,
monthlyWeekdayOptions,
SubscriptionBaseProps,
Expand Down Expand Up @@ -59,15 +58,14 @@ export function EditSubscription({
})

const { meFirstMembers, membersLoading } = useValues(membersLogic)
const { subscription, subscriptionLoading, isSubscriptionSubmitting, subscriptionChanged, isMemberOfSlackChannel } =
useValues(logic)
const { subscription, subscriptionLoading, isSubscriptionSubmitting, subscriptionChanged } = useValues(logic)
const { preflight, siteUrlMisconfigured } = useValues(preflightLogic)
const { deleteSubscription } = useActions(subscriptionslogic)
const { slackChannels, slackChannelsLoading, slackIntegration, addToSlackButtonUrl } = useValues(integrationsLogic)
const { loadSlackChannels } = useActions(integrationsLogic)
const { slackIntegrations, addToSlackButtonUrl } = useValues(integrationsLogic)
// TODO: Fix this so that we use the appropriate config...
const firstSlackIntegration = slackIntegrations?.[0]

const emailDisabled = !preflight?.email_service_available
const slackDisabled = !slackIntegration

const _onDelete = (): void => {
if (id !== 'new') {
Expand All @@ -76,23 +74,6 @@ export function EditSubscription({
}
}

useEffect(() => {
if (subscription?.target_type === 'slack' && slackIntegration) {
loadSlackChannels()
}
}, [subscription?.target_type, slackIntegration])

// If slackChannels aren't loaded, make sure we display only the channel name and not the actual underlying value
const slackChannelOptions: LemonInputSelectOption[] = useMemo(
() => getSlackChannelOptions(subscription?.target_value, slackChannels),
[slackChannels, subscription?.target_value]
)

const showSlackMembershipWarning =
subscription.target_value &&
subscription.target_type === 'slack' &&
!isMemberOfSlackChannel(subscription.target_value)

const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'shortGeneric' })
const parts = formatter.formatToParts(new Date())
const currentTimezone = parts?.find((part) => part.type === 'timeZoneName')?.value
Expand Down Expand Up @@ -222,7 +203,7 @@ export function EditSubscription({

{subscription.target_type === 'slack' ? (
<>
{slackDisabled ? (
{!firstSlackIntegration ? (
<>
{addToSlackButtonUrl() ? (
<LemonBanner type="info">
Expand Down Expand Up @@ -278,45 +259,13 @@ export function EditSubscription({
}
>
{({ value, onChange }) => (
<LemonInputSelect
onChange={(val) => onChange(val[0] ?? null)}
value={value ? [value] : []}
disabled={slackDisabled}
mode="single"
data-attr="select-slack-channel"
placeholder="Select a channel..."
options={slackChannelOptions}
loading={slackChannelsLoading}
<SlackChannelPicker
value={value}
onChange={onChange}
integration={firstSlackIntegration}
/>
)}
</LemonField>

{showSlackMembershipWarning ? (
<LemonField name="memberOfSlackChannel">
<LemonBanner type="info">
<div className="flex gap-2 items-center">
<span>
The PostHog Slack App is not in this channel. Please add it
to the channel otherwise Subscriptions will fail to be
delivered.{' '}
<Link
to="https://posthog.com/docs/webhooks/slack"
target="_blank"
>
See the Docs for more information
</Link>
</span>
<LemonButton
type="secondary"
onClick={loadSlackChannels}
loading={slackChannelsLoading}
>
Check again
</LemonButton>
</div>
</LemonBanner>
</LemonField>
) : null}
</>
)}
</>
Expand Down
130 changes: 130 additions & 0 deletions frontend/src/lib/integrations/SlackIntegrationHelpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { LemonBanner, LemonButton, LemonInputSelect, LemonInputSelectOption, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator'
import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons'
import { useMemo } from 'react'

import { IntegrationType, SlackChannelType } from '~/types'

import { slackIntegrationLogic } from './slackIntegrationLogic'

const getSlackChannelOptions = (slackChannels?: SlackChannelType[] | null): LemonInputSelectOption[] | null => {
return slackChannels
? slackChannels.map((x) => ({
key: `${x.id}|#${x.name}`,
labelComponent: (
<span className="flex items-center">
{x.is_private ? `🔒${x.name}` : `#${x.name}`}
{x.is_ext_shared ? <IconSlackExternal className="ml-2" /> : null}
</span>
),
label: `${x.id} #${x.name}`,
}))
: null
}

export type SlackChannelPickerProps = {
integration: IntegrationType
value?: string
onChange?: (value: string | null) => void
disabled?: boolean
}

export function SlackChannelPicker({ onChange, value, integration, disabled }: SlackChannelPickerProps): JSX.Element {
const { slackChannels, slackChannelsLoading, isMemberOfSlackChannel } = useValues(
slackIntegrationLogic({ id: integration.id })
)
const { loadSlackChannels } = useActions(slackIntegrationLogic({ id: integration.id }))

// If slackChannels aren't loaded, make sure we display only the channel name and not the actual underlying value
const slackChannelOptions = useMemo(() => getSlackChannelOptions(slackChannels), [slackChannels])
const showSlackMembershipWarning = value && isMemberOfSlackChannel(value) === false

// Sometimes the parent will only store the channel ID and not the name, so we need to handle that

const modifiedValue = useMemo(() => {
if (value?.split('|').length === 1) {
const channel = slackChannels?.find((x) => x.id === value)

if (channel) {
return `${channel.id}|#${channel.name}`
}
}

return value
}, [value, slackChannels])

return (
<>
<LemonInputSelect
onChange={(val) => onChange?.(val[0] ?? null)}
value={modifiedValue ? [modifiedValue] : []}
onFocus={() => !slackChannels && !slackChannelsLoading && loadSlackChannels()}
disabled={disabled}
mode="single"
data-attr="select-slack-channel"
placeholder="Select a channel..."
options={
slackChannelOptions ??
(modifiedValue
? [
{
key: modifiedValue,
label: modifiedValue?.split('|')[1] ?? modifiedValue,
},
]
: [])
}
loading={slackChannelsLoading}
/>

{showSlackMembershipWarning ? (
<LemonBanner type="info">
<div className="flex gap-2 items-center">
<span>
The PostHog Slack App is not in this channel. Please add it to the channel otherwise
Subscriptions will fail to be delivered.{' '}
<Link to="https://posthog.com/docs/webhooks/slack" target="_blank">
See the Docs for more information
</Link>
</span>
<LemonButton type="secondary" onClick={loadSlackChannels} loading={slackChannelsLoading}>
Check again
</LemonButton>
</div>
</LemonBanner>
) : null}
</>
)
}

export function SlackIntegrationView({
integration,
suffix,
}: {
integration: IntegrationType
suffix?: JSX.Element
}): JSX.Element {
return (
<div className="rounded border flex justify-between items-center p-2 bg-bg-light">
<div className="flex items-center gap-4 ml-2">
<IconSlack className="text-2xl" />
<div>
<div>
Connected to <strong>{integration.config.team.name}</strong> workspace
</div>
{integration.created_by ? (
<UserActivityIndicator
at={integration.created_at}
by={integration.created_by}
prefix="Updated"
className="text-muted"
/>
) : null}
</div>
</div>

{suffix}
</div>
)
}
Loading
Loading