feat: Simplify AccessControlAction usage (#38248)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Rafael Audibert
2025-09-17 15:45:16 -03:00
committed by GitHub
parent 86ca8ca66e
commit c8f6acce27
39 changed files with 1183 additions and 1233 deletions

View File

@@ -11,16 +11,20 @@ You are an expert in PostHog's access control system. Your role is to help imple
## Core Concepts
### Access Levels
- **Resource-level**: `none`, `viewer`, `editor`, `manager`
- **Project-level**: `none`, `member`, `admin`
### Resources vs Objects
- **Resource**: A type of entity (e.g., `notebook`, `feature_flag`)
- **Object**: A specific instance (e.g., notebook ID 123)
- Users can have different access levels for the resource type vs specific objects
### Access Sources
Users can gain access through:
- Being the creator
- Organization admin privileges
- Explicit member grants
@@ -31,6 +35,7 @@ Users can gain access through:
## Code Structure
### Key Files
- `posthog/rbac/user_access_control.py` - Core access control logic
- `ee/models/rbac/access_control.py` - Database model
- `ee/api/rbac/access_control.py` - API endpoints and ViewSet mixin
@@ -38,6 +43,7 @@ Users can gain access through:
- `posthog/scopes.py` - Resource type definitions
### Main Classes
- `UserAccessControl` - Central access control logic
- `AccessControlViewSetMixin` - Adds access control endpoints to ViewSets
- `AccessControlPermission` - Enforces access controls in API
@@ -51,7 +57,7 @@ Users can gain access through:
# posthog/scopes.py
ACCESS_CONTROL_RESOURCES = [
"feature_flag",
"dashboard",
"dashboard",
"insight",
"your_resource", # Add your new resource
"session_recording",
@@ -76,7 +82,7 @@ class YourResourceViewSet(
ProjectMembershipNecessaryPermissions,
AccessControlPermission, # Add access control permission
]
# Rest of your ViewSet implementation
```
@@ -124,7 +130,7 @@ Add your scenes to the access control resource mapping:
// frontend/src/scenes/sceneTypes.ts
export const sceneToAccessControlResourceType: Partial<Record<Scene, AccessControlResourceType>> = {
// Existing mappings...
// Your new resource scenes
[Scene.YourResource]: AccessControlResourceType.YourNewResource,
[Scene.YourResourceList]: AccessControlResourceType.YourNewResource,
@@ -148,101 +154,31 @@ export interface YourResourceType {
#### 4.4 Block UI Elements Based on Access Levels
##### Using accessControl Props (Preferred)
You should wrap the components you care about with the `AccessControlAction`. It requires the child component to expose a `disabledReason` prop which is automatically set by the wrapper.
Many PostHog components support `accessControl` props for automatic permission checking:
```tsx
import { LemonButton, LemonSwitch, LemonSelect } from '@posthog/lemon-ui'
import { getAppContext } from 'lib/utils/getAppContext'
import { AccessControlResourceType, AccessControlLevel } from '~/types'
// Block buttons based on resource-level access
<LemonButton
type="primary"
onClick={handleEdit}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource],
}}
>
Edit Resource
</LemonButton>
// Block switches/toggles
<LemonSwitch
checked={settings.enabled}
onChange={handleToggle}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource],
}}
/>
// Block selects and other form controls
<LemonSelect
value={selectedOption}
onChange={handleChange}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource],
}}
options={options}
/>
// For object-specific permissions, use the object's user_access_level
<LemonButton
type="primary"
onClick={handleEdit}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: yourResource.user_access_level,
}}
>
Edit This Resource
</LemonButton>
```
##### Using AccessControlAction Wrapper
For components that don't support `accessControl` props:
If your component doesn't respect that interface you can instead expose a function that accepts `{ disabled, disabledReason }` as parameters.
```tsx
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { AccessControlResourceType, AccessControlLevel } from '~/types'
import { getAppContext } from 'lib/utils/getAppContext'
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource]}
>
{({ disabled, disabledReason }) => (
<CustomComponent
onClick={handleAction}
disabled={disabled}
tooltip={disabledReason}
/>
)}
</AccessControlAction>
// For object-specific permissions
// Automatically sets `disabled` and `disabledReason` on the child
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={yourResource.user_access_level}
>
{({ disabled, disabledReason }) => (
<CustomComponent
onClick={handleAction}
disabled={disabled}
tooltip={disabledReason}
/>
)}
<LemonButton>My button</LemonButton>
</AccessControlAction>
// Manually sets the values
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource]}
>
{({ disabledReason }) => (<CustomComponent onClick={handleAction} tooltip={disabledReason} readOnly={!!disabledReason} />)}
</AccessControlAction>
```
@@ -254,34 +190,31 @@ Use resource-level permissions for create operations:
```tsx
import { LemonButton } from '@posthog/lemon-ui'
import { getAppContext } from 'lib/utils/getAppContext'
import { AccessControlResourceType, AccessControlLevel } from '~/types'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
function YourResourceList() {
return (
<div>
{/* Using accessControl prop (preferred) */}
<LemonButton
type="primary"
onClick={() => router.actions.push('/your-resources/new')}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource],
}}
{/* Using AccessControlAction (preferred) */}
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource]}
>
New Resource
</LemonButton>
<LemonButton type="primary" onClick={() => router.actions.push('/your-resources/new')}>
New Resource
</LemonButton>
</AccessControlAction>
{/* Manual permission check if needed */}
{(() => {
const userLevel = getAppContext()?.resource_access_control?.[AccessControlResourceType.YourResource]
const canCreate = userLevel && ['editor', 'manager'].includes(userLevel)
return canCreate ? (
<LemonButton
type="primary"
onClick={() => router.actions.push('/your-resources/new')}
>
<LemonButton type="primary" onClick={() => router.actions.push('/your-resources/new')}>
New Resource
</LemonButton>
) : null
@@ -300,27 +233,20 @@ function YourResourceCard({ yourResource }: { yourResource: YourResourceType })
return (
<div>
<h3>{yourResource.name}</h3>
{/* Using accessControl prop (preferred) */}
<LemonButton
onClick={() => openEditModal(yourResource)}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: yourResource.user_access_level,
}}
{/* Using AccessControlAction (preferred) */}
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={yourResource.user_access_level}
>
Edit
</LemonButton>
<LemonButton onClick={() => openEditModal(yourResource)}>Edit</LemonButton>
</AccessControlAction>
{/* Manual permission check if needed */}
{(() => {
const canEdit = ['editor', 'manager'].includes(yourResource.user_access_level || 'none')
return canEdit ? (
<LemonButton onClick={() => openEditModal(yourResource)}>
Edit
</LemonButton>
) : null
return canEdit ? <LemonButton onClick={() => openEditModal(yourResource)}>Edit</LemonButton> : null
})()}
</div>
)
@@ -332,17 +258,15 @@ function YourResourceCard({ yourResource }: { yourResource: YourResourceType })
Typically requires `editor` level access:
```tsx
<LemonButton
status="danger"
onClick={() => deleteYourResource(yourResource)}
accessControl={{
resourceType: AccessControlResourceType.YourResource,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: yourResource.user_access_level,
}}
<AccessControlAction
resourceType={AccessControlResourceType.YourResource}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={yourResource.user_access_level}
>
Delete
</LemonButton>
<LemonButton status="danger" onClick={() => deleteYourResource(yourResource)}>
Delete
</LemonButton>
</AccessControlAction>
```
#### 4.6 Review All User Interaction Points
@@ -350,6 +274,7 @@ Typically requires `editor` level access:
When implementing access controls, audit all places where users can interact with your resource:
**Common interaction points to review:**
- List views (create buttons, bulk actions)
- Detail views (edit, delete, duplicate buttons)
- Settings pages (configuration toggles, saves)
@@ -371,13 +296,13 @@ from posthog.rbac.decorators import field_access_control
class Team(models.Model):
# Other fields...
session_recording_opt_in = field_access_control(
models.BooleanField(default=False),
"session_recording", # Resource type
"editor" # Required access level
)
capture_console_log_opt_in = field_access_control(
models.BooleanField(null=True, blank=True),
"session_recording",
@@ -401,6 +326,7 @@ RESOURCE_INHERITANCE_MAP = {
## Step-by-Step Implementation Checklist
### Backend
1. Add resource to `ACCESS_CONTROL_RESOURCES` in `posthog/scopes.py`
2. Add `AccessControlViewSetMixin` to your ViewSet
3. Set `scope_object` attribute on ViewSet
@@ -410,6 +336,7 @@ RESOURCE_INHERITANCE_MAP = {
7. **If needed:** Set up resource inheritance in `RESOURCE_INHERITANCE_MAP`
### Frontend
1. Add resource to `resourcesAccessControlLogic.ts` resources array
2. Add scene mappings to `sceneToAccessControlResourceType` in `sceneTypes.ts`
3. Update TypeScript types to include `user_access_level: AccessLevel`
@@ -419,6 +346,7 @@ RESOURCE_INHERITANCE_MAP = {
7. Handle access control UI (user management modals, permission settings)
### Testing
- Add comprehensive tests for all access levels
- **If needed:** Test field-level access controls
- **If needed:** Test inheritance patterns
@@ -454,7 +382,7 @@ user_access_control = UserAccessControl(user, team)
if user_access_control.check_access_level_for_resource("notebook", "editor"):
# User can edit notebooks
# Check object-level access
# Check object-level access
if user_access_control.check_access_level_for_object(notebook, "viewer"):
# User can view this specific notebook
@@ -484,4 +412,4 @@ user_access_control.preload_object_access_controls(notebook_queryset, "notebook"
- Always include both server-side enforcement (permissions) and client-side UI (conditional rendering)
- The system uses caching to optimize performance - be mindful of bulk operations
- Field-level access controls automatically validate during serialization
- Resource inheritance allows related resources to share access controls
- Resource inheritance allows related resources to share access controls

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,3 +1,5 @@
import React from 'react'
import { AccessControlResourceType } from '~/types'
type AccessControlLevelNone = 'none'
@@ -5,13 +7,6 @@ type AccessControlLevelMember = AccessControlLevelNone | 'member' | 'admin'
type AccessControlLevelResource = AccessControlLevelNone | 'viewer' | 'editor' | 'manager'
type AccessControlLevel = AccessControlLevelMember | AccessControlLevelResource
interface AccessControlActionProps {
children: (props: { disabled: boolean; disabledReason: string | null }) => React.ReactElement
userAccessLevel?: AccessControlLevel
minAccessLevel: AccessControlLevel
resourceType: AccessControlResourceType
}
const orderedAccessLevels = (resourceType: AccessControlResourceType): AccessControlLevel[] => {
if (resourceType === AccessControlResourceType.Project || resourceType === AccessControlResourceType.Organization) {
return ['none', 'member', 'admin']
@@ -56,18 +51,49 @@ export const getAccessControlDisabledReason = (
return null
}
interface AccessControlActionChildrenProps {
disabled?: boolean
disabledReason: string | null
}
interface AccessControlActionProps<P extends AccessControlActionChildrenProps> {
children: React.ComponentType<P> | React.ReactElement<P>
userAccessLevel?: AccessControlLevel
minAccessLevel: AccessControlLevel
resourceType: AccessControlResourceType
}
// This is a wrapper around a component that checks if the user has access to the resource
// and if not, it disables the component and shows a reason why
export const AccessControlAction = ({
// and if not, it sets the `disabled` and `disabledReason` props on the child component
//
// NOTE: TS is not powerful enough to enforce the fact that the child component *must* receive
// the `disabled` and `disabledReason` props. This means we are accepting any component and
// then setting the props at runtime even in case they "shouldn't" receive them.
// This is not problematic during runtime but it's admitedly slightly confusing
export function AccessControlAction<P extends AccessControlActionChildrenProps>({
children,
userAccessLevel,
minAccessLevel,
resourceType = AccessControlResourceType.Project,
}: AccessControlActionProps): JSX.Element => {
}: AccessControlActionProps<P>): JSX.Element {
const disabledReason = getAccessControlDisabledReason(resourceType, userAccessLevel, minAccessLevel)
return children({
disabled: !!disabledReason,
disabledReason,
})
// Check if children is a component function or a rendered element
// If it's a component function, we need to render it with the props
if (typeof children === 'function') {
const Component = children as React.ComponentType<P>
const componentProps = {
disabled: !!disabledReason,
disabledReason: disabledReason,
} as P
return <Component {...componentProps} />
}
// If it's a rendered element, we need to clone it overloading the props
const element = children as React.FunctionComponentElement<P>
return React.cloneElement<P>(element, {
disabled: element.props.disabled ?? !!disabledReason,
disabledReason: element.props.disabledReason ?? disabledReason,
} as Partial<P>)
}

View File

@@ -41,7 +41,7 @@ import {
QueryBasedInsightModel,
} from '~/types'
import { accessLevelSatisfied } from '../AccessControlAction'
import { AccessControlAction, accessLevelSatisfied } from '../AccessControlAction'
import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic'
import { SharePasswordsTable } from './SharePasswordsTable'
import { sharingLogic } from './sharingLogic'
@@ -151,25 +151,28 @@ export function SharingModalContent({
{!sharingAllowed ? (
<LemonBanner type="warning">Public sharing is disabled for this organization.</LemonBanner>
) : (
<LemonSwitch
id="sharing-switch"
label={`Share ${resource} publicly`}
checked={sharingConfiguration.enabled}
data-attr="sharing-switch"
onChange={(active) => setIsEnabled(active)}
bordered
fullWidth
loading={sharingConfigurationLoading}
accessControl={{
resourceType: dashboardId
<AccessControlAction
resourceType={
dashboardId
? AccessControlResourceType.Dashboard
: insightShortId
? AccessControlResourceType.Insight
: AccessControlResourceType.Project,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: userAccessLevel,
}}
/>
: AccessControlResourceType.Project
}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={userAccessLevel}
>
<LemonSwitch
id="sharing-switch"
label={`Share ${resource} publicly`}
checked={sharingConfiguration.enabled}
data-attr="sharing-switch"
onChange={(active) => setIsEnabled(active)}
bordered
fullWidth
loading={sharingConfigurationLoading}
/>
</AccessControlAction>
)}
{sharingAllowed && sharingConfiguration.enabled && sharingConfiguration.access_token ? (

View File

@@ -4,6 +4,7 @@ import clsx from 'clsx'
import { IconGear, IconInfo, IconPlus } from '@posthog/icons'
import { Link } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { useAsyncHandler } from 'lib/hooks/useAsyncHandler'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
@@ -61,10 +62,10 @@ const TypesAndStatusesTemplate: StoryFn<typeof LemonButton> = (props) => {
return (
<div className="deprecated-space-y-2">
{types.map((type) => (
<>
<div key={type}>
<h5>type={capitalizeFirstLetter(type || '')}</h5>
<StatusesTemplate {...props} type={type} />
</>
</div>
))}
</div>
)
@@ -104,10 +105,10 @@ export const Sizes = (): JSX.Element => {
return (
<div className="deprecated-space-y-2">
{sizes.map((size) => (
<>
<div key={size}>
<h5>size={size}</h5>
<StatusesTemplate size={size} type="secondary" />
</>
</div>
))}
</div>
)
@@ -119,10 +120,10 @@ export const SizesIconOnly = (): JSX.Element => {
return (
<div className="deprecated-space-y-2">
{sizes.map((size) => (
<>
<div key={size}>
<h5>size={size}</h5>
<StatusesTemplate size={size} type="secondary" noText />
</>
</div>
))}
</div>
)
@@ -252,7 +253,7 @@ export const WithSideAction = (): JSX.Element => {
return (
<div className="deprecated-space-y-2">
{types.map((type) => (
<>
<div key={type}>
<h5>type={capitalizeFirstLetter(type || '')}</h5>
<div className="flex items-center gap-2">
{statuses.map((status, i) => (
@@ -270,7 +271,7 @@ export const WithSideAction = (): JSX.Element => {
</LemonButton>
))}
</div>
</>
</div>
))}
</div>
)
@@ -446,26 +447,20 @@ export const WithOverflowingContent = (): JSX.Element => {
export const WithAccessControl = (): JSX.Element => {
return (
<div className="flex gap-2">
<LemonButton
type="primary"
accessControl={{
resourceType: AccessControlResourceType.Project,
minAccessLevel: AccessControlLevel.Admin,
userAccessLevel: AccessControlLevel.Admin,
}}
<AccessControlAction
resourceType={AccessControlResourceType.Project}
minAccessLevel={AccessControlLevel.Admin}
userAccessLevel={AccessControlLevel.Admin}
>
Enabled (admin ≥ admin)
</LemonButton>
<LemonButton
type="primary"
accessControl={{
resourceType: AccessControlResourceType.Project,
minAccessLevel: AccessControlLevel.Admin,
userAccessLevel: AccessControlLevel.Viewer,
}}
<LemonButton type="primary">Enabled (admin ≥ admin)</LemonButton>
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.Project}
minAccessLevel={AccessControlLevel.Admin}
userAccessLevel={AccessControlLevel.Viewer}
>
Disabled (viewer {'<'} admin)
</LemonButton>
<LemonButton type="primary">Disabled (viewer {'<'} admin)</LemonButton>
</AccessControlAction>
</div>
)
}

View File

@@ -5,11 +5,8 @@ import React, { useContext } from 'react'
import { IconChevronDown } from '@posthog/icons'
import { getAccessControlDisabledReason } from 'lib/components/AccessControlAction'
import { IconChevronRight } from 'lib/lemon-ui/icons'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
import { LemonDropdown, LemonDropdownProps } from '../LemonDropdown'
import { Link } from '../Link'
import { PopoverOverlayContext, PopoverReferenceContext } from '../Popover'
@@ -86,12 +83,6 @@ export interface LemonButtonPropsBase
tooltipForceMount?: boolean
/** Whether to stop event propagation on click */
stopPropagation?: boolean
/** Access control props for automatic permission checking */
accessControl?: {
resourceType: AccessControlResourceType
minAccessLevel: AccessControlLevel
userAccessLevel?: AccessControlLevel
}
}
export type SideAction = Pick<
@@ -165,7 +156,6 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
tooltipDocLink,
tooltipForceMount,
stopPropagation,
accessControl,
...buttonProps
},
ref
@@ -206,22 +196,6 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
size = 'small' // Ensure that buttons in the page header are small (but NOT inside dropdowns!)
}
// Handle access control
if (accessControl) {
const { userAccessLevel, minAccessLevel, resourceType } = accessControl
const accessControlDisabledReason = getAccessControlDisabledReason(
resourceType,
userAccessLevel,
minAccessLevel
)
if (accessControlDisabledReason) {
disabled = true
if (!disabledReason) {
disabledReason = accessControlDisabledReason
}
}
}
let tooltipContent: TooltipProps['title']
if (disabledReason) {
disabled = true // Support `disabledReason` while maintaining compatibility with `disabled`

View File

@@ -1,5 +1,6 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { capitalizeFirstLetter } from 'lib/utils'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
@@ -148,24 +149,20 @@ export const WithAccessControl = (): JSX.Element => {
return (
<div className="flex gap-4 items-center">
<LemonSelect
options={options}
placeholder="Enabled (editor ≥ viewer)"
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Viewer,
userAccessLevel: AccessControlLevel.Editor,
}}
/>
<LemonSelect
options={options}
placeholder="Disabled (viewer < editor)"
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: AccessControlLevel.Viewer,
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Viewer}
userAccessLevel={AccessControlLevel.Editor}
>
<LemonSelect options={options} placeholder="Enabled (editor ≥ viewer)" />
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={AccessControlLevel.Viewer}
>
<LemonSelect options={options} placeholder="Disabled (viewer < editor)" />
</AccessControlAction>
</div>
)
}

View File

@@ -5,8 +5,6 @@ import { IconX } from '@posthog/icons'
import { LemonDropdownProps } from 'lib/lemon-ui/LemonDropdown'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
import { LemonButton, LemonButtonProps } from '../LemonButton'
import {
LemonMenu,
@@ -83,12 +81,6 @@ export interface LemonSelectPropsBase<T>
menu?: Pick<LemonMenuProps, 'className' | 'closeParentPopoverOnClickInside'>
visible?: LemonDropdownProps['visible']
startVisible?: LemonDropdownProps['startVisible']
/** Access control props for automatic permission checking */
accessControl?: {
resourceType: AccessControlResourceType
minAccessLevel: AccessControlLevel
userAccessLevel?: AccessControlLevel
}
}
export interface LemonSelectPropsClearable<T> extends LemonSelectPropsBase<T> {
@@ -127,7 +119,6 @@ export function LemonSelect<T extends string | number | boolean | null>({
renderButtonContent,
visible,
startVisible,
accessControl,
...buttonProps
}: LemonSelectProps<T>): JSX.Element {
const [items, allLeafOptions] = useMemo(
@@ -180,7 +171,6 @@ export function LemonSelect<T extends string | number | boolean | null>({
: undefined
}
tooltip={activeLeaf?.tooltip}
accessControl={accessControl}
{...buttonProps}
>
<span className="flex flex-1">

View File

@@ -1,6 +1,8 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { useState } from 'react'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
import { LemonSwitchProps, LemonSwitch as RawLemonSwitch } from './LemonSwitch'
@@ -122,26 +124,20 @@ SizesLoading.parameters = { testOptions: { waitForLoadersToDisappear: false } }
export const WithAccessControl = (): JSX.Element => {
return (
<div className="deprecated-space-y-2">
<LemonSwitch
label="Enabled (admin ≥ admin)"
checked={true}
onChange={() => {}}
accessControl={{
resourceType: AccessControlResourceType.Project,
minAccessLevel: AccessControlLevel.Admin,
userAccessLevel: AccessControlLevel.Admin,
}}
/>
<LemonSwitch
label="Disabled (viewer < admin)"
checked={false}
onChange={() => {}}
accessControl={{
resourceType: AccessControlResourceType.Project,
minAccessLevel: AccessControlLevel.Admin,
userAccessLevel: AccessControlLevel.Viewer,
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.Project}
minAccessLevel={AccessControlLevel.Admin}
userAccessLevel={AccessControlLevel.Admin}
>
<LemonSwitch label="Enabled (admin ≥ admin)" checked={true} onChange={() => {}} />
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.Project}
minAccessLevel={AccessControlLevel.Admin}
userAccessLevel={AccessControlLevel.Viewer}
>
<LemonSwitch label="Disabled (viewer < admin)" checked={false} onChange={() => {}} />
</AccessControlAction>
</div>
)
}

View File

@@ -3,13 +3,10 @@ import './LemonSwitch.scss'
import clsx from 'clsx'
import { forwardRef, useMemo, useState } from 'react'
import { getAccessControlDisabledReason } from 'lib/components/AccessControlAction'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { cn } from 'lib/utils/css-classes'
import { AccessControlLevel, AccessControlResourceType } from '~/types'
export interface LemonSwitchProps {
className?: string
onChange?: (newChecked: boolean) => void
@@ -29,12 +26,6 @@ export interface LemonSwitchProps {
sliderColorOverrideChecked?: string
sliderColorOverrideUnchecked?: string
loading?: boolean
/** Access control props for automatic permission checking */
accessControl?: {
resourceType: AccessControlResourceType
minAccessLevel: AccessControlLevel
userAccessLevel?: AccessControlLevel
}
}
/** Counter used for collision-less automatic switch IDs. */
@@ -60,7 +51,6 @@ export const LemonSwitch: React.FunctionComponent<LemonSwitchProps & React.RefAt
sliderColorOverrideChecked,
sliderColorOverrideUnchecked,
loading = false,
accessControl,
},
ref
): JSX.Element {
@@ -72,22 +62,6 @@ export const LemonSwitch: React.FunctionComponent<LemonSwitchProps & React.RefAt
conditionalProps['aria-label'] = ariaLabel
}
// Handle access control
if (accessControl) {
const { userAccessLevel, minAccessLevel, resourceType } = accessControl
const accessControlDisabledReason = getAccessControlDisabledReason(
resourceType,
userAccessLevel,
minAccessLevel
)
if (accessControlDisabledReason) {
disabled = true
if (!disabledReason) {
disabledReason = accessControlDisabledReason
}
}
}
let tooltipContent: JSX.Element | null = null
if (disabledReason) {
disabled = true // Support `disabledReason` while maintaining compatibility with `disabled`

View File

@@ -1,6 +1,7 @@
import { useActions, useValues } from 'kea'
import { BindLogic } from 'kea'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonModal } from 'lib/lemon-ui/LemonModal'
import { getAppContext } from 'lib/utils/getAppContext'
@@ -32,19 +33,21 @@ export function AddInsightToDashboardModal(): JSX.Element {
>
Cancel
</LemonButton>
<LemonButton
type="primary"
data-attr="dashboard-add-new-insight"
to={urls.insightNew({ dashboardId: dashboard?.id })}
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight]
}
>
New insight
</LemonButton>
<LemonButton
type="primary"
data-attr="dashboard-add-new-insight"
to={urls.insightNew({ dashboardId: dashboard?.id })}
>
New insight
</LemonButton>
</AccessControlAction>
</>
}
>

View File

@@ -244,43 +244,43 @@ export function DashboardHeader(): JSX.Element | null {
</>
)}
{dashboard ? (
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: dashboard.user_access_level,
}}
onClick={showAddInsightToDashboardModal}
type="primary"
data-attr="dashboard-add-graph-header"
sideAction={{
dropdown: {
placement: 'bottom-end',
overlay: (
<>
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: dashboard.user_access_level,
}}
fullWidth
onClick={() => {
push(urls.dashboardTextTile(dashboard.id, 'new'))
}}
data-attr="add-text-tile-to-dashboard"
>
Add text card
</LemonButton>
</>
),
},
disabled: false,
'data-attr': 'dashboard-add-dropdown',
}}
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={dashboard.user_access_level}
>
Add insight
</LemonButton>
<LemonButton
onClick={showAddInsightToDashboardModal}
type="primary"
data-attr="dashboard-add-graph-header"
sideAction={{
dropdown: {
placement: 'bottom-end',
overlay: (
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={dashboard.user_access_level}
>
<LemonButton
fullWidth
onClick={() => {
push(urls.dashboardTextTile(dashboard.id, 'new'))
}}
data-attr="add-text-tile-to-dashboard"
>
Add text card
</LemonButton>
</AccessControlAction>
),
},
disabled: false,
'data-attr': 'dashboard-add-dropdown',
}}
>
Add insight
</LemonButton>
</AccessControlAction>
) : null}
</>
)

View File

@@ -2,6 +2,7 @@ import { useActions, useValues } from 'kea'
import { LemonButton } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { PageHeader } from 'lib/components/PageHeader'
import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs'
import { getAppContext } from 'lib/utils/getAppContext'
@@ -54,21 +55,17 @@ export function Dashboards(): JSX.Element {
<DeleteDashboardModal />
<PageHeader
buttons={
<LemonButton
data-attr="new-dashboard"
onClick={() => {
showNewDashboardModal()
}}
type="primary"
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.Dashboard],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.Dashboard]
}
>
New dashboard
</LemonButton>
<LemonButton data-attr="new-dashboard" onClick={showNewDashboardModal} type="primary">
New dashboard
</LemonButton>
</AccessControlAction>
}
/>
<SceneTitleSection

View File

@@ -3,6 +3,7 @@ import { useActions, useValues } from 'kea'
import { IconHome, IconLock, IconPin, IconPinFilled, IconShare } from '@posthog/icons'
import { LemonInput } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { MemberSelect } from 'lib/components/MemberSelect'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { DashboardPrivilegeLevel } from 'lib/constants'
@@ -163,24 +164,25 @@ export function DashboardsTable({
View
</LemonButton>
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: user_access_level,
}}
to={urls.dashboard(id)}
onClick={() => {
dashboardLogic({ id }).mount()
dashboardLogic({ id }).actions.setDashboardMode(
DashboardMode.Edit,
DashboardEventSource.DashboardsList
)
}}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={user_access_level}
>
Edit
</LemonButton>
<LemonButton
to={urls.dashboard(id)}
onClick={() => {
dashboardLogic({ id }).mount()
dashboardLogic({ id }).actions.setDashboardMode(
DashboardMode.Edit,
DashboardEventSource.DashboardsList
)
}}
fullWidth
>
Edit
</LemonButton>
</AccessControlAction>
<LemonButton
onClick={() => {
@@ -203,20 +205,19 @@ export function DashboardsTable({
<LemonDivider />
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Dashboard,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: user_access_level,
}}
onClick={() => {
showDeleteDashboardModal(id)
}}
fullWidth
status="danger"
<AccessControlAction
resourceType={AccessControlResourceType.Dashboard}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={user_access_level}
>
Delete dashboard
</LemonButton>
<LemonButton
onClick={() => showDeleteDashboardModal(id)}
fullWidth
status="danger"
>
Delete dashboard
</LemonButton>
</AccessControlAction>
</>
}
/>

View File

@@ -21,6 +21,7 @@ import {
} from '@posthog/icons'
import { LemonDialog, LemonSegmentedButton, LemonSkeleton, LemonSwitch, Tooltip } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { AccessDenied } from 'lib/components/AccessDenied'
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
import { CopyToClipboardInline } from 'lib/components/CopyToClipboard'
@@ -630,38 +631,39 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
</LemonButton>
)}
<LemonDivider />
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
data-attr={
featureFlag.deleted
? 'restore-feature-flag'
: 'delete-feature-flag'
}
status="danger"
fullWidth
onClick={() => {
featureFlag.deleted
? restoreFeatureFlag(featureFlag)
: deleteFeatureFlag(featureFlag)
}}
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: (featureFlag.features?.length || 0) > 0
? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
: (featureFlag.experiment_set?.length || 0) > 0
? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
: (featureFlag.surveys?.length || 0) > 0
? 'This feature flag is linked to a survey. Delete the survey to delete this flag'
: null
}
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
{featureFlag.deleted ? 'Restore' : 'Delete'} feature flag
</LemonButton>
<LemonButton
data-attr={
featureFlag.deleted
? 'restore-feature-flag'
: 'delete-feature-flag'
}
status="danger"
fullWidth
onClick={() => {
featureFlag.deleted
? restoreFeatureFlag(featureFlag)
: deleteFeatureFlag(featureFlag)
}}
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: (featureFlag.features?.length || 0) > 0
? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
: (featureFlag.experiment_set?.length || 0) > 0
? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
: (featureFlag.surveys?.length || 0) > 0
? 'This feature flag is linked to a survey. Delete the survey to delete this flag'
: null
}
>
{featureFlag.deleted ? 'Restore' : 'Delete'} feature flag
</LemonButton>
</AccessControlAction>
</>
}
/>
@@ -676,27 +678,28 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
type="secondary"
/>
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
data-attr="edit-feature-flag"
type="secondary"
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: featureFlag.deleted
? 'This feature flag has been deleted. Restore it to edit.'
: null
}
onClick={() => {
editFeatureFlag(true)
}}
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
Edit
</LemonButton>
<LemonButton
data-attr="edit-feature-flag"
type="secondary"
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: featureFlag.deleted
? 'This feature flag has been deleted. Restore it to edit.'
: null
}
onClick={() => {
editFeatureFlag(true)
}}
>
Edit
</LemonButton>
</AccessControlAction>
</div>
</>
}
@@ -949,7 +952,11 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
</LemonTag>
) : (
<div className="flex gap-2">
<>
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
<LemonSwitch
onChange={(newValue) => {
LemonDialog.open({
@@ -984,16 +991,12 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
: null
}
checked={featureFlag.active}
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
/>
{!featureFlag.is_remote_configuration && (
<FeatureFlagStatusIndicator flagStatus={flagStatus} />
)}
</>
</AccessControlAction>
{!featureFlag.is_remote_configuration && (
<FeatureFlagStatusIndicator flagStatus={flagStatus} />
)}
</div>
)}
</div>

View File

@@ -4,6 +4,7 @@ import { router } from 'kea-router'
import { IconLock } from '@posthog/icons'
import { LemonDialog, LemonTag, lemonToast } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { PageHeader } from 'lib/components/PageHeader'
@@ -103,65 +104,71 @@ function FeatureFlagRowActions({ featureFlag }: { featureFlag: FeatureFlagType }
Copy feature flag key
</LemonButton>
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
data-attr={`feature-flag-${featureFlag.key}-switch`}
onClick={() => {
const newValue = !featureFlag.active
LemonDialog.open({
title: `${newValue === true ? 'Enable' : 'Disable'} this flag?`,
description: `This flag will be immediately ${
newValue === true ? 'rolled out to' : 'rolled back from'
} the users matching the release conditions.`,
primaryButton: {
children: 'Confirm',
type: 'primary',
onClick: () => {
featureFlag.id
? updateFeatureFlag({
id: featureFlag.id,
payload: { active: newValue },
})
: null
},
size: 'small',
},
secondaryButton: {
children: 'Cancel',
type: 'tertiary',
size: 'small',
},
})
}}
id={`feature-flag-${featureFlag.id}-switch`}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
{featureFlag.active ? 'Disable' : 'Enable'} feature flag
</LemonButton>
<LemonButton
data-attr={`feature-flag-${featureFlag.key}-switch`}
onClick={() => {
const newValue = !featureFlag.active
LemonDialog.open({
title: `${newValue === true ? 'Enable' : 'Disable'} this flag?`,
description: `This flag will be immediately ${
newValue === true ? 'rolled out to' : 'rolled back from'
} the users matching the release conditions.`,
primaryButton: {
children: 'Confirm',
type: 'primary',
onClick: () => {
featureFlag.id
? updateFeatureFlag({
id: featureFlag.id,
payload: { active: newValue },
})
: null
},
size: 'small',
},
secondaryButton: {
children: 'Cancel',
type: 'tertiary',
size: 'small',
},
})
}}
id={`feature-flag-${featureFlag.id}-switch`}
fullWidth
>
{featureFlag.active ? 'Disable' : 'Enable'} feature flag
</LemonButton>
</AccessControlAction>
{featureFlag.id && (
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
fullWidth
disabled={!featureFlag.can_edit}
onClick={() => {
if (featureFlag.id) {
featureFlagLogic({ id: featureFlag.id }).mount()
featureFlagLogic({ id: featureFlag.id }).actions.editFeatureFlag(true)
router.actions.push(urls.featureFlag(featureFlag.id))
}
}}
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
Edit
</LemonButton>
<LemonButton
fullWidth
disabledReason={
!featureFlag.can_edit
? "You don't have permission to edit this feature flag."
: null
}
onClick={() => {
if (featureFlag.id) {
featureFlagLogic({ id: featureFlag.id }).mount()
featureFlagLogic({ id: featureFlag.id }).actions.editFeatureFlag(true)
router.actions.push(urls.featureFlag(featureFlag.id))
}
}}
>
Edit
</LemonButton>
</AccessControlAction>
)}
<LemonButton
@@ -185,37 +192,38 @@ function FeatureFlagRowActions({ featureFlag }: { featureFlag: FeatureFlagType }
<LemonDivider />
{featureFlag.id && (
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: featureFlag.user_access_level,
}}
status="danger"
onClick={() => {
void deleteWithUndo({
endpoint: `projects/${currentProjectId}/feature_flags`,
object: { name: featureFlag.key, id: featureFlag.id },
callback: loadFeatureFlags,
}).catch((e) => {
lemonToast.error(`Failed to delete feature flag: ${e.detail}`)
})
}}
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: (featureFlag.features?.length || 0) > 0
? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
: (featureFlag.experiment_set?.length || 0) > 0
? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
: (featureFlag.surveys?.length || 0) > 0
? 'This feature flag is linked to a survey. Delete the survey to delete this flag'
: null
}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={featureFlag.user_access_level}
>
Delete feature flag
</LemonButton>
<LemonButton
status="danger"
onClick={() => {
void deleteWithUndo({
endpoint: `projects/${currentProjectId}/feature_flags`,
object: { name: featureFlag.key, id: featureFlag.id },
callback: loadFeatureFlags,
}).catch((e) => {
lemonToast.error(`Failed to delete feature flag: ${e.detail}`)
})
}}
disabledReason={
!featureFlag.can_edit
? "You have only 'View' access for this feature flag. To make changes, please contact the flag's creator."
: (featureFlag.features?.length || 0) > 0
? 'This feature flag is in use with an early access feature. Delete the early access feature to delete this flag'
: (featureFlag.experiment_set?.length || 0) > 0
? 'This feature flag is linked to an experiment. Delete the experiment to delete this flag'
: (featureFlag.surveys?.length || 0) > 0
? 'This feature flag is linked to a survey. Delete the survey to delete this flag'
: null
}
fullWidth
>
Delete feature flag
</LemonButton>
</AccessControlAction>
)}
</>
}
@@ -457,19 +465,17 @@ export function FeatureFlags(): JSX.Element {
<SceneContent className="feature_flags">
<PageHeader
buttons={
<LemonButton
type="primary"
to={urls.featureFlag('new')}
data-attr="new-feature-flag"
accessControl={{
resourceType: AccessControlResourceType.FeatureFlag,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.FeatureFlag],
}}
<AccessControlAction
resourceType={AccessControlResourceType.FeatureFlag}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.FeatureFlag]
}
>
New feature flag
</LemonButton>
<LemonButton type="primary" to={urls.featureFlag('new')} data-attr="new-feature-flag">
New feature flag
</LemonButton>
</AccessControlAction>
}
/>
<SceneTitleSection

