Skip to content

Commit 746756d

Browse files
improvement(misc): settings organization, chat agent deploy request keys (#6195)
* improvement(misc): settings organization, chat agent deploy request keys * fix subtitle * adjust perms
1 parent b8ec114 commit 746756d

27 files changed

Lines changed: 919 additions & 110 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
168168
<Billing
169169
scope={organizationId ? 'organization' : 'account'}
170170
organizationId={organizationId ?? undefined}
171+
governingWorkspaceName={hostContext.workspace.name}
171172
creditUsageHref={`/workspace/${hostContext.workspace.id}/settings/billing/credit-usage`}
172173
/>
173174
)}

apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function CreditUsageView({ backHref = '/account/settings/billing' }: Cred
151151
return (
152152
<SettingsPanel
153153
back={{
154-
text: 'Billing',
154+
text: 'Subscription',
155155
icon: ArrowLeft,
156156
onSelect: () => router.push(backHref),
157157
}}

apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function CreditUsageLoading({ backHref }: CreditUsageLoadingProps) {
1717
return (
1818
<SettingsPanel
1919
back={{
20-
text: 'Billing',
20+
text: 'Subscription',
2121
icon: ArrowLeft,
2222
onSelect: () => router.push(backHref),
2323
}}

apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.test.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,12 @@ vi.mock(
161161
)
162162

163163
vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-panel', () => ({
164-
SettingsPanel: ({ children }: { children: ReactNode }) => <main>{children}</main>,
164+
SettingsPanel: ({ children, description }: { children: ReactNode; description?: string }) => (
165+
<main>
166+
{description && <p>{description}</p>}
167+
{children}
168+
</main>
169+
),
165170
}))
166171

167172
vi.mock(
@@ -259,7 +264,13 @@ describe('Billing payer scope', () => {
259264

260265
it('uses the target organization DTO for annual, canceled, credit, cap, and link state', async () => {
261266
await act(async () => {
262-
root.render(<Billing scope='organization' organizationId='org-target' />)
267+
root.render(
268+
<Billing
269+
scope='organization'
270+
organizationId='org-target'
271+
governingWorkspaceName='Production'
272+
/>
273+
)
263274
})
264275

265276
expect(mockUseSubscriptionData).toHaveBeenCalledWith(
@@ -270,6 +281,9 @@ describe('Billing payer scope', () => {
270281
container.querySelector('a[href="/workspace/organization-workspace/upgrade"]')?.textContent
271282
).toBe('Explore organization plans')
272283
expect(container.textContent).toContain('Organization Max for Teams plan')
284+
expect(container.textContent).toContain(
285+
'Target organization’s subscription governs Production.'
286+
)
273287
expect(container.textContent).toContain('billed annually')
274288
expect(container.textContent).toContain('Access until')
275289
expect(container.textContent).toContain('Subscription canceled')
@@ -293,13 +307,35 @@ describe('Billing payer scope', () => {
293307

294308
it('uses a guaranteed personal payer workspace for account upgrades', async () => {
295309
await act(async () => {
296-
root.render(<Billing scope='account' />)
310+
root.render(<Billing scope='account' governingWorkspaceName='Personal workspace' />)
297311
})
298312

299313
expect(
300314
container.querySelector('a[href="/workspace/personal-workspace/upgrade"]')?.textContent
301315
).toBe('Explore personal plans')
302316
expect(container.textContent).toContain('Personal Pro plan')
317+
expect(container.textContent).toContain(
318+
'Your personal subscription governs Personal workspace.'
319+
)
320+
})
321+
322+
it('does not show a governing subscription description for a free personal workspace', async () => {
323+
mockPersonalQuery.current = {
324+
data: {
325+
success: true,
326+
context: 'user',
327+
data: { ...PERSONAL_DATA, plan: 'free', status: 'active' },
328+
},
329+
isLoading: false,
330+
refetch: vi.fn(),
331+
}
332+
333+
await act(async () => {
334+
root.render(<Billing scope='account' governingWorkspaceName='Free workspace' />)
335+
})
336+
337+
expect(container.textContent).toContain('Personal Free plan')
338+
expect(container.querySelector('main > p')).toBeNull()
303339
})
304340

305341
it('renders an explicit free organization state without subscription controls', async () => {
@@ -319,12 +355,19 @@ describe('Billing payer scope', () => {
319355
}
320356

321357
await act(async () => {
322-
root.render(<Billing scope='organization' organizationId='org-target' />)
358+
root.render(
359+
<Billing
360+
scope='organization'
361+
organizationId='org-target'
362+
governingWorkspaceName='Free organization workspace'
363+
/>
364+
)
323365
})
324366

325367
expect(container.textContent).toContain('Organization Free plan')
326368
expect(container.textContent).toContain('No active organization subscription')
327369
expect(container.textContent).not.toContain('Payment method')
370+
expect(container.querySelector('main > p')).toBeNull()
328371
})
329372

330373
it('renders lapsed organization plans as ended rather than active', async () => {
@@ -342,12 +385,19 @@ describe('Billing payer scope', () => {
342385
}
343386

344387
await act(async () => {
345-
root.render(<Billing scope='organization' organizationId='org-target' />)
388+
root.render(
389+
<Billing
390+
scope='organization'
391+
organizationId='org-target'
392+
governingWorkspaceName='Lapsed organization workspace'
393+
/>
394+
)
346395
})
347396

348397
expect(container.textContent).toContain('Organization Max for Teams plan ended')
349398
expect(container.textContent).toContain('Choose a new plan for this organization')
350399
expect(container.textContent).not.toContain('Cancel subscription')
400+
expect(container.querySelector('main > p')).toBeNull()
351401
expect(
352402
container.querySelector('a[href="/workspace/organization-workspace/upgrade"]')?.textContent
353403
).toBe('Explore organization plans')

apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,15 @@ interface BillingProps {
102102
scope: 'account' | 'organization'
103103
organizationId?: string
104104
creditUsageHref?: string
105+
governingWorkspaceName?: string
105106
}
106107

107-
export function Billing({ scope, organizationId, creditUsageHref }: BillingProps) {
108+
export function Billing({
109+
scope,
110+
organizationId,
111+
creditUsageHref,
112+
governingWorkspaceName,
113+
}: BillingProps) {
108114
const router = useRouter()
109115
const isOrganizationScope = scope === 'organization'
110116

@@ -447,9 +453,16 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
447453
const explorePlansLabel = isOrganizationScope
448454
? 'Explore organization plans'
449455
: 'Explore personal plans'
456+
const subscriptionOwner = isOrganizationScope
457+
? `${organizationBilling?.organizationName ?? 'The organization'}’s subscription`
458+
: 'Your personal subscription'
459+
const settingsDescription =
460+
governingWorkspaceName && subscription.isPaid
461+
? `${subscriptionOwner} governs ${governingWorkspaceName}.`
462+
: undefined
450463

451464
return (
452-
<SettingsPanel>
465+
<SettingsPanel description={settingsDescription}>
453466
<div className='flex items-center justify-between gap-3'>
454467
<div className='flex items-center gap-2.5'>
455468
<div className='size-9 flex-shrink-0'>

apps/sim/app/workspace/[workspaceId]/settings/navigation.test.ts

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,88 @@ import {
2121
} from '@/app/workspace/[workspaceId]/settings/navigation'
2222

2323
describe('unified settings navigation', () => {
24-
it('preserves the original settings groups', () => {
24+
it('groups settings by the scope they affect', () => {
2525
expect(sectionConfig).toEqual([
2626
{ key: 'account', title: 'Account' },
27-
{ key: 'tools', title: 'Tools' },
28-
{ key: 'subscription', title: 'Subscription' },
29-
{ key: 'system', title: 'System' },
30-
{ key: 'desktop', title: 'Desktop' },
31-
{ key: 'enterprise', title: 'Enterprise' },
32-
{ key: 'superuser', title: 'Superuser' },
27+
{ key: 'workspace', title: 'Workspace' },
28+
{ key: 'organization', title: 'Organization' },
29+
{ key: 'platform', title: 'Platform' },
3330
])
3431
})
3532

3633
it('keeps account, workspace, organization, and platform settings in one catalog', () => {
3734
expect(allNavigationItems.map(({ id, label, section }) => ({ id, label, section }))).toEqual([
3835
{ id: 'general', label: 'General', section: 'account' },
39-
{ id: 'desktop', label: 'Desktop', section: 'desktop' },
40-
{ id: 'browser', label: 'Browser', section: 'desktop' },
41-
{ id: 'terminal', label: 'Terminal', section: 'desktop' },
42-
{ id: 'access-control', label: 'Access control', section: 'enterprise' },
43-
{ id: 'audit-logs', label: 'Audit logs', section: 'enterprise' },
44-
{ id: 'forks', label: 'Workspace Forks', section: 'enterprise' },
45-
{ id: 'billing', label: 'Billing', section: 'subscription' },
46-
{ id: 'teammates', label: 'Teammates', section: 'subscription' },
47-
{ id: 'organization', label: 'Organization', section: 'subscription' },
48-
{ id: 'secrets', label: 'Secrets', section: 'account' },
49-
{ id: 'custom-tools', label: 'Custom tools', section: 'tools' },
50-
{ id: 'mcp', label: 'MCP tools', section: 'tools' },
51-
{ id: 'apikeys', label: 'Sim API keys', section: 'system' },
52-
{ id: 'workflow-mcp-servers', label: 'MCP servers', section: 'system' },
53-
{ id: 'byok', label: 'BYOK', section: 'system' },
54-
{ id: 'sandboxes', label: 'Sandboxes', section: 'system' },
55-
{ id: 'inbox', label: 'Sim mailer', section: 'system' },
56-
{ id: 'recently-deleted', label: 'Recently deleted', section: 'system' },
57-
{ id: 'sso', label: 'Single sign-on', section: 'enterprise' },
58-
{ id: 'sessions', label: 'Session policies', section: 'enterprise' },
59-
{ id: 'data-retention', label: 'Data retention', section: 'enterprise' },
60-
{ id: 'data-drains', label: 'Data drains', section: 'enterprise' },
61-
{ id: 'whitelabeling', label: 'Whitelabeling', section: 'enterprise' },
62-
{ id: 'custom-blocks', label: 'Custom blocks', section: 'enterprise' },
63-
{ id: 'admin', label: 'Admin', section: 'superuser' },
64-
{ id: 'mothership', label: 'Mothership', section: 'superuser' },
36+
{ id: 'desktop', label: 'Desktop', section: 'account' },
37+
{ id: 'browser', label: 'Browser', section: 'account' },
38+
{ id: 'terminal', label: 'Terminal', section: 'account' },
39+
{ id: 'access-control', label: 'Permission groups', section: 'organization' },
40+
{ id: 'audit-logs', label: 'Audit logs', section: 'organization' },
41+
{ id: 'forks', label: 'Workspace forks', section: 'organization' },
42+
{ id: 'billing', label: 'Subscription', section: 'account' },
43+
{ id: 'teammates', label: 'Teammates', section: 'workspace' },
44+
{ id: 'organization', label: 'Members', section: 'organization' },
45+
{ id: 'secrets', label: 'Secrets', section: 'workspace' },
46+
{ id: 'custom-tools', label: 'Custom tools', section: 'workspace' },
47+
{ id: 'mcp', label: 'MCP tools', section: 'workspace' },
48+
{ id: 'apikeys', label: 'Sim API keys', section: 'workspace' },
49+
{ id: 'workflow-mcp-servers', label: 'MCP servers', section: 'workspace' },
50+
{ id: 'byok', label: 'BYOK', section: 'workspace' },
51+
{ id: 'sandboxes', label: 'Sandboxes', section: 'workspace' },
52+
{ id: 'inbox', label: 'Sim Mailer', section: 'workspace' },
53+
{ id: 'recently-deleted', label: 'Recently deleted', section: 'workspace' },
54+
{ id: 'sso', label: 'Single sign-on', section: 'organization' },
55+
{ id: 'sessions', label: 'Session policies', section: 'organization' },
56+
{ id: 'data-retention', label: 'Data retention', section: 'organization' },
57+
{ id: 'data-drains', label: 'Data drains', section: 'organization' },
58+
{ id: 'whitelabeling', label: 'White-labeling', section: 'organization' },
59+
{ id: 'custom-blocks', label: 'Custom blocks', section: 'organization' },
60+
{ id: 'admin', label: 'Admin', section: 'platform' },
61+
{ id: 'mothership', label: 'Mothership', section: 'platform' },
6562
])
6663
})
6764

65+
it('orders each scope around its primary settings', () => {
66+
const idsForSection = (section: (typeof sectionConfig)[number]['key']) =>
67+
allNavigationItems
68+
.filter((item) => item.section === section)
69+
.sort((left, right) => left.order - right.order)
70+
.map(({ id }) => id)
71+
72+
expect(idsForSection('account')).toEqual([
73+
'general',
74+
'billing',
75+
'desktop',
76+
'browser',
77+
'terminal',
78+
])
79+
expect(idsForSection('workspace')).toEqual([
80+
'teammates',
81+
'secrets',
82+
'mcp',
83+
'custom-tools',
84+
'byok',
85+
'inbox',
86+
'workflow-mcp-servers',
87+
'apikeys',
88+
'sandboxes',
89+
'recently-deleted',
90+
])
91+
expect(idsForSection('organization')).toEqual([
92+
'organization',
93+
'custom-blocks',
94+
'forks',
95+
'access-control',
96+
'audit-logs',
97+
'whitelabeling',
98+
'sso',
99+
'sessions',
100+
'data-retention',
101+
'data-drains',
102+
])
103+
expect(idsForSection('platform')).toEqual(['admin', 'mothership'])
104+
})
105+
68106
it('derives every unified item from exactly one registry entry', () => {
69107
expect(allNavigationItems).toHaveLength(
70108
SETTINGS_SECTION_REGISTRY.filter(({ unified }) => unified).length

apps/sim/app/workspace/[workspaceId]/settings/navigation.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@ export const isBillingEnabled = SETTINGS_NAVIGATION_BILLING_ENABLED
1616

1717
export const sectionConfig: { key: NavigationSection; title: string }[] = [
1818
{ key: 'account', title: 'Account' },
19-
{ key: 'tools', title: 'Tools' },
20-
{ key: 'subscription', title: 'Subscription' },
21-
{ key: 'system', title: 'System' },
22-
{ key: 'desktop', title: 'Desktop' },
23-
{ key: 'enterprise', title: 'Enterprise' },
24-
{ key: 'superuser', title: 'Superuser' },
19+
{ key: 'workspace', title: 'Workspace' },
20+
{ key: 'organization', title: 'Organization' },
21+
{ key: 'platform', title: 'Platform' },
2522
]
2623

2724
export const allNavigationItems: NavigationItem[] = buildUnifiedSettingsNavigation()

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ export function SettingsSidebar({
316316
.map(({ key, title }) => ({
317317
key,
318318
title,
319-
items: navigationItems.filter((item) => item.section === key),
319+
items: navigationItems
320+
.filter((item) => item.section === key)
321+
.sort((left, right) => left.order - right.order),
320322
}))
321323
.filter(({ items }) => items.length > 0)
322324
.map(({ key, title, items: sectionItems }, index) => (

apps/sim/components/settings/navigation.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,45 @@ describe('settings navigation boundaries', () => {
191191
expect(organizationSso?.docsLink).toBe(unifiedSso?.docsLink)
192192
})
193193

194-
it('keeps scope-specific labels only where the surface genuinely differs', () => {
194+
it('uses scope-specific labels consistently across settings surfaces', () => {
195195
const organizationMembers = ORGANIZATION_SETTINGS_ITEMS.find(({ id }) => id === 'members')
196196
const unifiedOrganization = buildUnifiedSettingsNavigation().find(
197197
({ id }) => id === 'organization'
198198
)
199199

200200
expect(organizationMembers?.label).toBe('Members')
201201
expect(organizationMembers?.description).toBe('Manage organization members, roles, and seats.')
202-
expect(unifiedOrganization?.label).toBe('Organization')
202+
expect(unifiedOrganization?.label).toBe('Members')
203+
})
204+
205+
it('keeps self-host settings on their standalone account projection', () => {
206+
expect(
207+
SELFHOST_SETTINGS_ITEMS.map(({ id, label, description, group }) => ({
208+
id,
209+
label,
210+
description,
211+
group,
212+
}))
213+
).toEqual([
214+
{
215+
id: 'general',
216+
label: 'General',
217+
description: 'Manage your profile, appearance, and preferences.',
218+
group: 'account',
219+
},
220+
{
221+
id: 'billing',
222+
label: 'Subscription',
223+
description: 'Manage your personal plan, usage, and invoices.',
224+
group: 'account',
225+
},
226+
{
227+
id: 'chat-keys',
228+
label: 'Chat keys',
229+
description: 'Manage the model-provider keys that power Chat.',
230+
group: 'developer',
231+
},
232+
])
203233
})
204234

205235
it('builds canonical settings hrefs across all three planes', () => {

0 commit comments

Comments
 (0)