From 403614e771e1fa5edfa21aedcd412b945e74d9a8 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 12:22:45 +0100 Subject: [PATCH 01/56] Started adding new settings scene --- .../CommandPalette/commandPaletteLogic.tsx | 2 +- .../lib/components/PersonalAPIKeys/index.tsx | 1 - frontend/src/scenes/appScenes.ts | 3 +- .../me/Settings}/PersonalAPIKeys.tsx | 2 +- frontend/src/scenes/me/Settings/index.tsx | 2 +- .../me/Settings}/personalAPIKeysLogic.ts | 0 frontend/src/scenes/sceneTypes.ts | 1 + frontend/src/scenes/scenes.ts | 5 + frontend/src/scenes/settings/SettingsMap.tsx | 60 ++++++ .../src/scenes/settings/SettingsScene.tsx | 57 ++++++ .../scenes/settings/user/ChangePassword.tsx | 46 +++++ .../scenes/settings/user/OptOutCapture.tsx | 36 ++++ .../scenes/settings/user/PersonalAPIKeys.tsx | 181 ++++++++++++++++++ .../settings/user/TwoFactorAuthentication.tsx | 57 ++++++ .../settings/user/UpdateEmailPreferences.tsx | 44 +++++ .../src/scenes/settings/user/UserDetails.tsx | 52 +++++ .../settings/user/changePasswordLogic.ts | 49 +++++ frontend/src/scenes/settings/user/index.tsx | 57 ++++++ .../settings/user/personalAPIKeysLogic.ts | 43 +++++ frontend/src/scenes/urls.ts | 2 + 20 files changed, 695 insertions(+), 5 deletions(-) delete mode 100644 frontend/src/lib/components/PersonalAPIKeys/index.tsx rename frontend/src/{lib/components/PersonalAPIKeys => scenes/me/Settings}/PersonalAPIKeys.tsx (98%) rename frontend/src/{lib/components/PersonalAPIKeys => scenes/me/Settings}/personalAPIKeysLogic.ts (100%) create mode 100644 frontend/src/scenes/settings/SettingsMap.tsx create mode 100644 frontend/src/scenes/settings/SettingsScene.tsx create mode 100644 frontend/src/scenes/settings/user/ChangePassword.tsx create mode 100644 frontend/src/scenes/settings/user/OptOutCapture.tsx create mode 100644 frontend/src/scenes/settings/user/PersonalAPIKeys.tsx create mode 100644 frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx create mode 100644 frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx create mode 100644 frontend/src/scenes/settings/user/UserDetails.tsx create mode 100644 frontend/src/scenes/settings/user/changePasswordLogic.ts create mode 100644 frontend/src/scenes/settings/user/index.tsx create mode 100644 frontend/src/scenes/settings/user/personalAPIKeysLogic.ts diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index a29dca16342ae..15ee95c2d0b6f 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -8,7 +8,7 @@ import { DashboardType, InsightType } from '~/types' import api from 'lib/api' import { copyToClipboard, isMobile, isURL, sample, uniqueBy } from 'lib/utils' import { userLogic } from 'scenes/userLogic' -import { personalAPIKeysLogic } from '../PersonalAPIKeys/personalAPIKeysLogic' +import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' import { teamLogic } from 'scenes/teamLogic' import posthog from 'posthog-js' import { debugCHQueries } from './DebugCHQueries' diff --git a/frontend/src/lib/components/PersonalAPIKeys/index.tsx b/frontend/src/lib/components/PersonalAPIKeys/index.tsx deleted file mode 100644 index 4b79332193d2c..0000000000000 --- a/frontend/src/lib/components/PersonalAPIKeys/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { PersonalAPIKeys } from './PersonalAPIKeys' diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index c1c1f9b35de01..594aa1cf6de36 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -57,7 +57,7 @@ export const appScenes: Record any> = { [Scene.Site]: () => import('./sites/Site'), [Scene.AsyncMigrations]: () => import('./instance/AsyncMigrations/AsyncMigrations'), [Scene.DeadLetterQueue]: () => import('./instance/DeadLetterQueue/DeadLetterQueue'), - [Scene.MySettings]: () => import('./me/Settings'), + [Scene.MySettings]: () => import('./me/settings'), [Scene.Annotations]: () => import('./annotations/Annotations'), [Scene.PreflightCheck]: () => import('./PreflightCheck/PreflightCheck'), [Scene.Signup]: () => import('./authentication/signup/SignupContainer'), @@ -83,4 +83,5 @@ export const appScenes: Record any> = { [Scene.Canvas]: () => import('./notebooks/NotebookCanvasScene'), [Scene.Products]: () => import('./products/Products'), [Scene.Onboarding]: () => import('./onboarding/Onboarding'), + [Scene.Settings]: () => import('./settings/SettingsScene'), } diff --git a/frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx b/frontend/src/scenes/me/Settings/PersonalAPIKeys.tsx similarity index 98% rename from frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx rename to frontend/src/scenes/me/Settings/PersonalAPIKeys.tsx index 1df2bf9437018..0030081af9a97 100644 --- a/frontend/src/lib/components/PersonalAPIKeys/PersonalAPIKeys.tsx +++ b/frontend/src/scenes/me/Settings/PersonalAPIKeys.tsx @@ -5,7 +5,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons' import { personalAPIKeysLogic } from './personalAPIKeysLogic' import { PersonalAPIKeyType } from '~/types' import { humanFriendlyDetailedTime } from 'lib/utils' -import { CopyToClipboardInline } from '../CopyToClipboard' +import { CopyToClipboardInline } from '../../../lib/components/CopyToClipboard' import { ColumnsType } from 'antd/lib/table' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInput, LemonModal, Link } from '@posthog/lemon-ui' diff --git a/frontend/src/scenes/me/Settings/index.tsx b/frontend/src/scenes/me/Settings/index.tsx index 7409de959e3fa..a9ff96fb49106 100644 --- a/frontend/src/scenes/me/Settings/index.tsx +++ b/frontend/src/scenes/me/Settings/index.tsx @@ -4,7 +4,7 @@ import { useAnchor } from 'lib/hooks/useAnchor' import { router } from 'kea-router' import { UpdateEmailPreferences } from './UpdateEmailPreferences' import { ChangePassword } from './ChangePassword' -import { PersonalAPIKeys } from 'lib/components/PersonalAPIKeys' +import { PersonalAPIKeys } from 'scenes/settings/user/PersonalAPIKeys' import { OptOutCapture } from './OptOutCapture' import { PageHeader } from 'lib/components/PageHeader' import { SceneExport } from 'scenes/sceneTypes' diff --git a/frontend/src/lib/components/PersonalAPIKeys/personalAPIKeysLogic.ts b/frontend/src/scenes/me/Settings/personalAPIKeysLogic.ts similarity index 100% rename from frontend/src/lib/components/PersonalAPIKeys/personalAPIKeysLogic.ts rename to frontend/src/scenes/me/Settings/personalAPIKeysLogic.ts diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index dc17598a8ee81..ba81678a868cb 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -86,6 +86,7 @@ export enum Scene { Canvas = 'Canvas', Products = 'Products', Onboarding = 'Onboarding', + Settings = 'Settings', } export type SceneProps = Record diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 3d3b3baa214db..3b4c7dd90dc98 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -336,6 +336,10 @@ export const sceneConfigurations: Partial> = { name: 'Canvas', layout: 'app-raw', }, + [Scene.Settings]: { + projectBased: true, + name: 'Settings', + }, } const preserveParams = (url: string) => (_params: Params, searchParams: Params, hashParams: Params) => { @@ -514,4 +518,5 @@ export const routes: Record = { [urls.notebook(':shortId')]: Scene.Notebook, [urls.notebooks()]: Scene.Notebooks, [urls.canvas()]: Scene.Canvas, + [urls.settings()]: Scene.Settings, } diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx new file mode 100644 index 0000000000000..31f2bcdbb6128 --- /dev/null +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -0,0 +1,60 @@ +import { ChangePassword } from './user/ChangePassword' +import { OptOutCapture } from './user/OptOutCapture' +import { PersonalAPIKeys } from './user/PersonalAPIKeys' +import { TwoFactorAuthentication } from './user/TwoFactorAuthentication' +import { UpdateEmailPreferences } from './user/UpdateEmailPreferences' +import { UserDetails } from './user/UserDetails' + +export type Setting = { + id: string + title: string + description?: JSX.Element | string + component: JSX.Element +} + +export type SettingSection = { + id: string + title: string + settings: Setting[] +} + +const UserSettings: Setting[] = [ + { + id: 'details', + title: 'Details', + component: , + }, + { + id: 'change-password', + title: 'Change password', + component: , + }, + { + id: 'personal-api-keys', + title: 'Personal API keys', + component: , + }, + { + id: 'two-factor-authentication', + title: 'Two-factor authentication', + component: , + }, + { + id: 'notifications', + title: 'Notifications', + component: , + }, + { + id: 'optout', + title: 'Anonymize Data Collection', + component: , + }, +] + +export const AllSettings: SettingSection[] = [ + { + id: 'user', + title: 'User', + settings: UserSettings, + }, +] diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx new file mode 100644 index 0000000000000..530f122e912ae --- /dev/null +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -0,0 +1,57 @@ +import { PageHeader } from 'lib/components/PageHeader' +import { SceneExport } from 'scenes/sceneTypes' +import { AllSettings } from './SettingsMap' + +export const SettingsSections = {} + +export const scene: SceneExport = { + component: SettingsScene, +} + +/** + * + * Settings can be accessed in multiple ways: + * 1. Via the main settings page - each section is a separate page + * 2. Via small popups for individual settings + * 3. Via the sidepanel (3000) for any section + */ + +export function SettingsScene(): JSX.Element { + // const { location } = useValues(router) + + // useAnchor(location.hash) + + return ( + <> +
+
+
    + {AllSettings.map((section) => ( +
  • + {section.title} +
      + {section.settings.map((setting) => ( +
    • + {setting.title} +
    • + ))} +
    +
  • + ))} +
+
+ +
+ {AllSettings.map((x) => ( +
+

{x.title}

+ {x.description &&

{x.description}

} + + {x.component} +
+ ))} +
+
+ + ) +} diff --git a/frontend/src/scenes/settings/user/ChangePassword.tsx b/frontend/src/scenes/settings/user/ChangePassword.tsx new file mode 100644 index 0000000000000..b7cb1139c40b4 --- /dev/null +++ b/frontend/src/scenes/settings/user/ChangePassword.tsx @@ -0,0 +1,46 @@ +import { useValues } from 'kea' +import { Form } from 'kea-forms' +import { Field } from 'lib/forms/Field' +import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import PasswordStrength from 'lib/components/PasswordStrength' +import { changePasswordLogic } from './changePasswordLogic' + +export function ChangePassword(): JSX.Element { + const { changePassword, isChangePasswordSubmitting } = useValues(changePasswordLogic) + + return ( +
+ + + + + + Password + + + + + } + > + + + + + Change password + +
+ ) +} diff --git a/frontend/src/scenes/settings/user/OptOutCapture.tsx b/frontend/src/scenes/settings/user/OptOutCapture.tsx new file mode 100644 index 0000000000000..b4cd0ce8b4ccf --- /dev/null +++ b/frontend/src/scenes/settings/user/OptOutCapture.tsx @@ -0,0 +1,36 @@ +import { useActions, useValues } from 'kea' +import { Switch } from 'antd' +import { userLogic } from 'scenes/userLogic' + +export function OptOutCapture(): JSX.Element { + const { user, userLoading } = useValues(userLogic) + const { updateUser } = useActions(userLogic) + + return ( +
+

+ PostHog uses PostHog (unsurprisingly!) to capture information about how people are using the product. We + believe that product analytics is crucial to making PostHog the most useful it can be, for everyone. +

+

+ We also understand there are many reasons why people don't want to or aren't allowed to send this usage + data. If you would like to anonymize your personal usage data, just tick the box below. +

+ updateUser({ anonymize_data: checked })} + defaultChecked={user?.anonymize_data} + loading={userLoading} + disabled={userLoading} + /> + +
+ ) +} diff --git a/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx new file mode 100644 index 0000000000000..0030081af9a97 --- /dev/null +++ b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx @@ -0,0 +1,181 @@ +import { useState, useCallback, Dispatch, SetStateAction } from 'react' +import { Table, Popconfirm } from 'antd' +import { useActions, useValues } from 'kea' +import { ExclamationCircleOutlined } from '@ant-design/icons' +import { personalAPIKeysLogic } from './personalAPIKeysLogic' +import { PersonalAPIKeyType } from '~/types' +import { humanFriendlyDetailedTime } from 'lib/utils' +import { CopyToClipboardInline } from '../../../lib/components/CopyToClipboard' +import { ColumnsType } from 'antd/lib/table' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonInput, LemonModal, Link } from '@posthog/lemon-ui' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { IconPlus } from 'lib/lemon-ui/icons' + +function CreateKeyModal({ + isOpen, + setIsOpen, +}: { + isOpen: boolean + setIsOpen: Dispatch> +}): JSX.Element { + const { createKey } = useActions(personalAPIKeysLogic) + + const [errorMessage, setErrorMessage] = useState(null) + const [label, setLabel] = useState('') + + const closeModal: () => void = useCallback(() => { + setErrorMessage(null) + setIsOpen(false) + }, [setIsOpen]) + + return ( + + + Cancel + + + { + if (label) { + setErrorMessage(null) + createKey(label) + setLabel('') + closeModal() + } else { + setErrorMessage('Your key needs a label!') + } + }} + > + Create key + + + } + > +
+ + {errorMessage && {errorMessage}} +

+ Key value will only ever be shown once, immediately after creation. +
+ Copy it to your destination right away. +