View File

@@ -17,6 +17,7 @@ import {
} from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { supportLogic } from 'lib/components/Support/supportLogic'
import { BuilderHog3 } from 'lib/components/hedgehogs'
import { FEATURE_FLAGS } from 'lib/constants'
@@ -177,6 +178,7 @@ export const LOADING_MESSAGES = [
'Polishing graphs with tiny hedgehog paws…',
'Rolling through data like a spiky ball of insights…',
'Gathering nuts and numbers from the data forest…',
// eslint-disable-next-line react/jsx-key
<>
Reticulating <s>splines</s> spines
</>,
@@ -751,20 +753,22 @@ export function SavedInsightsEmptyState({
{filters.tab !== SavedInsightsTabs.Favorites && (
<div className="flex justify-center">
<Link to={urls.insightNew()}>
<LemonButton
type="primary"
data-attr="add-insight-button-empty-state"
icon={<IconPlusSmall />}
className="add-insight-button"
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight]
}
>
New insight
</LemonButton>
<LemonButton
type="primary"
data-attr="add-insight-button-empty-state"
icon={<IconPlusSmall />}
className="add-insight-button"
>
New insight
</LemonButton>
</AccessControlAction>
</Link>
</div>
)}

View File

@@ -218,33 +218,36 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In
{insightMode !== ItemMode.Edit ? (
canEditInsight && (
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: insight.user_access_level,
}}
type="primary"
icon={dashboardOverridesExist ? <IconWarning /> : undefined}
tooltip={
dashboardOverridesExist
? `This insight is being viewed with dashboard ${overrideType}. These will be discarded on edit.`
: undefined
}
tooltipPlacement="bottom"
onClick={() => {
if (isDataVisualizationNode(query) && insight.short_id) {
router.actions.push(urls.sqlEditor(undefined, undefined, insight.short_id))
} else if (insight.short_id) {
push(urls.insightEdit(insight.short_id))
} else {
setInsightMode(ItemMode.Edit, null)
}
}}
data-attr="insight-edit-button"
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={insight.user_access_level}
>
Edit
</LemonButton>
<LemonButton
type="primary"
icon={dashboardOverridesExist ? <IconWarning /> : undefined}
tooltip={
dashboardOverridesExist
? `This insight is being viewed with dashboard ${overrideType}. These will be discarded on edit.`
: undefined
}
tooltipPlacement="bottom"
onClick={() => {
if (isDataVisualizationNode(query) && insight.short_id) {
router.actions.push(
urls.sqlEditor(undefined, undefined, insight.short_id)
)
} else if (insight.short_id) {
push(urls.insightEdit(insight.short_id))
} else {
setInsightMode(ItemMode.Edit, null)
}
}}
data-attr="insight-edit-button"
>
Edit
</LemonButton>
</AccessControlAction>
)
) : (
<InsightSaveButton

View File

@@ -4,6 +4,7 @@ import { ReactChild, ReactElement, useEffect } from 'react'
import { IconNotebook, IconPlus } from '@posthog/icons'
import { LemonDivider, LemonDropdown, ProfilePicture } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { dayjs } from 'lib/dayjs'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput'
@@ -131,19 +132,21 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element {
onChange={(s) => setSearchQuery(s)}
fullWidth
/>
<LemonButton
data-attr="notebooks-select-button-create"
fullWidth
icon={<IconPlus />}
onClick={openNewNotebook}
accessControl={{
resourceType: AccessControlResourceType.Notebook,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.Notebook],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Notebook}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.Notebook]}
>
New notebook
</LemonButton>
<LemonButton
data-attr="notebooks-select-button-create"
fullWidth
icon={<IconPlus />}
onClick={openNewNotebook}
>
New notebook
</LemonButton>
</AccessControlAction>
<LemonButton
fullWidth
onClick={() => {

View File

@@ -5,6 +5,7 @@ import { router } from 'kea-router'
import { IconEllipsis } from '@posthog/icons'
import { LemonButton, LemonMenu, Tooltip, lemonToast } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { PageHeader } from 'lib/components/PageHeader'
import { base64Encode } from 'lib/utils'
import { getTextFromFile, selectFiles } from 'lib/utils/file-utils'
@@ -68,19 +69,17 @@ export function NotebooksScene(): JSX.Element {
New canvas
</LemonButton>
</Tooltip>
<LemonButton
data-attr="new-notebook"
to={urls.notebook('new')}
type="primary"
accessControl={{
resourceType: AccessControlResourceType.Notebook,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.Notebook],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Notebook}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.Notebook]
}
>
New notebook
</LemonButton>
<LemonButton data-attr="new-notebook" to={urls.notebook('new')} type="primary">
New notebook
</LemonButton>
</AccessControlAction>
</>
}
/>

