diff --git a/cypress/fixtures/api/notebooks/notebook.json b/cypress/fixtures/api/notebooks/notebook.json index d9a9a0b2694d6..a2e4cb7430eea 100644 --- a/cypress/fixtures/api/notebooks/notebook.json +++ b/cypress/fixtures/api/notebooks/notebook.json @@ -60,6 +60,5 @@ "first_name": "Employee 427", "email": "test@posthog.com", "is_email_verified": null - }, - "user_access_level": "editor" + } } diff --git a/cypress/fixtures/api/notebooks/notebooks.json b/cypress/fixtures/api/notebooks/notebooks.json index 7c52f5bfc985d..1cfd9a7850315 100644 --- a/cypress/fixtures/api/notebooks/notebooks.json +++ b/cypress/fixtures/api/notebooks/notebooks.json @@ -65,8 +65,7 @@ "first_name": "Employee 427", "email": "test@posthog.com", "is_email_verified": null - }, - "user_access_level": "editor" + } } ] } diff --git a/frontend/__snapshots__/components-sharing--dashboard-sharing--dark.png b/frontend/__snapshots__/components-sharing--dashboard-sharing--dark.png index 9ab3d98f3577d..e23321530c8ff 100644 Binary files a/frontend/__snapshots__/components-sharing--dashboard-sharing--dark.png and b/frontend/__snapshots__/components-sharing--dashboard-sharing--dark.png differ diff --git a/frontend/__snapshots__/components-sharing--dashboard-sharing--light.png b/frontend/__snapshots__/components-sharing--dashboard-sharing--light.png index e904c3bf6d99a..87fd2d661366e 100644 Binary files a/frontend/__snapshots__/components-sharing--dashboard-sharing--light.png and b/frontend/__snapshots__/components-sharing--dashboard-sharing--light.png differ diff --git a/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--dark.png b/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--dark.png index 8563fab7bb03f..697989b30eb5c 100644 Binary files a/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--dark.png and b/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--dark.png differ diff --git a/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--light.png b/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--light.png index a50de7a9d7eda..37aac937be4dc 100644 Binary files a/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--light.png and b/frontend/__snapshots__/components-sharing--dashboard-sharing-licensed--light.png differ diff --git a/frontend/__snapshots__/components-sharing--insight-sharing--dark.png b/frontend/__snapshots__/components-sharing--insight-sharing--dark.png index 6683dfaec9684..ee46e956f9965 100644 Binary files a/frontend/__snapshots__/components-sharing--insight-sharing--dark.png and b/frontend/__snapshots__/components-sharing--insight-sharing--dark.png differ diff --git a/frontend/__snapshots__/components-sharing--insight-sharing--light.png b/frontend/__snapshots__/components-sharing--insight-sharing--light.png index 4f3142465f5a4..d352a9d91d9af 100644 Binary files a/frontend/__snapshots__/components-sharing--insight-sharing--light.png and b/frontend/__snapshots__/components-sharing--insight-sharing--light.png differ diff --git a/frontend/__snapshots__/components-sharing--insight-sharing-licensed--dark.png b/frontend/__snapshots__/components-sharing--insight-sharing-licensed--dark.png index af3b2f58aceaf..9fc969c540f49 100644 Binary files a/frontend/__snapshots__/components-sharing--insight-sharing-licensed--dark.png and b/frontend/__snapshots__/components-sharing--insight-sharing-licensed--dark.png differ diff --git a/frontend/__snapshots__/components-sharing--insight-sharing-licensed--light.png b/frontend/__snapshots__/components-sharing--insight-sharing-licensed--light.png index 92014379a2bda..8a8d5000bbba0 100644 Binary files a/frontend/__snapshots__/components-sharing--insight-sharing-licensed--light.png and b/frontend/__snapshots__/components-sharing--insight-sharing-licensed--light.png differ diff --git a/frontend/__snapshots__/components-sharing--recording-sharing-licensed--dark.png b/frontend/__snapshots__/components-sharing--recording-sharing-licensed--dark.png index 8b7df18f2525f..391bffcae7f8f 100644 Binary files a/frontend/__snapshots__/components-sharing--recording-sharing-licensed--dark.png and b/frontend/__snapshots__/components-sharing--recording-sharing-licensed--dark.png differ diff --git a/frontend/__snapshots__/components-sharing--recording-sharing-licensed--light.png b/frontend/__snapshots__/components-sharing--recording-sharing-licensed--light.png index 74e95371f39a3..08eb57f620393 100644 Binary files a/frontend/__snapshots__/components-sharing--recording-sharing-licensed--light.png and b/frontend/__snapshots__/components-sharing--recording-sharing-licensed--light.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png index 8f7f4f982a03f..9533045495757 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png index 554d344888b78..e6876dcaf9403 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png index c36d7a90c5649..c299d9caf9d5d 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png index 7897cc41d9912..3a632563f3dd4 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png index 8f7f4f982a03f..9533045495757 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png index 554d344888b78..e6876dcaf9403 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png index 8de0050efb48e..32d397abf284c 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png index d1592c6883fe3..7ae06bd540053 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--dark.png index 13b181094b3d0..d16b8c42a3cd8 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--light.png b/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--light.png index 706089d18de26..44413dfa61cd9 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-web-vitals--light.png differ diff --git a/frontend/src/layout/ErrorProjectUnavailable.tsx b/frontend/src/layout/ErrorProjectUnavailable.tsx index c38571870c723..1888661bb42de 100644 --- a/frontend/src/layout/ErrorProjectUnavailable.tsx +++ b/frontend/src/layout/ErrorProjectUnavailable.tsx @@ -3,7 +3,6 @@ import { useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' import { useEffect, useState } from 'react' import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' -import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' @@ -12,7 +11,6 @@ import { organizationLogic } from '../scenes/organizationLogic' export function ErrorProjectUnavailable(): JSX.Element { const { projectCreationForbiddenReason } = useValues(organizationLogic) const { user } = useValues(userLogic) - const { currentTeam } = useValues(teamLogic) const [options, setOptions] = useState([]) useEffect(() => { @@ -47,8 +45,7 @@ export function ErrorProjectUnavailable(): JSX.Element { {!user?.organization ? ( - ) : (user?.team && !user.organization?.teams.some((team) => team.id === user?.team?.id || user.team)) || - currentTeam?.user_access_level === 'none' ? ( + ) : user?.team && !user.organization?.teams.some((team) => team.id === user?.team?.id) ? ( <>

Project access has been removed

diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index a99679f92f88d..34c18f4fc6ff2 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -1,6 +1,6 @@ import './SidePanel.scss' -import { IconEllipsis, IconFeatures, IconGear, IconInfo, IconLock, IconNotebook, IconSupport } from '@posthog/icons' +import { IconEllipsis, IconFeatures, IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons' import { LemonButton, LemonMenu, LemonMenuItems, LemonModal } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' @@ -16,7 +16,6 @@ import { import { themeLogic } from '~/layout/navigation-3000/themeLogic' import { SidePanelTab } from '~/types' -import { SidePanelAccessControl } from './panels/access_control/SidePanelAccessControl' import { SidePanelActivation, SidePanelActivationIcon } from './panels/activation/SidePanelActivation' import { SidePanelActivity, SidePanelActivityIcon } from './panels/activity/SidePanelActivity' import { SidePanelDiscussion, SidePanelDiscussionIcon } from './panels/discussion/SidePanelDiscussion' @@ -88,11 +87,6 @@ export const SIDE_PANEL_TABS: Record< Content: SidePanelStatus, noModalSupport: true, }, - [SidePanelTab.AccessControl]: { - label: 'Access control', - Icon: IconLock, - Content: SidePanelAccessControl, - }, } const DEFAULT_WIDTH = 512 diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/AccessControlObject.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/AccessControlObject.tsx deleted file mode 100644 index 93e14755e12d5..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/AccessControlObject.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import { IconX } from '@posthog/icons' -import { - LemonBanner, - LemonButton, - LemonDialog, - LemonInputSelect, - LemonSelect, - LemonSelectProps, - LemonTable, -} from '@posthog/lemon-ui' -import { BindLogic, useActions, useAsyncActions, useValues } from 'kea' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' -import { UserSelectItem } from 'lib/components/UserSelectItem' -import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' -import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { ProfileBubbles, ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { capitalizeFirstLetter } from 'lib/utils' -import { useEffect, useState } from 'react' -import { urls } from 'scenes/urls' -import { userLogic } from 'scenes/userLogic' - -import { - AccessControlType, - AccessControlTypeMember, - AccessControlTypeRole, - AvailableFeature, - OrganizationMemberType, -} from '~/types' - -import { accessControlLogic, AccessControlLogicProps } from './accessControlLogic' - -export function AccessControlObject(props: AccessControlLogicProps): JSX.Element | null { - const { canEditAccessControls, humanReadableResource } = useValues(accessControlLogic(props)) - - const suffix = `this ${humanReadableResource}` - - return ( - -

- {canEditAccessControls === false ? ( - - You don't have permission to edit access controls for {suffix}. -
- You must be the creator of it, a Project Admin, or an Organization Admin. -
- ) : null} -

Default access to {suffix}

- - -

Members

- - - - -

Roles

- - - -
- - ) -} - -function AccessControlObjectDefaults(): JSX.Element | null { - const { accessControlDefault, accessControlDefaultOptions, accessControlsLoading, canEditAccessControls } = - useValues(accessControlLogic) - const { updateAccessControlDefault } = useActions(accessControlLogic) - const { guardAvailableFeature } = useValues(upgradeModalLogic) - - return ( - { - guardAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING, () => { - updateAccessControlDefault(newValue) - }) - }} - disabledReason={ - accessControlsLoading ? 'Loading…' : !canEditAccessControls ? 'You cannot edit this' : undefined - } - dropdownMatchSelectWidth={false} - options={accessControlDefaultOptions} - /> - ) -} - -function AccessControlObjectUsers(): JSX.Element | null { - const { user } = useValues(userLogic) - const { membersById, addableMembers, accessControlMembers, accessControlsLoading, availableLevels } = - useValues(accessControlLogic) - const { updateAccessControlMembers } = useAsyncActions(accessControlLogic) - const { guardAvailableFeature } = useValues(upgradeModalLogic) - - if (!user) { - return null - } - - const member = (ac: AccessControlTypeMember): OrganizationMemberType => { - return membersById[ac.organization_member] - } - - // TODO: WHAT A MESS - Fix this to do the index mapping beforehand... - const columns: LemonTableColumns = [ - { - key: 'user_profile_picture', - render: function ProfilePictureRender(_, ac) { - return - }, - width: 32, - }, - { - title: 'Name', - key: 'user_first_name', - render: (_, ac) => ( - - {member(ac)?.user.uuid == user.uuid - ? `${member(ac)?.user.first_name} (you)` - : member(ac)?.user.first_name} - - ), - sorter: (a, b) => member(a)?.user.first_name.localeCompare(member(b)?.user.first_name), - }, - { - title: 'Email', - key: 'user_email', - render: (_, ac) => member(ac)?.user.email, - sorter: (a, b) => member(a)?.user.email.localeCompare(member(b)?.user.email), - }, - { - title: 'Level', - key: 'level', - width: 0, - render: function LevelRender(_, { access_level, organization_member }) { - return ( -
- - void updateAccessControlMembers([{ member: organization_member, level }]) - } - /> -
- ) - }, - }, - { - key: 'remove', - width: 0, - render: (_, { organization_member }) => { - return ( - - void updateAccessControlMembers([{ member: organization_member, level: null }]) - } - /> - ) - }, - }, - ] - - return ( -
- { - if (guardAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING)) { - await updateAccessControlMembers(newValues.map((member) => ({ member, level }))) - } - }} - options={addableMembers.map((member) => ({ - key: member.id, - label: `${member.user.first_name} ${member.user.email}`, - labelComponent: , - }))} - /> - - -
- ) -} - -function AccessControlObjectRoles(): JSX.Element | null { - const { accessControlRoles, accessControlsLoading, addableRoles, rolesById, availableLevels } = - useValues(accessControlLogic) - const { updateAccessControlRoles } = useAsyncActions(accessControlLogic) - const { guardAvailableFeature } = useValues(upgradeModalLogic) - - const columns: LemonTableColumns = [ - { - title: 'Role', - key: 'role', - width: 0, - render: (_, { role }) => ( - - - - ), - }, - { - title: 'Members', - key: 'members', - render: (_, { role }) => { - return ( - ({ - email: member.user.email, - name: member.user.first_name, - title: `${member.user.first_name} <${member.user.email}>`, - })) ?? [] - } - /> - ) - }, - }, - { - title: 'Level', - key: 'level', - width: 0, - render: (_, { access_level, role }) => { - return ( -
- void updateAccessControlRoles([{ role, level }])} - /> -
- ) - }, - }, - { - key: 'remove', - width: 0, - render: (_, { role }) => { - return ( - void updateAccessControlRoles([{ role, level: null }])} - /> - ) - }, - }, - ] - - return ( -
- { - if (guardAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING)) { - await updateAccessControlRoles(newValues.map((role) => ({ role, level }))) - } - }} - options={addableRoles.map((role) => ({ - key: role.id, - label: role.name, - }))} - /> - - -
- ) -} - -function SimplLevelComponent(props: { - size?: LemonSelectProps['size'] - level: AccessControlType['access_level'] | null - levels: AccessControlType['access_level'][] - onChange: (newValue: AccessControlType['access_level']) => void -}): JSX.Element | null { - const { canEditAccessControls } = useValues(accessControlLogic) - - return ( - props.onChange(newValue)} - disabledReason={!canEditAccessControls ? 'You cannot edit this' : undefined} - options={props.levels.map((level) => ({ - value: level, - label: capitalizeFirstLetter(level ?? ''), - }))} - /> - ) -} - -function RemoveAccessButton({ - onConfirm, - subject, -}: { - onConfirm: () => void - subject: 'member' | 'role' -}): JSX.Element { - const { canEditAccessControls } = useValues(accessControlLogic) - - return ( - } - status="danger" - size="small" - disabledReason={!canEditAccessControls ? 'You cannot edit this' : undefined} - onClick={() => - LemonDialog.open({ - title: 'Remove access', - content: `Are you sure you want to remove this ${subject}'s explicit access?`, - primaryButton: { - children: 'Remove', - status: 'danger', - onClick: () => onConfirm(), - }, - }) - } - /> - ) -} - -function AddItemsControls(props: { - placeholder: string - onAdd: (newValues: string[], level: AccessControlType['access_level']) => Promise - options: { - key: string - label: string - }[] -}): JSX.Element | null { - const { availableLevels, canEditAccessControls } = useValues(accessControlLogic) - // TODO: Move this into a form logic - const [items, setItems] = useState([]) - const [level, setLevel] = useState(availableLevels[0] ?? null) - - useEffect(() => { - setLevel(availableLevels[0] ?? null) - }, [availableLevels]) - - const onSubmit = - items.length && level - ? (): void => - void props.onAdd(items, level).then(() => { - setItems([]) - setLevel(availableLevels[0] ?? null) - }) - : undefined - - return ( -
-
- setItems(newValues)} - mode="multiple" - options={props.options} - disabled={!canEditAccessControls} - /> -
- - - - Add - -
- ) -} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/RolesAndResourceAccessControls.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/RolesAndResourceAccessControls.tsx deleted file mode 100644 index c235eeacb01ea..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/RolesAndResourceAccessControls.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { IconPlus } from '@posthog/icons' -import { - LemonButton, - LemonDialog, - LemonInput, - LemonInputSelect, - LemonModal, - LemonSelect, - LemonTable, - LemonTableColumns, - ProfileBubbles, - ProfilePicture, -} from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { capitalizeFirstLetter, Form } from 'kea-forms' -import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' -import { LemonField } from 'lib/lemon-ui/LemonField' -import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { fullName } from 'lib/utils' -import { useMemo, useState } from 'react' -import { userLogic } from 'scenes/userLogic' - -import { AvailableFeature } from '~/types' - -import { roleBasedAccessControlLogic, RoleWithResourceAccessControls } from './roleBasedAccessControlLogic' - -export type RolesAndResourceAccessControlsProps = { - noAccessControls?: boolean -} - -export function RolesAndResourceAccessControls({ noAccessControls }: RolesAndResourceAccessControlsProps): JSX.Element { - const { - rolesWithResourceAccessControls, - rolesLoading, - roleBasedAccessControlsLoading, - resources, - availableLevels, - selectedRoleId, - defaultAccessLevel, - } = useValues(roleBasedAccessControlLogic) - - const { updateRoleBasedAccessControls, selectRoleId, setEditingRoleId } = useActions(roleBasedAccessControlLogic) - - const roleColumns = noAccessControls - ? [] - : resources.map((resource) => ({ - title: resource.replace(/_/g, ' ') + 's', - key: resource, - width: 0, - render: (_: any, { accessControlByResource, role }: RoleWithResourceAccessControls) => { - const ac = accessControlByResource[resource] - - return ( - - updateRoleBasedAccessControls([ - { - resource, - role: role?.id ?? null, - access_level: newValue, - }, - ]) - } - options={availableLevels.map((level) => ({ - value: level, - label: capitalizeFirstLetter(level ?? ''), - }))} - /> - ) - }, - })) - - const columns: LemonTableColumns = [ - { - title: 'Role', - key: 'role', - width: 0, - render: (_, { role }) => ( - - (role.id === selectedRoleId ? selectRoleId(null) : selectRoleId(role.id)) - : undefined - } - title={role?.name ?? 'Default'} - /> - - ), - }, - { - title: 'Members', - key: 'members', - render: (_, { role }) => { - return role ? ( - role.members.length ? ( - ({ - email: member.user.email, - name: member.user.first_name, - title: `${member.user.first_name} <${member.user.email}>`, - }))} - onClick={() => (role.id === selectedRoleId ? selectRoleId(null) : selectRoleId(role.id))} - /> - ) : ( - 'No members' - ) - ) : ( - 'All members' - ) - }, - }, - - ...roleColumns, - ] - - return ( -
-