+
+
+ ) +} + +function RowValue(value: string): JSX.Element { + return value ? ( + {value} + ) : ( + secret + ) +} + +function RowActionsCreator( + deleteKey: (key: PersonalAPIKeyType) => void +): (personalAPIKey: PersonalAPIKeyType) => JSX.Element { + return function RowActions(personalAPIKey: PersonalAPIKeyType) { + return ( + } + placement="left" + onConfirm={() => { + deleteKey(personalAPIKey) + }} + > + Danger + + ) + } +} + +function PersonalAPIKeysTable(): JSX.Element { + const { keys } = useValues(personalAPIKeysLogic) as { keys: PersonalAPIKeyType[] } + const { deleteKey } = useActions(personalAPIKeysLogic) + + const columns: ColumnsType> = [ + { + title: 'Label', + dataIndex: 'label', + key: 'label', + }, + { + title: 'Value', + dataIndex: 'value', + key: 'value', + className: 'ph-no-capture', + render: RowValue, + }, + { + title: 'Last Used', + dataIndex: 'last_used_at', + key: 'lastUsedAt', + render: (lastUsedAt: string | null) => humanFriendlyDetailedTime(lastUsedAt, 'MMMM DD, YYYY', 'h A'), + }, + { + title: 'Created', + dataIndex: 'created_at', + key: 'createdAt', + render: (createdAt: string | null) => humanFriendlyDetailedTime(createdAt), + }, + { + title: '', + key: 'actions', + align: 'center', + render: RowActionsCreator(deleteKey), + }, + ] + + return ( + + ) +} + +export function PersonalAPIKeys(): JSX.Element { + const [modalIsOpen, setModalIsOpen] = useState(false) + + return ( + <> +

+ These keys allow full access to your personal account through the API, as if you were logged in. You can + also use them in integrations, such as{' '} + our premium Zapier one. +
+ Try not to keep disused keys around. If you have any suspicion that one of these may be compromised, + delete it and use a new one. +
+ + More about API authentication in PostHog Docs. + +

+ { + setModalIsOpen(true) + }} + icon={} + > + Create personal API key + + + + + ) +} diff --git a/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx new file mode 100644 index 0000000000000..b31e3d92da0b1 --- /dev/null +++ b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx @@ -0,0 +1,57 @@ +import { useValues, useActions } from 'kea' +import { userLogic } from 'scenes/userLogic' +import { LemonButton, LemonModal } from '@posthog/lemon-ui' +import { IconCheckmark, IconWarning } from 'lib/lemon-ui/icons' +import { useState } from 'react' +import { Setup2FA } from 'scenes/authentication/Setup2FA' +import { membersLogic } from 'scenes/organization/Settings/membersLogic' + +export function TwoFactorAuthentication(): JSX.Element { + const { user } = useValues(userLogic) + const { updateUser } = useActions(userLogic) + const { loadMembers } = useActions(membersLogic) + const [modalVisible, setModalVisible] = useState(false) + + return ( +
+ {modalVisible && ( + setModalVisible(false)}> + <> + + Use an authenticator app like Google Authenticator or 1Password to scan the QR code below. + + { + setModalVisible(false) + updateUser({}) + loadMembers() + }} + /> + + + )} + + {user?.is_2fa_enabled ? ( + <> +
+ + 2FA enabled. +
+ + Manage or disable 2FA + + + ) : ( +
+
+ + 2FA is not enabled. +
+ setModalVisible(true)}> + Set up 2FA + +
+ )} +
+ ) +} diff --git a/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx new file mode 100644 index 0000000000000..4849e56ebb8dc --- /dev/null +++ b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx @@ -0,0 +1,44 @@ +import { useValues, useActions } from 'kea' +import { userLogic } from 'scenes/userLogic' +import { LemonSwitch } from '@posthog/lemon-ui' + +export function UpdateEmailPreferences(): JSX.Element { + const { user, userLoading } = useValues(userLogic) + const { updateUser } = useActions(userLogic) + + return ( +
+ { + updateUser({ email_opt_in: !user?.email_opt_in }) + }} + checked={user?.email_opt_in || false} + disabled={userLoading} + label="Receive security and feature updates via email. You can easily unsubscribe at any time." + fullWidth + bordered + /> +
+ + { + user?.notification_settings && + updateUser({ + notification_settings: { + ...user?.notification_settings, + plugin_disabled: !user?.notification_settings.plugin_disabled, + }, + }) + }} + checked={user?.notification_settings.plugin_disabled || false} + disabled={userLoading} + fullWidth + bordered + label="Get notified when plugins are disabled due to errors." + /> +
+ ) +} diff --git a/frontend/src/scenes/settings/user/UserDetails.tsx b/frontend/src/scenes/settings/user/UserDetails.tsx new file mode 100644 index 0000000000000..324e70bc2818d --- /dev/null +++ b/frontend/src/scenes/settings/user/UserDetails.tsx @@ -0,0 +1,52 @@ +import { useValues } from 'kea' +import { userLogic } from 'scenes/userLogic' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { Field } from 'lib/forms/Field' +import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' +import { Form } from 'kea-forms' +import { LemonTag } from '@posthog/lemon-ui' + +export function UserDetails(): JSX.Element { + const { userLoading, isUserDetailsSubmitting, userDetailsChanged, user } = useValues(userLogic) + + return ( +
+ + + + + + + + {user?.pending_email && Pending verification for {user.pending_email}} + + + Save name and email + + + ) +} diff --git a/frontend/src/scenes/settings/user/changePasswordLogic.ts b/frontend/src/scenes/settings/user/changePasswordLogic.ts new file mode 100644 index 0000000000000..99053bdf8da10 --- /dev/null +++ b/frontend/src/scenes/settings/user/changePasswordLogic.ts @@ -0,0 +1,49 @@ +import { lemonToast } from '@posthog/lemon-ui' +import { kea, path, connect } from 'kea' +import { forms } from 'kea-forms' +import api from 'lib/api' +import { userLogic } from 'scenes/userLogic' + +import type { changePasswordLogicType } from './changePasswordLogicType' + +export interface ChangePasswordForm { + current_password: string + password: string +} + +export const changePasswordLogic = kea([ + path(['scenes', 'me', 'settings', 'changePasswordLogic']), + connect({ + values: [userLogic, ['user']], + }), + forms(({ values, actions }) => ({ + changePassword: { + defaults: {} as unknown as ChangePasswordForm, + errors: ({ current_password, password }) => ({ + current_password: + (!values.user || values.user.has_password) && !current_password + ? 'Please enter your current password' + : undefined, + password: !password + ? 'Please enter your password to continue' + : password.length < 8 + ? 'Password must be at least 8 characters' + : undefined, + }), + submit: async ({ password, current_password }, breakpoint) => { + await breakpoint(150) + + try { + await api.update('api/users/@me/', { + current_password, + password, + }) + actions.resetChangePassword({ password: '', current_password: '' }) + lemonToast.success('Password changed') + } catch (e: any) { + actions.setChangePasswordManualErrors({ [e.attr]: e.detail }) + } + }, + }, + })), +]) diff --git a/frontend/src/scenes/settings/user/index.tsx b/frontend/src/scenes/settings/user/index.tsx new file mode 100644 index 0000000000000..a9ff96fb49106 --- /dev/null +++ b/frontend/src/scenes/settings/user/index.tsx @@ -0,0 +1,57 @@ +import { useValues } from 'kea' +import { Divider } from 'antd' +import { useAnchor } from 'lib/hooks/useAnchor' +import { router } from 'kea-router' +import { UpdateEmailPreferences } from './UpdateEmailPreferences' +import { ChangePassword } from './ChangePassword' +import { PersonalAPIKeys } from 'scenes/settings/user/PersonalAPIKeys' +import { OptOutCapture } from './OptOutCapture' +import { PageHeader } from 'lib/components/PageHeader' +import { SceneExport } from 'scenes/sceneTypes' +import { UserDetails } from './UserDetails' +import { TwoFactorAuthentication } from './TwoFactorAuthentication' + +export const scene: SceneExport = { + component: MySettings, +} + +export function MySettings(): JSX.Element { + const { location } = useValues(router) + + useAnchor(location.hash) + + return ( + <> + +
+ + + +

+ Change Password +

+ + +

+ Personal API Keys +

+ + +

+ Two-Factor Authentication +

+ + +
+

Notifications

+ +
+ +

+ Anonymize Data Collection +

+ +
+ + ) +} diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts new file mode 100644 index 0000000000000..ec44ba725b377 --- /dev/null +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts @@ -0,0 +1,43 @@ +import { loaders } from 'kea-loaders' +import { kea, path, listeners, events } from 'kea' +import api from 'lib/api' +import { PersonalAPIKeyType } from '~/types' +import type { personalAPIKeysLogicType } from './personalAPIKeysLogicType' +import { copyToClipboard } from 'lib/utils' +import { lemonToast } from 'lib/lemon-ui/lemonToast' + +export const personalAPIKeysLogic = kea([ + path(['lib', 'components', 'PersonalAPIKeys', 'personalAPIKeysLogic']), + loaders(({ values }) => ({ + keys: [ + [] as PersonalAPIKeyType[], + { + loadKeys: async () => { + const response: PersonalAPIKeyType[] = await api.get('api/personal_api_keys/') + return response + }, + createKey: async (label: string) => { + const newKey: PersonalAPIKeyType = await api.create('api/personal_api_keys/', { + label, + }) + return [newKey, ...values.keys] + }, + deleteKey: async (key: PersonalAPIKeyType) => { + await api.delete(`api/personal_api_keys/${key.id}/`) + return (values.keys as PersonalAPIKeyType[]).filter((filteredKey) => filteredKey.id != key.id) + }, + }, + ], + })), + listeners(() => ({ + createKeySuccess: async ({ keys }: { keys: PersonalAPIKeyType[] }) => { + keys[0]?.value && (await copyToClipboard(keys[0].value, 'personal API key value')) + }, + deleteKeySuccess: () => { + lemonToast.success(`Personal API key deleted`) + }, + })), + events(({ actions }) => ({ + afterMount: [actions.loadKeys], + })), +]) diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index 3e6823c7fa1b6..d4310d9132523 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -133,6 +133,8 @@ export const urls = { combineUrl(`/app/${pluginConfigId}/logs`, searchParams).url, projectCreateFirst: (): string => '/project/create', projectHomepage: (): string => '/home', + // TODO: Change this later to be more strict type + settings: (section?: string): string => `/settings${section ? `#${section}` : ''}`, projectSettings: (section?: string): string => `/project/settings${section ? `#${section}` : ''}`, mySettings: (): string => '/me/settings', organizationSettings: (): string => '/organization/settings', From 9cf60e0259c97e05d2804b6ae19c45c72a50f0c3 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 13:46:56 +0100 Subject: [PATCH 02/56] Fixed up settings --- frontend/src/scenes/settings/SettingsMap.tsx | 83 ++++++----- .../src/scenes/settings/SettingsScene.tsx | 47 ++++-- frontend/src/scenes/settings/settingsLogic.ts | 48 +++++++ .../scenes/settings/user/OptOutCapture.tsx | 17 +-- .../scenes/settings/user/PersonalAPIKeys.tsx | 136 +++++++++--------- frontend/src/scenes/settings/user/index.tsx | 57 -------- 6 files changed, 200 insertions(+), 188 deletions(-) create mode 100644 frontend/src/scenes/settings/settingsLogic.ts delete mode 100644 frontend/src/scenes/settings/user/index.tsx diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 31f2bcdbb6128..0b8b9007683ba 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -5,6 +5,11 @@ import { TwoFactorAuthentication } from './user/TwoFactorAuthentication' import { UpdateEmailPreferences } from './user/UpdateEmailPreferences' import { UserDetails } from './user/UserDetails' +export type SettingLevel = 'user' | 'project' | 'organization' +export type SettingSectionId = 'user-details' | 'user-api-keys' | 'user-notifications' + +export const SettingLevels: SettingLevel[] = ['project', 'organization', 'user'] + export type Setting = { id: string title: string @@ -13,48 +18,62 @@ export type Setting = { } export type SettingSection = { - id: string + id: SettingSectionId title: string + level: SettingLevel settings: Setting[] } -const UserSettings: Setting[] = [ +export const SettingsSections: SettingSection[] = [ { - id: 'details', + level: 'user', + id: 'user-details', title: 'Details', - component: , - }, - { - id: 'change-password', - title: 'Change password', - component: , - }, - { - id: 'personal-api-keys', - title: 'Personal API keys', - component: , + settings: [ + { + id: 'details', + title: 'Details', + component: , + }, + { + id: 'change-password', + title: 'Change password', + component: , + }, + { + id: 'two-factor-authentication', + title: 'Two-factor authentication', + component: , + }, + ], }, { - id: 'two-factor-authentication', - title: 'Two-factor authentication', - component: , + level: 'user', + id: 'user-api-keys', + title: 'Personal API Keys', + settings: [ + { + id: 'personal-api-keys', + title: 'Personal API keys', + component: , + }, + ], }, { - id: 'notifications', + level: 'user', + id: 'user-notifications', title: 'Notifications', - component: , - }, - { - id: 'optout', - title: 'Anonymize Data Collection', - component: , - }, -] - -export const AllSettings: SettingSection[] = [ - { - id: 'user', - title: 'User', - settings: UserSettings, + settings: [ + { + id: 'notifications', + title: 'Notifications', + component: , + }, + { + id: 'optout', + title: 'Anonymize Data Collection', + component: , + }, + ], }, ] diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx index 530f122e912ae..5172695ab0e43 100644 --- a/frontend/src/scenes/settings/SettingsScene.tsx +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -1,8 +1,10 @@ -import { PageHeader } from 'lib/components/PageHeader' import { SceneExport } from 'scenes/sceneTypes' -import { AllSettings } from './SettingsMap' - -export const SettingsSections = {} +import { SettingLevels, SettingsSections } from './SettingsMap' +import { capitalizeFirstLetter } from 'lib/utils' +import { useActions, useValues } from 'kea' +import { settingsLogic } from './settingsLogic' +import { LemonButton } from '@posthog/lemon-ui' +import clsx from 'clsx' export const scene: SceneExport = { component: SettingsScene, @@ -17,6 +19,9 @@ export const scene: SceneExport = { */ export function SettingsScene(): JSX.Element { + const { selectedSectionId, selectedLevel, settings } = useValues(settingsLogic) + const { selectSection, selectLevel } = useActions(settingsLogic) + // const { location } = useValues(router) // useAnchor(location.hash) @@ -24,15 +29,27 @@ export function SettingsScene(): JSX.Element { return ( <>
-
-
    - {AllSettings.map((section) => ( -
  • - {section.title} -
      - {section.settings.map((setting) => ( -
    • - {setting.title} +
      +
        + {SettingLevels.map((level) => ( +
      • + selectLevel(level)} size="small" fullWidth> + + {capitalizeFirstLetter(level)} + + + +
          + {SettingsSections.filter((x) => x.level === level).map((section) => ( +
        • + selectSection(section.id)} + size="small" + fullWidth + active={selectedSectionId === section.id} + > + {section.title} +
        • ))}
        @@ -41,8 +58,8 @@ export function SettingsScene(): JSX.Element {
      -
      - {AllSettings.map((x) => ( +
      + {settings.map((x) => (

      {x.title}

      {x.description &&

      {x.description}

      } diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts new file mode 100644 index 0000000000000..c50531a1ea39e --- /dev/null +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -0,0 +1,48 @@ +import { actions, kea, path, reducers, selectors } from 'kea' +import { Setting, SettingLevel, SettingSectionId, SettingsSections } from './SettingsMap' + +import type { settingsLogicType } from './settingsLogicType' + +export const settingsLogic = kea([ + path(['scenes', 'settings']), + + actions({ + selectSection: (section: SettingSectionId) => ({ section }), + selectLevel: (level: SettingLevel) => ({ level }), + }), + + reducers({ + selectedLevel: [ + 'user' as SettingLevel, + { + selectLevel: (_, { level }) => level, + }, + ], + selectedSectionId: [ + null as SettingSectionId | null, + { + selectSection: (_, { section }) => section, + }, + ], + }), + + selectors({ + settings: [ + (s) => [s.selectedLevel, s.selectedSectionId], + (selectedLevel, selectedSectionId): Setting[] => { + if (!selectedSectionId) { + console.log( + 'wat', + SettingsSections.filter((section) => section.level === selectedLevel) + ) + return SettingsSections.filter((section) => section.level === selectedLevel).reduce( + (acc, section) => [...acc, ...section.settings], + [] as Setting[] + ) + } + + return SettingsSections.find((x) => x.id === selectedSectionId)?.settings || [] + }, + ], + }), +]) diff --git a/frontend/src/scenes/settings/user/OptOutCapture.tsx b/frontend/src/scenes/settings/user/OptOutCapture.tsx index b4cd0ce8b4ccf..4290928ebb5d1 100644 --- a/frontend/src/scenes/settings/user/OptOutCapture.tsx +++ b/frontend/src/scenes/settings/user/OptOutCapture.tsx @@ -1,6 +1,6 @@ import { useActions, useValues } from 'kea' -import { Switch } from 'antd' import { userLogic } from 'scenes/userLogic' +import { LemonSwitch } from '@posthog/lemon-ui' export function OptOutCapture(): JSX.Element { const { user, userLoading } = useValues(userLogic) @@ -16,21 +16,14 @@ export function OptOutCapture(): JSX.Element { We also understand there are many reasons why people don't want to or aren't allowed to send this usage data. If you would like to anonymize your personal usage data, just tick the box below.

      - updateUser({ anonymize_data: checked })} - defaultChecked={user?.anonymize_data} - loading={userLoading} + checked={user?.anonymize_data ?? false} disabled={userLoading} + bordered /> -
      ) } diff --git a/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx index 0030081af9a97..9ef2211f8633c 100644 --- a/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx +++ b/frontend/src/scenes/settings/user/PersonalAPIKeys.tsx @@ -1,14 +1,11 @@ import { useState, useCallback, Dispatch, SetStateAction } from 'react' -import { Table, Popconfirm } from 'antd' import { useActions, useValues } from 'kea' -import { ExclamationCircleOutlined } from '@ant-design/icons' import { personalAPIKeysLogic } from './personalAPIKeysLogic' import { PersonalAPIKeyType } from '~/types' import { humanFriendlyDetailedTime } from 'lib/utils' import { CopyToClipboardInline } from '../../../lib/components/CopyToClipboard' -import { ColumnsType } from 'antd/lib/table' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonInput, LemonModal, Link } from '@posthog/lemon-ui' +import { LemonDialog, LemonInput, LemonModal, LemonTable, Link } from '@posthog/lemon-ui' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconPlus } from 'lib/lemon-ui/icons' @@ -71,79 +68,72 @@ function CreateKeyModal({ ) } -function RowValue(value: string): JSX.Element { - return value ? ( - {value} - ) : ( - secret - ) -} - -function RowActionsCreator( - deleteKey: (key: PersonalAPIKeyType) => void -): (personalAPIKey: PersonalAPIKeyType) => JSX.Element { - return function RowActions(personalAPIKey: PersonalAPIKeyType) { - return ( - } - placement="left" - onConfirm={() => { - deleteKey(personalAPIKey) - }} - > - Danger - - ) - } -} - function PersonalAPIKeysTable(): JSX.Element { const { keys } = useValues(personalAPIKeysLogic) as { keys: PersonalAPIKeyType[] } const { deleteKey } = useActions(personalAPIKeysLogic) - const columns: ColumnsType> = [ - { - title: 'Label', - dataIndex: 'label', - key: 'label', - }, - { - title: 'Value', - dataIndex: 'value', - key: 'value', - className: 'ph-no-capture', - render: RowValue, - }, - { - title: 'Last Used', - dataIndex: 'last_used_at', - key: 'lastUsedAt', - render: (lastUsedAt: string | null) => humanFriendlyDetailedTime(lastUsedAt, 'MMMM DD, YYYY', 'h A'), - }, - { - title: 'Created', - dataIndex: 'created_at', - key: 'createdAt', - render: (createdAt: string | null) => humanFriendlyDetailedTime(createdAt), - }, - { - title: '', - key: 'actions', - align: 'center', - render: RowActionsCreator(deleteKey), - }, - ] - return ( -
{`${value}`} + ) : ( + secret + ) + }, + }, + { + title: 'Last Used', + dataIndex: 'last_used_at', + key: 'lastUsedAt', + render: (_, key) => humanFriendlyDetailedTime(key.last_used_at, 'MMMM DD, YYYY', 'h A'), + }, + { + title: 'Created', + dataIndex: 'created_at', + key: 'createdAt', + render: (_, key) => humanFriendlyDetailedTime(key.created_at), + }, + { + title: '', + key: 'actions', + align: 'right', + width: 0, + render: (_, key) => { + return ( + { + LemonDialog.open({ + title: `Permanently delete key "${key.label}"?`, + description: 'This action cannot be undone.', + primaryButton: { + status: 'danger', + children: 'Permanently delete', + onClick: () => deleteKey(key), + }, + }) + }} + > + Delete + + ) + }, + }, + ]} /> ) } @@ -174,8 +164,10 @@ export function PersonalAPIKeys(): JSX.Element { > Create personal API key - + + + ) } diff --git a/frontend/src/scenes/settings/user/index.tsx b/frontend/src/scenes/settings/user/index.tsx deleted file mode 100644 index a9ff96fb49106..0000000000000 --- a/frontend/src/scenes/settings/user/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useValues } from 'kea' -import { Divider } from 'antd' -import { useAnchor } from 'lib/hooks/useAnchor' -import { router } from 'kea-router' -import { UpdateEmailPreferences } from './UpdateEmailPreferences' -import { ChangePassword } from './ChangePassword' -import { PersonalAPIKeys } from 'scenes/settings/user/PersonalAPIKeys' -import { OptOutCapture } from './OptOutCapture' -import { PageHeader } from 'lib/components/PageHeader' -import { SceneExport } from 'scenes/sceneTypes' -import { UserDetails } from './UserDetails' -import { TwoFactorAuthentication } from './TwoFactorAuthentication' - -export const scene: SceneExport = { - component: MySettings, -} - -export function MySettings(): JSX.Element { - const { location } = useValues(router) - - useAnchor(location.hash) - - return ( - <> - -
- - - -

- Change Password -

- - -

- Personal API Keys -

- - -

- Two-Factor Authentication -

- - -
-

Notifications

- -
- -

- Anonymize Data Collection -

- -
- - ) -} From 75657a7679152b91f311897a096eec2fcb225c5d Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 14:15:50 +0100 Subject: [PATCH 03/56] Added more settings --- .../src/layout/navigation/ProjectNotice.tsx | 2 +- .../layout/navigation/TopBar/SitePopover.tsx | 2 +- .../src/layout/navigation/TopBar/TopBar.tsx | 4 +- .../src/layout/navigation/navigationLogic.ts | 2 +- .../ActivationSidebar/activationLogic.test.ts | 4 +- .../ActivationSidebar/activationLogic.ts | 4 +- .../DefinitionPopover/DefinitionPopover.tsx | 2 +- .../src/lib/components/RestrictedArea.tsx | 23 ++++-- .../Subscriptions/views/EditSubscription.tsx | 2 +- frontend/src/scenes/App.tsx | 2 +- .../src/scenes/ResourcePermissionModal.tsx | 4 +- frontend/src/scenes/appScenes.ts | 2 +- .../dashboard/dashboards/DashboardsTable.tsx | 2 +- .../featureFlagPermissionsLogic.tsx | 2 +- .../IngestionInviteMembersButton.tsx | 2 +- .../src/scenes/ingestion/IngestionWizard.tsx | 4 +- .../src/scenes/ingestion/ingestionLogic.ts | 2 +- .../ingestion/panels/InviteTeamPanel.tsx | 2 +- .../me/Settings/TwoFactorAuthentication.tsx | 2 +- .../NotebooksTable/NotebooksTable.tsx | 2 +- .../scenes/organization/TeamMembers/index.tsx | 0 .../project-homepage/ProjectHomepage.tsx | 2 +- .../project/Settings/teamMembersLogic.tsx | 2 +- .../saved-insights/SavedInsightsFilters.tsx | 2 +- .../SavedSessionRecordingPlaylists.tsx | 2 +- frontend/src/scenes/settings/SettingsMap.tsx | 75 ++++++++++++++++++- .../organization}/DangerZone.tsx | 0 .../organization}/InviteModal.scss | 0 .../organization}/InviteModal.tsx | 0 .../organization}/Invites.tsx | 4 +- .../organization}/Members.tsx | 20 +++-- .../settings/organization/OrgDisplayName.tsx | 32 ++++++++ .../organization/OrgEmailPreferences.tsx | 25 +++++++ .../organization}/Permissions/Permissions.tsx | 0 .../Permissions/PermissionsGrid.tsx | 0 .../Permissions/Roles/CreateRoleModal.tsx | 0 .../organization}/Permissions/Roles/Roles.tsx | 0 .../Permissions/Roles/rolesLogic.tsx | 0 .../Permissions/permissionsLogic.tsx | 0 .../organization}/Permissions/utils.ts | 0 .../VerifiedDomains/AddDomainModal.tsx | 0 .../VerifiedDomains/ConfigureSAMLModal.tsx | 0 .../VerifiedDomains/SSOSelect.stories.tsx | 0 .../VerifiedDomains/SSOSelect.tsx | 0 .../VerifiedDomains/VerifiedDomains.tsx | 31 ++++---- .../VerifiedDomains/VerifyDomainModal.tsx | 0 .../verifiedDomainsLogic.test.ts.snap | 0 .../verifiedDomainsLogic.test.ts | 0 .../VerifiedDomains/verifiedDomainsLogic.ts | 0 .../organization}/index.tsx | 6 +- .../organization}/inviteLogic.ts | 0 .../organization}/invitesLogic.tsx | 0 .../organization}/membersLogic.tsx | 0 frontend/src/scenes/settings/settingsLogic.ts | 14 ++-- .../settings/user/TwoFactorAuthentication.tsx | 2 +- .../settings/user/UpdateEmailPreferences.tsx | 2 - 56 files changed, 210 insertions(+), 80 deletions(-) delete mode 100644 frontend/src/scenes/organization/TeamMembers/index.tsx rename frontend/src/scenes/{organization/Settings => settings/organization}/DangerZone.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/InviteModal.scss (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/InviteModal.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Invites.tsx (97%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Members.tsx (97%) create mode 100644 frontend/src/scenes/settings/organization/OrgDisplayName.tsx create mode 100644 frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/Permissions.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/PermissionsGrid.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/Roles/CreateRoleModal.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/Roles/Roles.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/Roles/rolesLogic.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/permissionsLogic.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/Permissions/utils.ts (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/AddDomainModal.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/ConfigureSAMLModal.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/SSOSelect.stories.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/SSOSelect.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/VerifiedDomains.tsx (93%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/VerifyDomainModal.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/verifiedDomainsLogic.test.ts (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/VerifiedDomains/verifiedDomainsLogic.ts (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/index.tsx (97%) rename frontend/src/scenes/{organization/Settings => settings/organization}/inviteLogic.ts (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/invitesLogic.tsx (100%) rename frontend/src/scenes/{organization/Settings => settings/organization}/membersLogic.tsx (100%) diff --git a/frontend/src/layout/navigation/ProjectNotice.tsx b/frontend/src/layout/navigation/ProjectNotice.tsx index e4e19d9abddf0..5d8205628e86c 100644 --- a/frontend/src/layout/navigation/ProjectNotice.tsx +++ b/frontend/src/layout/navigation/ProjectNotice.tsx @@ -1,7 +1,7 @@ import { useActions, useValues } from 'kea' import { Link } from 'lib/lemon-ui/Link' import { navigationLogic, ProjectNoticeVariant } from './navigationLogic' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconPlus, IconSettings } from 'lib/lemon-ui/icons' import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner' diff --git a/frontend/src/layout/navigation/TopBar/SitePopover.tsx b/frontend/src/layout/navigation/TopBar/SitePopover.tsx index c59fb8b443ec5..ac40a7f7bc34b 100644 --- a/frontend/src/layout/navigation/TopBar/SitePopover.tsx +++ b/frontend/src/layout/navigation/TopBar/SitePopover.tsx @@ -29,7 +29,7 @@ import { NewOrganizationButton, OtherOrganizationButton, } from '~/layout/navigation/OrganizationSwitcher' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { LemonButtonPropsBase } from '@posthog/lemon-ui' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index b81bbed16648e..9c0b9ce3692a4 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -6,12 +6,12 @@ import { navigationLogic } from '../navigationLogic' import { HelpButton } from 'lib/components/HelpButton/HelpButton' import { CommandPalette } from 'lib/components/CommandPalette' import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' -import { InviteModal } from 'scenes/organization/Settings/InviteModal' +import { InviteModal } from 'scenes/settings/organization/InviteModal' import { Link } from 'lib/lemon-ui/Link' import { IconMenu, IconMenuOpen } from 'lib/lemon-ui/icons' import { CreateProjectModal } from 'scenes/project/CreateProjectModal' import './TopBar.scss' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { UniversalSearchPopover } from 'lib/components/UniversalSearch/UniversalSearchPopover' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { groupsModel } from '~/models/groupsModel' diff --git a/frontend/src/layout/navigation/navigationLogic.ts b/frontend/src/layout/navigation/navigationLogic.ts index 5fa4fa296b86e..1bc02a742958f 100644 --- a/frontend/src/layout/navigation/navigationLogic.ts +++ b/frontend/src/layout/navigation/navigationLogic.ts @@ -8,7 +8,7 @@ import { sceneLogic } from 'scenes/sceneLogic' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' import type { navigationLogicType } from './navigationLogicType' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { Scene } from 'scenes/sceneTypes' diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts index 827d5eb5c3643..0e4cd61df767e 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.test.ts @@ -1,6 +1,6 @@ import { expectLogic } from 'kea-test-utils' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { teamLogic } from 'scenes/teamLogic' import { navigationLogic } from '~/layout/navigation/navigationLogic' diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts index 71df41f6c1305..70d43af448926 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts @@ -3,8 +3,8 @@ import { loaders } from 'kea-loaders' import { router, urlToAction } from 'kea-router' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { teamLogic } from 'scenes/teamLogic' import { navigationLogic } from '~/layout/navigation/navigationLogic' diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx index 559bd573735e4..201cd1f4fe4bf 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopover.tsx @@ -8,7 +8,7 @@ import { KeyMapping, UserBasicType, PropertyDefinition } from '~/types' import { Owner } from 'scenes/events/Owner' import { dayjs } from 'lib/dayjs' import { Divider, DividerProps, Select } from 'antd' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { Link } from 'lib/lemon-ui/Link' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' diff --git a/frontend/src/lib/components/RestrictedArea.tsx b/frontend/src/lib/components/RestrictedArea.tsx index 9d86714ce8cd6..c334a3a2235e6 100644 --- a/frontend/src/lib/components/RestrictedArea.tsx +++ b/frontend/src/lib/components/RestrictedArea.tsx @@ -18,17 +18,16 @@ export enum RestrictionScope { Project = 'project', } -export interface RestrictedAreaProps { - Component: (props: RestrictedComponentProps) => JSX.Element +export interface UseRestrictedAreaProps { minimumAccessLevel: EitherMembershipLevel scope?: RestrictionScope } -export function RestrictedArea({ - Component, - minimumAccessLevel, - scope = RestrictionScope.Organization, -}: RestrictedAreaProps): JSX.Element { +export interface RestrictedAreaProps extends UseRestrictedAreaProps { + Component: (props: RestrictedComponentProps) => JSX.Element +} + +export function useRestrictedArea({ scope, minimumAccessLevel }: UseRestrictedAreaProps): null | string { const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) @@ -59,6 +58,16 @@ export function RestrictedArea({ return null }, [currentOrganization]) + return restrictionReason +} + +export function RestrictedArea({ + Component, + minimumAccessLevel, + scope = RestrictionScope.Organization, +}: RestrictedAreaProps): JSX.Element { + const restrictionReason = useRestrictedArea({ minimumAccessLevel, scope }) + return restrictionReason ? ( diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index ec244d2324764..ddb4cdb590d52 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react' import { useActions, useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { Field } from 'lib/forms/Field' import { dayjs } from 'lib/dayjs' diff --git a/frontend/src/scenes/App.tsx b/frontend/src/scenes/App.tsx index deecc672d59ec..2324ecbe289de 100644 --- a/frontend/src/scenes/App.tsx +++ b/frontend/src/scenes/App.tsx @@ -20,7 +20,7 @@ import { inAppPromptLogic } from 'lib/logic/inAppPrompt/inAppPromptLogic' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { LemonModal } from '@posthog/lemon-ui' import { Setup2FA } from './authentication/Setup2FA' -import { membersLogic } from './organization/Settings/membersLogic' +import { membersLogic } from './settings/organization/membersLogic' import { FEATURE_FLAGS } from 'lib/constants' import { Navigation as Navigation3000 } from '~/layout/navigation-3000/Navigation' import { Prompt } from 'lib/logic/newPrompt/Prompt' diff --git a/frontend/src/scenes/ResourcePermissionModal.tsx b/frontend/src/scenes/ResourcePermissionModal.tsx index de8706a13e266..17c6238657914 100644 --- a/frontend/src/scenes/ResourcePermissionModal.tsx +++ b/frontend/src/scenes/ResourcePermissionModal.tsx @@ -12,8 +12,8 @@ import { FormattedResourceLevel, permissionsLogic, ResourcePermissionMapping, -} from './organization/Settings/Permissions/permissionsLogic' -import { rolesLogic } from './organization/Settings/Permissions/Roles/rolesLogic' +} from './settings/organization/Permissions/permissionsLogic' +import { rolesLogic } from './settings/organization/Permissions/Roles/rolesLogic' import { urls } from './urls' interface ResourcePermissionProps { diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 594aa1cf6de36..2893d15294ca9 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -46,7 +46,7 @@ export const appScenes: Record any> = { [Scene.DataWarehouseExternal]: () => import('./data-warehouse/external/DataWarehouseExternalScene'), [Scene.DataWarehouseSavedQueries]: () => import('./data-warehouse/saved_queries/DataWarehouseSavedQueriesScene'), [Scene.DataWarehouseSettings]: () => import('./data-warehouse/settings/DataWarehouseSettingsScene'), - [Scene.OrganizationSettings]: () => import('./organization/Settings'), + [Scene.OrganizationSettings]: () => import('./settings/organization'), [Scene.OrganizationCreateFirst]: () => import('./organization/Create'), [Scene.OrganizationCreationConfirm]: () => import('./organization/ConfirmOrganization/ConfirmOrganization'), [Scene.ProjectHomepage]: () => import('./project-homepage/ProjectHomepage'), diff --git a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx index 31e4eb0f8a471..ae1d71a351bf7 100644 --- a/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx +++ b/frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx @@ -22,7 +22,7 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonRow } from 'lib/lemon-ui/LemonRow' import { DASHBOARD_CANNOT_EDIT_MESSAGE } from '../DashboardHeader' import { LemonInput, LemonSelect } from '@posthog/lemon-ui' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' export function DashboardsTableContainer(): JSX.Element { diff --git a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx index 56d20fa28d25d..43c0733e877e7 100644 --- a/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx +++ b/frontend/src/scenes/feature-flags/featureFlagPermissionsLogic.tsx @@ -2,7 +2,7 @@ import { actions, afterMount, connect, kea, key, path, props, reducers, selector import { loaders } from 'kea-loaders' import api from 'lib/api' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { rolesLogic } from 'scenes/organization/Settings/Permissions/Roles/rolesLogic' +import { rolesLogic } from 'scenes/settings/organization/Permissions/Roles/rolesLogic' import { AccessLevel, FeatureFlagAssociatedRoleType, Resource, RoleType } from '~/types' import type { featureFlagPermissionsLogicType } from './featureFlagPermissionsLogicType' diff --git a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx index 43370a7d7f86c..439390bd83164 100644 --- a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx +++ b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx @@ -2,7 +2,7 @@ import { LemonButton } from '@posthog/lemon-ui' import { useActions } from 'kea' import { IconArrowRight } from 'lib/lemon-ui/icons' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' export function IngestionInviteMembersButton(): JSX.Element { const { showInviteModal } = useActions(inviteLogic) diff --git a/frontend/src/scenes/ingestion/IngestionWizard.tsx b/frontend/src/scenes/ingestion/IngestionWizard.tsx index 9c5e22cb38794..80c8d8c8690a2 100644 --- a/frontend/src/scenes/ingestion/IngestionWizard.tsx +++ b/frontend/src/scenes/ingestion/IngestionWizard.tsx @@ -12,8 +12,8 @@ import { GeneratingDemoDataPanel } from './panels/GeneratingDemoDataPanel' import { ThirdPartyPanel } from './panels/ThirdPartyPanel' import { BillingPanel } from './panels/BillingPanel' import { Sidebar } from './Sidebar' -import { InviteModal } from 'scenes/organization/Settings/InviteModal' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { InviteModal } from 'scenes/settings/organization/InviteModal' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { Logo } from '~/toolbar/assets/Logo' import { SitePopover } from '~/layout/navigation/TopBar/SitePopover' import { HelpButton } from 'lib/components/HelpButton/HelpButton' diff --git a/frontend/src/scenes/ingestion/ingestionLogic.ts b/frontend/src/scenes/ingestion/ingestionLogic.ts index 224949320e52b..cf64522b8e27e 100644 --- a/frontend/src/scenes/ingestion/ingestionLogic.ts +++ b/frontend/src/scenes/ingestion/ingestionLogic.ts @@ -11,7 +11,7 @@ import { windowValues } from 'kea-window-values' import { subscriptions } from 'kea-subscriptions' import { TeamType } from '~/types' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import api from 'lib/api' import { loaders } from 'kea-loaders' import type { ingestionLogicType } from './ingestionLogicType' diff --git a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx index a8ded3f51bd97..c6a30c5cf484f 100644 --- a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx +++ b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx @@ -4,7 +4,7 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import './Panels.scss' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { IconChevronRight } from 'lib/lemon-ui/icons' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { DemoProjectButton } from './PanelComponents' diff --git a/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx b/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx index b31e3d92da0b1..fe511e7a89ad2 100644 --- a/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx +++ b/frontend/src/scenes/me/Settings/TwoFactorAuthentication.tsx @@ -4,7 +4,7 @@ import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { IconCheckmark, IconWarning } from 'lib/lemon-ui/icons' import { useState } from 'react' import { Setup2FA } from 'scenes/authentication/Setup2FA' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' export function TwoFactorAuthentication(): JSX.Element { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx index 8d17fa0ef0e90..13cc83434e21d 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx @@ -10,7 +10,7 @@ import { useEffect } from 'react' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonMenu } from 'lib/lemon-ui/LemonMenu' import { IconDelete, IconEllipsis } from 'lib/lemon-ui/icons' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { ContainsTypeFilters } from 'scenes/notebooks/NotebooksTable/ContainsTypeFilter' import { DEFAULT_FILTERS, notebooksTableLogic } from 'scenes/notebooks/NotebooksTable/notebooksTableLogic' import { notebookPanelLogic } from '../NotebookPanel/notebookPanelLogic' diff --git a/frontend/src/scenes/organization/TeamMembers/index.tsx b/frontend/src/scenes/organization/TeamMembers/index.tsx deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx index 69d8233fdacf2..c8fea74e75c9c 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.tsx @@ -6,7 +6,7 @@ import { useActions, useValues } from 'kea' import { teamLogic } from 'scenes/teamLogic' import { Scene, SceneExport } from 'scenes/sceneTypes' import { DashboardPlacement } from '~/types' -import { inviteLogic } from 'scenes/organization/Settings/inviteLogic' +import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { IconCottage } from 'lib/lemon-ui/icons' import { projectHomepageLogic } from 'scenes/project-homepage/projectHomepageLogic' diff --git a/frontend/src/scenes/project/Settings/teamMembersLogic.tsx b/frontend/src/scenes/project/Settings/teamMembersLogic.tsx index 67455ac28c39f..cfa0135d5be58 100644 --- a/frontend/src/scenes/project/Settings/teamMembersLogic.tsx +++ b/frontend/src/scenes/project/Settings/teamMembersLogic.tsx @@ -13,7 +13,7 @@ import { UserType, } from '~/types' import type { teamMembersLogicType } from './teamMembersLogicType' -import { membersLogic } from '../../organization/Settings/membersLogic' +import { membersLogic } from '../../settings/organization/membersLogic' import { membershipLevelToName } from 'lib/utils/permissioning' import { userLogic } from '../../userLogic' import { teamLogic } from '../../teamLogic' diff --git a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx index 2edb70bf0f233..34d2ba0442237 100644 --- a/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx @@ -6,7 +6,7 @@ import { INSIGHT_TYPE_OPTIONS } from 'scenes/saved-insights/SavedInsights' import { useActions, useValues } from 'kea' import { dashboardsModel } from '~/models/dashboardsModel' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput' export function SavedInsightsFilters(): JSX.Element { diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx index d4f2adba93632..6ae7cf16d4b2b 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylists.tsx @@ -6,7 +6,7 @@ import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { urls } from 'scenes/urls' import { createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' import { TZLabel } from '@posthog/apps-common' import { SavedSessionRecordingPlaylistsEmptyState } from 'scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState' import clsx from 'clsx' diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 0b8b9007683ba..594f454fdcf65 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -1,12 +1,25 @@ +import { OrganizationMembershipLevel } from 'lib/constants' import { ChangePassword } from './user/ChangePassword' import { OptOutCapture } from './user/OptOutCapture' import { PersonalAPIKeys } from './user/PersonalAPIKeys' import { TwoFactorAuthentication } from './user/TwoFactorAuthentication' import { UpdateEmailPreferences } from './user/UpdateEmailPreferences' import { UserDetails } from './user/UserDetails' +import { EitherMembershipLevel } from 'lib/utils/permissioning' +import { OrganizationDisplayName } from './organization/OrgDisplayName' +import { Invites } from './organization/Invites' +import { Members } from './organization/Members' +import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' +import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' export type SettingLevel = 'user' | 'project' | 'organization' -export type SettingSectionId = 'user-details' | 'user-api-keys' | 'user-notifications' +export type SettingSectionId = + | 'user-details' + | 'user-api-keys' + | 'user-notifications' + | 'organization-details' + | 'organization-members' + | 'organization-authentication' export const SettingLevels: SettingLevel[] = ['project', 'organization', 'user'] @@ -22,9 +35,69 @@ export type SettingSection = { title: string level: SettingLevel settings: Setting[] + minimumAccessLevel?: EitherMembershipLevel +} + +{ + /*
+ + + + +
*/ } export const SettingsSections: SettingSection[] = [ + { + level: 'organization', + id: 'organization-details', + title: 'Details', + settings: [ + { + id: 'organization-details', + title: 'Details', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-members', + title: 'Members', + settings: [ + { + id: 'organization-invites', + title: 'Pending Invites', + component: , + }, + { + id: 'organization-members', + title: 'Members', + component: , + }, + { + id: 'organization-email-members', + title: 'Notification preferences', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-authentication', + title: 'Authentication Domains & SSO', + settings: [ + { + id: 'organization-domains', + title: 'Authentication Domains', + component: , + }, + ], + }, + { level: 'user', id: 'user-details', diff --git a/frontend/src/scenes/organization/Settings/DangerZone.tsx b/frontend/src/scenes/settings/organization/DangerZone.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/DangerZone.tsx rename to frontend/src/scenes/settings/organization/DangerZone.tsx diff --git a/frontend/src/scenes/organization/Settings/InviteModal.scss b/frontend/src/scenes/settings/organization/InviteModal.scss similarity index 100% rename from frontend/src/scenes/organization/Settings/InviteModal.scss rename to frontend/src/scenes/settings/organization/InviteModal.scss diff --git a/frontend/src/scenes/organization/Settings/InviteModal.tsx b/frontend/src/scenes/settings/organization/InviteModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/InviteModal.tsx rename to frontend/src/scenes/settings/organization/InviteModal.tsx diff --git a/frontend/src/scenes/organization/Settings/Invites.tsx b/frontend/src/scenes/settings/organization/Invites.tsx similarity index 97% rename from frontend/src/scenes/organization/Settings/Invites.tsx rename to frontend/src/scenes/settings/organization/Invites.tsx index 4fe50084b5b95..e9c4708817102 100644 --- a/frontend/src/scenes/organization/Settings/Invites.tsx +++ b/frontend/src/scenes/settings/organization/Invites.tsx @@ -98,12 +98,12 @@ export function Invites(): JSX.Element { return (
-

+ {/*

Pending Invites Invite team member -

+ */} {!preflight?.email_service_available && } = [ { @@ -262,8 +261,7 @@ export function Members({ user }: MembersProps): JSX.Element { return ( <> -

Members

- +
updateOrganization({ enforce_2fa })} /> - +
+ + { + e.preventDefault() + updateOrganization({ name }) + }} + disabled={isRestricted || !name || !currentOrganization || name === currentOrganization.name} + loading={currentOrganizationLoading} + > + Rename Organization + +
+ ) +} diff --git a/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx new file mode 100644 index 0000000000000..e2e4a1524e9e8 --- /dev/null +++ b/frontend/src/scenes/settings/organization/OrgEmailPreferences.tsx @@ -0,0 +1,25 @@ +import { LemonSwitch } from '@posthog/lemon-ui' +import { useValues, useActions } from 'kea' +import { useRestrictedArea } from 'lib/components/RestrictedArea' +import { OrganizationMembershipLevel } from 'lib/constants' +import { organizationLogic } from 'scenes/organizationLogic' + +export function OrganizationEmailPreferences(): JSX.Element { + const { currentOrganization } = useValues(organizationLogic) + const { updateOrganization } = useActions(organizationLogic) + + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) + + return ( + { + updateOrganization({ is_member_join_email_enabled: checked }) + }} + checked={!!currentOrganization?.is_member_join_email_enabled} + disabled={isRestricted || !currentOrganization} + label="Email all current members when a new member joins" + bordered + /> + ) +} diff --git a/frontend/src/scenes/organization/Settings/Permissions/Permissions.tsx b/frontend/src/scenes/settings/organization/Permissions/Permissions.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/Permissions.tsx rename to frontend/src/scenes/settings/organization/Permissions/Permissions.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/PermissionsGrid.tsx b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/PermissionsGrid.tsx rename to frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/CreateRoleModal.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/CreateRoleModal.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/CreateRoleModal.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/Roles.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/Roles.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/Roles.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/Roles/rolesLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/Roles/rolesLogic.tsx rename to frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/permissionsLogic.tsx rename to frontend/src/scenes/settings/organization/Permissions/permissionsLogic.tsx diff --git a/frontend/src/scenes/organization/Settings/Permissions/utils.ts b/frontend/src/scenes/settings/organization/Permissions/utils.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/Permissions/utils.ts rename to frontend/src/scenes/settings/organization/Permissions/utils.ts diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/AddDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/AddDomainModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/AddDomainModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/ConfigureSAMLModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/ConfigureSAMLModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSAMLModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.stories.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.stories.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.stories.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/SSOSelect.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/SSOSelect.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx similarity index 93% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx index 62610b74d3461..461d76a3d137f 100644 --- a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifiedDomains.tsx +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifiedDomains.tsx @@ -26,25 +26,20 @@ export function VerifiedDomains(): JSX.Element { return ( <> -
-
-

- Authentication domains -

-

- Enable users to sign up automatically with an email address on verified domains and enforce SSO - for accounts under your domains. -

-
- setAddModalShown(true)} - disabled={verifiedDomainsLoading || updatingDomainLoading} - > - Add domain - -
+

+ Enable users to sign up automatically with an email address on verified domains and enforce SSO for + accounts under your domains. +

+ + setAddModalShown(true)} + className="mt-4" + disabledReason={verifiedDomainsLoading || updatingDomainLoading ? 'loading...' : null} + > + Add domain + ) } diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/VerifyDomainModal.tsx b/frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/VerifyDomainModal.tsx rename to frontend/src/scenes/settings/organization/VerifiedDomains/VerifyDomainModal.tsx diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap rename to frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.test.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.test.ts rename to frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts diff --git a/frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts similarity index 100% rename from frontend/src/scenes/organization/Settings/VerifiedDomains/verifiedDomainsLogic.ts rename to frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts diff --git a/frontend/src/scenes/organization/Settings/index.tsx b/frontend/src/scenes/settings/organization/index.tsx similarity index 97% rename from frontend/src/scenes/organization/Settings/index.tsx rename to frontend/src/scenes/settings/organization/index.tsx index f37152a173da0..43affb102283d 100644 --- a/frontend/src/scenes/organization/Settings/index.tsx +++ b/frontend/src/scenes/settings/organization/index.tsx @@ -39,9 +39,6 @@ function DisplayName({ isRestricted }: RestrictedComponentProps): JSX.Element { return (
-

- Display Name -

([ ]) export function OrganizationSettings(): JSX.Element { - const { user } = useValues(userLogic) const { featureFlags } = useValues(featureFlagLogic) useAnchor(location.hash) const { tab } = useValues(organizationSettingsTabsLogic) @@ -125,7 +121,7 @@ export function OrganizationSettings(): JSX.Element { - {user && } + ([ ], }), + listeners(({ actions }) => ({ + selectSection: ({ section }) => { + if (section) { + actions.selectLevel(SettingsSections.find((x) => x.id === section)?.level || 'user') + } + }, + })), + selectors({ settings: [ (s) => [s.selectedLevel, s.selectedSectionId], (selectedLevel, selectedSectionId): Setting[] => { if (!selectedSectionId) { - console.log( - 'wat', - SettingsSections.filter((section) => section.level === selectedLevel) - ) return SettingsSections.filter((section) => section.level === selectedLevel).reduce( (acc, section) => [...acc, ...section.settings], [] as Setting[] diff --git a/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx index b31e3d92da0b1..fe511e7a89ad2 100644 --- a/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx +++ b/frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx @@ -4,7 +4,7 @@ import { LemonButton, LemonModal } from '@posthog/lemon-ui' import { IconCheckmark, IconWarning } from 'lib/lemon-ui/icons' import { useState } from 'react' import { Setup2FA } from 'scenes/authentication/Setup2FA' -import { membersLogic } from 'scenes/organization/Settings/membersLogic' +import { membersLogic } from 'scenes/settings/organization/membersLogic' export function TwoFactorAuthentication(): JSX.Element { const { user } = useValues(userLogic) diff --git a/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx index 4849e56ebb8dc..5d1387d376324 100644 --- a/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx +++ b/frontend/src/scenes/settings/user/UpdateEmailPreferences.tsx @@ -16,7 +16,6 @@ export function UpdateEmailPreferences(): JSX.Element { checked={user?.email_opt_in || false} disabled={userLoading} label="Receive security and feature updates via email. You can easily unsubscribe at any time." - fullWidth bordered />
@@ -35,7 +34,6 @@ export function UpdateEmailPreferences(): JSX.Element { }} checked={user?.notification_settings.plugin_disabled || false} disabled={userLoading} - fullWidth bordered label="Get notified when plugins are disabled due to errors." /> From c97ebc131bc75ddfdc17547e7b4fbc7e7c0e3297 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 14:33:47 +0100 Subject: [PATCH 04/56] Fix --- frontend/src/scenes/settings/SettingsMap.tsx | 45 +++++++++++++----- .../src/scenes/settings/SettingsScene.tsx | 35 ++++++++------ .../settings/organization/DangerZone.tsx | 40 ++++++++-------- .../Permissions/PermissionsGrid.tsx | 47 ++++++++++--------- .../scenes/settings/organization/index.tsx | 10 +--- frontend/src/scenes/settings/settingsLogic.ts | 42 ++++++++++------- 6 files changed, 124 insertions(+), 95 deletions(-) diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 594f454fdcf65..ca68d057d5ea3 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -1,4 +1,3 @@ -import { OrganizationMembershipLevel } from 'lib/constants' import { ChangePassword } from './user/ChangePassword' import { OptOutCapture } from './user/OptOutCapture' import { PersonalAPIKeys } from './user/PersonalAPIKeys' @@ -11,6 +10,9 @@ import { Invites } from './organization/Invites' import { Members } from './organization/Members' import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' +import { DangerZone } from './organization/DangerZone' +import { PermissionsGrid } from './organization/Permissions/PermissionsGrid' +import { FEATURE_FLAGS } from 'lib/constants' export type SettingLevel = 'user' | 'project' | 'organization' export type SettingSectionId = @@ -20,6 +22,8 @@ export type SettingSectionId = | 'organization-details' | 'organization-members' | 'organization-authentication' + | 'organization-danger-zone' + | 'organization-rbac' export const SettingLevels: SettingLevel[] = ['project', 'organization', 'user'] @@ -35,21 +39,10 @@ export type SettingSection = { title: string level: SettingLevel settings: Setting[] + flag?: keyof typeof FEATURE_FLAGS minimumAccessLevel?: EitherMembershipLevel } -{ - /*
- - - - -
*/ -} - export const SettingsSections: SettingSection[] = [ { level: 'organization', @@ -98,6 +91,32 @@ export const SettingsSections: SettingSection[] = [ ], }, + { + level: 'organization', + id: 'organization-danger-zone', + title: 'Danger zone', + settings: [ + { + id: 'organization-delete', + title: 'Delete organization', + component: , + }, + ], + }, + { + level: 'organization', + id: 'organization-rbac', + title: 'Role-based access', + flag: 'ROLE_BASED_ACCESS', + settings: [ + { + id: 'organization-rbac', + title: 'Role-based access', + component: , + }, + ], + }, + { level: 'user', id: 'user-details', diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx index 5172695ab0e43..6dd6acd78822d 100644 --- a/frontend/src/scenes/settings/SettingsScene.tsx +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -19,7 +19,7 @@ export const scene: SceneExport = { */ export function SettingsScene(): JSX.Element { - const { selectedSectionId, selectedLevel, settings } = useValues(settingsLogic) + const { selectedSectionId, selectedLevel, settings, sections } = useValues(settingsLogic) const { selectSection, selectLevel } = useActions(settingsLogic) // const { location } = useValues(router) @@ -33,25 +33,32 @@ export function SettingsScene(): JSX.Element {
    {SettingLevels.map((level) => (
  • - selectLevel(level)} size="small" fullWidth> + selectLevel(level)} + size="small" + fullWidth + active={selectedLevel === level && !selectedSectionId} + > {capitalizeFirstLetter(level)}
      - {SettingsSections.filter((x) => x.level === level).map((section) => ( -
    • - selectSection(section.id)} - size="small" - fullWidth - active={selectedSectionId === section.id} - > - {section.title} - -
    • - ))} + {sections + .filter((x) => x.level === level) + .map((section) => ( +
    • + selectSection(section.id)} + size="small" + fullWidth + active={selectedSectionId === section.id} + > + {section.title} + +
    • + ))}
  • ))} diff --git a/frontend/src/scenes/settings/organization/DangerZone.tsx b/frontend/src/scenes/settings/organization/DangerZone.tsx index a4fa581b337e9..86c4a36c24c9a 100644 --- a/frontend/src/scenes/settings/organization/DangerZone.tsx +++ b/frontend/src/scenes/settings/organization/DangerZone.tsx @@ -1,9 +1,10 @@ import { useActions, useValues } from 'kea' import { organizationLogic } from 'scenes/organizationLogic' -import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { RestrictedComponentProps, useRestrictedArea } from 'lib/components/RestrictedArea' import { Dispatch, SetStateAction, useState } from 'react' import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' import { IconDelete } from 'lib/lemon-ui/icons' +import { OrganizationMembershipLevel } from 'lib/constants' export function DeleteOrganizationModal({ isOpen, @@ -62,32 +63,29 @@ export function DeleteOrganizationModal({ ) } -export function DangerZone({ isRestricted }: RestrictedComponentProps): JSX.Element { +export function DangerZone(): JSX.Element { const { currentOrganization } = useValues(organizationLogic) - const [isModalVisible, setIsModalVisible] = useState(false) + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) return ( <>
    -

    Danger Zone

    -
    - {!isRestricted && ( -

    - This is irreversible. Please be certain. -

    - )} - setIsModalVisible(true)} - data-attr="delete-organization-button" - icon={} - disabled={isRestricted} - > - Delete {currentOrganization?.name || 'the current organization'} - -
    + {!isRestricted && ( +

    + This is irreversible. Please be certain. +

    + )} + setIsModalVisible(true)} + data-attr="delete-organization-button" + icon={} + disabled={isRestricted} + > + Delete {currentOrganization?.name || 'the current organization'} +
    diff --git a/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx index 418510115015a..5f97a4c778e31 100644 --- a/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/PermissionsGrid.tsx @@ -2,22 +2,25 @@ import { LemonButton, LemonCheckbox, LemonTable } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { IconInfo } from 'lib/lemon-ui/icons' import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { useRestrictedArea } from 'lib/components/RestrictedArea' import { TitleWithIcon } from 'lib/components/TitleWithIcon' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { organizationLogic } from 'scenes/organizationLogic' -import { AccessLevel, Resource, RoleType } from '~/types' +import { AccessLevel, AvailableFeature, Resource, RoleType } from '~/types' import { permissionsLogic } from './permissionsLogic' import { CreateRoleModal } from './Roles/CreateRoleModal' import { rolesLogic } from './Roles/rolesLogic' import { getSingularType } from './utils' +import { OrganizationMembershipLevel } from 'lib/constants' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -export function PermissionsGrid({ isRestricted }: RestrictedComponentProps): JSX.Element { +export function PermissionsGrid(): JSX.Element { const { resourceRolesAccess, organizationResourcePermissionsLoading } = useValues(permissionsLogic) const { updatePermission } = useActions(permissionsLogic) const { roles, rolesLoading } = useValues(rolesLogic) const { setRoleInFocus, openCreateRoleModal } = useActions(rolesLogic) const { isAdminOrOwner } = useValues(organizationLogic) + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) // TODO: check if this is correct const columns: LemonTableColumns = [ { @@ -95,24 +98,26 @@ export function PermissionsGrid({ isRestricted }: RestrictedComponentProps): JSX ] return ( - <> -
    -
    - Edit organizational default permission levels for posthog resources. Use roles to apply permissions - to specific sets of users. + + <> +
    +
    + Edit organizational default permission levels for posthog resources. Use roles to apply + permissions to specific sets of users. +
    + {!isRestricted && ( + + Create role + + )}
    - {!isRestricted && ( - - Create role - - )} -
    - - - + + + + ) } diff --git a/frontend/src/scenes/settings/organization/index.tsx b/frontend/src/scenes/settings/organization/index.tsx index 43affb102283d..8ebc4d9651189 100644 --- a/frontend/src/scenes/settings/organization/index.tsx +++ b/frontend/src/scenes/settings/organization/index.tsx @@ -8,13 +8,10 @@ import { kea, useActions, useValues, path, actions, reducers } from 'kea' import { DangerZone } from './DangerZone' import { RestrictedArea, RestrictedComponentProps } from 'lib/components/RestrictedArea' import { FEATURE_FLAGS, OrganizationMembershipLevel } from 'lib/constants' -import { userLogic } from 'scenes/userLogic' import { SceneExport } from 'scenes/sceneTypes' import { useAnchor } from 'lib/hooks/useAnchor' import { VerifiedDomains } from './VerifiedDomains/VerifiedDomains' import { LemonButton, LemonDivider, LemonInput, LemonSwitch } from '@posthog/lemon-ui' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { AvailableFeature } from '~/types' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { urls } from 'scenes/urls' import type { organizationSettingsTabsLogicType } from './indexType' @@ -144,12 +141,7 @@ export function OrganizationSettings(): JSX.Element { key: OrganizationSettingsTabs.ROLE_BASED_ACCESS, label: 'Role-based access', content: ( - - - + ), }) } diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index cbf00fd91087d..f96d06e57b8ae 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -1,11 +1,16 @@ -import { actions, kea, listeners, path, reducers, selectors } from 'kea' -import { Setting, SettingLevel, SettingSectionId, SettingsSections } from './SettingsMap' +import { actions, connect, kea, path, reducers, selectors } from 'kea' +import { Setting, SettingLevel, SettingSection, SettingSectionId, SettingsSections } from './SettingsMap' import type { settingsLogicType } from './settingsLogicType' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' export const settingsLogic = kea([ path(['scenes', 'settings']), + connect({ + values: [featureFlagLogic, ['featureFlags']], + }), + actions({ selectSection: (section: SettingSectionId) => ({ section }), selectLevel: (level: SettingLevel) => ({ level }), @@ -16,36 +21,39 @@ export const settingsLogic = kea([ 'user' as SettingLevel, { selectLevel: (_, { level }) => level, + selectSection: (_, { section }) => SettingsSections.find((x) => x.id === section)?.level || 'user', }, ], selectedSectionId: [ null as SettingSectionId | null, { + selectLevel: () => null, selectSection: (_, { section }) => section, }, ], }), - listeners(({ actions }) => ({ - selectSection: ({ section }) => { - if (section) { - actions.selectLevel(SettingsSections.find((x) => x.id === section)?.level || 'user') - } - }, - })), - selectors({ + sections: [ + (s) => [s.featureFlags], + (featureFlags): SettingSection[] => { + return SettingsSections.filter((x) => (x.flag ? featureFlags[x.flag] : true)) + }, + ], settings: [ - (s) => [s.selectedLevel, s.selectedSectionId], - (selectedLevel, selectedSectionId): Setting[] => { + (s) => [s.selectedLevel, s.selectedSectionId, s.sections], + (selectedLevel, selectedSectionId, sections): Setting[] => { + let settings: Setting[] = [] + if (!selectedSectionId) { - return SettingsSections.filter((section) => section.level === selectedLevel).reduce( - (acc, section) => [...acc, ...section.settings], - [] as Setting[] - ) + settings = sections + .filter((section) => section.level === selectedLevel) + .reduce((acc, section) => [...acc, ...section.settings], [] as Setting[]) + } else { + settings = sections.find((x) => x.id === selectedSectionId)?.settings || [] } - return SettingsSections.find((x) => x.id === selectedSectionId)?.settings || [] + return settings }, ], }), From 691f0b1489bb14a2924a1231fbbba16771c55790 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 14:39:00 +0100 Subject: [PATCH 05/56] Fix --- frontend/src/scenes/settings/settingsLogic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index f96d06e57b8ae..5212c752d7b05 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -3,10 +3,10 @@ import { Setting, SettingLevel, SettingSection, SettingSectionId, SettingsSectio import type { settingsLogicType } from './settingsLogicType' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export const settingsLogic = kea([ path(['scenes', 'settings']), - connect({ values: [featureFlagLogic, ['featureFlags']], }), @@ -37,7 +37,7 @@ export const settingsLogic = kea([ sections: [ (s) => [s.featureFlags], (featureFlags): SettingSection[] => { - return SettingsSections.filter((x) => (x.flag ? featureFlags[x.flag] : true)) + return SettingsSections.filter((x) => (x.flag ? featureFlags[FEATURE_FLAGS[x.flag]] : true)) }, ], settings: [ From 9f194efaa992672544694ff0264316c910e042a5 Mon Sep 17 00:00:00 2001 From: Ben White Date: Mon, 6 Nov 2023 16:07:25 +0100 Subject: [PATCH 06/56] More refactoring --- frontend/src/scenes/settings/SettingsMap.tsx | 168 +++++++++- .../src/scenes/settings/SettingsScene.tsx | 6 +- .../settings/project/AddMembersModal.tsx | 80 +++++ .../settings/project/AutocaptureSettings.tsx | 99 ++++++ .../settings/project/CorrelationConfig.tsx | 92 ++++++ .../settings/project/DataAttributes.tsx | 57 ++++ .../settings/project/ExtraTeamSettings.tsx | 102 ++++++ .../settings/project/GroupAnalytics.tsx | 97 ++++++ .../src/scenes/settings/project/IPCapture.tsx | 20 ++ .../scenes/settings/project/IngestionInfo.tsx | 127 ++++++++ .../project/PathCleaningFiltersConfig.tsx | 21 ++ .../project/PersonDisplayNameProperties.tsx | 49 +++ .../settings/project/ProjectAccessControl.tsx | 264 +++++++++++++++ .../settings/project/ProjectDangerZone.tsx | 96 ++++++ .../settings/project/ProjectSettings.tsx | 261 +++++++++++++++ .../project/SessionRecordingSettings.tsx | 304 ++++++++++++++++++ .../project/SlackIntegration.stories.tsx | 53 +++ .../settings/project/SlackIntegration.tsx | 126 ++++++++ .../src/scenes/settings/project/Survey.tsx | 20 ++ .../project/TestAccountFiltersConfig.tsx | 85 +++++ .../settings/project/TimezoneConfig.tsx | 70 ++++ .../settings/project/WebhookIntegration.tsx | 86 +++++ .../settings/project/WeekStartConfig.tsx | 33 ++ .../project/autocaptureExceptionsLogic.ts | 49 +++ .../project/filterTestAccountDefaultsLogic.ts | 55 ++++ .../project/groupAnalyticsConfigLogic.ts | 59 ++++ .../src/scenes/settings/project/index.tsx | 181 +++++++++++ .../settings/project/integrationsLogic.ts | 180 +++++++++++ .../settings/project/teamMembersLogic.tsx | 191 +++++++++++ .../project/webhookIntegrationLogic.ts | 67 ++++ frontend/src/scenes/settings/settingsLogic.ts | 2 +- 31 files changed, 3082 insertions(+), 18 deletions(-) create mode 100644 frontend/src/scenes/settings/project/AddMembersModal.tsx create mode 100644 frontend/src/scenes/settings/project/AutocaptureSettings.tsx create mode 100644 frontend/src/scenes/settings/project/CorrelationConfig.tsx create mode 100644 frontend/src/scenes/settings/project/DataAttributes.tsx create mode 100644 frontend/src/scenes/settings/project/ExtraTeamSettings.tsx create mode 100644 frontend/src/scenes/settings/project/GroupAnalytics.tsx create mode 100644 frontend/src/scenes/settings/project/IPCapture.tsx create mode 100644 frontend/src/scenes/settings/project/IngestionInfo.tsx create mode 100644 frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx create mode 100644 frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx create mode 100644 frontend/src/scenes/settings/project/ProjectAccessControl.tsx create mode 100644 frontend/src/scenes/settings/project/ProjectDangerZone.tsx create mode 100644 frontend/src/scenes/settings/project/ProjectSettings.tsx create mode 100644 frontend/src/scenes/settings/project/SessionRecordingSettings.tsx create mode 100644 frontend/src/scenes/settings/project/SlackIntegration.stories.tsx create mode 100644 frontend/src/scenes/settings/project/SlackIntegration.tsx create mode 100644 frontend/src/scenes/settings/project/Survey.tsx create mode 100644 frontend/src/scenes/settings/project/TestAccountFiltersConfig.tsx create mode 100644 frontend/src/scenes/settings/project/TimezoneConfig.tsx create mode 100644 frontend/src/scenes/settings/project/WebhookIntegration.tsx create mode 100644 frontend/src/scenes/settings/project/WeekStartConfig.tsx create mode 100644 frontend/src/scenes/settings/project/autocaptureExceptionsLogic.ts create mode 100644 frontend/src/scenes/settings/project/filterTestAccountDefaultsLogic.ts create mode 100644 frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts create mode 100644 frontend/src/scenes/settings/project/index.tsx create mode 100644 frontend/src/scenes/settings/project/integrationsLogic.ts create mode 100644 frontend/src/scenes/settings/project/teamMembersLogic.tsx create mode 100644 frontend/src/scenes/settings/project/webhookIntegrationLogic.ts diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index ca68d057d5ea3..3a79b979923ee 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -13,9 +13,25 @@ import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences import { DangerZone } from './organization/DangerZone' import { PermissionsGrid } from './organization/Permissions/PermissionsGrid' import { FEATURE_FLAGS } from 'lib/constants' +import { + Bookmarklet, + ProjectDisplayName, + ProjectTimezone, + ProjectToolbarURLs, + ProjectVariables, + WebSnippet, +} from './project/ProjectSettings' +import { AutocaptureSettings, ExceptionAutocaptureSettings } from './project/AutocaptureSettings' +import { DataAttributes } from './project/DataAttributes' +import { ReplayAuthorizedDomains, ReplayCostControl, ReplayGeneral } from './project/SessionRecordingSettings' +import { ProjectDangerZone } from './project/ProjectDangerZone' +import { ProjectAccessControl } from './project/ProjectAccessControl' export type SettingLevel = 'user' | 'project' | 'organization' export type SettingSectionId = + | 'project-details' + | 'project-autocapture' + | 'project-replay' | 'user-details' | 'user-api-keys' | 'user-notifications' @@ -44,6 +60,130 @@ export type SettingSection = { } export const SettingsSections: SettingSection[] = [ + // PROJECT + { + level: 'project', + id: 'project-details', + title: 'Details', + settings: [ + { + id: 'project-display-name', + title: 'Display name', + component: , + }, + { + id: 'project-snippet', + title: 'Web snippet', + component: , + }, + { + id: 'project-bookmarklet', + title: 'Bookmarklet', + component: , + }, + { + id: 'project-variables', + title: 'Project ID', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-autocapture', + title: 'Autocapture', + + settings: [ + { + id: 'project-autocapture', + title: 'Autocapture', + component: , + }, + { + id: 'project-exception-autocapture', + title: 'Exception Autocapture', + component: , + }, + { + id: 'project-autocapture-data-attributes', + title: 'Data attributes', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-display', + title: 'Date & Time', + settings: [ + { + id: 'project-date-and-time', + title: 'Date & Time', + component: , + }, + ], + }, + + { + level: 'project', + id: 'project-replay', + title: 'Replay', + settings: [ + { + id: 'project-replay-general', + title: 'Session Replay', + component: , + }, + { + id: 'project-replay-authorized-domains', + title: 'Authorized Domains for Replay', + component: , + }, + { + id: 'project-replay-ingestion', + title: 'Ingestion controls', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-toolbar', + title: 'Toolbar', + settings: [ + { + id: 'project-authorized-toolbar-urls', + title: 'Authorized Toolbar URLs', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-rbac', + title: 'Access control', + settings: [ + { + id: 'project-rbac', + title: 'Access Control', + component: , + }, + ], + }, + { + level: 'project', + id: 'project-danger-zone', + title: 'Danger zone', + settings: [ + { + id: 'project-delete', + title: 'Delete project', + component: , + }, + ], + }, + + // ORGANIZATION { level: 'organization', id: 'organization-details', @@ -51,7 +191,7 @@ export const SettingsSections: SettingSection[] = [ settings: [ { id: 'organization-details', - title: 'Details', + title: 'General', component: , }, ], @@ -90,37 +230,37 @@ export const SettingsSections: SettingSection[] = [ }, ], }, - { level: 'organization', - id: 'organization-danger-zone', - title: 'Danger zone', + id: 'organization-rbac', + title: 'Role-based access', + flag: 'ROLE_BASED_ACCESS', settings: [ { - id: 'organization-delete', - title: 'Delete organization', - component: , + id: 'organization-rbac', + title: 'Role-based access', + component: , }, ], }, { level: 'organization', - id: 'organization-rbac', - title: 'Role-based access', - flag: 'ROLE_BASED_ACCESS', + id: 'organization-danger-zone', + title: 'Danger zone', settings: [ { - id: 'organization-rbac', - title: 'Role-based access', - component: , + id: 'organization-delete', + title: 'Delete organization', + component: , }, ], }, + // USER { level: 'user', id: 'user-details', - title: 'Details', + title: 'Profile', settings: [ { id: 'details', diff --git a/frontend/src/scenes/settings/SettingsScene.tsx b/frontend/src/scenes/settings/SettingsScene.tsx index 6dd6acd78822d..f6b9b64e5794a 100644 --- a/frontend/src/scenes/settings/SettingsScene.tsx +++ b/frontend/src/scenes/settings/SettingsScene.tsx @@ -1,5 +1,5 @@ import { SceneExport } from 'scenes/sceneTypes' -import { SettingLevels, SettingsSections } from './SettingsMap' +import { SettingLevels } from './SettingsMap' import { capitalizeFirstLetter } from 'lib/utils' import { useActions, useValues } from 'kea' import { settingsLogic } from './settingsLogic' @@ -29,7 +29,7 @@ export function SettingsScene(): JSX.Element { return ( <>
    -
    +
      {SettingLevels.map((level) => (
    • @@ -65,7 +65,7 @@ export function SettingsScene(): JSX.Element {
    -
    +
    {settings.map((x) => (

    {x.title}

    diff --git a/frontend/src/scenes/settings/project/AddMembersModal.tsx b/frontend/src/scenes/settings/project/AddMembersModal.tsx new file mode 100644 index 0000000000000..65ca193f8bb00 --- /dev/null +++ b/frontend/src/scenes/settings/project/AddMembersModal.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react' +import { useValues } from 'kea' +import { teamMembersLogic } from './teamMembersLogic' +import { teamLogic } from 'scenes/teamLogic' +import { membershipLevelToName, teamMembershipLevelIntegers } from 'lib/utils/permissioning' +import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { LemonButton, LemonModal, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' +import { Form } from 'kea-forms' +import { Field } from 'lib/forms/Field' +import { IconPlus } from 'lib/lemon-ui/icons' +import { TeamMembershipLevel } from 'lib/constants' + +export function AddMembersModalWithButton({ isRestricted }: RestrictedComponentProps): JSX.Element { + const { addableMembers, allMembersLoading } = useValues(teamMembersLogic) + const { currentTeam } = useValues(teamLogic) + + const [isVisible, setIsVisible] = useState(false) + + function closeModal(): void { + setIsVisible(false) + } + + return ( + <> + { + setIsVisible(true) + }} + icon={} + disabled={isRestricted} + > + Add members to project + + +
    + +

    {`Adding members${currentTeam?.name ? ` to project ${currentTeam.name}` : ''}`}

    +
    + + + x.user), + 'uuid' + )} + /> + + + + ({ + value: teamMembershipLevel, + label: membershipLevelToName.get(teamMembershipLevel), + } as LemonSelectOption) + )} + /> + + + + + Cancel + + + Add members to project + + + +
    + + ) +} diff --git a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx new file mode 100644 index 0000000000000..54e218046a3f9 --- /dev/null +++ b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx @@ -0,0 +1,99 @@ +import { useValues, useActions } from 'kea' +import { userLogic } from 'scenes/userLogic' +import { LemonSwitch, LemonTag, LemonTextArea, Link } from '@posthog/lemon-ui' +import { teamLogic } from 'scenes/teamLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import clsx from 'clsx' +import { autocaptureExceptionsLogic } from 'scenes/project/Settings/autocaptureExceptionsLogic' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' + +export function AutocaptureSettings(): JSX.Element { + const { userLoading } = useValues(userLogic) + const { currentTeam } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const { reportIngestionAutocaptureToggled } = useActions(eventUsageLogic) + + return ( + <> +

    + Automagically capture front-end interactions like pageviews, clicks, and more when using our web + JavaScript SDK.{' '} +

    +

    + Autocapture is also available for React Native, where it has to be{' '} + + configured directly in code + + . +

    +
    + { + updateCurrentTeam({ + autocapture_opt_out: !checked, + }) + reportIngestionAutocaptureToggled(!checked) + }} + checked={!currentTeam?.autocapture_opt_out} + disabled={userLoading} + label="Enable autocapture for web" + bordered + /> +
    + + ) +} + +export function ExceptionAutocaptureSettings(): JSX.Element { + const { userLoading } = useValues(userLogic) + const { currentTeam } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const { reportIngestionAutocaptureExceptionsToggled } = useActions(eventUsageLogic) + + const { errorsToIgnoreRules, rulesCharacters } = useValues(autocaptureExceptionsLogic) + const { setErrorsToIgnoreRules } = useActions(autocaptureExceptionsLogic) + + return ( + <> + { + updateCurrentTeam({ + autocapture_exceptions_opt_in: checked, + }) + reportIngestionAutocaptureExceptionsToggled(checked) + }} + checked={!!currentTeam?.autocapture_exceptions_opt_in} + disabled={userLoading} + label={ + <> + Enable exception autocapture ALPHA + + } + bordered + /> +

    Ignore errors

    +

    + If you're experiencing a high volume of unhelpful errors, add regular expressions here to ignore them. + This will ignore all errors that match, including those that are not autocaptured. +

    +

    + You can enter a regular expression that matches values of{' '} + here to ignore them. One per line. For example, if you + want to drop all errors that contain the word "bot", or you can enter "bot" here. Or if you want to drop + all errors that are exactly "bot", you can enter "^bot$". +

    +

    Only up to 300 characters of config are allowed here.

    + +
    300 ? 'text-danger' : 'text-muted')}> + {rulesCharacters} / 300 characters +
    + + ) +} diff --git a/frontend/src/scenes/settings/project/CorrelationConfig.tsx b/frontend/src/scenes/settings/project/CorrelationConfig.tsx new file mode 100644 index 0000000000000..fd357cd23e0f5 --- /dev/null +++ b/frontend/src/scenes/settings/project/CorrelationConfig.tsx @@ -0,0 +1,92 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' +import { EventSelect } from 'lib/components/EventSelect/EventSelect' +import { IconPlus, IconSelectEvents, IconSelectProperties } from 'lib/lemon-ui/icons' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonButton } from '@posthog/lemon-ui' + +export function CorrelationConfig(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam, funnelCorrelationConfig } = useValues(teamLogic) + + const handleChange = ( + excludedProperties?: string[], + excludedEvents?: string[], + excludedEventProperties?: string[] + ): void => { + if (currentTeam) { + const updatedConfig = { ...funnelCorrelationConfig } + if (excludedProperties !== undefined) { + updatedConfig.excluded_person_property_names = excludedProperties + } + if (excludedEventProperties !== undefined) { + updatedConfig.excluded_event_property_names = excludedEventProperties + } + if (excludedEvents !== undefined) { + updatedConfig.excluded_event_names = excludedEvents + } + if (updatedConfig && JSON.stringify(updatedConfig) !== JSON.stringify(funnelCorrelationConfig)) { + updateCurrentTeam({ correlation_config: updatedConfig }) + } + } + } + + return ( + <> +

    + Correlation analysis exclusions +

    +

    Globally exclude events or properties that do not provide relevant signals for your conversions.

    + + + Correlation analysis can automatically surface relevant signals for conversion, and help you understand + why your users dropped off and what makes them convert. + + {currentTeam && ( +
    +
    +

    + + Excluded person properties +

    + handleChange(properties)} + selectedProperties={funnelCorrelationConfig.excluded_person_property_names || []} + addText="Add exclusion" + /> +
    +
    +

    + + Excluded events +

    + handleChange(undefined, excludedEvents)} + selectedEvents={funnelCorrelationConfig.excluded_event_names || []} + addElement={ + } sideIcon={null}> + Add exclusion + + } + /> +
    +
    +

    + + Excluded event properties +

    +
    + handleChange(undefined, undefined, properties)} + value={funnelCorrelationConfig.excluded_event_property_names || []} + /> +
    +
    +
    + )} + + ) +} diff --git a/frontend/src/scenes/settings/project/DataAttributes.tsx b/frontend/src/scenes/settings/project/DataAttributes.tsx new file mode 100644 index 0000000000000..4efdb3c1ca763 --- /dev/null +++ b/frontend/src/scenes/settings/project/DataAttributes.tsx @@ -0,0 +1,57 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' +import { Skeleton } from 'antd' +import { useActions, useValues } from 'kea' +import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { useEffect, useState } from 'react' +import { teamLogic } from 'scenes/teamLogic' + +export function DataAttributes(): JSX.Element { + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const [value, setValue] = useState([] as string[]) + + useEffect(() => setValue(currentTeam?.data_attributes || []), [currentTeam]) + + if (!currentTeam) { + return + } + + return ( + <> +

    + Specify a comma-separated list of{' '} + + data attributes + {' '} + used in your app. For example: data-attr, data-custom-id, data-myref-*. These attributes + will be used when using the toolbar and defining actions to match unique elements on your pages. You can + use * as a wildcard. +

    +

    + For example, when creating an action on your CTA button, the best selector could be something like:{' '} + div > form > button:nth-child(2). However all buttons in your app have a{' '} + data-custom-id attribute. If you allow it here, the selector for your button will instead + be button[data-custom-id='cta-button']. +

    +
    + setValue(values || [])} + value={value} + data-attr="data-attribute-select" + placeholder={'data-attr, ...'} + loading={currentTeamLoading} + disabled={currentTeamLoading} + /> + + updateCurrentTeam({ data_attributes: value.map((s) => s.trim()).filter((a) => a) || [] }) + } + > + Save + +
    + + ) +} diff --git a/frontend/src/scenes/settings/project/ExtraTeamSettings.tsx b/frontend/src/scenes/settings/project/ExtraTeamSettings.tsx new file mode 100644 index 0000000000000..7c8be42a75ede --- /dev/null +++ b/frontend/src/scenes/settings/project/ExtraTeamSettings.tsx @@ -0,0 +1,102 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { LemonButton, LemonDivider, LemonInput, LemonSwitch, Link } from '@posthog/lemon-ui' +import { useState } from 'react' + +export enum SettingValueType { + Boolean = 'boolean', + Text = 'text', + Number = 'number', +} + +export interface ExtraSettingType { + name: string + description: string + key: string + moreInfo: string + valueType: SettingValueType +} + +const AVAILABLE_EXTRA_SETTINGS: ExtraSettingType[] = [ + { + name: 'Person on Events (Beta)', + description: `We have updated our data model to also store person properties directly on events, making queries significantly faster. This means that person properties will no longer be "timeless", but rather point-in-time i.e. on filters we'll consider a person's properties at the time of the event, rather than at present time. This may cause data to change on some of your insights, but will be the default way we handle person properties going forward. For now, you can control whether you want this on or not, and should feel free to let us know of any concerns you might have. If you do enable this, you should see speed improvements of around 3-5x on average on most of your insights.`, + moreInfo: + 'https://github.com/PostHog/posthog/blob/75a2111f2c4f9183dd45f85c7b103c7b0524eabf/plugin-server/src/worker/ingestion/PoE.md', + key: 'poe_v2_enabled', + valueType: SettingValueType.Boolean, + }, +] + +function ExtraSettingInput({ + defaultValue, + type, + settingKey, +}: { + defaultValue?: string | number + type: 'number' | 'text' + settingKey: string +}): JSX.Element { + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + + const [value, setValue] = useState(defaultValue) + + return ( +
    + + + updateCurrentTeam({ extra_settings: { ...currentTeam?.extra_settings, [settingKey]: value } }) + } + loading={currentTeamLoading} + > + Update + +
    + ) +} + +export function ExtraTeamSettings(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + + return ( + <> + {AVAILABLE_EXTRA_SETTINGS.map((setting) => ( + <> +

    + {setting.name} +

    +
    +

    + {setting.description} + {setting.moreInfo ? More info. : null} +

    + {setting.valueType === SettingValueType.Boolean ? ( + { + updateCurrentTeam({ + extra_settings: { ...currentTeam?.extra_settings, [setting.key]: checked }, + }) + }} + label={`Enable ${setting.name}`} + checked={!!currentTeam?.extra_settings?.[setting.key]} + bordered + /> + ) : ( + + )} +
    + + + ))} + + ) +} diff --git a/frontend/src/scenes/settings/project/GroupAnalytics.tsx b/frontend/src/scenes/settings/project/GroupAnalytics.tsx new file mode 100644 index 0000000000000..4d563af695bdb --- /dev/null +++ b/frontend/src/scenes/settings/project/GroupAnalytics.tsx @@ -0,0 +1,97 @@ +import { useActions, useValues } from 'kea' +import { GroupType } from '~/types' +import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { groupsAccessLogic, GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic' +import { groupAnalyticsConfigLogic } from 'scenes/project/Settings/groupAnalyticsConfigLogic' +import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' + +export function GroupAnalytics(): JSX.Element | null { + const { groupTypes, groupTypesLoading, singularChanges, pluralChanges, hasChanges } = + useValues(groupAnalyticsConfigLogic) + const { setSingular, setPlural, reset, save } = useActions(groupAnalyticsConfigLogic) + + const { groupsAccessStatus } = useValues(groupsAccessLogic) + + if (groupsAccessStatus === GroupsAccessStatus.NoAccess) { + // Hide settings if the user doesn't have access + return null + } + + const columns: LemonTableColumns = [ + { + title: 'Group type', + tooltip: 'As used in code', + dataIndex: 'group_type', + key: 'name', + render: function RenderName(name) { + return name + }, + }, + { + title: 'Singular name', + key: 'singular', + render: function Render(_, groupType) { + return ( + setSingular(groupType.group_type_index, e)} + /> + ) + }, + }, + { + title: 'Plural name', + key: 'plural', + render: function Render(_, groupType) { + return ( + setPlural(groupType.group_type_index, e)} + /> + ) + }, + }, + ] + + return ( +
    +

    Group Analytics

    +

    + This project has access to group analytics. Below you can configure how various group types are + displayed throughout the app. +

    + + {groupsAccessStatus !== GroupsAccessStatus.HasGroupTypes && ( + + Group types will show up here after you send your first event associated with a group. Take a look + at{' '} + + this guide + + for more information on getting started. + + )} + + + +
    + + Save + + + Cancel + +
    + +
    + ) +} diff --git a/frontend/src/scenes/settings/project/IPCapture.tsx b/frontend/src/scenes/settings/project/IPCapture.tsx new file mode 100644 index 0000000000000..07311b4c58e7a --- /dev/null +++ b/frontend/src/scenes/settings/project/IPCapture.tsx @@ -0,0 +1,20 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { LemonSwitch } from '@posthog/lemon-ui' + +export function IPCapture(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + + return ( + { + updateCurrentTeam({ anonymize_ips: checked }) + }} + checked={!!currentTeam?.anonymize_ips} + disabled={currentTeamLoading} + label="Discard client IP data" + bordered + /> + ) +} diff --git a/frontend/src/scenes/settings/project/IngestionInfo.tsx b/frontend/src/scenes/settings/project/IngestionInfo.tsx new file mode 100644 index 0000000000000..07ebf14058244 --- /dev/null +++ b/frontend/src/scenes/settings/project/IngestionInfo.tsx @@ -0,0 +1,127 @@ +import { useActions, useValues } from 'kea' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { JSSnippet } from 'lib/components/JSSnippet' +import { JSBookmarklet } from 'lib/components/JSBookmarklet' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { CodeSnippet } from 'lib/components/CodeSnippet' +import { IconRefresh } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { AutocaptureSettings } from './AutocaptureSettings' +import { LemonSkeleton } from '@posthog/lemon-ui' + +export function IngestionInfo(): JSX.Element { + const { currentTeam, currentTeamLoading, isTeamTokenResetAvailable } = useValues(teamLogic) + const { resetToken } = useActions(teamLogic) + + if (currentTeam?.is_demo) { + return ( + <> +

    + Event ingestion +

    +

    + PostHog can ingest events from almost anywhere - JavaScript, Android, iOS, React Native, Node.js, + Ruby, Go, and more. +

    +

    + Demo projects like this one can't ingest events, but you can{' '} + + read about ingestion in our Docs + {' '} + and use a non-demo project to ingest your own events. +

    + + ) + } + + return ( + <> +

    + Web snippet +

    +

    + PostHog's configurable web snippet allows you to (optionally) autocapture events, record user sessions, + and more with no extra work. Place the following snippet in your website's HTML, ideally just above the{' '} + {''} tag. +

    +

    + For more guidance, including on identifying users,{' '} + see PostHog Docs. +

    + {currentTeamLoading && !currentTeam ? ( +
    + + +
    + ) : ( + + )} + + + +

    Need to test PostHog on a live site without changing any code?

    +

    + Just drag the bookmarklet below to your bookmarks bar, open the website you want to test PostHog on and + click it. This will enable our tracking, on the currently loaded page only. The data will show up in + this project. +

    +
    {isAuthenticatedTeam(currentTeam) && }
    + +

    + Send custom events +

    + To send custom events visit PostHog Docs and + integrate the library for the specific language or platform you're using. We support Python, Ruby, Node, Go, + PHP, iOS, Android, and more. + +

    + Project Variables +

    +

    + Project API Key +

    +

    + You can use this write-only key in any one of{' '} + our libraries. +

    + , + title: 'Reset project API key', + popconfirmProps: { + title: ( + <> + Reset the project's API key?{' '} + This will invalidate the current API key and cannot be undone. + + ), + okText: 'Reset key', + okType: 'danger', + placement: 'left', + }, + callback: resetToken, + }, + ] + : [] + } + thing="project API key" + > + {currentTeam?.api_token || ''} + +

    + Write-only means it can only create new events. It can't read events or any of your other data stored + with PostHog, so it's safe to use in public apps. +

    +

    + Project ID +

    +

    + You can use this ID to reference your project in our API. +

    + {String(currentTeam?.id || '')} + + ) +} diff --git a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx new file mode 100644 index 0000000000000..d19fcf987d786 --- /dev/null +++ b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx @@ -0,0 +1,21 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { PathCleanFilters } from 'lib/components/PathCleanFilters/PathCleanFilters' + +export function PathCleaningFiltersConfig(): JSX.Element | null { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + + if (!currentTeam) { + return null + } + + return ( + { + updateCurrentTeam({ path_cleaning_filters: filters }) + }} + /> + ) +} diff --git a/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx b/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx new file mode 100644 index 0000000000000..df85da1d32db3 --- /dev/null +++ b/frontend/src/scenes/settings/project/PersonDisplayNameProperties.tsx @@ -0,0 +1,49 @@ +import { LemonButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { PersonPropertySelect } from 'lib/components/PersonPropertySelect/PersonPropertySelect' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { PERSON_DEFAULT_DISPLAY_NAME_PROPERTIES } from 'lib/constants' +import { useEffect, useState } from 'react' +import { teamLogic } from 'scenes/teamLogic' + +export function PersonDisplayNameProperties(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const [value, setValue] = useState([] as string[]) + + useEffect( + () => setValue(currentTeam?.person_display_name_properties || PERSON_DEFAULT_DISPLAY_NAME_PROPERTIES), + [currentTeam] + ) + + if (!currentTeam) { + return + } + + return ( + <> +

    + Choose which properties of an identified Person will be used for their Display Name. The first + property to be found on the Person will be used. Drag the items to re-order the priority. +

    +
    + setValue(properties)} + selectedProperties={value || []} + addText="Add" + sortable + /> + + updateCurrentTeam({ + person_display_name_properties: value.map((s) => s.trim()).filter((a) => a) || [], + }) + } + > + Save + +
    + + ) +} diff --git a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx new file mode 100644 index 0000000000000..1e84517f4baeb --- /dev/null +++ b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx @@ -0,0 +1,264 @@ +import { useValues, useActions } from 'kea' +import { MINIMUM_IMPLICIT_ACCESS_LEVEL, teamMembersLogic } from './teamMembersLogic' +import { CloseCircleOutlined, LogoutOutlined, CrownFilled, LockOutlined, UnlockOutlined } from '@ant-design/icons' +import { humanFriendlyDetailedTime } from 'lib/utils' +import { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' +import { FusedTeamMemberType, AvailableFeature } from '~/types' +import { userLogic } from 'scenes/userLogic' +import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { + getReasonForAccessLevelChangeProhibition, + membershipLevelToName, + teamMembershipLevelIntegers, +} from 'lib/utils/permissioning' +import { AddMembersModalWithButton } from './AddMembersModal' +import { RestrictedArea, RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' +import { LemonButton, LemonSelect, LemonSelectOption, LemonSwitch, LemonTable } from '@posthog/lemon-ui' +import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { organizationLogic } from 'scenes/organizationLogic' +import { sceneLogic } from 'scenes/sceneLogic' + +function LevelComponent(member: FusedTeamMemberType): JSX.Element | null { + const { user } = useValues(userLogic) + const { currentTeam } = useValues(teamLogic) + const { changeUserAccessLevel } = useActions(teamMembersLogic) + + const myMembershipLevel = isAuthenticatedTeam(currentTeam) ? currentTeam.effective_membership_level : null + + if (!user) { + return null + } + + const isImplicit = member.organization_level >= OrganizationMembershipLevel.Admin + const levelName = membershipLevelToName.get(member.level) ?? `unknown (${member.level})` + + const allowedLevels = teamMembershipLevelIntegers.filter( + (listLevel) => !getReasonForAccessLevelChangeProhibition(myMembershipLevel, user, member, listLevel) + ) + + const possibleOptions = member.explicit_team_level + ? allowedLevels.concat([member.explicit_team_level]) + : allowedLevels + + const disallowedReason = isImplicit + ? `This user is a member of the project implicitly due to being an organization ${levelName}.` + : getReasonForAccessLevelChangeProhibition(myMembershipLevel, user, member, allowedLevels) + + const levelButton = disallowedReason ? ( +
    + {member.level === OrganizationMembershipLevel.Owner && } + {levelName} +
    + ) : ( + { + if (listLevel !== null) { + changeUserAccessLevel(member.user, listLevel) + } + }} + options={possibleOptions.map( + (listLevel) => + ({ + value: listLevel, + disabled: listLevel === member.explicit_team_level, + label: + listLevel > member.level + ? membershipLevelToName.get(listLevel) + : membershipLevelToName.get(listLevel), + } as LemonSelectOption) + )} + value={member.explicit_team_level} + /> + ) + + return disallowedReason ? {levelButton} : levelButton +} + +function ActionsComponent(member: FusedTeamMemberType): JSX.Element | null { + const { user } = useValues(userLogic) + const { currentTeam } = useValues(teamLogic) + const { removeMember } = useActions(teamMembersLogic) + + if (!user) { + return null + } + + function handleClick(): void { + LemonDialog.open({ + title: `${ + member.user.uuid == user?.uuid + ? 'Leave' + : `Remove ${member.user.first_name} (${member.user.email}) from` + } project ${currentTeam?.name}?`, + secondaryButton: { + children: 'Cancel', + }, + primaryButton: { + status: 'danger', + children: member.user.uuid == user?.uuid ? 'Leave' : 'Remove', + onClick: () => removeMember({ member }), + }, + }) + } + + const allowDeletion = + // You can leave, but only project admins can remove others + ((currentTeam?.effective_membership_level && + currentTeam.effective_membership_level >= OrganizationMembershipLevel.Admin) || + member.user.uuid === user.uuid) && + // Only members without implicit access can leave or be removed + member.organization_level < MINIMUM_IMPLICIT_ACCESS_LEVEL + + return allowDeletion ? ( + + {member.user.uuid !== user.uuid ? ( + + ) : ( + + )} + + ) : null +} + +export function ProjectTeamMembers(): JSX.Element | null { + const { user } = useValues(userLogic) + const { allMembers, allMembersLoading } = useValues(teamMembersLogic) + + if (!user) { + return null + } + + const columns: LemonTableColumns = [ + { + key: 'user_profile_picture', + render: function ProfilePictureRender(_, member) { + return + }, + width: 32, + }, + { + title: 'Name', + key: 'user_first_name', + render: (_, member) => + member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name, + sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name), + }, + { + title: 'Email', + key: 'user_email', + render: (_, member) => member.user.email, + sorter: (a, b) => a.user.email.localeCompare(b.user.email), + }, + { + title: 'Level', + key: 'level', + render: function LevelRender(_, member) { + return LevelComponent(member) + }, + sorter: (a, b) => a.level - b.level, + }, + { + title: 'Joined At', + dataIndex: 'joined_at', + key: 'joined_at', + render: (_, member) => humanFriendlyDetailedTime(member.joined_at), + sorter: (a, b) => a.joined_at.localeCompare(b.joined_at), + }, + { + key: 'actions', + align: 'center', + render: function ActionsRender(_, member) { + return ActionsComponent(member) + }, + }, + ] + + return ( + <> +

    + Members with Project Access + +

    + + + + ) +} + +export function ProjectAccessControl(): JSX.Element { + const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic) + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + const { guardAvailableFeature } = useActions(sceneLogic) + const { hasAvailableFeature } = useValues(userLogic) + + const projectPermissioningEnabled = + hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && currentTeam?.access_control + const isRestricted = !!useRestrictedArea({ + minimumAccessLevel: OrganizationMembershipLevel.Admin, + }) + + return ( + <> +

    + {projectPermissioningEnabled ? ( + <> + This project is{' '} + + + private + + . Only members listed below are allowed to access it. + + ) : ( + <> + This project is{' '} + + + open + + . Any member of the organization can access it. To enable granular access control, make it + private. + + )} +

    + { + guardAvailableFeature( + AvailableFeature.PROJECT_BASED_PERMISSIONING, + 'project-based permissioning', + 'Set permissions granularly for each project. Make sure only the right people have access to protected data.', + () => updateCurrentTeam({ access_control: checked }) + ) + }} + checked={!!projectPermissioningEnabled} + disabled={ + isRestricted || + !currentOrganization || + !currentTeam || + currentOrganizationLoading || + currentTeamLoading + } + bordered + label="Make project private" + /> + + {currentTeam?.access_control && hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && ( + + )} + + ) +} diff --git a/frontend/src/scenes/settings/project/ProjectDangerZone.tsx b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx new file mode 100644 index 0000000000000..82dd00ba309ef --- /dev/null +++ b/frontend/src/scenes/settings/project/ProjectDangerZone.tsx @@ -0,0 +1,96 @@ +import { Dispatch, SetStateAction, useState } from 'react' +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' +import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui' +import { IconDelete } from 'lib/lemon-ui/icons' +import { TeamType } from '~/types' +import { OrganizationMembershipLevel } from 'lib/constants' + +export function DeleteProjectModal({ + isOpen, + setIsOpen, +}: { + isOpen: boolean + setIsOpen: Dispatch> +}): JSX.Element { + const { currentTeam, teamBeingDeleted } = useValues(teamLogic) + const { deleteTeam } = useActions(teamLogic) + + const [isDeletionConfirmed, setIsDeletionConfirmed] = useState(false) + const isDeletionInProgress = !!currentTeam && teamBeingDeleted?.id === currentTeam.id + + return ( + setIsOpen(false) : undefined} + footer={ + <> + setIsOpen(false)}> + Cancel + + deleteTeam(currentTeam as TeamType) : undefined} + >{`Delete ${currentTeam ? currentTeam.name : 'the current project'}`} + + } + isOpen={isOpen} + > +

    + Project deletion cannot be undone. You will lose all data, including events, related to + the project. +

    +

    + Please type {currentTeam ? currentTeam.name : "this project's name"} to confirm. +

    + { + if (currentTeam) { + setIsDeletionConfirmed(value.toLowerCase() === currentTeam.name.toLowerCase()) + } + }} + /> +
    + ) +} + +export function ProjectDangerZone(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + const [isModalVisible, setIsModalVisible] = useState(false) + + const restrictedReason = useRestrictedArea({ + minimumAccessLevel: OrganizationMembershipLevel.Admin, + scope: RestrictionScope.Project, + }) + + return ( + <> +
    +
    + {!restrictedReason && ( +

    + This is irreversible. Please be certain. +

    + )} + setIsModalVisible(true)} + data-attr="delete-project-button" + icon={} + disabledReason={restrictedReason} + > + Delete {currentTeam?.name || 'the current project'} + +
    +
    + + + ) +} diff --git a/frontend/src/scenes/settings/project/ProjectSettings.tsx b/frontend/src/scenes/settings/project/ProjectSettings.tsx new file mode 100644 index 0000000000000..49a07fbbe1188 --- /dev/null +++ b/frontend/src/scenes/settings/project/ProjectSettings.tsx @@ -0,0 +1,261 @@ +import { useActions, useValues } from 'kea' +import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' +import { JSSnippet } from 'lib/components/JSSnippet' +import { JSBookmarklet } from 'lib/components/JSBookmarklet' +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' +import { CodeSnippet } from 'lib/components/CodeSnippet' +import { IconRefresh } from 'lib/lemon-ui/icons' +import { Link } from 'lib/lemon-ui/Link' +import { LemonButton, LemonInput, LemonLabel, LemonSkeleton } from '@posthog/lemon-ui' +import { useState } from 'react' +import { TimezoneConfig } from './TimezoneConfig' +import { WeekStartConfig } from './WeekStartConfig' +import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' +import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { urls } from '@posthog/apps-common' + +export function ProjectDisplayName(): JSX.Element { + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + const { updateCurrentTeam } = useActions(teamLogic) + + const [name, setName] = useState(currentTeam?.name || '') + + if (currentTeam?.is_demo) { + return ( +

    + The demo project cannot be renamed. +

    + ) + } + + return ( +
    + + updateCurrentTeam({ name })} + disabled={!name || !currentTeam || name === currentTeam.name} + loading={currentTeamLoading} + > + Rename Project + +
    + ) +} + +export function WebSnippet(): JSX.Element { + const { currentTeam, currentTeamLoading } = useValues(teamLogic) + + return ( + <> +

    + PostHog's configurable web snippet allows you to (optionally) autocapture events, record user sessions, + and more with no extra work. Place the following snippet in your website's HTML, ideally just above the{' '} + {''} tag. +

    +

    + For more guidance, including on identifying users,{' '} + see PostHog Docs. +

    + {currentTeamLoading && !currentTeam ? ( +
    + + +
    + ) : ( + + )} + + ) +} + +export function Misc(): JSX.Element { + const { currentTeam, isTeamTokenResetAvailable } = useValues(teamLogic) + const { resetToken } = useActions(teamLogic) + + return ( + <> +

    Need to test PostHog on a live site without changing any code?

    +

    + Just drag the bookmarklet below to your bookmarks bar, open the website you want to test PostHog on and + click it. This will enable our tracking, on the currently loaded page only. The data will show up in + this project. +

    +
    {isAuthenticatedTeam(currentTeam) && }
    + +

    + Send custom events +

    + To send custom events visit PostHog Docs and + integrate the library for the specific language or platform you're using. We support Python, Ruby, Node, Go, + PHP, iOS, Android, and more. + +

    + Project Variables +

    +

    + Project API Key +

    +

    + You can use this write-only key in any one of{' '} + our libraries. +

    + , + title: 'Reset project API key', + popconfirmProps: { + title: ( + <> + Reset the project's API key?{' '} + This will invalidate the current API key and cannot be undone. + + ), + okText: 'Reset key', + okType: 'danger', + placement: 'left', + }, + callback: resetToken, + }, + ] + : [] + } + thing="project API key" + > + {currentTeam?.api_token || ''} + +

    + Write-only means it can only create new events. It can't read events or any of your other data stored + with PostHog, so it's safe to use in public apps. +

    +

    + Project ID +

    +

    + You can use this ID to reference your project in our API. +

    + {String(currentTeam?.id || '')} + + ) +} + +export function Bookmarklet(): JSX.Element { + const { currentTeam } = useValues(teamLogic) + + return ( + <> +

    Need to test PostHog on a live site without changing any code?

    +

    + Just drag the bookmarklet below to your bookmarks bar, open the website you want to test PostHog on and + click it. This will enable our tracking, on the currently loaded page only. The data will show up in + this project. +

    +
    {isAuthenticatedTeam(currentTeam) && }
    + + ) +} + +export function ProjectVariables(): JSX.Element { + const { currentTeam, isTeamTokenResetAvailable } = useValues(teamLogic) + const { resetToken } = useActions(teamLogic) + + return ( +
    + {/*

    + Send custom events +

    + To send custom events visit PostHog Docs and + integrate the library for the specific language or platform you're using. We support Python, Ruby, Node, Go, + PHP, iOS, Android, and more. */} +
    +

    + Project API Key +

    +

    + You can use this write-only key in any one of{' '} + our libraries. +

    + , + title: 'Reset project API key', + popconfirmProps: { + title: ( + <> + Reset the project's API key?{' '} + This will invalidate the current API key and cannot be undone. + + ), + okText: 'Reset key', + okType: 'danger', + placement: 'left', + }, + callback: resetToken, + }, + ] + : [] + } + thing="project API key" + > + {currentTeam?.api_token || ''} + +

    + Write-only means it can only create new events. It can't read events or any of your other data + stored with PostHog, so it's safe to use in public apps. +

    +
    +
    +

    + Project ID +

    +

    + You can use this ID to reference your project in our{' '} + API. +

    + {String(currentTeam?.id || '')} +
    +
    + ) +} + +export function ProjectTimezone(): JSX.Element { + return ( + <> +

    + These settings affect how PostHog displays, buckets, and filters time-series data. You may need to + refresh insights for new settings to apply. +

    +
    + Time zone + + Week starts on + +
    + + ) +} + +export function ProjectToolbarURLs(): JSX.Element { + return ( + <> +

    + These are the URLs where the{' '} + + Toolbar will automatically launch + {' '} + (if you're logged in). +

    +

    + Domains and wildcard subdomains are allowed (example: https://*.example.com). + However, wildcarded top-level domains cannot be used (for security reasons). +

    + + + ) +} diff --git a/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx new file mode 100644 index 0000000000000..6a4783446819e --- /dev/null +++ b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx @@ -0,0 +1,304 @@ +import { useActions, useValues } from 'kea' +import { teamLogic } from 'scenes/teamLogic' +import { LemonBanner, LemonButton, LemonSelect, LemonSwitch, Link } from '@posthog/lemon-ui' +import { urls } from 'scenes/urls' +import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' +import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' +import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FEATURE_FLAGS } from 'lib/constants' +import { IconCancel } from 'lib/lemon-ui/icons' +import { FlagSelector } from 'lib/components/FlagSelector' + +export function ReplayGeneral(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + + const { currentTeam } = useValues(teamLogic) + + return ( +
    +

    Watch recordings of how users interact with your web app to see what can be improved.

    + +
    + { + updateCurrentTeam({ + session_recording_opt_in: checked, + capture_console_log_opt_in: checked, + capture_performance_opt_in: checked, + }) + }} + label="Record user sessions" + bordered + checked={!!currentTeam?.session_recording_opt_in} + /> + +

    + Please note your website needs to have the{' '} + PostHog snippet or the latest version of{' '} + + posthog-js + {' '} + directly installed. For more details, check out our{' '} + + docs + + . +

    +
    +
    + { + updateCurrentTeam({ capture_console_log_opt_in: checked }) + }} + label="Capture console logs" + bordered + checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_console_log_opt_in : false} + disabled={!currentTeam?.session_recording_opt_in} + /> +

    + This setting controls if browser console logs will be captured as a part of recordings. The console + logs will be shown in the recording player to help you debug any issues. +

    +
    +
    + { + updateCurrentTeam({ capture_performance_opt_in: checked }) + }} + label="Capture network performance" + bordered + checked={currentTeam?.session_recording_opt_in ? !!currentTeam?.capture_performance_opt_in : false} + disabled={!currentTeam?.session_recording_opt_in} + /> +

    + This setting controls if performance and network information will be captured alongside recordings. + The network requests and timings will be shown in the recording player to help you debug any issues. +

    +
    +
    + ) +} + +export function ReplayAuthorizedDomains(): JSX.Element { + return ( +
    +

    + Use the settings below to restrict the domains where recordings will be captured. If no domains are + selected, then there will be no domain restriction. +

    +

    + Domains and wildcard subdomains are allowed (e.g. https://*.example.com). However, + wildcarded top-level domains cannot be used (for security reasons). +

    + +
    + ) +} + +export function ReplayCostControl(): JSX.Element { + const { updateCurrentTeam } = useActions(teamLogic) + const { currentTeam } = useValues(teamLogic) + + return ( + + <> + {/*

    + Replay ingestion controls BETA +

    */} +

    + PostHog offers several tools to let you control the number of recordings you collect and which users + you collect recordings for.{' '} + + Learn more in our docs + +

    + + Requires posthog-js version 1.85.0 or greater + +
    + Sampling + { + updateCurrentTeam({ session_recording_sample_rate: v }) + }} + dropdownMatchSelectWidth={false} + options={[ + { + label: '100% (no sampling)', + value: '1.00', + }, + { + label: '95%', + value: '0.95', + }, + { + label: '90%', + value: '0.90', + }, + { + label: '85%', + value: '0.85', + }, + { + label: '80%', + value: '0.80', + }, + { + label: '75%', + value: '0.75', + }, + { + label: '70%', + value: '0.70', + }, + { + label: '65%', + value: '0.65', + }, + { + label: '60%', + value: '0.60', + }, + { + label: '55%', + value: '0.55', + }, + { + label: '50%', + value: '0.50', + }, + { + label: '45%', + value: '0.45', + }, + { + label: '40%', + value: '0.40', + }, + { + label: '35%', + value: '0.35', + }, + { + label: '30%', + value: '0.30', + }, + { + label: '25%', + value: '0.25', + }, + { + label: '20%', + value: '0.20', + }, + { + label: '15%', + value: '0.15', + }, + { + label: '10%', + value: '0.10', + }, + { + label: '5%', + value: '0.05', + }, + { + label: '0% (replay disabled)', + value: '0.00', + }, + ]} + value={ + typeof currentTeam?.session_recording_sample_rate === 'string' + ? currentTeam?.session_recording_sample_rate + : '1.00' + } + /> +
    +

    + Use this setting to restrict the percentage of sessions that will be recorded. This is useful if you + want to reduce the amount of data you collect. 100% means all sessions will be collected. 50% means + roughly half of sessions will be collected. +

    +
    + Minimum session duration (seconds) + { + updateCurrentTeam({ session_recording_minimum_duration_milliseconds: v }) + }} + options={[ + { + label: 'no minimum', + value: null, + }, + { + label: '1', + value: 1000, + }, + { + label: '2', + value: 2000, + }, + { + label: '5', + value: 5000, + }, + { + label: '10', + value: 10000, + }, + { + label: '15', + value: 15000, + }, + ]} + value={currentTeam?.session_recording_minimum_duration_milliseconds} + /> +
    +

    + Setting a minimum session duration will ensure that only sessions that last longer than that value + are collected. This helps you avoid collecting sessions that are too short to be useful. +

    +
    + Enable recordings using feature flag +
    + { + updateCurrentTeam({ session_recording_linked_flag: { id, key } }) + }} + /> + {currentTeam?.session_recording_linked_flag && ( + } + size="small" + status="stealth" + onClick={() => updateCurrentTeam({ session_recording_linked_flag: null })} + title="Clear selected flag" + /> + )} +
    +
    +

    + Linking a flag means that recordings will only be collected for users who have the flag enabled. + Only supports release toggles (boolean flags). +

    + +
    + ) +} diff --git a/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx b/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx new file mode 100644 index 0000000000000..aa21717e2cbf5 --- /dev/null +++ b/frontend/src/scenes/settings/project/SlackIntegration.stories.tsx @@ -0,0 +1,53 @@ +import { Meta } from '@storybook/react' +import { AvailableFeature } from '~/types' +import { useAvailableFeatures } from '~/mocks/features' +import { useStorybookMocks } from '~/mocks/browser' +import { mockIntegration } from '~/test/mocks' +import { SlackIntegration } from './SlackIntegration' + +const meta: Meta = { + title: 'Components/Integrations/Slack', + component: SlackIntegration, + parameters: {}, +} +export default meta + +const Template = (args: { instanceConfigured?: boolean; integrated?: boolean }): JSX.Element => { + const { instanceConfigured = true, integrated = false } = args + + useAvailableFeatures([AvailableFeature.SUBSCRIPTIONS]) + + useStorybookMocks({ + get: { + '/api/projects/:id/integrations': { results: integrated ? [mockIntegration] : [] }, + '/api/instance_settings': { + results: instanceConfigured + ? [ + { + key: 'SLACK_APP_CLIENT_ID', + value: '910200304849.3676478528614', + }, + { + key: 'SLACK_APP_CLIENT_SECRET', + value: '*****', + }, + ] + : [], + }, + }, + }) + + return +} + +export const SlackIntegration_ = (): JSX.Element => { + return