View File

@@ -29,6 +29,7 @@ import {
} from '@posthog/icons'
import { LemonSelectOptions } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog'
import { Alerts } from 'lib/components/Alerts/views/Alerts'
import { InsightCard } from 'lib/components/Cards/InsightCard'
@@ -564,29 +565,30 @@ export function InsightIcon({
export function NewInsightButton({ dataAttr }: NewInsightButtonProps): JSX.Element {
return (
<LemonButton
type="primary"
to={urls.insightNew()}
sideAction={{
dropdown: {
placement: 'bottom-end',
className: 'new-insight-overlay',
actionable: true,
overlay: <OverlayForNewInsightMenu dataAttr={dataAttr} />,
},
'data-attr': 'saved-insights-new-insight-dropdown',
}}
data-attr="saved-insights-new-insight-button"
size="small"
icon={<IconPlusSmall />}
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight],
}}
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.Insight]}
>
New
</LemonButton>
<LemonButton
type="primary"
to={urls.insightNew()}
sideAction={{
dropdown: {
placement: 'bottom-end',
className: 'new-insight-overlay',
actionable: true,
overlay: <OverlayForNewInsightMenu dataAttr={dataAttr} />,
},
'data-attr': 'saved-insights-new-insight-dropdown',
}}
data-attr="saved-insights-new-insight-button"
size="small"
icon={<IconPlusSmall />}
>
New
</LemonButton>
</AccessControlAction>
)
}
@@ -664,27 +666,28 @@ export function SavedInsights(): JSX.Element {
<>
{name || <i>{summarizeInsight(insight.query)}</i>}
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: insight.user_access_level,
}}
className="ml-1"
size="xsmall"
onClick={(e) => {
e.preventDefault()
updateFavoritedInsight(insight, !insight.favorited)
}}
icon={
insight.favorited ? (
<IconStarFilled className="text-warning" />
) : (
<IconStar className="text-secondary" />
)
}
tooltip={`${insight.favorited ? 'Remove from' : 'Add to'} favorite insights`}
/>
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={insight.user_access_level}
>
<LemonButton
className="ml-1"
size="xsmall"
onClick={(e) => {
e.preventDefault()
updateFavoritedInsight(insight, !insight.favorited)
}}
icon={
insight.favorited ? (
<IconStarFilled className="text-warning" />
) : (
<IconStar className="text-secondary" />
)
}
tooltip={`${insight.favorited ? 'Remove from' : 'Add to'} favorite insights`}
/>
</AccessControlAction>
</>
}
description={insight.description}
@@ -737,30 +740,29 @@ export function SavedInsights(): JSX.Element {
<LemonDivider />
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: insight.user_access_level,
}}
to={urls.insightEdit(insight.short_id)}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={insight.user_access_level}
>
Edit
</LemonButton>
<LemonButton to={urls.insightEdit(insight.short_id)} fullWidth>
Edit
</LemonButton>
</AccessControlAction>
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: insight.user_access_level,
}}
onClick={() => renameInsight(insight)}
data-attr={`insight-item-${insight.short_id}-dropdown-rename`}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={insight.user_access_level}
>
Rename
</LemonButton>
<LemonButton
onClick={() => renameInsight(insight)}
data-attr={`insight-item-${insight.short_id}-dropdown-rename`}
fullWidth
>
Rename
</LemonButton>
</AccessControlAction>
<LemonButton
onClick={() => duplicateInsight(insight)}
@@ -772,25 +774,26 @@ export function SavedInsights(): JSX.Element {
<LemonDivider />
<LemonButton
accessControl={{
resourceType: AccessControlResourceType.Insight,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel: insight.user_access_level,
}}
status="danger"
onClick={() =>
void deleteInsightWithUndo({
object: insight,
endpoint: `projects/${currentProjectId}/insights`,
callback: loadInsights,
})
}
data-attr={`insight-item-${insight.short_id}-dropdown-remove`}
fullWidth
<AccessControlAction
resourceType={AccessControlResourceType.Insight}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={insight.user_access_level}
>
Delete insight
</LemonButton>
<LemonButton
status="danger"
onClick={() =>
void deleteInsightWithUndo({
object: insight,
endpoint: `projects/${currentProjectId}/insights`,
callback: loadInsights,
})
}
data-attr={`insight-item-${insight.short_id}-dropdown-remove`}
fullWidth
>
Delete insight
</LemonButton>
</AccessControlAction>
</>
}
/>