Use roles to group your organization members and assign them permissions.

- - -
- !!selectedRoleId && role?.id === selectedRoleId, - onRowExpand: ({ role }) => (role ? selectRoleId(role.id) : undefined), - onRowCollapse: () => selectRoleId(null), - expandedRowRender: ({ role }) => (role ? : null), - rowExpandable: ({ role }) => !!role, - }} - /> - - setEditingRoleId('new')} icon={}> - Add a role - - -
-
-
- ) -} - -function RoleDetails({ roleId }: { roleId: string }): JSX.Element | null { - const { user } = useValues(userLogic) - const { sortedMembers, roles, canEditRoleBasedAccessControls } = useValues(roleBasedAccessControlLogic) - const { addMembersToRole, removeMemberFromRole, setEditingRoleId } = useActions(roleBasedAccessControlLogic) - const [membersToAdd, setMembersToAdd] = useState([]) - - const role = roles?.find((role) => role.id === roleId) - - const onSubmit = membersToAdd.length - ? () => { - role && addMembersToRole(role, membersToAdd) - setMembersToAdd([]) - } - : undefined - - const membersNotInRole = useMemo(() => { - const membersInRole = new Set(role?.members.map((member) => member.user.uuid)) - return sortedMembers?.filter((member) => !membersInRole.has(member.user.uuid)) ?? [] - }, [role?.members, sortedMembers]) - - if (!role) { - // This is mostly for typing - return null - } - - return ( -
-
-
-
- setMembersToAdd(newValues)} - mode="multiple" - disabled={!canEditRoleBasedAccessControls} - options={usersLemonSelectOptions( - membersNotInRole.map((member) => member.user), - 'uuid' - )} - /> -
- - - Add members - -
-
- setEditingRoleId(role.id)} - disabledReason={!canEditRoleBasedAccessControls ? 'You cannot edit this' : undefined} - > - Edit - -
-
- - - }, - width: 32, - }, - { - title: 'Name', - key: 'user_name', - render: (_, member) => - member.user.uuid == user?.uuid ? `${fullName(member.user)} (you)` : fullName(member.user), - sorter: (a, b) => fullName(a.user).localeCompare(fullName(b.user)), - }, - { - title: 'Email', - key: 'user_email', - render: (_, member) => { - return <>{member.user.email} - }, - sorter: (a, b) => a.user.email.localeCompare(b.user.email), - }, - { - key: 'actions', - width: 0, - render: (_, member) => { - return ( -
- removeMemberFromRole(role, member.id)} - > - Remove - -
- ) - }, - }, - ]} - dataSource={role.members} - /> -
- ) -} - -function RoleModal(): JSX.Element { - const { editingRoleId } = useValues(roleBasedAccessControlLogic) - const { setEditingRoleId, submitEditingRole, deleteRole } = useActions(roleBasedAccessControlLogic) - const isEditing = editingRoleId !== 'new' - - const onDelete = (): void => { - LemonDialog.open({ - title: 'Delete role', - content: 'Are you sure you want to delete this role? This action cannot be undone.', - primaryButton: { - children: 'Delete permanently', - onClick: () => deleteRole(editingRoleId as string), - status: 'danger', - }, - secondaryButton: { - children: 'Cancel', - }, - }) - } - - return ( -
- setEditingRoleId(null)} - title={!isEditing ? 'Create' : `Edit`} - footer={ - <> -
- {isEditing ? ( - onDelete()}> - Delete - - ) : null} -
- - setEditingRoleId(null)}> - Cancel - - - - {!isEditing ? 'Create' : 'Save'} - - - } - > - - - -
-
- ) -} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/SidePanelAccessControl.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/SidePanelAccessControl.tsx deleted file mode 100644 index 266b012ebcd77..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/SidePanelAccessControl.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useValues } from 'kea' - -import { SidePanelPaneHeader } from '../../components/SidePanelPaneHeader' -import { sidePanelContextLogic } from '../sidePanelContextLogic' -import { AccessControlObject } from './AccessControlObject' - -export const SidePanelAccessControl = (): JSX.Element => { - const { sceneSidePanelContext } = useValues(sidePanelContextLogic) - - return ( -
- -
- {sceneSidePanelContext.access_control_resource && sceneSidePanelContext.access_control_resource_id ? ( - - ) : ( -

Not supported

- )} -
-
- ) -} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/accessControlLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/accessControlLogic.ts deleted file mode 100644 index 8182b41c2b602..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/accessControlLogic.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { LemonSelectOption } from '@posthog/lemon-ui' -import { actions, afterMount, connect, kea, key, listeners, path, props, selectors } from 'kea' -import { loaders } from 'kea-loaders' -import api from 'lib/api' -import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' -import { toSentenceCase } from 'lib/utils' -import { membersLogic } from 'scenes/organization/membersLogic' -import { teamLogic } from 'scenes/teamLogic' - -import { - AccessControlResponseType, - AccessControlType, - AccessControlTypeMember, - AccessControlTypeProject, - AccessControlTypeRole, - AccessControlUpdateType, - APIScopeObject, - OrganizationMemberType, - RoleType, -} from '~/types' - -import type { accessControlLogicType } from './accessControlLogicType' -import { roleBasedAccessControlLogic } from './roleBasedAccessControlLogic' - -export type AccessControlLogicProps = { - resource: APIScopeObject - resource_id: string -} - -export const accessControlLogic = kea([ - props({} as AccessControlLogicProps), - key((props) => `${props.resource}-${props.resource_id}`), - path((key) => ['scenes', 'accessControl', 'accessControlLogic', key]), - connect({ - values: [ - membersLogic, - ['sortedMembers'], - teamLogic, - ['currentTeam'], - roleBasedAccessControlLogic, - ['roles'], - upgradeModalLogic, - ['guardAvailableFeature'], - ], - actions: [membersLogic, ['ensureAllMembersLoaded']], - }), - actions({ - updateAccessControl: ( - accessControl: Pick - ) => ({ accessControl }), - updateAccessControlDefault: (level: AccessControlType['access_level']) => ({ - level, - }), - updateAccessControlRoles: ( - accessControls: { - role: RoleType['id'] - level: AccessControlType['access_level'] - }[] - ) => ({ accessControls }), - updateAccessControlMembers: ( - accessControls: { - member: OrganizationMemberType['id'] - level: AccessControlType['access_level'] - }[] - ) => ({ accessControls }), - }), - loaders(({ values }) => ({ - accessControls: [ - null as AccessControlResponseType | null, - { - loadAccessControls: async () => { - try { - const response = await api.get(values.endpoint) - return response - } catch (error) { - // Return empty access controls - return { - access_controls: [], - available_access_levels: ['none', 'viewer', 'editor'], - user_access_level: 'none', - default_access_level: 'none', - user_can_edit_access_levels: false, - } - } - }, - - updateAccessControlDefault: async ({ level }) => { - await api.put(values.endpoint, { - access_level: level, - }) - - return values.accessControls - }, - - updateAccessControlRoles: async ({ accessControls }) => { - for (const { role, level } of accessControls) { - await api.put(values.endpoint, { - role: role, - access_level: level, - }) - } - - return values.accessControls - }, - - updateAccessControlMembers: async ({ accessControls }) => { - for (const { member, level } of accessControls) { - await api.put(values.endpoint, { - organization_member: member, - access_level: level, - }) - } - - return values.accessControls - }, - }, - ], - })), - listeners(({ actions }) => ({ - updateAccessControlDefaultSuccess: () => actions.loadAccessControls(), - updateAccessControlRolesSuccess: () => actions.loadAccessControls(), - updateAccessControlMembersSuccess: () => actions.loadAccessControls(), - })), - selectors({ - endpoint: [ - () => [(_, props) => props], - (props): string => { - // TODO: This is far from perfect... but it's a start - if (props.resource === 'project') { - return `api/projects/@current/access_controls` - } - return `api/projects/@current/${props.resource}s/${props.resource_id}/access_controls` - }, - ], - humanReadableResource: [ - () => [(_, props) => props], - (props): string => { - return props.resource.replace(/_/g, ' ') - }, - ], - - availableLevelsWithNone: [ - (s) => [s.accessControls], - (accessControls): string[] => { - return accessControls?.available_access_levels ?? [] - }, - ], - - availableLevels: [ - (s) => [s.availableLevelsWithNone], - (availableLevelsWithNone): string[] => { - return availableLevelsWithNone.filter((level) => level !== 'none') - }, - ], - - canEditAccessControls: [ - (s) => [s.accessControls], - (accessControls): boolean | null => { - return accessControls?.user_can_edit_access_levels ?? null - }, - ], - - accessControlDefaultLevel: [ - (s) => [s.accessControls], - (accessControls): string | null => { - return accessControls?.default_access_level ?? null - }, - ], - - accessControlDefaultOptions: [ - (s) => [s.availableLevelsWithNone, (_, props) => props.resource], - (availableLevelsWithNone): LemonSelectOption[] => { - const options = availableLevelsWithNone.map((level) => ({ - value: level, - // TODO: Correct "a" and "an" - label: level === 'none' ? 'No access' : toSentenceCase(level), - })) - - return options - }, - ], - accessControlDefault: [ - (s) => [s.accessControls, s.accessControlDefaultLevel], - (accessControls, accessControlDefaultLevel): AccessControlTypeProject => { - const found = accessControls?.access_controls?.find( - (accessControl) => !accessControl.organization_member && !accessControl.role - ) as AccessControlTypeProject - return ( - found ?? { - access_level: accessControlDefaultLevel, - } - ) - }, - ], - - accessControlMembers: [ - (s) => [s.accessControls], - (accessControls): AccessControlTypeMember[] => { - return (accessControls?.access_controls || []).filter( - (accessControl) => !!accessControl.organization_member - ) as AccessControlTypeMember[] - }, - ], - - accessControlRoles: [ - (s) => [s.accessControls], - (accessControls): AccessControlTypeRole[] => { - return (accessControls?.access_controls || []).filter( - (accessControl) => !!accessControl.role - ) as AccessControlTypeRole[] - }, - ], - - rolesById: [ - (s) => [s.roles], - (roles): Record => { - return Object.fromEntries((roles || []).map((role) => [role.id, role])) - }, - ], - - addableRoles: [ - (s) => [s.roles, s.accessControlRoles], - (roles, accessControlRoles): RoleType[] => { - return roles ? roles.filter((role) => !accessControlRoles.find((ac) => ac.role === role.id)) : [] - }, - ], - - membersById: [ - (s) => [s.sortedMembers], - (members): Record => { - return Object.fromEntries((members || []).map((member) => [member.id, member])) - }, - ], - - addableMembers: [ - (s) => [s.sortedMembers, s.accessControlMembers], - (members, accessControlMembers): any[] => { - return members - ? members.filter( - (member) => !accessControlMembers.find((ac) => ac.organization_member === member.id) - ) - : [] - }, - ], - }), - afterMount(({ actions }) => { - actions.loadAccessControls() - actions.ensureAllMembersLoaded() - }), -]) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/roleBasedAccessControlLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/roleBasedAccessControlLogic.ts deleted file mode 100644 index 87d885844bfb1..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/access_control/roleBasedAccessControlLogic.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { lemonToast } from '@posthog/lemon-ui' -import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' -import { forms } from 'kea-forms' -import { loaders } from 'kea-loaders' -import { actionToUrl, router } from 'kea-router' -import api from 'lib/api' -import { membersLogic } from 'scenes/organization/membersLogic' -import { teamLogic } from 'scenes/teamLogic' -import { userLogic } from 'scenes/userLogic' - -import { - AccessControlResponseType, - AccessControlType, - AccessControlTypeRole, - AccessControlUpdateType, - APIScopeObject, - AvailableFeature, - RoleType, -} from '~/types' - -import type { roleBasedAccessControlLogicType } from './roleBasedAccessControlLogicType' - -export type RoleWithResourceAccessControls = { - role?: RoleType - accessControlByResource: Record -} - -export const roleBasedAccessControlLogic = kea([ - path(['scenes', 'accessControl', 'roleBasedAccessControlLogic']), - connect({ - values: [membersLogic, ['sortedMembers'], teamLogic, ['currentTeam'], userLogic, ['hasAvailableFeature']], - actions: [membersLogic, ['ensureAllMembersLoaded']], - }), - actions({ - updateRoleBasedAccessControls: ( - accessControls: Pick[] - ) => ({ accessControls }), - selectRoleId: (roleId: RoleType['id'] | null) => ({ roleId }), - deleteRole: (roleId: RoleType['id']) => ({ roleId }), - removeMemberFromRole: (role: RoleType, roleMemberId: string) => ({ role, roleMemberId }), - addMembersToRole: (role: RoleType, members: string[]) => ({ role, members }), - setEditingRoleId: (roleId: string | null) => ({ roleId }), - }), - reducers({ - selectedRoleId: [ - null as string | null, - { - selectRoleId: (_, { roleId }) => roleId, - }, - ], - editingRoleId: [ - null as string | null, - { - setEditingRoleId: (_, { roleId }) => roleId, - }, - ], - }), - loaders(({ values }) => ({ - roleBasedAccessControls: [ - null as AccessControlResponseType | null, - { - loadRoleBasedAccessControls: async () => { - const response = await api.get( - 'api/projects/@current/global_access_controls' - ) - return response - }, - - updateRoleBasedAccessControls: async ({ accessControls }) => { - for (const control of accessControls) { - await api.put('api/projects/@current/global_access_controls', { - ...control, - }) - } - - return values.roleBasedAccessControls - }, - }, - ], - - roles: [ - null as RoleType[] | null, - { - loadRoles: async () => { - const response = await api.roles.list() - return response?.results || [] - }, - addMembersToRole: async ({ role, members }) => { - if (!values.roles) { - return null - } - const newMembers = await Promise.all( - members.map(async (userUuid: string) => await api.roles.members.create(role.id, userUuid)) - ) - - role.members = [...role.members, ...newMembers] - - return [...values.roles] - }, - removeMemberFromRole: async ({ role, roleMemberId }) => { - if (!values.roles) { - return null - } - await api.roles.members.delete(role.id, roleMemberId) - role.members = role.members.filter((roleMember) => roleMember.id !== roleMemberId) - return [...values.roles] - }, - deleteRole: async ({ roleId }) => { - const role = values.roles?.find((r) => r.id === roleId) - if (!role) { - return values.roles - } - await api.roles.delete(role.id) - lemonToast.success(`Role "${role.name}" deleted`) - return values.roles?.filter((r) => r.id !== role.id) || [] - }, - }, - ], - })), - - forms(({ values, actions }) => ({ - editingRole: { - defaults: { - name: '', - }, - errors: ({ name }) => { - return { - name: !name ? 'Please choose a name for the role' : null, - } - }, - submit: async ({ name }) => { - if (!values.editingRoleId) { - return - } - let role: RoleType | null = null - if (values.editingRoleId === 'new') { - role = await api.roles.create(name) - } else { - role = await api.roles.update(values.editingRoleId, { name }) - } - - actions.loadRoles() - actions.setEditingRoleId(null) - actions.selectRoleId(role.id) - }, - }, - })), - - listeners(({ actions, values }) => ({ - updateRoleBasedAccessControlsSuccess: () => actions.loadRoleBasedAccessControls(), - loadRolesSuccess: () => { - if (router.values.hashParams.role) { - actions.selectRoleId(router.values.hashParams.role) - } - }, - deleteRoleSuccess: () => { - actions.loadRoles() - actions.setEditingRoleId(null) - actions.selectRoleId(null) - }, - - setEditingRoleId: () => { - const existingRole = values.roles?.find((role) => role.id === values.editingRoleId) - actions.resetEditingRole({ - name: existingRole?.name || '', - }) - }, - })), - - selectors({ - availableLevels: [ - (s) => [s.roleBasedAccessControls], - (roleBasedAccessControls): string[] => { - return roleBasedAccessControls?.available_access_levels ?? [] - }, - ], - - defaultAccessLevel: [ - (s) => [s.roleBasedAccessControls], - (roleBasedAccessControls): string | null => { - return roleBasedAccessControls?.default_access_level ?? null - }, - ], - - defaultResourceAccessControls: [ - (s) => [s.roleBasedAccessControls], - (roleBasedAccessControls): RoleWithResourceAccessControls => { - const accessControls = roleBasedAccessControls?.access_controls ?? [] - - // Find all acs without a roles (they are the default ones) - const accessControlByResource = accessControls - .filter((control) => !control.role) - .reduce( - (acc, control) => ({ - ...acc, - [control.resource]: control, - }), - {} as Record - ) - - return { accessControlByResource } - }, - ], - - rolesWithResourceAccessControls: [ - (s) => [s.roles, s.roleBasedAccessControls, s.defaultResourceAccessControls], - (roles, roleBasedAccessControls, defaultResourceAccessControls): RoleWithResourceAccessControls[] => { - if (!roles) { - return [] - } - - const accessControls = roleBasedAccessControls?.access_controls ?? [] - - return [ - defaultResourceAccessControls, - ...roles.map((role) => { - const accessControlByResource = accessControls - .filter((control) => control.role === role.id) - .reduce( - (acc, control) => ({ - ...acc, - [control.resource]: control, - }), - {} as Record - ) - - return { role, accessControlByResource } - }), - ] - }, - ], - - resources: [ - () => [], - (): AccessControlType['resource'][] => { - // TODO: Sync this as an enum - return ['feature_flag', 'dashboard', 'insight', 'notebook'] - }, - ], - - canEditRoleBasedAccessControls: [ - (s) => [s.roleBasedAccessControls], - (roleBasedAccessControls): boolean | null => { - return roleBasedAccessControls?.user_can_edit_access_levels ?? null - }, - ], - }), - afterMount(({ actions, values }) => { - if (values.hasAvailableFeature(AvailableFeature.ROLE_BASED_ACCESS)) { - actions.loadRoles() - actions.loadRoleBasedAccessControls() - actions.ensureAllMembersLoaded() - } - }), - - actionToUrl(({ values }) => ({ - selectRoleId: () => { - const { currentLocation } = router.values - return [ - currentLocation.pathname, - currentLocation.searchParams, - { - ...currentLocation.hashParams, - role: values.selectedRoleId ?? undefined, - }, - ] - }, - })), -]) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelContextLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic.ts similarity index 59% rename from frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelContextLogic.ts rename to frontend/src/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic.ts index 1de9b8e00e251..641c0900638ef 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelContextLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic.ts @@ -1,15 +1,22 @@ import { connect, kea, path, selectors } from 'kea' import { router } from 'kea-router' import { objectsEqual } from 'kea-test-utils' +import { ActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' import { removeProjectIdIfPresent } from 'lib/utils/router-utils' import { sceneLogic } from 'scenes/sceneLogic' import { SceneConfig } from 'scenes/sceneTypes' -import { SidePanelSceneContext } from '../types' -import { SIDE_PANEL_CONTEXT_KEY } from '../types' -import type { sidePanelContextLogicType } from './sidePanelContextLogicType' +import { ActivityScope, UserBasicType } from '~/types' -export const activityFiltersForScene = (sceneConfig: SceneConfig | null): SidePanelSceneContext | null => { +import type { activityForSceneLogicType } from './activityForSceneLogicType' + +export type ActivityFilters = { + scope?: ActivityScope + item_id?: ActivityLogItem['item_id'] + user?: UserBasicType['id'] +} + +export const activityFiltersForScene = (sceneConfig: SceneConfig | null): ActivityFilters | null => { if (sceneConfig?.activityScope) { // NOTE: - HACKY, we are just parsing the item_id from the url optimistically... const pathParts = removeProjectIdIfPresent(router.values.currentLocation.pathname).split('/') @@ -17,43 +24,38 @@ export const activityFiltersForScene = (sceneConfig: SceneConfig | null): SidePa // Loose check for the item_id being a number, a short_id (8 chars) or a uuid if (item_id && (item_id.length === 8 || item_id.length === 36 || !isNaN(parseInt(item_id)))) { - return { activity_scope: sceneConfig.activityScope, activity_item_id: item_id } + return { scope: sceneConfig.activityScope, item_id } } - return { activity_scope: sceneConfig.activityScope } + return { scope: sceneConfig.activityScope } } return null } -export const sidePanelContextLogic = kea([ - path(['scenes', 'navigation', 'sidepanel', 'sidePanelContextLogic']), +export const activityForSceneLogic = kea([ + path(['scenes', 'navigation', 'sidepanel', 'activityForSceneLogic']), connect({ values: [sceneLogic, ['sceneConfig']], }), selectors({ - sceneSidePanelContext: [ + sceneActivityFilters: [ (s) => [ - s.sceneConfig, // Similar to "breadcrumbs" (state, props) => { const activeSceneLogic = sceneLogic.selectors.activeSceneLogic(state, props) - if (activeSceneLogic && SIDE_PANEL_CONTEXT_KEY in activeSceneLogic.selectors) { + const sceneConfig = s.sceneConfig(state, props) + if (activeSceneLogic && 'activityFilters' in activeSceneLogic.selectors) { const activeLoadedScene = sceneLogic.selectors.activeLoadedScene(state, props) - return activeSceneLogic.selectors[SIDE_PANEL_CONTEXT_KEY]( + return activeSceneLogic.selectors.activityFilters( state, activeLoadedScene?.paramsToProps?.(activeLoadedScene?.sceneParams) || props ) } - return null + return activityFiltersForScene(sceneConfig) }, ], - (sceneConfig, context): SidePanelSceneContext => { - return { - ...(context ?? {}), - ...(!context?.activity_scope ? activityFiltersForScene(sceneConfig) : {}), - } - }, + (filters): ActivityFilters | null => filters, { equalityCheck: objectsEqual }, ], }), diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/activity/sidePanelActivityLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/activity/sidePanelActivityLogic.tsx index 079433affb717..244e42c52d936 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/activity/sidePanelActivityLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/activity/sidePanelActivityLogic.tsx @@ -10,21 +10,12 @@ import { toParams } from 'lib/utils' import posthog from 'posthog-js' import { projectLogic } from 'scenes/projectLogic' -import { ActivityScope, UserBasicType } from '~/types' - import { sidePanelStateLogic } from '../../sidePanelStateLogic' -import { SidePanelSceneContext } from '../../types' -import { sidePanelContextLogic } from '../sidePanelContextLogic' +import { ActivityFilters, activityForSceneLogic } from './activityForSceneLogic' import type { sidePanelActivityLogicType } from './sidePanelActivityLogicType' const POLL_TIMEOUT = 5 * 60 * 1000 -export type ActivityFilters = { - scope?: ActivityScope - item_id?: ActivityLogItem['item_id'] - user?: UserBasicType['id'] -} - export interface ChangelogFlagPayload { notificationDate: dayjs.Dayjs markdown: string @@ -45,7 +36,7 @@ export enum SidePanelActivityTab { export const sidePanelActivityLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelActivityLogic']), connect({ - values: [sidePanelContextLogic, ['sceneSidePanelContext'], projectLogic, ['currentProjectId']], + values: [activityForSceneLogic, ['sceneActivityFilters'], projectLogic, ['currentProjectId']], actions: [sidePanelStateLogic, ['openSidePanel']], }), actions({ @@ -276,16 +267,8 @@ export const sidePanelActivityLogic = kea([ }), subscriptions(({ actions, values }) => ({ - sceneSidePanelContext: (sceneSidePanelContext: SidePanelSceneContext) => { - actions.setFiltersForCurrentPage( - sceneSidePanelContext - ? { - ...values.filters, - scope: sceneSidePanelContext.activity_scope, - item_id: sceneSidePanelContext.activity_item_id, - } - : null - ) + sceneActivityFilters: (activityFilters) => { + actions.setFiltersForCurrentPage(activityFilters ? { ...values.filters, ...activityFilters } : null) }, filters: () => { if (values.activeTab === SidePanelActivityTab.All) { @@ -297,7 +280,7 @@ export const sidePanelActivityLogic = kea([ afterMount(({ actions, values }) => { actions.loadImportantChanges() - const activityFilters = values.sceneSidePanelContext + const activityFilters = values.sceneActivityFilters actions.setFiltersForCurrentPage(activityFilters ? { ...values.filters, ...activityFilters } : null) }), diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/discussion/sidePanelDiscussionLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/discussion/sidePanelDiscussionLogic.ts index 9d1ba1d536d9b..5793deba3469f 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/discussion/sidePanelDiscussionLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/discussion/sidePanelDiscussionLogic.ts @@ -6,7 +6,7 @@ import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { CommentsLogicProps } from 'scenes/comments/commentsLogic' -import { sidePanelContextLogic } from '../sidePanelContextLogic' +import { activityForSceneLogic } from '../activity/activityForSceneLogic' import type { sidePanelDiscussionLogicType } from './sidePanelDiscussionLogicType' export const sidePanelDiscussionLogic = kea([ @@ -16,7 +16,7 @@ export const sidePanelDiscussionLogic = kea([ resetCommentCount: true, }), connect({ - values: [featureFlagLogic, ['featureFlags'], sidePanelContextLogic, ['sceneSidePanelContext']], + values: [featureFlagLogic, ['featureFlags'], activityForSceneLogic, ['sceneActivityFilters']], }), loaders(({ values }) => ({ commentCount: [ @@ -45,12 +45,12 @@ export const sidePanelDiscussionLogic = kea([ selectors({ commentsLogicProps: [ - (s) => [s.sceneSidePanelContext], - (sceneSidePanelContext): CommentsLogicProps | null => { - return sceneSidePanelContext.activity_scope + (s) => [s.sceneActivityFilters], + (activityFilters): CommentsLogicProps | null => { + return activityFilters?.scope ? { - scope: sceneSidePanelContext.activity_scope, - item_id: sceneSidePanelContext.activity_item_id, + scope: activityFilters.scope, + item_id: activityFilters.item_id, } : null }, diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/exports/sidePanelExportsLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/exports/sidePanelExportsLogic.ts index 8f26e5927842e..c9107c4ac695f 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/exports/sidePanelExportsLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/exports/sidePanelExportsLogic.ts @@ -1,14 +1,23 @@ import { afterMount, connect, kea, path } from 'kea' import { exportsLogic } from 'lib/components/ExportButton/exportsLogic' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' +import { activityForSceneLogic } from '../activity/activityForSceneLogic' import type { sidePanelExportsLogicType } from './sidePanelExportsLogicType' export const sidePanelExportsLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelExportsLogic']), connect({ - values: [exportsLogic, ['exports', 'freshUndownloadedExports']], + values: [ + featureFlagLogic, + ['featureFlags'], + activityForSceneLogic, + ['sceneActivityFilters'], + exportsLogic, + ['exports', 'freshUndownloadedExports'], + ], actions: [sidePanelStateLogic, ['openSidePanel'], exportsLogic, ['loadExports', 'removeFresh']], }), afterMount(({ actions }) => { diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx index b220fd505c4a8..029b34b6cbf4a 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx @@ -8,7 +8,6 @@ import { activationLogic } from '~/layout/navigation-3000/sidepanel/panels/activ import { AvailableFeature, SidePanelTab } from '~/types' import { sidePanelActivityLogic } from './panels/activity/sidePanelActivityLogic' -import { sidePanelContextLogic } from './panels/sidePanelContextLogic' import { sidePanelStatusLogic } from './panels/sidePanelStatusLogic' import type { sidePanelLogicType } from './sidePanelLogicType' import { sidePanelStateLogic } from './sidePanelStateLogic' @@ -40,16 +39,14 @@ export const sidePanelLogic = kea([ ['status'], userLogic, ['hasAvailableFeature'], - sidePanelContextLogic, - ['sceneSidePanelContext'], ], actions: [sidePanelStateLogic, ['closeSidePanel', 'openSidePanel']], }), selectors({ enabledTabs: [ - (s) => [s.isCloudOrDev, s.isReady, s.hasCompletedAllTasks, s.featureFlags, s.sceneSidePanelContext], - (isCloudOrDev, isReady, hasCompletedAllTasks, featureflags, sceneSidePanelContext) => { + (s) => [s.isCloudOrDev, s.isReady, s.hasCompletedAllTasks, s.featureFlags], + (isCloudOrDev, isReady, hasCompletedAllTasks, featureflags) => { const tabs: SidePanelTab[] = [] tabs.push(SidePanelTab.Notebooks) @@ -64,13 +61,6 @@ export const sidePanelLogic = kea([ if (isReady && !hasCompletedAllTasks) { tabs.push(SidePanelTab.Activation) } - if ( - featureflags[FEATURE_FLAGS.ROLE_BASED_ACCESS_CONTROL] && - sceneSidePanelContext.access_control_resource && - sceneSidePanelContext.access_control_resource_id - ) { - tabs.push(SidePanelTab.AccessControl) - } tabs.push(SidePanelTab.Exports) tabs.push(SidePanelTab.FeaturePreviews) tabs.push(SidePanelTab.Settings) diff --git a/frontend/src/layout/navigation-3000/sidepanel/types.ts b/frontend/src/layout/navigation-3000/sidepanel/types.ts deleted file mode 100644 index 28da07acb1c89..0000000000000 --- a/frontend/src/layout/navigation-3000/sidepanel/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' - -import { ActivityScope, APIScopeObject } from '~/types' - -/** Allows scenes to set a context which enables richer features of the side panel */ -export type SidePanelSceneContext = { - access_control_resource?: APIScopeObject - access_control_resource_id?: string - activity_scope?: ActivityScope - activity_item_id?: ActivityLogItem['item_id'] -} -export const SIDE_PANEL_CONTEXT_KEY = 'sidePanelContext' diff --git a/frontend/src/lib/components/Metalytics/metalyticsLogic.ts b/frontend/src/lib/components/Metalytics/metalyticsLogic.ts index 06d0f384d81b5..8ddc838701121 100644 --- a/frontend/src/lib/components/Metalytics/metalyticsLogic.ts +++ b/frontend/src/lib/components/Metalytics/metalyticsLogic.ts @@ -4,8 +4,7 @@ import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' import { membersLogic } from 'scenes/organization/membersLogic' -import { sidePanelContextLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelContextLogic' -import { SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { activityForSceneLogic } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { HogQLQuery, NodeKind } from '~/queries/schema' import { hogql } from '~/queries/utils' @@ -14,7 +13,7 @@ import type { metalyticsLogicType } from './metalyticsLogicType' export const metalyticsLogic = kea([ path(['lib', 'components', 'metalytics', 'metalyticsLogic']), connect({ - values: [sidePanelContextLogic, ['sceneSidePanelContext'], membersLogic, ['members']], + values: [activityForSceneLogic, ['sceneActivityFilters'], membersLogic, ['members']], }), loaders(({ values }) => ({ @@ -63,16 +62,11 @@ export const metalyticsLogic = kea([ selectors({ instanceId: [ - (s) => [s.sceneSidePanelContext], - (sidePanelContext: SidePanelSceneContext) => - sidePanelContext?.activity_item_id - ? `${sidePanelContext.activity_scope}:${sidePanelContext.activity_item_id}` - : null, - ], - scope: [ - (s) => [s.sceneSidePanelContext], - (sidePanelContext: SidePanelSceneContext) => sidePanelContext?.activity_scope, + (s) => [s.sceneActivityFilters], + (sceneActivityFilters) => + sceneActivityFilters?.item_id ? `${sceneActivityFilters.scope}:${sceneActivityFilters.item_id}` : null, ], + scope: [(s) => [s.sceneActivityFilters], (sceneActivityFilters) => sceneActivityFilters?.scope], recentUserMembers: [ (s) => [s.recentUsers, s.members], diff --git a/frontend/src/lib/components/RestrictedArea.tsx b/frontend/src/lib/components/RestrictedArea.tsx index 852d1606bb0d0..ade847740c42a 100644 --- a/frontend/src/lib/components/RestrictedArea.tsx +++ b/frontend/src/lib/components/RestrictedArea.tsx @@ -27,10 +27,7 @@ export interface RestrictedAreaProps extends UseRestrictedAreaProps { Component: (props: RestrictedComponentProps) => JSX.Element } -export function useRestrictedArea({ - scope = RestrictionScope.Organization, - minimumAccessLevel, -}: UseRestrictedAreaProps): null | string { +export function useRestrictedArea({ scope, minimumAccessLevel }: UseRestrictedAreaProps): null | string { const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) diff --git a/frontend/src/lib/components/Sharing/SharingModal.tsx b/frontend/src/lib/components/Sharing/SharingModal.tsx index 7ef76f6fc54d6..7348b1bc56610 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.tsx @@ -92,7 +92,6 @@ export function SharingModalContent({

Something went wrong...

) : ( <> -

Sharing

void +} + +export function roleLemonSelectOptions(roles: RoleType[]): LemonInputSelectOption[] { return roles.map((role) => ({ key: role.id, label: `${role.name}`, @@ -39,52 +41,35 @@ function roleLemonSelectOptions(roles: RoleType[]): LemonInputSelectOption[] { })) } -export function FeatureFlagPermissions({ featureFlag }: { featureFlag: FeatureFlagType }): JSX.Element { - const { addableRoles, unfilteredAddableRolesLoading, rolesToAdd, derivedRoles } = useValues( - featureFlagPermissionsLogic({ flagId: featureFlag.id }) - ) - const { setRolesToAdd, addAssociatedRoles, deleteAssociatedRole } = useActions( - featureFlagPermissionsLogic({ flagId: featureFlag.id }) - ) - const { openSidePanel } = useActions(sidePanelStateLogic) - - const newAccessControls = useFeatureFlag('ROLE_BASED_ACCESS_CONTROL') - if (newAccessControls) { - if (!featureFlag.id) { - return

Please save the feature flag before changing the access controls.

- } - return ( -
- - Permissions have moved! We're rolling out our new access control system. Click below to open it. - - } - onClick={() => { - openSidePanel(SidePanelTab.AccessControl) - }} - > - Open access control - -
- ) - } - +export function ResourcePermissionModal({ + title, + visible, + onClose, + rolesToAdd, + addableRoles, + onChange, + addableRolesLoading, + onAdd, + roles, + deleteAssociatedRole, + canEdit, +}: ResourcePermissionModalProps): JSX.Element { return ( - - setRolesToAdd(roleIds)} - rolesToAdd={rolesToAdd} - addableRoles={addableRoles} - addableRolesLoading={unfilteredAddableRolesLoading} - onAdd={() => addAssociatedRoles()} - roles={derivedRoles} - deleteAssociatedRole={(id) => deleteAssociatedRole({ roleId: id })} - canEdit={featureFlag.can_edit} - /> - + <> + + + + ) } @@ -123,7 +108,7 @@ export function ResourcePermission({ icon={ } - to={`${urls.settings('organization-roles')}`} + to={`${urls.settings('organization-rbac')}`} targetBlank size="small" noPadding diff --git a/frontend/src/scenes/actions/actionLogic.ts b/frontend/src/scenes/actions/actionLogic.ts index f650b616ba944..a3101cb8d9daf 100644 --- a/frontend/src/scenes/actions/actionLogic.ts +++ b/frontend/src/scenes/actions/actionLogic.ts @@ -5,7 +5,7 @@ import { DataManagementTab } from 'scenes/data-management/DataManagementScene' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { ActionType, ActivityScope, Breadcrumb, HogFunctionType } from '~/types' import { actionEditLogic } from './actionEditLogic' @@ -106,15 +106,13 @@ export const actionLogic = kea([ (action) => action?.steps?.some((step) => step.properties?.find((p) => p.type === 'cohort')) ?? false, ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ (s) => [s.action], - (action): SidePanelSceneContext | null => { + (action): ActivityFilters | null => { return action?.id ? { - activity_scope: ActivityScope.ACTION, - activity_item_id: `${action.id}`, - // access_control_resource: 'action', - // access_control_resource_id: `${action.id}`, + scope: ActivityScope.ACTION, + item_id: String(action.id), } : null }, diff --git a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx index 75b83719330d4..048d668bc71fd 100644 --- a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx +++ b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx @@ -1,10 +1,8 @@ -import { IconLock, IconOpenSidebar, IconTrash, IconUnlock } from '@posthog/icons' +import { IconLock, IconTrash, IconUnlock } from '@posthog/icons' import { useActions, useValues } from 'kea' -import { router } from 'kea-router' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { DashboardPrivilegeLevel, DashboardRestrictionLevel, privilegeLevelToName } from 'lib/constants' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' @@ -12,10 +10,8 @@ import { LemonSelect, LemonSelectOptions } from 'lib/lemon-ui/LemonSelect' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' -import { urls } from 'scenes/urls' -import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' -import { AvailableFeature, DashboardType, FusedDashboardCollaboratorType, SidePanelTab, UserType } from '~/types' +import { AvailableFeature, DashboardType, FusedDashboardCollaboratorType, UserType } from '~/types' import { dashboardCollaboratorsLogic } from './dashboardCollaboratorsLogic' @@ -40,96 +36,73 @@ export function DashboardCollaboration({ dashboardId }: { dashboardId: Dashboard const { deleteExplicitCollaborator, setExplicitCollaboratorsToBeAdded, addExplicitCollaborators } = useActions( dashboardCollaboratorsLogic({ dashboardId }) ) - const { push } = useActions(router) - const { openSidePanel } = useActions(sidePanelStateLogic) - - const newAccessControl = useFeatureFlag('ROLE_BASED_ACCESS_CONTROL') - - if (!dashboard) { - return null - } - - if (newAccessControl) { - return ( -
-

Access control

- - Permissions have moved! We're rolling out our new access control system. Click below to open it. - - } - onClick={() => { - openSidePanel(SidePanelTab.AccessControl) - push(urls.dashboard(dashboard.id)) - }} - > - Open access control - -
- ) - } return ( - - {(!canEditDashboard || !canRestrictDashboard) && ( - - {canEditDashboard - ? "You aren't allowed to change the restriction level – only the dashboard owner and project admins can." - : "You aren't allowed to change sharing settings – only dashboard collaborators with edit settings can."} - - )} - - triggerDashboardUpdate({ - restriction_level: newValue, - }) - } - options={DASHBOARD_RESTRICTION_OPTIONS} - loading={dashboardLoading} - fullWidth - disabled={!canRestrictDashboard} - /> - {dashboard.restriction_level > DashboardRestrictionLevel.EveryoneInProjectCanEdit && ( -
-
Collaborators
- {canEditDashboard && ( -
-
- setExplicitCollaboratorsToBeAdded(newValues)} - mode="multiple" - data-attr="subscribed-emails" - options={usersLemonSelectOptions(addableMembers, 'uuid')} - /> + dashboard && ( + <> + + {(!canEditDashboard || !canRestrictDashboard) && ( + + {canEditDashboard + ? "You aren't allowed to change the restriction level – only the dashboard owner and project admins can." + : "You aren't allowed to change sharing settings – only dashboard collaborators with edit settings can."} + + )} + + triggerDashboardUpdate({ + restriction_level: newValue, + }) + } + options={DASHBOARD_RESTRICTION_OPTIONS} + loading={dashboardLoading} + fullWidth + disabled={!canRestrictDashboard} + /> + {dashboard.restriction_level > DashboardRestrictionLevel.EveryoneInProjectCanEdit && ( +
+
Collaborators
+ {canEditDashboard && ( +
+
+ + setExplicitCollaboratorsToBeAdded(newValues) + } + mode="multiple" + data-attr="subscribed-emails" + options={usersLemonSelectOptions(addableMembers, 'uuid')} + /> +
+ addExplicitCollaborators()} + > + Add + +
+ )} +
Project members with access
+
+ {allCollaborators.map((collaborator) => ( + + ))}
- addExplicitCollaborators()} - > - Add -
)} -
Project members with access
-
- {allCollaborators.map((collaborator) => ( - - ))} -
-
- )} - + + + ) ) } diff --git a/frontend/src/scenes/dashboard/dashboardLogic.tsx b/frontend/src/scenes/dashboard/dashboardLogic.tsx index 0b6236931cf2b..4addf1f04f4c0 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboardLogic.tsx @@ -30,7 +30,6 @@ import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' import { dashboardsModel } from '~/models/dashboardsModel' import { insightsModel } from '~/models/insightsModel' import { variableDataLogic } from '~/queries/nodes/DataVisualization/Components/Variables/variableDataLogic' @@ -39,7 +38,6 @@ import { getQueryBasedDashboard, getQueryBasedInsightModel } from '~/queries/nod import { pollForResults } from '~/queries/query' import { DashboardFilter, DataVisualizationNode, HogQLVariable, NodeKind, RefreshType } from '~/queries/schema' import { - ActivityScope, AnyPropertyFilter, Breadcrumb, DashboardLayoutSize, @@ -993,21 +991,6 @@ export const dashboardLogic = kea([ }, ], ], - - [SIDE_PANEL_CONTEXT_KEY]: [ - (s) => [s.dashboard], - (dashboard): SidePanelSceneContext | null => { - return dashboard - ? { - activity_scope: ActivityScope.DASHBOARD, - activity_item_id: `${dashboard.id}`, - access_control_resource: 'dashboard', - access_control_resource_id: `${dashboard.id}`, - } - : null - }, - ], - sortTilesByLayout: [ (s) => [s.layoutForItem], (layoutForItem) => (tiles: Array) => { diff --git a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx index a8e8107380ab5..ae61570189150 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx @@ -29,7 +29,7 @@ export const dataWarehouseViewsLogic = kea([ const savedQueries = await api.dataWarehouseSavedQueries.list() if (router.values.location.pathname.includes(urls.dataModel()) && !cache.pollingInterval) { - cache.pollingInterval = setInterval(() => actions.loadDataWarehouseSavedQueries(), 5000) + cache.pollingInterval = setInterval(actions.loadDataWarehouseSavedQueries, 5000) } else { clearInterval(cache.pollingInterval) } diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index c9d7b50b6ab78..d7e2ad01c9133 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -10,6 +10,7 @@ import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' import { NotFound } from 'lib/components/NotFound' import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' +import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { FEATURE_FLAGS } from 'lib/constants' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -33,9 +34,9 @@ import { dashboardLogic } from 'scenes/dashboard/dashboardLogic' import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent' import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets' import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput' -import { FeatureFlagPermissions } from 'scenes/FeatureFlagPermissions' import { concatWithPunctuation } from 'scenes/insights/utils' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' +import { ResourcePermission } from 'scenes/ResourcePermissionModal' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' @@ -57,12 +58,14 @@ import { PropertyOperator, QueryBasedInsightModel, ReplayTabs, + Resource, } from '~/types' import { AnalysisTab } from './FeatureFlagAnalysisTab' import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout' import { FeatureFlagCodeExample } from './FeatureFlagCodeExample' import { featureFlagLogic, getRecordingFilterForFlagVariant } from './featureFlagLogic' +import { featureFlagPermissionsLogic } from './featureFlagPermissionsLogic' import FeatureFlagProjects from './FeatureFlagProjects' import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions' import FeatureFlagSchedule from './FeatureFlagSchedule' @@ -100,6 +103,13 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { setActiveTab, } = useActions(featureFlagLogic) + const { addableRoles, unfilteredAddableRolesLoading, rolesToAdd, derivedRoles } = useValues( + featureFlagPermissionsLogic({ flagId: featureFlag.id }) + ) + const { setRolesToAdd, addAssociatedRoles, deleteAssociatedRole } = useActions( + featureFlagPermissionsLogic({ flagId: featureFlag.id }) + ) + const { tags } = useValues(tagsModel) const { hasAvailableFeature } = useValues(userLogic) @@ -211,7 +221,21 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element { tabs.push({ label: 'Permissions', key: FeatureFlagsTab.PERMISSIONS, - content: , + content: ( + + setRolesToAdd(roleIds)} + rolesToAdd={rolesToAdd} + addableRoles={addableRoles} + addableRolesLoading={unfilteredAddableRolesLoading} + onAdd={() => addAssociatedRoles()} + roles={derivedRoles} + deleteAssociatedRole={(id) => deleteAssociatedRole({ roleId: id })} + canEdit={featureFlag.can_edit} + /> + + ), }) } @@ -409,7 +433,21 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {

Permissions

- + + setRolesToAdd(roleIds)} + rolesToAdd={rolesToAdd} + addableRoles={addableRoles} + addableRolesLoading={unfilteredAddableRolesLoading} + onAdd={() => addAssociatedRoles()} + roles={derivedRoles} + deleteAssociatedRole={(id) => + deleteAssociatedRole({ roleId: id }) + } + canEdit={featureFlag.can_edit} + /> +
diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index 4a9fd05113d3e..978348e795149 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -23,11 +23,9 @@ import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' import { groupsModel } from '~/models/groupsModel' import { getQueryBasedInsightModel } from '~/queries/nodes/InsightViz/utils' import { - ActivityScope, AvailableFeature, Breadcrumb, CohortType, @@ -975,19 +973,6 @@ export const featureFlagLogic = kea([ { key: [Scene.FeatureFlag, featureFlag.id || 'unknown'], name: featureFlag.key || 'Unnamed' }, ], ], - [SIDE_PANEL_CONTEXT_KEY]: [ - (s) => [s.featureFlag], - (featureFlag): SidePanelSceneContext | null => { - return featureFlag?.id - ? { - activity_scope: ActivityScope.FEATURE_FLAG, - activity_item_id: `${featureFlag.id}`, - access_control_resource: 'feature_flag', - access_control_resource_id: `${featureFlag.id}`, - } - : null - }, - ], filteredDashboards: [ (s) => [s.dashboards, s.featureFlag], (dashboards, featureFlag) => { diff --git a/frontend/src/scenes/insights/insightSceneLogic.tsx b/frontend/src/scenes/insights/insightSceneLogic.tsx index ab58df3d1ec86..3f79ace2432d9 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.tsx +++ b/frontend/src/scenes/insights/insightSceneLogic.tsx @@ -15,7 +15,7 @@ import { teamLogic } from 'scenes/teamLogic' import { mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { cohortsModel } from '~/models/cohortsModel' import { groupsModel } from '~/models/groupsModel' import { getDefaultQuery } from '~/queries/nodes/InsightViz/utils' @@ -210,15 +210,13 @@ export const insightSceneLogic = kea([ ] }, ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ (s) => [s.insight], - (insight): SidePanelSceneContext | null => { - return insight?.id + (insight): ActivityFilters | null => { + return insight ? { - activity_scope: ActivityScope.INSIGHT, - activity_item_id: `${insight.id}`, - access_control_resource: 'insight', - access_control_resource_id: `${insight.id}`, + scope: ActivityScope.INSIGHT, + item_id: `${insight.id}`, } : null }, diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx new file mode 100644 index 0000000000000..1a9233289616c --- /dev/null +++ b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx @@ -0,0 +1,104 @@ +import { IconCopy } from '@posthog/icons' +import { LemonBanner, LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { LemonDialog } from 'lib/lemon-ui/LemonDialog' +import { base64Encode } from 'lib/utils' +import { copyToClipboard } from 'lib/utils/copyToClipboard' +import posthog from 'posthog-js' +import { useState } from 'react' +import { urls } from 'scenes/urls' + +import { notebookLogic } from './notebookLogic' + +export type NotebookShareProps = { + shortId: string +} +export function NotebookShare({ shortId }: NotebookShareProps): JSX.Element { + const { content, isLocalOnly } = useValues(notebookLogic({ shortId })) + + const notebookUrl = urls.absolute(urls.currentProject(urls.notebook(shortId))) + const canvasUrl = urls.absolute(urls.canvas()) + `#🦔=${base64Encode(JSON.stringify(content))}` + + const [interestTracked, setInterestTracked] = useState(false) + + const trackInterest = (): void => { + posthog.capture('pressed interested in notebook sharing', { url: notebookUrl }) + } + + return ( +
+

Internal Link

+ {!isLocalOnly ? ( + <> +

+ Click the button below to copy a direct link to this Notebook. Make sure the person you + share it with has access to this PostHog project. +

+ } + onClick={() => void copyToClipboard(notebookUrl, 'notebook link')} + title={notebookUrl} + > + {notebookUrl} + + + + + ) : ( + +

This Notebook cannot be shared directly with others as it is only visible to you.

+
+ )} + +

Template Link

+

+ The link below will open a Canvas with the contents of this Notebook, allowing the receiver to view it, + edit it or create their own Notebook without affecting this one. +

+ } + onClick={() => void copyToClipboard(canvasUrl, 'canvas link')} + title={canvasUrl} + > + {canvasUrl} + + + + +

External Sharing

+ + { + if (!interestTracked) { + trackInterest() + setInterestTracked(true) + } + }, + }} + > + We don’t currently support sharing notebooks externally, but it’s on our roadmap! + +
+ ) +} + +export function openNotebookShareDialog({ shortId }: NotebookShareProps): void { + LemonDialog.open({ + title: 'Share notebook', + content: , + width: 600, + primaryButton: { + children: 'Close', + type: 'secondary', + }, + }) +} diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShareModal.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShareModal.tsx deleted file mode 100644 index 534599664149f..0000000000000 --- a/frontend/src/scenes/notebooks/Notebook/NotebookShareModal.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { IconCopy, IconOpenSidebar } from '@posthog/icons' -import { LemonBanner, LemonButton, LemonDivider, LemonModal } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { SHARING_MODAL_WIDTH } from 'lib/components/Sharing/SharingModal' -import { base64Encode } from 'lib/utils' -import { copyToClipboard } from 'lib/utils/copyToClipboard' -import posthog from 'posthog-js' -import { useState } from 'react' -import { urls } from 'scenes/urls' - -import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' -import { SidePanelTab } from '~/types' - -import { notebookLogic } from './notebookLogic' - -export type NotebookShareModalProps = { - shortId: string -} - -export function NotebookShareModal({ shortId }: NotebookShareModalProps): JSX.Element { - const { content, isLocalOnly, isShareModalOpen } = useValues(notebookLogic({ shortId })) - const { closeShareModal } = useActions(notebookLogic({ shortId })) - const { openSidePanel } = useActions(sidePanelStateLogic) - - const notebookUrl = urls.absolute(urls.currentProject(urls.notebook(shortId))) - const canvasUrl = urls.absolute(urls.canvas()) + `#🦔=${base64Encode(JSON.stringify(content))}` - - const [interestTracked, setInterestTracked] = useState(false) - - const trackInterest = (): void => { - posthog.capture('pressed interested in notebook sharing', { url: notebookUrl }) - } - - return ( - closeShareModal()} - isOpen={isShareModalOpen} - width={SHARING_MODAL_WIDTH} - footer={ - - Done - - } - > -
- - <> -
-

Access control

- - Permissions have moved! We're rolling out our new access control system. Click below to - open it. - - } - onClick={() => { - openSidePanel(SidePanelTab.AccessControl) - closeShareModal() - }} - > - Open access control - -
- - -
-

Internal Link

- {!isLocalOnly ? ( - <> -

- Click the button below to copy a direct link to this Notebook. Make sure the person - you share it with has access to this PostHog project. -

- } - onClick={() => void copyToClipboard(notebookUrl, 'notebook link')} - title={notebookUrl} - > - {notebookUrl} - - - - - ) : ( - -

This Notebook cannot be shared directly with others as it is only visible to you.

-
- )} - -

Template Link

-

- The link below will open a Canvas with the contents of this Notebook, allowing the receiver to view - it, edit it or create their own Notebook without affecting this one. -

- } - onClick={() => void copyToClipboard(canvasUrl, 'canvas link')} - title={canvasUrl} - > - {canvasUrl} - - - - -

