Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: update organization member abilities #27232

Merged
merged 6 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions frontend/src/layout/navigation/OrganizationSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconPlus } from '@posthog/icons'
import { IconPlusSmall } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
Expand Down Expand Up @@ -57,7 +57,7 @@ export function NewOrganizationButton(): JSX.Element {

return (
<LemonButton
icon={<IconPlus />}
icon={<IconPlusSmall />}
onClick={() =>
guardAvailableFeature(
AvailableFeature.ORGANIZATIONS_PROJECTS,
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface LemonFileInputProps extends Pick<HTMLInputElement, 'multiple' |
* are the files currently being uploaded?
*/
loading?: boolean
/** Whether input field is disabled */
disabled?: boolean
Comment on lines +16 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our pattern is usually disabledReason to force someone to give a reason why something is disabled, which is a better user experience. Should we follow that pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabled reason looks to be used for buttons and such but just disabled on inputs because we don't show the tooltip on those.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just wondering if we should change that

/** if this is not provided then this component is the drop target
* and is styled when a file is dragged over it
* if this alternativeDropTargetRef is provided,
Expand All @@ -35,6 +37,7 @@ export const LemonFileInput = ({
onChange,
multiple,
loading,
disabled,
// e.g. '.json' or 'image/*'
accept,
alternativeDropTargetRef,
Expand Down Expand Up @@ -142,14 +145,21 @@ export const LemonFileInput = ({
'FileDropTarget flex flex-col gap-1',
!alternativeDropTargetRef?.current && drag && 'FileDropTarget--active'
)}
aria-disabled={disabled}
>
<label className="text-muted inline-flex flow-row items-center gap-1 cursor-pointer">
<label
className={clsx(
'text-muted inline-flex flow-row items-center gap-1',
disabled ? 'cursor-not-allowed' : 'cursor-pointer'
)}
>
<input
className="hidden"
type="file"
multiple={multiple}
accept={accept}
onChange={onInputChange}
disabled={disabled}
/>
{callToAction || (
<>
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/scenes/billing/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { Field, Form } from 'kea-forms'
import { router } from 'kea-router'
import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea'
import { supportLogic } from 'lib/components/Support/supportLogic'
import { OrganizationMembershipLevel } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
Expand Down Expand Up @@ -36,6 +38,11 @@ export function Billing(): JSX.Element {
const { preflight, isCloudOrDev } = useValues(preflightLogic)
const { openSupportForm } = useActions(supportLogic)

const restrictionReason = useRestrictedArea({
zlwaterfield marked this conversation as resolved.
Show resolved Hide resolved
minimumAccessLevel: OrganizationMembershipLevel.Admin,
scope: RestrictionScope.Organization,
})

if (preflight && !isCloudOrDev) {
router.actions.push(urls.default())
}
Expand All @@ -59,6 +66,20 @@ export function Billing(): JSX.Element {
)
}

if (restrictionReason) {
return (
<div className="space-y-4">
<h1>Billing</h1>
<LemonBanner type="warning">{restrictionReason}</LemonBanner>
<div className="flex">
<LemonButton type="primary" to={urls.default()}>
Go back home
</LemonButton>
</div>
</div>
)
}

if (!billing && !billingLoading) {
return (
<div className="space-y-4">
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/scenes/settings/environment/ManagedReverseProxy.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconEllipsis, IconInfo, IconPlus } from '@posthog/icons'
import { IconEllipsis, IconInfo } from '@posthog/icons'
import {
LemonBanner,
LemonButton,
Expand Down Expand Up @@ -148,13 +148,8 @@ export function ManagedReverseProxy(): JSX.Element {
</LemonBanner>
) : (
<div className="flex">
<LemonButton
onClick={showForm}
type="secondary"
icon={<IconPlus />}
disabledReason={restrictionReason}
>
New managed proxy
<LemonButton onClick={showForm} type="primary" disabledReason={restrictionReason}>
Add managed proxy
</LemonButton>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/settings/organization/InviteModal.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for making it so that members cannot invite new members generally? Since we don't charge for seats I don't think this is a good idea. Getting more people into posthog is always better, and bottlenecking that through admins and owners is probably a bad thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I've went back and forth on this. Generally it's not great practice to allow anyone to join your organization but I agree with you. I think I'll wind this back to how it is today and on another PR add a configurable option for organizations to set this - could be a teams add-on feature. We've had multiple customers ask for limiting who can invite.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated - there are no changes on this PR for sending invites, although it still blocks deleting invites.

Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export function InviteModal({ isOpen, onClose }: { isOpen: boolean; onClose: ()
<LemonButton
onClick={() => inviteTeamMembers()}
type="primary"
disabled={!canSubmit}
disabledReason={canSubmit ? undefined : 'Please fill out all fields'}
zlwaterfield marked this conversation as resolved.
Show resolved Hide resolved
data-attr="invite-team-member-submit"
>
{validInvitesCount
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/scenes/settings/organization/Invites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { IconX } from '@posthog/icons'
import { LemonTag } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { CopyToClipboardInline } from 'lib/components/CopyToClipboard'
import { useRestrictedArea } from 'lib/components/RestrictedArea'
import { RestrictionScope } from 'lib/components/RestrictedArea'
import { OrganizationMembershipLevel } from 'lib/constants'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonDialog } from 'lib/lemon-ui/LemonDialog'
Expand Down Expand Up @@ -61,6 +63,11 @@ export function Invites(): JSX.Element {
const { deleteInvite, showInviteModal } = useActions(inviteLogic)
const { preflight } = useValues(preflightLogic)

const restrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Admin,
scope: RestrictionScope.Organization,
})

const columns: LemonTableColumns<OrganizationInviteType> = [
{
key: 'user_profile_picture',
Expand Down Expand Up @@ -106,7 +113,7 @@ export function Invites(): JSX.Element {
title: '',
key: 'actions',
width: 24,
render: makeActionsComponent(deleteInvite),
render: restrictionReason ? undefined : makeActionsComponent(deleteInvite),
zlwaterfield marked this conversation as resolved.
Show resolved Hide resolved
},
]

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/scenes/settings/organization/OrgDisplayName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export function OrganizationDisplayName(): JSX.Element {
updateOrganization({ name })
}}
disabledReason={
!name
restrictionReason
? restrictionReason
: !name
? 'You must provide a name'
: !currentOrganization
? 'Organization not loaded'
: currentOrganization.name === name
? 'Name unchanged'
: restrictionReason
: undefined
}
loading={currentOrganizationLoading}
>
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/scenes/settings/organization/OrgLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function OrganizationLogo(): JSX.Element {
onChange={setFilesToUpload}
loading={uploading}
value={filesToUpload}
disabled={!!restrictionReason}
callToAction={
<>
<div className="relative">
Expand Down Expand Up @@ -76,11 +77,13 @@ export function OrganizationLogo(): JSX.Element {
updateOrganization({ logo_media_id: logoMediaId })
}}
disabledReason={
!currentOrganization
restrictionReason
? restrictionReason
: !currentOrganization
? 'Organization not loaded'
: logoMediaId === currentOrganization.logo_media_id
? 'Logo unchanged'
: restrictionReason
: undefined
}
loading={currentOrganizationLoading || uploading}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IconTrash } from '@posthog/icons'
import { LemonButton, LemonInput, LemonModal } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { useRestrictedArea } from 'lib/components/RestrictedArea'
import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea'
import { OrganizationMembershipLevel } from 'lib/constants'
import { Dispatch, SetStateAction, useState } from 'react'
import { organizationLogic } from 'scenes/organizationLogic'
Expand Down Expand Up @@ -67,7 +67,11 @@ export function DeleteOrganizationModal({
export function OrganizationDangerZone(): JSX.Element {
const { currentOrganization } = useValues(organizationLogic)
const [isModalVisible, setIsModalVisible] = useState(false)
const restrictionReason = useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin })

const restrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Owner,
scope: RestrictionScope.Organization,
})

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IconInfo } from '@posthog/icons'
import { LemonButton, LemonCheckbox, LemonTable } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
import { useRestrictedArea } from 'lib/components/RestrictedArea'
import { RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea'
import { TitleWithIcon } from 'lib/components/TitleWithIcon'
import { OrganizationMembershipLevel } from 'lib/constants'
import { LemonTableColumns } from 'lib/lemon-ui/LemonTable'
Expand All @@ -22,7 +22,11 @@ export function PermissionsGrid(): JSX.Element {
const { roles, rolesLoading } = useValues(rolesLogic)
const { setRoleInFocus, openCreateRoleModal } = useActions(rolesLogic)
const { isAdminOrOwner } = useValues(organizationLogic)
const restrictionReason = useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) // TODO: check if this is correct

const restrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Admin,
scope: RestrictionScope.Organization,
})

const columns: LemonTableColumns<RoleType> = [
{
Expand Down Expand Up @@ -107,17 +111,24 @@ export function PermissionsGrid(): JSX.Element {
Edit organizational default permission levels for PostHog resources. Use roles to apply
permissions to specific sets of users.
</div>
{!restrictionReason && (
<LemonButton type="primary" onClick={openCreateRoleModal} data-attr="create-role-button">
Create role
</LemonButton>
)}
</div>

<LemonTable
columns={columns}
loading={rolesLoading || organizationResourcePermissionsLoading}
dataSource={[{ name: 'organization_default' } as RoleType, ...roles]}
/>

<LemonButton
type="primary"
onClick={openCreateRoleModal}
className="mt-4"
data-attr="create-role-button"
disabledReason={restrictionReason}
>
Create role
</LemonButton>

<CreateRoleModal />
</>
</PayGateMini>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ export interface SSOSelectInterface {
loading: boolean
onChange: (value: SSOProvider | '') => void
samlAvailable: boolean
disabledReason?: string | null
}

export function SSOSelect({ value, loading, onChange, samlAvailable }: SSOSelectInterface): JSX.Element | null {
export function SSOSelect({
value,
loading,
onChange,
samlAvailable,
disabledReason,
}: SSOSelectInterface): JSX.Element | null {
const { preflight } = useValues(preflightLogic)

if (!preflight) {
Expand Down Expand Up @@ -46,7 +53,7 @@ export function SSOSelect({ value, loading, onChange, samlAvailable }: SSOSelect
value={value}
options={options}
loading={loading}
disabledReason={loading ? 'Cannot change while loading' : undefined}
disabledReason={loading ? 'Cannot change while loading' : disabledReason}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this component not handle disabling while loading? I kinda figure it would since it has the loading prop. if not, it probably should?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but this is just adding a tooltip over it while it's loading. The loading props just disables it, doesn't give a tooltip.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, might be worth changing, so if no reason it just says "loading..." Otherwise can override if you want a custom loading message with disabledReason

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hestitant to make changes that affect such a high number of parts of the app - that would make it so anytime a button is loading there is a tooltip over it showing text (the LemonSelect uses LemonButton).

I think it's better to have a more explicit option like this for when you actually want a tooltip.

fullWidth
onChange={onChange}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { IconCheckCircle, IconInfo, IconLock, IconTrash, IconWarning } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
import { RestrictionScope } from 'lib/components/RestrictedArea'
import { useRestrictedArea } from 'lib/components/RestrictedArea'
import { OrganizationMembershipLevel } from 'lib/constants'
import { IconExclamation, IconOffline } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { More } from 'lib/lemon-ui/LemonButton/More'
Expand All @@ -27,6 +30,11 @@ export function VerifiedDomains(): JSX.Element {
const { verifiedDomainsLoading, updatingDomainLoading } = useValues(verifiedDomainsLogic)
const { setAddModalShown } = useActions(verifiedDomainsLogic)

const restrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Admin,
scope: RestrictionScope.Organization,
})

return (
<PayGateMini feature={AvailableFeature.AUTOMATIC_PROVISIONING}>
<p>
Expand All @@ -39,7 +47,7 @@ export function VerifiedDomains(): JSX.Element {
type="primary"
onClick={() => setAddModalShown(true)}
className="mt-4"
disabledReason={verifiedDomainsLoading || updatingDomainLoading ? 'loading...' : null}
disabledReason={verifiedDomainsLoading || updatingDomainLoading ? 'loading...' : restrictionReason}
>
Add domain
</LemonButton>
Expand All @@ -60,6 +68,11 @@ function VerifiedDomainsTable(): JSX.Element {
useActions(verifiedDomainsLogic)
const { preflight } = useValues(preflightLogic)

const restrictionReason = useRestrictedArea({
minimumAccessLevel: OrganizationMembershipLevel.Admin,
scope: RestrictionScope.Organization,
})

const columns: LemonTableColumns<OrganizationDomainType> = [
{
key: 'domain',
Expand Down Expand Up @@ -119,6 +132,7 @@ function VerifiedDomainsTable(): JSX.Element {
<LemonSwitch
checked={jit_provisioning_enabled}
disabled={updatingDomainLoading || !is_verified}
disabledReason={restrictionReason}
onChange={(checked) => updateDomain({ id, jit_provisioning_enabled: checked })}
label={
<span className="font-normal">{jit_provisioning_enabled ? 'Enabled' : 'Disabled'}</span>
Expand Down Expand Up @@ -157,6 +171,7 @@ function VerifiedDomainsTable(): JSX.Element {
loading={updatingDomainLoading}
onChange={(val) => updateDomain({ id, sso_enforcement: val })}
samlAvailable={has_saml}
disabledReason={restrictionReason}
/>
) : (
<i className="text-muted-alt">Verify domain to enable</i>
Expand Down Expand Up @@ -205,7 +220,7 @@ function VerifiedDomainsTable(): JSX.Element {
return is_verified ? (
<></>
) : (
<LemonButton type="primary" onClick={() => setVerifyModal(id)}>
<LemonButton type="primary" onClick={() => setVerifyModal(id)} disabledReason={restrictionReason}>
Verify
</LemonButton>
)
Expand All @@ -224,8 +239,10 @@ function VerifiedDomainsTable(): JSX.Element {
<LemonButton
onClick={() => setConfigureSAMLModalId(id)}
fullWidth
disabled={!isSAMLAvailable}
title={isSAMLAvailable ? undefined : 'Upgrade to enable SAML'}
disabledReason={
restrictionReason ||
(!isSAMLAvailable ? 'Upgrade to enable SAML' : undefined)
}
>
Configure SAML
</LemonButton>
Expand All @@ -249,6 +266,7 @@ function VerifiedDomainsTable(): JSX.Element {
}
fullWidth
icon={<IconTrash />}
disabledReason={restrictionReason}
>
Remove domain
</LemonButton>
Expand Down
Loading