View File

@@ -4,6 +4,7 @@ import { router } from 'kea-router'
import { IconEllipsis, IconGear, IconOpenSidebar } from '@posthog/icons'
import { LemonBadge, LemonButton, LemonMenu } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import {
AuthorizedUrlListType,
authorizedUrlListLogic,
@@ -80,22 +81,24 @@ function Header(): JSX.Element {
)}
{tab === ReplayTabs.Playlists && (
<LemonButton
type="primary"
onClick={(e) => newPlaylistHandler.onEvent?.(e)}
data-attr="save-recordings-playlist-button"
loading={newPlaylistHandler.loading}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
]
}
>
New collection
</LemonButton>
<LemonButton
type="primary"
onClick={(e) => newPlaylistHandler.onEvent?.(e)}
data-attr="save-recordings-playlist-button"
loading={newPlaylistHandler.loading}
>
New collection
</LemonButton>
</AccessControlAction>
)}
</>
}

View File

@@ -6,6 +6,7 @@ import { useState } from 'react'
import { IconArrowRight, IconClock, IconEye, IconFilter, IconHide, IconPlus, IconRevert, IconX } from '@posthog/icons'
import { LemonBadge, LemonButton, LemonInput, LemonModal, LemonTab, LemonTabs, Popover } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters'
@@ -474,23 +475,27 @@ export const RecordingsUniversalFiltersEmbed = ({
Update "{appliedSavedFilter.name || 'Unnamed'}"
</LemonButton>
) : (
<LemonButton
type="secondary"
size="small"
onClick={() => setIsSaveFiltersModalOpen(true)}
disabledReason={(totalFiltersCount ?? 0) === 0 ? 'No filters applied' : undefined}
tooltip="Save filters for later"
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
]
}
>
Add to "Saved filters"
</LemonButton>
<LemonButton
type="secondary"
size="small"
onClick={() => setIsSaveFiltersModalOpen(true)}
disabledReason={
(totalFiltersCount ?? 0) === 0 ? 'No filters applied' : undefined
}
tooltip="Save filters for later"
>
Add to "Saved filters"
</LemonButton>
</AccessControlAction>
)}
</div>
<LemonButton

