Skip to content

Commit

Permalink
chore: disable membership settings for developer plans (#80226)
Browse files Browse the repository at this point in the history
since developer plans can only have 1 member, we want to disable
membership settings for organizations on a developer plan. we can do
this using a new `component:organization-membership-settings` hook (see
getsentry/getsentry#15697 for full context)

<img width="793" alt="Screenshot 2024-11-12 at 10 51 42 AM"
src="https://github.com/user-attachments/assets/1d159406-a3cd-40c7-a48f-e626881eaba8">
  • Loading branch information
ameliahsu authored Nov 12, 2024
1 parent c018f01 commit 129b482
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 85 deletions.
81 changes: 0 additions & 81 deletions static/app/data/forms/organizationGeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {JsonFormObject} from 'sentry/components/forms/types';
import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import ConfigStore from 'sentry/stores/configStore';
import type {BaseRole} from 'sentry/types/organization';
import slugify from 'sentry/utils/slugify';

// Export route to make these forms searchable by label/help
Expand Down Expand Up @@ -72,86 +71,6 @@ const formGroups: JsonFormObject[] = [
},
],
},

{
title: 'Membership',
fields: [
{
name: 'defaultRole',
type: 'select',
label: t('Default Role'),
// seems weird to have choices in initial form data
choices: ({initialData} = {}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
help: t('The default role new members will receive'),
disabled: ({access}) => !access.has('org:admin'),
},
{
name: 'openMembership',
type: 'boolean',
label: t('Open Team Membership'),
help: t('Allow organization members to freely join any team'),
},
{
name: 'allowMemberInvite',
type: 'boolean',
label: t('Let Members Invite Others'),
help: t(
'Allow organization members to invite other members via email without needing org owner or manager approval.'
),
visible: ({features}) => features.has('members-invite-teammates'),
},
{
name: 'allowMemberProjectCreation',
type: 'boolean',
label: t('Let Members Create Projects'),
help: t('Allow organization members to create and configure new projects.'),
disabled: ({features, access}) =>
!access.has('org:write') || !features.has('team-roles'),
disabledReason: ({features}) =>
!features.has('team-roles')
? t('You must be on a business plan to toggle this feature.')
: undefined,
},
{
name: 'eventsMemberAdmin',
type: 'boolean',
label: t('Let Members Delete Events'),
help: t(
'Allow members to delete events (including the delete & discard action) by granting them the `event:admin` scope.'
),
},
{
name: 'alertsMemberWrite',
type: 'boolean',
label: t('Let Members Create and Edit Alerts'),
help: t(
'Allow members to create, edit, and delete alert rules by granting them the `alerts:write` scope.'
),
},
{
name: 'attachmentsRole',
type: 'select',
choices: ({initialData = {}}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
label: t('Attachments Access'),
help: t(
'Role required to download event attachments, such as native crash reports or log files.'
),
visible: ({features}) => features.has('event-attachments'),
},
{
name: 'debugFilesRole',
type: 'select',
choices: ({initialData = {}}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
label: t('Debug Files Access'),
help: t(
'Role required to download debug information files, proguard mappings and source maps.'
),
},
],
},
];

export default formGroups;
84 changes: 84 additions & 0 deletions static/app/data/forms/organizationMembershipSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type {JsonFormObject} from 'sentry/components/forms/types';
import {t} from 'sentry/locale';
import type {BaseRole} from 'sentry/types/organization';

// Export route to make these forms searchable by label/help
export const route = '/settings/:orgId/';

const formGroups: JsonFormObject[] = [
{
title: 'Membership',
fields: [
{
name: 'defaultRole',
type: 'select',
label: t('Default Role'),
// seems weird to have choices in initial form data
choices: ({initialData} = {}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
help: t('The default role new members will receive'),
disabled: ({access}) => !access.has('org:admin'),
},
{
name: 'openMembership',
type: 'boolean',
label: t('Open Team Membership'),
help: t('Allow organization members to freely join any team'),
},
{
name: 'allowMemberInvite',
type: 'boolean',
label: t('Let Members Invite Others'),
help: t(
'Allow organization members to invite other members via email without needing org owner or manager approval.'
),
visible: ({features}) => features.has('members-invite-teammates'),
},
{
name: 'allowMemberProjectCreation',
type: 'boolean',
label: t('Let Members Create Projects'),
help: t('Allow organization members to create and configure new projects.'),
},
{
name: 'eventsMemberAdmin',
type: 'boolean',
label: t('Let Members Delete Events'),
help: t(
'Allow members to delete events (including the delete & discard action) by granting them the `event:admin` scope.'
),
},
{
name: 'alertsMemberWrite',
type: 'boolean',
label: t('Let Members Create and Edit Alerts'),
help: t(
'Allow members to create, edit, and delete alert rules by granting them the `alerts:write` scope.'
),
},
{
name: 'attachmentsRole',
type: 'select',
choices: ({initialData = {}}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
label: t('Attachments Access'),
help: t(
'Role required to download event attachments, such as native crash reports or log files.'
),
visible: ({features}) => features.has('event-attachments'),
},
{
name: 'debugFilesRole',
type: 'select',
choices: ({initialData = {}}) =>
initialData?.orgRoleList?.map((r: BaseRole) => [r.id, r.name]) ?? [],
label: t('Debug Files Access'),
help: t(
'Role required to download debug information files, proguard mappings and source maps.'
),
},
],
},
];

export default formGroups;
11 changes: 11 additions & 0 deletions static/app/types/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {ChildrenRenderFn} from 'sentry/components/acl/feature';
import type {Guide} from 'sentry/components/assistant/types';
import type {ButtonProps} from 'sentry/components/button';
import type {FormPanelProps} from 'sentry/components/forms/formPanel';
import type {JsonFormObject} from 'sentry/components/forms/types';
import type {
ProductSelectionProps,
ProductSolution,
Expand Down Expand Up @@ -170,6 +172,14 @@ export type PartnershipAgreementProps = {
organizationSlug?: string;
};

export type MembershipSettingsProps = {
forms: JsonFormObject[];
jsonFormSettings: Omit<
FormPanelProps,
'highlighted' | 'fields' | 'additionalFieldProps'
>;
};

/**
* Component wrapping hooks
*/
Expand Down Expand Up @@ -198,6 +208,7 @@ export type ComponentHooks = {
'component:monitor-status-toggle': () => React.ComponentType<StatusToggleButtonProps>;
'component:org-stats-banner': () => React.ComponentType<DashboardHeadersProps>;
'component:organization-header': () => React.ComponentType<OrganizationHeaderProps>;
'component:organization-membership-settings': () => React.ComponentType<MembershipSettingsProps>;
'component:partnership-agreement': React.ComponentType<PartnershipAgreementProps>;
'component:product-selection-availability': () => React.ComponentType<ProductSelectionAvailabilityProps>;
'component:product-unavailable-cta': () => React.ComponentType<ProductUnavailableCTAProps>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import JsonForm from 'sentry/components/forms/jsonForm';
import type {FieldObject} from 'sentry/components/forms/types';
import HookOrDefault from 'sentry/components/hookOrDefault';
import {Hovercard} from 'sentry/components/hovercard';
import organizationSettingsFields from 'sentry/data/forms/organizationGeneralSettings';
import organizationGeneralSettingsFields from 'sentry/data/forms/organizationGeneralSettings';
import organizationMembershipSettingsFields from 'sentry/data/forms/organizationMembershipSettings';
import {IconCodecov, IconLock} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {MembershipSettingsProps} from 'sentry/types/hooks';
import type {Organization} from 'sentry/types/organization';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
Expand All @@ -26,6 +28,15 @@ const HookCodecovSettingsLink = HookOrDefault({
hookName: 'component:codecov-integration-settings-link',
});

const HookOrganizationMembershipSettings = HookOrDefault({
hookName: 'component:organization-membership-settings',
defaultComponent: defaultMembershipSettings,
});

function defaultMembershipSettings({jsonFormSettings, forms}: MembershipSettingsProps) {
return <JsonForm {...jsonFormSettings} forms={forms} />;
}

interface Props {
initialData: Organization;
onSave: (previous: Organization, updated: Organization) => void;
Expand All @@ -48,8 +59,8 @@ function OrganizationSettingsForm({initialData, onSave}: Props) {
[access, location, organization]
);

const forms = useMemo(() => {
const formsConfig = cloneDeep(organizationSettingsFields);
const generalForms = useMemo(() => {
const formsConfig = cloneDeep(organizationGeneralSettingsFields);
const organizationIdField: FieldObject = {
name: 'organizationId',
type: 'string',
Expand Down Expand Up @@ -129,7 +140,11 @@ function OrganizationSettingsForm({initialData, onSave}: Props) {
}}
onSubmitError={() => addErrorMessage('Unable to save change')}
>
<JsonForm {...jsonFormSettings} forms={forms} />
<JsonForm {...jsonFormSettings} forms={generalForms} />
<HookOrganizationMembershipSettings
jsonFormSettings={jsonFormSettings}
forms={organizationMembershipSettingsFields}
/>
<AvatarChooser
type="organization"
allowGravatar={false}
Expand Down

0 comments on commit 129b482

Please sign in to comment.