External Sharing

- - { - if (!interestTracked) { - trackInterest() - setInterestTracked(true) - } - }, - }} - > - We don’t currently support sharing notebooks externally, but it’s on our roadmap! - -
-
- ) -} diff --git a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-12345.json b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-12345.json index 4e31800d43919..f2ac6bd3c8d16 100644 --- a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-12345.json +++ b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-12345.json @@ -59,6 +59,5 @@ "first_name": "Paul", "email": "paul@posthog.com", "is_email_verified": false - }, - "user_access_level": "editor" + } } diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index 68fc4d6e7f0f1..bc0593c22bff3 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -133,17 +133,8 @@ export const notebookLogic = kea([ setContainerSize: (containerSize: 'small' | 'medium') => ({ containerSize }), insertComment: (context: Record) => ({ context }), selectComment: (itemContextId: string) => ({ itemContextId }), - openShareModal: true, - closeShareModal: true, }), reducers(({ props }) => ({ - isShareModalOpen: [ - false, - { - openShareModal: () => true, - closeShareModal: () => false, - }, - ], localContent: [ null as JSONContent | null, { persist: props.mode !== 'canvas', prefix: NOTEBOOKS_VERSION }, @@ -357,9 +348,9 @@ export const notebookLogic = kea([ mode: [() => [(_, props) => props], (props): NotebookLogicMode => props.mode ?? 'notebook'], isTemplate: [(s) => [s.shortId], (shortId): boolean => shortId.startsWith('template-')], isLocalOnly: [ - (s) => [(_, props) => props, s.isTemplate], - (props, isTemplate): boolean => { - return props.shortId === 'scratchpad' || props.mode === 'canvas' || isTemplate + () => [(_, props) => props], + (props): boolean => { + return props.shortId === 'scratchpad' || props.mode === 'canvas' }, ], notebookMissing: [ @@ -452,9 +443,8 @@ export const notebookLogic = kea([ ], isEditable: [ - (s) => [s.shouldBeEditable, s.previewContent, s.notebook], - (shouldBeEditable, previewContent, notebook) => - shouldBeEditable && !previewContent && notebook?.user_access_level === 'editor', + (s) => [s.shouldBeEditable, s.previewContent], + (shouldBeEditable, previewContent) => shouldBeEditable && !previewContent, ], }), listeners(({ values, actions, cache }) => ({ @@ -528,11 +518,6 @@ export const notebookLogic = kea([ ) }, setLocalContent: async ({ updateEditor, jsonContent }, breakpoint) => { - if (values.notebook?.user_access_level !== 'editor') { - actions.clearLocalContent() - return - } - if (values.previewContent) { // We don't want to modify the content if we are viewing a preview return diff --git a/frontend/src/scenes/notebooks/NotebookMenu.tsx b/frontend/src/scenes/notebooks/NotebookMenu.tsx index aeeebfa35cdfa..9cea5e74fbe37 100644 --- a/frontend/src/scenes/notebooks/NotebookMenu.tsx +++ b/frontend/src/scenes/notebooks/NotebookMenu.tsx @@ -10,10 +10,10 @@ import { urls } from 'scenes/urls' import { notebooksModel } from '~/models/notebooksModel' import { notebookLogic, NotebookLogicProps } from './Notebook/notebookLogic' +import { openNotebookShareDialog } from './Notebook/NotebookShare' export function NotebookMenu({ shortId }: NotebookLogicProps): JSX.Element { const { notebook, showHistory, isLocalOnly } = useValues(notebookLogic({ shortId })) - const { openShareModal } = useActions(notebookLogic({ shortId })) const { exportJSON, setShowHistory } = useActions(notebookLogic({ shortId })) return ( @@ -32,17 +32,14 @@ export function NotebookMenu({ shortId }: NotebookLogicProps): JSX.Element { { label: 'Share', icon: , - onClick: () => openShareModal(), + onClick: () => openNotebookShareDialog({ shortId }), }, !isLocalOnly && !notebook?.is_template && { label: 'Delete', icon: , status: 'danger', - disabledReason: - notebook?.user_access_level !== 'editor' - ? 'You do not have permission to delete this notebook.' - : undefined, + onClick: () => { notebooksModel.actions.deleteNotebook(shortId, notebook?.title) router.actions.push(urls.notebooks()) diff --git a/frontend/src/scenes/notebooks/NotebookScene.tsx b/frontend/src/scenes/notebooks/NotebookScene.tsx index a0cc87a441c74..e24c3bdd498c5 100644 --- a/frontend/src/scenes/notebooks/NotebookScene.tsx +++ b/frontend/src/scenes/notebooks/NotebookScene.tsx @@ -14,7 +14,6 @@ import { Notebook } from './Notebook/Notebook' import { NotebookLoadingState } from './Notebook/NotebookLoadingState' import { notebookLogic } from './Notebook/notebookLogic' import { NotebookExpandButton, NotebookSyncInfo } from './Notebook/NotebookMeta' -import { NotebookShareModal } from './Notebook/NotebookShareModal' import { NotebookMenu } from './NotebookMenu' import { notebookPanelLogic } from './NotebookPanel/notebookPanelLogic' import { notebookSceneLogic, NotebookSceneLogicProps } from './notebookSceneLogic' @@ -129,7 +128,6 @@ export function NotebookScene(): JSX.Element {
- ) } diff --git a/frontend/src/scenes/notebooks/notebookSceneLogic.ts b/frontend/src/scenes/notebooks/notebookSceneLogic.ts index 6d987f3a780a4..592a1b39e09ed 100644 --- a/frontend/src/scenes/notebooks/notebookSceneLogic.ts +++ b/frontend/src/scenes/notebooks/notebookSceneLogic.ts @@ -2,9 +2,8 @@ import { afterMount, connect, kea, key, path, props, selectors } from 'kea' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' import { notebooksModel } from '~/models/notebooksModel' -import { ActivityScope, Breadcrumb } from '~/types' +import { Breadcrumb } from '~/types' import { notebookLogic } from './Notebook/notebookLogic' import type { notebookSceneLogicType } from './notebookSceneLogicType' @@ -17,12 +16,7 @@ export const notebookSceneLogic = kea([ props({} as NotebookSceneLogicProps), key(({ shortId }) => shortId), connect((props: NotebookSceneLogicProps) => ({ - values: [ - notebookLogic(props), - ['notebook', 'notebookLoading', 'isLocalOnly'], - notebooksModel, - ['notebooksLoading'], - ], + values: [notebookLogic(props), ['notebook', 'notebookLoading'], notebooksModel, ['notebooksLoading']], actions: [notebookLogic(props), ['loadNotebook'], notebooksModel, ['createNotebook']], })), selectors(() => ({ @@ -47,20 +41,6 @@ export const notebookSceneLogic = kea([ }, ], ], - - [SIDE_PANEL_CONTEXT_KEY]: [ - (s) => [s.notebookId, s.isLocalOnly], - (notebookId, isLocalOnly): SidePanelSceneContext | null => { - return notebookId && !isLocalOnly - ? { - activity_scope: ActivityScope.NOTEBOOK, - activity_item_id: notebookId, - access_control_resource: 'notebook', - access_control_resource_id: notebookId, - } - : null - }, - ], })), afterMount(({ actions, props }) => { diff --git a/frontend/src/scenes/persons/personsLogic.tsx b/frontend/src/scenes/persons/personsLogic.tsx index fcfb21200a7c4..d408ec3a74ed0 100644 --- a/frontend/src/scenes/persons/personsLogic.tsx +++ b/frontend/src/scenes/persons/personsLogic.tsx @@ -13,7 +13,7 @@ import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { hogqlQuery } from '~/queries/query' import { ActivityScope, @@ -256,13 +256,13 @@ export const personsLogic = kea([ }, ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ (s) => [s.person], - (person): SidePanelSceneContext => { + (person): ActivityFilters => { return { - activity_scope: ActivityScope.PERSON, + scope: ActivityScope.PERSON, // TODO: Is this correct? It doesn't seem to work... - activity_item_id: person?.id ? `${person?.id}` : undefined, + item_id: person?.id ? `${person?.id}` : undefined, } }, ], diff --git a/frontend/src/scenes/pipeline/pipelineLogic.tsx b/frontend/src/scenes/pipeline/pipelineLogic.tsx index 23438fbe86185..38ea5f1d54b4f 100644 --- a/frontend/src/scenes/pipeline/pipelineLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineLogic.tsx @@ -5,7 +5,7 @@ import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { ActivityScope, Breadcrumb, PipelineTab } from '~/types' import type { pipelineLogicType } from './pipelineLogicType' @@ -44,11 +44,11 @@ export const pipelineLogic = kea([ }, ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ () => [], - (): SidePanelSceneContext => { + (): ActivityFilters | null => { return { - activity_scope: ActivityScope.PLUGIN, + scope: ActivityScope.PLUGIN, } }, ], diff --git a/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx b/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx index 2d2e7b977aec5..4faedce085b8a 100644 --- a/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx @@ -4,7 +4,7 @@ import { capitalizeFirstLetter } from 'lib/utils' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { ActivityScope, Breadcrumb, PipelineNodeTab, PipelineStage } from '~/types' import type { pipelineNodeLogicType } from './pipelineNodeLogicType' @@ -78,15 +78,13 @@ export const pipelineNodeLogic = kea([ ], ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ (s) => [s.node], - (node): SidePanelSceneContext | null => { + (node): ActivityFilters | null => { return node.backend === PipelineBackend.Plugin ? { - activity_scope: ActivityScope.PLUGIN, - activity_item_id: `${node.id}`, - // access_control_resource: 'plugin', - // access_control_resource_id: `${node.id}`, + scope: ActivityScope.PLUGIN, + item_id: `${node.id}`, } : null }, diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 98990142b8d32..3f007fdaf0e6b 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -501,7 +501,6 @@ export const redirects: Record< '/apps': urls.pipeline(PipelineTab.Overview), '/apps/:id': ({ id }) => urls.pipelineNode(PipelineStage.Transformation, id), '/messaging': urls.messagingBroadcasts(), - '/settings/organization-rbac': urls.settings('organization-roles'), } export const routes: Record = { diff --git a/frontend/src/scenes/session-recordings/sessionReplaySceneLogic.ts b/frontend/src/scenes/session-recordings/sessionReplaySceneLogic.ts index 246fa495ba5a2..5f1bee532fdaa 100644 --- a/frontend/src/scenes/session-recordings/sessionReplaySceneLogic.ts +++ b/frontend/src/scenes/session-recordings/sessionReplaySceneLogic.ts @@ -6,7 +6,7 @@ import { capitalizeFirstLetter } from 'lib/utils' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { SIDE_PANEL_CONTEXT_KEY, SidePanelSceneContext } from '~/layout/navigation-3000/sidepanel/types' +import { ActivityFilters } from '~/layout/navigation-3000/sidepanel/panels/activity/activityForSceneLogic' import { ActivityScope, Breadcrumb, ReplayTabs } from '~/types' import type { sessionReplaySceneLogicType } from './sessionReplaySceneLogicType' @@ -92,13 +92,13 @@ export const sessionReplaySceneLogic = kea([ return breadcrumbs }, ], - [SIDE_PANEL_CONTEXT_KEY]: [ + activityFilters: [ () => [router.selectors.searchParams], - (searchParams): SidePanelSceneContext | null => { + (searchParams): ActivityFilters | null => { return searchParams.sessionRecordingId ? { - activity_scope: ActivityScope.REPLAY, - activity_item_id: searchParams.sessionRecordingId, + scope: ActivityScope.REPLAY, + item_id: searchParams.sessionRecordingId, } : null }, diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 0a2e3e432a2fb..8985441f89067 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -50,7 +50,7 @@ import { OrganizationDangerZone } from './organization/OrganizationDangerZone' import { OrganizationDisplayName } from './organization/OrgDisplayName' import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' import { OrganizationLogo } from './organization/OrgLogo' -import { RoleBasedAccess } from './organization/Permissions/RoleBasedAccess' +import { PermissionsGrid } from './organization/Permissions/PermissionsGrid' import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' import { ProjectDangerZone } from './project/ProjectDangerZone' import { ProjectDisplayName, ProjectProductDescription } from './project/ProjectSettings' @@ -314,11 +314,11 @@ export const SETTINGS_MAP: SettingSection[] = [ }, { level: 'environment', - id: 'environment-access-control', + id: 'environment-rbac', title: 'Access control', settings: [ { - id: 'environment-access-control', + id: 'environment-rbac', title: 'Access control', component: , }, @@ -413,25 +413,25 @@ export const SETTINGS_MAP: SettingSection[] = [ }, { level: 'organization', - id: 'organization-roles', - title: 'Roles', + id: 'organization-authentication', + title: 'Authentication domains & SSO', settings: [ { - id: 'organization-roles', - title: 'Roles', - component: , + id: 'authentication-domains', + title: 'Authentication Domains', + component: , }, ], }, { level: 'organization', - id: 'organization-authentication', - title: 'Authentication domains & SSO', + id: 'organization-rbac', + title: 'Role-based access', settings: [ { - id: 'authentication-domains', - title: 'Authentication Domains', - component: , + id: 'organization-rbac', + title: 'Role-based access', + component: , }, ], }, diff --git a/frontend/src/scenes/settings/environment/TeamAccessControl.tsx b/frontend/src/scenes/settings/environment/TeamAccessControl.tsx index 6674c261800e8..88cfdf5f2caee 100644 --- a/frontend/src/scenes/settings/environment/TeamAccessControl.tsx +++ b/frontend/src/scenes/settings/environment/TeamAccessControl.tsx @@ -4,7 +4,6 @@ import { useActions, useValues } from 'kea' import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { IconCancel } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' @@ -20,7 +19,6 @@ import { organizationLogic } from 'scenes/organizationLogic' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' -import { AccessControlObject } from '~/layout/navigation-3000/sidepanel/panels/access_control/AccessControlObject' import { AvailableFeature, FusedTeamMemberType } from '~/types' import { AddMembersModalWithButton } from './AddMembersModal' @@ -156,7 +154,7 @@ export function TeamMembers(): JSX.Element | null { title: 'Name', key: 'user_first_name', render: (_, member) => - member.user.uuid == user.uuid ? `${member.user.first_name} (you)` : member.user.first_name, + 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), }, { @@ -216,11 +214,6 @@ export function TeamAccessControl(): JSX.Element { minimumAccessLevel: OrganizationMembershipLevel.Admin, }) - const newAccessControl = useFeatureFlag('ROLE_BASED_ACCESS_CONTROL') - if (newAccessControl) { - return - } - return ( <>

diff --git a/frontend/src/scenes/settings/organization/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx index 42face838324a..997582fa81982 100644 --- a/frontend/src/scenes/settings/organization/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -1,7 +1,6 @@ import { LemonInput, LemonSwitch } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { useRestrictedArea } from 'lib/components/RestrictedArea' import { TZLabel } from 'lib/components/TZLabel' import { OrganizationMembershipLevel } from 'lib/constants' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -142,12 +141,11 @@ export function Members(): JSX.Element | null { const { currentOrganization } = useValues(organizationLogic) const { preflight } = useValues(preflightLogic) const { user } = useValues(userLogic) + const { setSearch, ensureAllMembersLoaded } = useActions(membersLogic) const { updateOrganization } = useActions(organizationLogic) const { openTwoFactorSetupModal } = useActions(twoFactorLogic) - const twoFactorRestrictionReason = useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) - useEffect(() => { ensureAllMembersLoaded() }, []) @@ -168,7 +166,7 @@ export function Members(): JSX.Element | null { title: 'Name', key: 'user_name', render: (_, member) => - member.user.uuid == user.uuid ? `${fullName(member.user)} (you)` : fullName(member.user), + member.user.uuid == user.uuid ? `${fullName(member.user)} (me)` : fullName(member.user), sorter: (a, b) => fullName(a.user).localeCompare(fullName(b.user)), }, { @@ -292,7 +290,6 @@ export function Members(): JSX.Element | null { bordered checked={!!currentOrganization?.enforce_2fa} onChange={(enforce_2fa) => updateOrganization({ enforce_2fa })} - disabledReason={twoFactorRestrictionReason} /> diff --git a/frontend/src/scenes/settings/organization/Permissions/RoleBasedAccess.tsx b/frontend/src/scenes/settings/organization/Permissions/RoleBasedAccess.tsx deleted file mode 100644 index 62ef8ff7a1f95..0000000000000 --- a/frontend/src/scenes/settings/organization/Permissions/RoleBasedAccess.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// NOTE: This is only to allow testing the new RBAC system - -import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' - -import { RolesAndResourceAccessControls } from '~/layout/navigation-3000/sidepanel/panels/access_control/RolesAndResourceAccessControls' - -import { PermissionsGrid } from './PermissionsGrid' - -export function RoleBasedAccess(): JSX.Element { - const newAccessControl = useFeatureFlag('ROLE_BASED_ACCESS_CONTROL') - return newAccessControl ? : -} diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index 56db33d95d3cf..0103298077232 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -24,8 +24,7 @@ export type SettingSectionId = | 'environment-surveys' | 'environment-toolbar' | 'environment-integrations' - | 'environment-access-control' - | 'environment-role-based-access-control' + | 'environment-rbac' | 'environment-danger-zone' | 'project-details' | 'project-autocapture' // TODO: This section is for backward compat – remove when Environments are rolled out @@ -34,13 +33,12 @@ export type SettingSectionId = | 'project-surveys' // TODO: This section is for backward compat – remove when Environments are rolled out | 'project-toolbar' // TODO: This section is for backward compat – remove when Environments are rolled out | 'project-integrations' // TODO: This section is for backward compat – remove when Environments are rolled out - | 'project-access-control' // TODO: This section is for backward compat – remove when Environments are rolled out - | 'project-role-based-access-control' // TODO: This section is for backward compat – remove when Environments are rolled out + | 'project-rbac' // TODO: This section is for backward compat – remove when Environments are rolled out | 'project-danger-zone' | 'organization-details' | 'organization-members' | 'organization-authentication' - | 'organization-roles' + | 'organization-rbac' | 'organization-proxy' | 'organization-danger-zone' | 'user-profile' @@ -74,8 +72,7 @@ export type SettingId = | 'integration-slack' | 'integration-other' | 'integration-ip-allowlist' - | 'environment-access-control' - | 'environment-role-based-access-control' + | 'environment-rbac' | 'environment-delete' | 'project-delete' | 'organization-logo' @@ -84,7 +81,7 @@ export type SettingId = | 'members' | 'email-members' | 'authentication-domains' - | 'organization-roles' + | 'organization-rbac' | 'organization-delete' | 'organization-proxy' | 'product-description' diff --git a/frontend/src/scenes/teamLogic.tsx b/frontend/src/scenes/teamLogic.tsx index 19cb9ac10c840..b27c8621db68a 100644 --- a/frontend/src/scenes/teamLogic.tsx +++ b/frontend/src/scenes/teamLogic.tsx @@ -188,8 +188,7 @@ export const teamLogic = kea([ (selectors) => [selectors.currentTeam, selectors.currentTeamLoading], // If project has been loaded and is still null, it means the user just doesn't have access. (currentTeam, currentTeamLoading): boolean => - (!currentTeam?.effective_membership_level || currentTeam.user_access_level === 'none') && - !currentTeamLoading, + !currentTeam?.effective_membership_level && !currentTeamLoading, ], demoOnlyProject: [ (selectors) => [selectors.currentTeam, organizationLogic.selectors.currentOrganization], @@ -211,9 +210,8 @@ export const teamLogic = kea([ isTeamTokenResetAvailable: [ (selectors) => [selectors.currentTeam], (currentTeam): boolean => - (!!currentTeam?.effective_membership_level && - currentTeam.effective_membership_level >= OrganizationMembershipLevel.Admin) || - currentTeam?.user_access_level === 'admin', + !!currentTeam?.effective_membership_level && + currentTeam.effective_membership_level >= OrganizationMembershipLevel.Admin, ], testAccountFilterFrequentMistakes: [ (selectors) => [selectors.currentTeam], diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d8e85d4cdc027..317b046e7acf6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -4488,7 +4488,7 @@ export enum SidePanelTab { Discussion = 'discussion', Status = 'status', Exports = 'exports', - AccessControl = 'access-control', + // AccessControl = 'access-control', } export interface SourceFieldOauthConfig {