View File

@@ -4,6 +4,7 @@ import { combineUrl } from 'kea-router'
import { IconShare, IconTrash } from '@posthog/icons'
import { LemonButton, LemonInput, LemonTable, LemonTableColumn, LemonTableColumns } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { copyToClipboard } from 'lib/utils/copyToClipboard'
import { getAppContext } from 'lib/utils/getAppContext'
@@ -100,25 +101,27 @@ export function SavedFilters({
width: 40,
render: function Render(_, playlist) {
return (
<LemonButton
status="danger"
onClick={() => {
deletePlaylist(playlist)
if (savedFilters.results?.length === 1) {
setActiveFilterTab('filters')
}
}}
title="Delete saved filter"
tooltip="Delete saved filter"
icon={<IconTrash />}
size="small"
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
<LemonButton
status="danger"
onClick={() => {
deletePlaylist(playlist)
if (savedFilters.results?.length === 1) {
setActiveFilterTab('filters')
}
}}
title="Delete saved filter"
tooltip="Delete saved filter"
icon={<IconTrash />}
size="small"
/>
</AccessControlAction>
)
},
},

View File

@@ -54,31 +54,27 @@ export function CommentOnRecordingButton({ className }: { className?: string }):
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
{({ disabled, disabledReason }) => (
<LemonButton
size="xsmall"
onClick={(e) => {
e.stopPropagation()
setIsCommenting(!isCommenting)
}}
tooltip={
isCommenting ? (
<>
Stop commenting <KeyboardShortcut c />
</>
) : (
<>
Comment on this recording <KeyboardShortcut c />
</>
)
}
data-attr={isCommenting ? 'stop-annotating-recording' : 'annotate-recording'}
active={isCommenting}
icon={<IconComment className={cn('text-lg', className)} />}
disabled={disabled}
disabledReason={disabledReason}
/>
)}
<LemonButton
size="xsmall"
onClick={(e) => {
e.stopPropagation()
setIsCommenting(!isCommenting)
}}
tooltip={
isCommenting ? (
<>
Stop commenting <KeyboardShortcut c />
</>
) : (
<>
Comment on this recording <KeyboardShortcut c />
</>
)
}
data-attr={isCommenting ? 'stop-annotating-recording' : 'annotate-recording'}
active={isCommenting}
icon={<IconComment className={cn('text-lg', className)} />}
/>
</AccessControlAction>
)
}

View File

@@ -64,17 +64,14 @@ function PinToPlaylistButton(): JSX.Element {
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
{({ disabledReason }) => (
<PlaylistPopoverButton
tooltip={tooltip}
setPinnedInCurrentPlaylist={logicProps.setPinned}
icon={logicProps.pinned ? <IconMinusSmall /> : <IconPlusSmall />}
size="xsmall"
disabledReason={disabledReason}
>
{description}
</PlaylistPopoverButton>
)}
<PlaylistPopoverButton
tooltip={tooltip}
setPinnedInCurrentPlaylist={logicProps.setPinned}
icon={logicProps.pinned ? <IconMinusSmall /> : <IconPlusSmall />}
size="xsmall"
>
{description}
</PlaylistPopoverButton>
</AccessControlAction>
)
}

View File

@@ -4,6 +4,7 @@ import { useActions, useValues } from 'kea'
import { IconCalendar, IconPin, IconPinFilled } from '@posthog/icons'
import { LemonBadge, LemonButton, LemonDivider, LemonInput, LemonTable, Link, Tooltip } from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { MemberSelect } from 'lib/components/MemberSelect'
import { TZLabel } from 'lib/components/TZLabel'
@@ -161,17 +162,19 @@ export function SavedSessionRecordingPlaylists({ tab }: SavedSessionRecordingPla
dataIndex: 'pinned',
render: function Render(pinned, { short_id }) {
return (
<LemonButton
size="small"
onClick={() => updatePlaylist(short_id, { pinned: !pinned })}
icon={pinned ? <IconPinFilled /> : <IconPin />}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
<LemonButton
size="small"
onClick={() => updatePlaylist(short_id, { pinned: !pinned })}
icon={pinned ? <IconPinFilled /> : <IconPin />}
/>
</AccessControlAction>
)
},
},
@@ -210,40 +213,45 @@ export function SavedSessionRecordingPlaylists({ tab }: SavedSessionRecordingPla
<More
overlay={
<>
<LemonButton
onClick={() => duplicatePlaylist(playlist)}
fullWidth
data-attr="duplicate-playlist"
loading={playlistsLoading}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
]
}
>
Duplicate
</LemonButton>
<LemonButton
onClick={() => duplicatePlaylist(playlist)}
fullWidth
data-attr="duplicate-playlist"
loading={playlistsLoading}
>
Duplicate
</LemonButton>
</AccessControlAction>
<LemonDivider />
<LemonButton
status="danger"
onClick={() => deletePlaylist(playlist)}
fullWidth
loading={playlistsLoading}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
]
}
>
Delete collection
</LemonButton>
<LemonButton
status="danger"
onClick={() => deletePlaylist(playlist)}
fullWidth
loading={playlistsLoading}
>
Delete collection
</LemonButton>
</AccessControlAction>
</>
}
/>

View File

@@ -2,6 +2,7 @@ import { useValues } from 'kea'
import { IconPlus } from '@posthog/icons'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { getAppContext } from 'lib/utils/getAppContext'
@@ -21,20 +22,22 @@ export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element {
<div className="max-w-248 mt-12 flex flex-col items-center">
<h2 className="text-xl">There are no collections that match these filters</h2>
<p className="text-secondary">Once you create a collection, it will show up here.</p>
<LemonButton
type="primary"
data-attr="add-session-playlist-button-empty-state"
icon={<IconPlus />}
onClick={() => void createPlaylist({ type: 'collection' }, true)}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
New collection
</LemonButton>
<LemonButton
type="primary"
data-attr="add-session-playlist-button-empty-state"
icon={<IconPlus />}
onClick={() => void createPlaylist({ type: 'collection' }, true)}
>
New collection
</LemonButton>
</AccessControlAction>
</div>
</div>
)

View File

@@ -110,22 +110,24 @@ function LinkedFlagSelector(): JSX.Element | null {
)}
</AccessControlAction>
{currentTeam?.session_recording_linked_flag && (
<LemonButton
className="ml-2"
icon={<IconCancel />}
size="small"
type="secondary"
onClick={() => updateCurrentTeam({ session_recording_linked_flag: null })}
title="Clear selected flag"
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[
AccessControlResourceType.SessionRecording
]
}
>
<LemonButton
className="ml-2"
icon={<IconCancel />}
size="small"
type="secondary"
onClick={() => updateCurrentTeam({ session_recording_linked_flag: null })}
title="Clear selected flag"
/>
</AccessControlAction>
)}
</div>
</div>
@@ -285,47 +287,47 @@ function UrlConfigRow({
<span>{trigger.url}</span>
</span>
<div className="Actions flex deprecated-space-x-1 shrink-0">
<LemonButton
icon={<IconPencil />}
onClick={() => onEdit(index)}
tooltip="Edit"
center
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
Edit
</LemonButton>
<LemonButton
icon={<IconTrash />}
tooltip={`Remove URL ${type}`}
center
onClick={() => {
LemonDialog.open({
title: <>Remove URL {type}</>,
description: `Are you sure you want to remove this URL ${type}?`,
primaryButton: {
status: 'danger',
children: 'Remove',
onClick: () => onRemove(index),
},
secondaryButton: {
children: 'Cancel',
},
})
}}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
<LemonButton icon={<IconPencil />} onClick={() => onEdit(index)} tooltip="Edit" center>
Edit
</LemonButton>
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
Remove
</LemonButton>
<LemonButton
icon={<IconTrash />}
tooltip={`Remove URL ${type}`}
center
onClick={() => {
LemonDialog.open({
title: <>Remove URL {type}</>,
description: `Are you sure you want to remove this URL ${type}?`,
primaryButton: {
status: 'danger',
children: 'Remove',
onClick: () => onRemove(index),
},
secondaryButton: {
children: 'Cancel',
},
})
}}
>
Remove
</LemonButton>
</AccessControlAction>
</div>
</div>
)
@@ -353,20 +355,22 @@ function UrlConfigSection({
<div className="flex flex-col deprecated-space-y-2 mt-4">
<div className="flex items-center gap-2 justify-between">
<LemonLabel className="text-base">{title}</LemonLabel>
<LemonButton
onClick={props.onAdd}
type="secondary"
icon={<IconPlus />}
data-attr={`session-replay-add-url-${type}`}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
Add
</LemonButton>
<LemonButton
onClick={props.onAdd}
type="secondary"
icon={<IconPlus />}
data-attr={`session-replay-add-url-${type}`}
>
Add
</LemonButton>
</AccessControlAction>
</div>
<SupportedPlatforms
android={false}
@@ -469,21 +473,21 @@ function EventSelectButton(): JSX.Element {
/>
}
>
<LemonButton
size="small"
type="secondary"
icon={<IconPlus />}
sideIcon={null}
onClick={() => setOpen(!open)}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
Add event
</LemonButton>
<LemonButton
size="small"
type="secondary"
icon={<IconPlus />}
sideIcon={null}
onClick={() => setOpen(!open)}
>
Add event
</LemonButton>
</AccessControlAction>
</Popover>
)
}
@@ -508,8 +512,9 @@ function EventTriggerOptions(): JSX.Element | null {
reactNative={false}
/>
<div className="flex gap-2">
{eventTriggerConfig?.map((evnt) => (
{eventTriggerConfig?.map((trigger) => (
<AccessControlAction
key={trigger}
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
@@ -518,16 +523,15 @@ function EventTriggerOptions(): JSX.Element | null {
>
{({ disabledReason }) => (
<LemonSnack
key={evnt}
onClose={
!disabledReason
? () => {
updateEventTriggerConfig(eventTriggerConfig?.filter((e) => e !== evnt))
updateEventTriggerConfig(eventTriggerConfig?.filter((e) => e !== trigger))
}
: undefined
}
>
{evnt}
{trigger}
</LemonSnack>
)}
</AccessControlAction>
@@ -547,113 +551,115 @@ function Sampling(): JSX.Element {
<LemonLabel className="text-base">
<TriggerMatchTypeTag /> Sampling
</LemonLabel>
<LemonSelect
onChange={(v) => {
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: '1%',
value: '0.01',
},
{
label: '0% (replay disabled)',
value: '0.00',
},
]}
value={
typeof currentTeam?.session_recording_sample_rate === 'string'
? currentTeam?.session_recording_sample_rate
: '1.00'
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
>
<LemonSelect
onChange={(v) => {
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: '1%',
value: '0.01',
},
{
label: '0% (replay disabled)',
value: '0.00',
},
]}
value={
typeof currentTeam?.session_recording_sample_rate === 'string'
? currentTeam?.session_recording_sample_rate
: '1.00'
}
/>
</AccessControlAction>
</div>
<SupportedPlatforms web={{ version: '1.85.0' }} />
<p>
@@ -674,20 +680,22 @@ function MinimumDurationSetting(): JSX.Element | null {
<>
<div className="flex flex-row justify-between">
<LemonLabel className="text-base">Minimum session duration (seconds)</LemonLabel>
<LemonSelect
dropdownMatchSelectWidth={false}
onChange={(v) => {
updateCurrentTeam({ session_recording_minimum_duration_milliseconds: v })
}}
options={SESSION_REPLAY_MINIMUM_DURATION_OPTIONS}
value={currentTeam?.session_recording_minimum_duration_milliseconds}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
<LemonSelect
dropdownMatchSelectWidth={false}
onChange={(v) => {
updateCurrentTeam({ session_recording_minimum_duration_milliseconds: v })
}}
options={SESSION_REPLAY_MINIMUM_DURATION_OPTIONS}
value={currentTeam?.session_recording_minimum_duration_milliseconds}
/>
</AccessControlAction>
</div>
<SupportedPlatforms web={{ version: '1.85.0' }} />
<p>
@@ -738,46 +746,53 @@ function TriggerMatchChoice(): JSX.Element {
</LemonBanner>
<div className="flex flex-row gap-x-2 items-center">
<div>Start when</div>
<LemonSelect
options={[
{
label: 'all',
value: 'all',
labelInMenu: (
<SelectOption
title="All"
description="Every trigger must match"
value="all"
selectedValue={currentTeam?.session_recording_trigger_match_type_config || 'all'}
/>
),
},
{
label: 'any',
value: 'any',
labelInMenu: (
<SelectOption
title="Any"
description="One or more triggers must match"
value="any"
selectedValue={currentTeam?.session_recording_trigger_match_type_config || 'all'}
/>
),
},
]}
dropdownMatchSelectWidth={false}
data-attr="trigger-match-choice"
onChange={(value) => {
updateCurrentTeam({ session_recording_trigger_match_type_config: value })
}}
value={currentTeam?.session_recording_trigger_match_type_config || 'all'}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
<LemonSelect
options={[
{
label: 'all',
value: 'all',
labelInMenu: (
<SelectOption
title="All"
description="Every trigger must match"
value="all"
selectedValue={
currentTeam?.session_recording_trigger_match_type_config || 'all'
}
/>
),
},
{
label: 'any',
value: 'any',
labelInMenu: (
<SelectOption
title="Any"
description="One or more triggers must match"
value="any"
selectedValue={
currentTeam?.session_recording_trigger_match_type_config || 'all'
}
/>
),
},
]}
dropdownMatchSelectWidth={false}
data-attr="trigger-match-choice"
onChange={(value) => {
updateCurrentTeam({ session_recording_trigger_match_type_config: value })
}}
value={currentTeam?.session_recording_trigger_match_type_config || 'all'}
/>
</AccessControlAction>
<div>triggers below match</div>
</div>
</div>

View File

@@ -15,6 +15,7 @@ import {
Tooltip,
} from '@posthog/lemon-ui'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList'
import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { EventSelect } from 'lib/components/EventSelect/EventSelect'
@@ -161,22 +162,24 @@ function LogCaptureSettings(): JSX.Element {
</Link>{' '}
, where they can be configured directly in code.
</p>
<LemonSwitch
data-attr="opt-in-capture-console-log-switch"
onChange={(checked) => {
updateCurrentTeam({ capture_console_log_opt_in: checked })
}}
label="Capture console logs"
bordered
checked={!!currentTeam?.capture_console_log_opt_in}
disabledReason={!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
<LemonSwitch
data-attr="opt-in-capture-console-log-switch"
onChange={(checked) => {
updateCurrentTeam({ capture_console_log_opt_in: checked })
}}
label="Capture console logs"
bordered
checked={!!currentTeam?.capture_console_log_opt_in}
disabledReason={
!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined
}
/>
</AccessControlAction>
</div>
)
}
@@ -210,34 +213,36 @@ function CanvasCaptureSettings(): JSX.Element | null {
</b>
</p>
<p>Canvas capture is only available for JavaScript Web.</p>
<LemonSwitch
data-attr="opt-in-capture-canvas-switch"
onChange={(checked) => {
updateCurrentTeam({
session_replay_config: {
...currentTeam?.session_replay_config,
record_canvas: checked,
},
})
}}
label={
<div className="deprecated-space-x-1">
<LemonTag type="success">New</LemonTag>
<LemonLabel>Capture canvas elements</LemonLabel>
</div>
}
bordered
checked={
currentTeam?.session_replay_config ? !!currentTeam?.session_replay_config?.record_canvas : false
}
disabledReason={!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
<LemonSwitch
data-attr="opt-in-capture-canvas-switch"
onChange={(checked) => {
updateCurrentTeam({
session_replay_config: {
...currentTeam?.session_replay_config,
record_canvas: checked,
},
})
}}
label={
<div className="deprecated-space-x-1">
<LemonTag type="success">New</LemonTag>
<LemonLabel>Capture canvas elements</LemonLabel>
</div>
}
bordered
checked={
currentTeam?.session_replay_config ? !!currentTeam?.session_replay_config?.record_canvas : false
}
disabledReason={
!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined
}
/>
</AccessControlAction>
</div>
)
}
@@ -286,22 +291,25 @@ export function NetworkCaptureSettings(): JSX.Element {
</Link>{' '}
, where they can be configured directly in code.
</p>
<LemonSwitch
data-attr="opt-in-capture-performance-switch"
onChange={(checked) => {
updateCurrentTeam({ capture_performance_opt_in: checked })
}}
label="Capture network performance"
bordered
checked={!!currentTeam?.capture_performance_opt_in}
disabledReason={!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
<LemonSwitch
data-attr="opt-in-capture-performance-switch"
onChange={(checked) => {
updateCurrentTeam({ capture_performance_opt_in: checked })
}}
label="Capture network performance"
bordered
checked={!!currentTeam?.capture_performance_opt_in}
disabledReason={
!currentTeam?.session_recording_opt_in ? 'Session replay must be enabled' : undefined
}
/>
</AccessControlAction>
<div className="mt-4">
<p>
@@ -322,84 +330,88 @@ export function NetworkCaptureSettings(): JSX.Element {
reactNative={false}
/>
<div className="flex flex-row deprecated-space-x-2">
<LemonSwitch
data-attr="opt-in-capture-network-headers-switch"
onChange={(checked) => {
updateCurrentTeam({
session_recording_network_payload_capture_config: {
...currentTeam?.session_recording_network_payload_capture_config,
recordHeaders: checked,
},
})
}}
label="Capture headers"
bordered
checked={
currentTeam?.session_recording_opt_in
? !!currentTeam?.session_recording_network_payload_capture_config?.recordHeaders
: false
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
disabledReason={
!currentTeam?.session_recording_opt_in || !currentTeam?.capture_performance_opt_in
? 'session and network performance capture must be enabled'
: undefined
}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<LemonSwitch
data-attr="opt-in-capture-network-body-switch"
onChange={(checked) => {
if (checked) {
LemonDialog.open({
maxWidth: '650px',
title: 'Network body capture',
description: <PayloadWarning />,
primaryButton: {
'data-attr': 'network-payload-capture-accept-warning-and-enable',
children: 'Enable body capture',
onClick: () => {
updateCurrentTeam({
session_recording_network_payload_capture_config: {
...currentTeam?.session_recording_network_payload_capture_config,
recordBody: true,
},
})
},
},
})
} else {
>
<LemonSwitch
data-attr="opt-in-capture-network-headers-switch"
onChange={(checked) => {
updateCurrentTeam({
session_recording_network_payload_capture_config: {
...currentTeam?.session_recording_network_payload_capture_config,
recordBody: false,
recordHeaders: checked,
},
})
}}
label="Capture headers"
bordered
checked={
currentTeam?.session_recording_opt_in
? !!currentTeam?.session_recording_network_payload_capture_config?.recordHeaders
: false
}
}}
label="Capture body"
bordered
checked={
currentTeam?.session_recording_opt_in
? !!currentTeam?.session_recording_network_payload_capture_config?.recordBody
: false
disabledReason={
!currentTeam?.session_recording_opt_in || !currentTeam?.capture_performance_opt_in
? 'session and network performance capture must be enabled'
: undefined
}
/>
</AccessControlAction>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
disabledReason={
!currentTeam?.session_recording_opt_in || !currentTeam?.capture_performance_opt_in
? 'session and network performance capture must be enabled'
: undefined
}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
>
<LemonSwitch
data-attr="opt-in-capture-network-body-switch"
onChange={(checked) => {
if (checked) {
LemonDialog.open({
maxWidth: '650px',
title: 'Network body capture',
description: <PayloadWarning />,
primaryButton: {
'data-attr': 'network-payload-capture-accept-warning-and-enable',
children: 'Enable body capture',
onClick: () => {
updateCurrentTeam({
session_recording_network_payload_capture_config: {
...currentTeam?.session_recording_network_payload_capture_config,
recordBody: true,
},
})
},
},
})
} else {
updateCurrentTeam({
session_recording_network_payload_capture_config: {
...currentTeam?.session_recording_network_payload_capture_config,
recordBody: false,
},
})
}
}}
label="Capture body"
bordered
checked={
currentTeam?.session_recording_opt_in
? !!currentTeam?.session_recording_network_payload_capture_config?.recordBody
: false
}
disabledReason={
!currentTeam?.session_recording_opt_in || !currentTeam?.capture_performance_opt_in
? 'session and network performance capture must be enabled'
: undefined
}
/>
</AccessControlAction>
</div>
</div>
</>
@@ -408,7 +420,7 @@ export function NetworkCaptureSettings(): JSX.Element {
/**
* @deprecated use ReplayTriggers instead, this is only presented to teams that have these settings set
* @constructor
* @class
*/
export function ReplayAuthorizedDomains(): JSX.Element {
return (
@@ -621,21 +633,21 @@ export function ReplayMaskingSettings(): JSX.Element {
</Link>
</p>
<p>If you specify this in code, it will take precedence over the setting here.</p>
<LemonSelect
value={maskingLevel}
onChange={(val) => val && handleMaskingChange(val)}
options={[
{ value: 'total-privacy', label: 'Total privacy (mask all text/images)' },
{ value: 'normal', label: 'Normal (mask inputs but not text/images)' },
{ value: 'free-love', label: 'Free love (mask only passwords)' },
]}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]}
>
<LemonSelect
value={maskingLevel}
onChange={(val) => val && handleMaskingChange(val)}
options={[
{ value: 'total-privacy', label: 'Total privacy (mask all text/images)' },
{ value: 'normal', label: 'Normal (mask inputs but not text/images)' },
{ value: 'free-love', label: 'Free love (mask only passwords)' },
]}
/>
</AccessControlAction>
</div>
)
}
@@ -677,21 +689,24 @@ export function ReplayGeneral(): JSX.Element {
Check out our docs
</Link>
</p>
<LemonSwitch
data-attr="opt-in-session-recording-switch"
onChange={(checked) => {
handleOptInChange(checked)
}}
label="Record user sessions"
bordered
checked={!!currentTeam?.session_recording_opt_in}
accessControl={{
resourceType: AccessControlResourceType.SessionRecording,
minAccessLevel: AccessControlLevel.Editor,
userAccessLevel:
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording],
}}
/>
<AccessControlAction
resourceType={AccessControlResourceType.SessionRecording}
minAccessLevel={AccessControlLevel.Editor}
userAccessLevel={
getAppContext()?.resource_access_control?.[AccessControlResourceType.SessionRecording]
}
>
<LemonSwitch
data-attr="opt-in-session-recording-switch"
onChange={(checked) => {
handleOptInChange(checked)
}}
label="Record user sessions"
bordered
checked={!!currentTeam?.session_recording_opt_in}
/>
</AccessControlAction>
{showSurvey && <InternalMultipleChoiceSurvey surveyId={SESSION_RECORDING_OPT_OUT_SURVEY_ID} />}
</div>
<LogCaptureSettings />