diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 69fc92e8a12..0cd2aa9dbae 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment | `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions | | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers | +| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email | +| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling | +| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring | | `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations | ### Organization Management diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 51cbe1222b6..7cab684f043 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -21,7 +21,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -29,6 +32,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -151,31 +155,34 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: ? { ...currentConfig, ...updates.config } : currentConfig - // If setting autoAddNewMembers to true, unset it on other groups in the org first - if (updates.autoAddNewMembers === true) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, result.group.organizationId), - eq(permissionGroup.autoAddNewMembers, true) + const now = new Date() + + await db.transaction(async (tx) => { + if (updates.autoAddNewMembers === true) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) ) - ) - } + } - await db - .update(permissionGroup) - .set({ - ...(updates.name !== undefined && { name: updates.name }), - ...(updates.description !== undefined && { description: updates.description }), - ...(updates.autoAddNewMembers !== undefined && { - autoAddNewMembers: updates.autoAddNewMembers, - }), - config: newConfig, - updatedAt: new Date(), - }) - .where(eq(permissionGroup.id, id)) + await tx + .update(permissionGroup) + .set({ + ...(updates.name !== undefined && { name: updates.name }), + ...(updates.description !== undefined && { description: updates.description }), + ...(updates.autoAddNewMembers !== undefined && { + autoAddNewMembers: updates.autoAddNewMembers, + }), + config: newConfig, + updatedAt: now, + }) + .where(eq(permissionGroup.id, id)) + }) const [updated] = await db .select() @@ -245,8 +252,10 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) } - await db.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) - await db.delete(permissionGroup).where(eq(permissionGroup.id, id)) + await db.transaction(async (tx) => { + await tx.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) + await tx.delete(permissionGroup).where(eq(permissionGroup.id, id)) + }) logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id }) diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index 9b88d482617..dd5c09e5453 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -23,7 +23,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -31,6 +34,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), @@ -167,19 +171,6 @@ export async function POST(req: Request) { ...config, } - // If autoAddNewMembers is true, unset it on any existing groups first - if (autoAddNewMembers) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, organizationId), - eq(permissionGroup.autoAddNewMembers, true) - ) - ) - } - const now = new Date() const newGroup = { id: generateId(), @@ -193,7 +184,20 @@ export async function POST(req: Request) { autoAddNewMembers: autoAddNewMembers || false, } - await db.insert(permissionGroup).values(newGroup) + await db.transaction(async (tx) => { + if (autoAddNewMembers) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + await tx.insert(permissionGroup).values(newGroup) + }) logger.info('Created permission group', { permissionGroupId: newGroup.id, diff --git a/apps/sim/app/api/settings/allowed-providers/route.ts b/apps/sim/app/api/settings/allowed-providers/route.ts new file mode 100644 index 00000000000..2880c9eca08 --- /dev/null +++ b/apps/sim/app/api/settings/allowed-providers/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags' + +export async function GET() { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + return NextResponse.json({ + blacklistedProviders: getBlacklistedProvidersFromEnv(), + }) +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index ca48abef01b..2c0db1d6e1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -1,8 +1,9 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' -import { prefetchGeneralSettings, prefetchUserProfile } from './prefetch' +import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch' import { SettingsPage } from './settings' const SECTION_TITLES: Record = { @@ -11,6 +12,7 @@ const SECTION_TITLES: Record = { secrets: 'Secrets', 'template-profile': 'Template Profile', 'access-control': 'Access Control', + 'audit-logs': 'Audit Logs', apikeys: 'Sim Keys', byok: 'BYOK', subscription: 'Subscription', @@ -46,6 +48,7 @@ export default async function SettingsSectionPage({ void prefetchGeneralSettings(queryClient) void prefetchUserProfile(queryClient) + if (isBillingEnabled) void prefetchSubscriptionData(queryClient) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts index 7ac50861e5b..d04d9481d1a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts @@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query' import { headers } from 'next/headers' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings' +import { subscriptionKeys } from '@/hooks/queries/subscription' import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile' /** @@ -35,6 +36,28 @@ export function prefetchGeneralSettings(queryClient: QueryClient) { }) } +/** + * Prefetch subscription data server-side via internal API fetch. + * Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false) + * so data is shared via HydrationBoundary — ensuring the settings sidebar renders + * with the correct Team/Enterprise tabs on the first paint, with no flash. + */ +export function prefetchSubscriptionData(queryClient: QueryClient) { + return queryClient.prefetchQuery({ + queryKey: subscriptionKeys.user(false), + queryFn: async () => { + const fwdHeaders = await getForwardedHeaders() + const baseUrl = getInternalApiBaseUrl() + const response = await fetch(`${baseUrl}/api/billing?context=user`, { + headers: fwdHeaders, + }) + if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`) + return response.json() + }, + staleTime: 5 * 60 * 1000, + }) +} + /** * Prefetch user profile server-side via internal API fetch. * Uses the same query keys as the client `useUserProfile` hook diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index b6f439635b9..9610babe879 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { Skeleton } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' +import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton' import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton' @@ -198,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) { }, [effectiveSection, sessionLoading, posthog]) return ( -
+

{label}

{effectiveSection === 'general' && } {effectiveSection === 'integrations' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx index 0fab587de01..06cebe3773b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx @@ -1,7 +1,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode }) { return (
-
+
{children}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index ff25389fc0c..222bb1962aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -74,6 +74,8 @@ const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const isCredentialSetsEnabled = isTruthy(getEnv('NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED')) const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED')) const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED')) +const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED')) +const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED')) export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) export { isCredentialSetsEnabled } @@ -106,6 +108,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, + selfHostedOverride: isAuditLogsEnabled, }, { id: 'subscription', @@ -181,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, - selfHostedOverride: isBillingEnabled, + selfHostedOverride: isWhitelabelingEnabled, }, { id: 'admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index ccb7cba760b..9c1bc3fafcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -34,7 +34,23 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' -const SKELETON_SECTIONS = [3, 2, 2] as const +const SKELETON_SECTIONS = sectionConfig + .map(({ key }) => + Math.min( + allNavigationItems.filter( + (item) => + item.section === key && + !(item.hideWhenBillingDisabled && !isBillingEnabled) && + !item.requiresTeam && + !item.requiresEnterprise && + !item.requiresSuperUser && + !item.requiresAdminRole && + item.id !== 'template-profile' + ).length, + 3 + ) + ) + .filter((count) => count > 0) interface SettingsSidebarProps { isCollapsed?: boolean @@ -61,14 +77,16 @@ export function SettingsSidebar({ const { data: session, isPending: sessionLoading } = useSession() const { data: organizationsData, isLoading: orgsLoading } = useOrganizations() const { data: generalSettings } = useGeneralSettings() - const { data: subscriptionData } = useSubscriptionData({ + const { data: subscriptionData, isLoading: subscriptionLoading } = useSubscriptionData({ enabled: isBillingEnabled, staleTime: 5 * 60 * 1000, }) - const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders() + const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders({ + enabled: !isHosted, + }) const activeOrganization = organizationsData?.activeOrganization - const { config: permissionConfig } = usePermissionConfig() + const { config: permissionConfig, isLoading: permissionLoading } = usePermissionConfig() const userEmail = session?.user?.email const userId = session?.user?.id @@ -100,9 +118,18 @@ export function SettingsSidebar({ if (item.id === 'template-profile') { return false } + if (item.id === 'integrations' && permissionConfig.hideIntegrationsTab) { + return false + } + if (item.id === 'secrets' && permissionConfig.hideSecretsTab) { + return false + } if (item.id === 'apikeys' && permissionConfig.hideApiKeysTab) { return false } + if (item.id === 'inbox' && permissionConfig.hideInboxTab) { + return false + } if (item.id === 'mcp' && permissionConfig.disableMcpTools) { return false } @@ -244,113 +271,102 @@ export function SettingsSidebar({ !isCollapsed && 'overflow-y-auto overflow-x-hidden' )} > - {sessionLoading || orgsLoading ? ( - isCollapsed ? ( - <> - {SKELETON_SECTIONS.map((count, sectionIdx) => ( -
- {Array.from({ length: count }, (_, i) => ( -
- -
- ))} -
- ))} - - ) : ( - Array.from({ length: 3 }, (_, i) => ( -
-
+ {sessionLoading || + orgsLoading || + (isBillingEnabled && subscriptionLoading) || + permissionLoading || + (!isHosted && isLoadingSSO) + ? SKELETON_SECTIONS.map((count, i) => ( +
+
- {Array.from({ length: i === 0 ? 3 : 2 }, (_, j) => ( -
- + {Array.from({ length: count }, (_, j) => ( +
+ +
))}
)) - ) - ) : ( - sectionConfig.map(({ key, title }) => { - const sectionItems = navigationItems.filter((item) => item.section === key) - if (sectionItems.length === 0) return null - - return ( -
-
-
{title}
-
-
- {sectionItems.map((item) => { - const Icon = item.icon - const active = activeSection === item.id - const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess - const itemClassName = cn( - 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', - !active && 'hover-hover:bg-[var(--surface-hover)]', - active && 'bg-[var(--surface-active)]' - ) - const content = ( - <> - - - {item.label} - - {isLocked && ( - - Max + : sectionConfig.map(({ key, title }) => { + const sectionItems = navigationItems.filter((item) => item.section === key) + if (sectionItems.length === 0) return null + + return ( +
+
+
{title}
+
+
+ {sectionItems.map((item) => { + const Icon = item.icon + const active = activeSection === item.id + const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess + const itemClassName = cn( + 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', + !active && 'hover-hover:bg-[var(--surface-hover)]', + active && 'bg-[var(--surface-active)]' + ) + const content = ( + <> + + + {item.label} - )} - - ) - - const element = item.externalUrl ? ( - - {content} - - ) : ( - - ) - - return ( - - {element} - - ) - })} + {isLocked && ( + + Max + + )} + + ) + + const element = item.externalUrl ? ( + + {content} + + ) : ( + + ) + + return ( + + {element} + + ) + })} +
-
- ) - }) - )} + ) + })}
!open && handleCancelDiscard()}> diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx index 3e2341ce0cf..91fc76ebd93 100644 --- a/apps/sim/components/emcn/components/modal/modal.tsx +++ b/apps/sim/components/emcn/components/modal/modal.tsx @@ -102,7 +102,7 @@ ModalOverlay.displayName = 'ModalOverlay' * Each size uses viewport units with sensible min/max constraints. */ const MODAL_SIZES = { - sm: 'w-[90vw] max-w-[400px]', + sm: 'w-[90vw] max-w-[440px]', md: 'w-[90vw] max-w-[500px]', lg: 'w-[90vw] max-w-[600px]', xl: 'w-[90vw] max-w-[800px]', @@ -120,7 +120,7 @@ export interface ModalContentProps showClose?: boolean /** * Modal size variant with responsive viewport-based sizing. - * - sm: max 400px (dialogs, confirmations) + * - sm: max 440px (dialogs, confirmations) * - md: max 500px (default, forms) * - lg: max 600px (content-heavy modals) * - xl: max 800px (complex editors) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2806cd4d314..0e4de0bf5e5 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3554,7 +3554,7 @@ export function FireworksIcon(props: SVGProps) { > ) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index cc636fb9df8..65cc642e13d 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -41,6 +41,7 @@ import { useRemovePermissionGroupMember, useUpdatePermissionGroup, } from '@/ee/access-control/hooks/permission-groups' +import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' import { useOrganization, useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { PROVIDER_DEFINITIONS } from '@/providers/models' @@ -48,10 +49,19 @@ import { getAllProviderIds } from '@/providers/utils' const logger = createLogger('AccessControl') +interface OrgMember { + userId: string + user: { + name: string | null + email: string + image?: string | null + } +} + interface AddMembersModalProps { open: boolean onOpenChange: (open: boolean) => void - availableMembers: any[] + availableMembers: OrgMember[] selectedMemberIds: Set setSelectedMemberIds: React.Dispatch>> onAddMembers: () => void @@ -72,7 +82,7 @@ function AddMembersModal({ const filteredMembers = useMemo(() => { if (!searchTerm.trim()) return availableMembers const query = searchTerm.toLowerCase() - return availableMembers.filter((m: any) => { + return availableMembers.filter((m) => { const name = m.user?.name || '' const email = m.user?.email || '' return name.toLowerCase().includes(query) || email.toLowerCase().includes(query) @@ -81,12 +91,12 @@ function AddMembersModal({ const allFilteredSelected = useMemo(() => { if (filteredMembers.length === 0) return false - return filteredMembers.every((m: any) => selectedMemberIds.has(m.userId)) + return filteredMembers.every((m) => selectedMemberIds.has(m.userId)) }, [filteredMembers, selectedMemberIds]) const handleToggleAll = () => { if (allFilteredSelected) { - const filteredIds = new Set(filteredMembers.map((m: any) => m.userId)) + const filteredIds = new Set(filteredMembers.map((m) => m.userId)) setSelectedMemberIds((prev) => { const next = new Set(prev) filteredIds.forEach((id) => next.delete(id)) @@ -95,7 +105,7 @@ function AddMembersModal({ } else { setSelectedMemberIds((prev) => { const next = new Set(prev) - filteredMembers.forEach((m: any) => next.add(m.userId)) + filteredMembers.forEach((m) => next.add(m.userId)) return next }) } @@ -140,7 +150,7 @@ function AddMembersModal({ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' />
-
@@ -152,7 +162,7 @@ function AddMembersModal({

) : (
- {filteredMembers.map((member: any) => { + {filteredMembers.map((member) => { const name = member.user?.name || 'Unknown' const email = member.user?.email || '' const avatarInitial = name.charAt(0).toUpperCase() @@ -314,16 +324,22 @@ export function AccessControl() { configKey: 'hideCopilot' as const, }, { - id: 'hide-api-keys', - label: 'API Keys', + id: 'hide-integrations', + label: 'Integrations', category: 'Settings Tabs', - configKey: 'hideApiKeysTab' as const, + configKey: 'hideIntegrationsTab' as const, }, { - id: 'hide-environment', - label: 'Environment', + id: 'hide-secrets', + label: 'Secrets', category: 'Settings Tabs', - configKey: 'hideEnvironmentTab' as const, + configKey: 'hideSecretsTab' as const, + }, + { + id: 'hide-api-keys', + label: 'API Keys', + category: 'Settings Tabs', + configKey: 'hideApiKeysTab' as const, }, { id: 'hide-files', @@ -391,6 +407,12 @@ export function AccessControl() { category: 'Collaboration', configKey: 'disableInvitations' as const, }, + { + id: 'hide-inbox', + label: 'Sim Mailer', + category: 'Features', + configKey: 'hideInboxTab' as const, + }, { id: 'disable-public-api', label: 'Public API', @@ -420,6 +442,29 @@ export function AccessControl() { return categories }, [filteredPlatformFeatures]) + const platformCategoryColumns = useMemo(() => { + const categoryGroups = [ + ['Sidebar', 'Deploy Tabs', 'Collaboration'], + ['Workflow Panel', 'Tools', 'Features'], + ['Settings Tabs', 'Logs'], + ] + + const assignedCategories = new Set(categoryGroups.flat()) + const unassigned = Object.keys(platformCategories).filter((c) => !assignedCategories.has(c)) + const groups = unassigned.length > 0 ? [...categoryGroups, unassigned] : categoryGroups + + return groups + .map((column) => + column + .map((category) => ({ + category, + features: platformCategories[category] ?? [], + })) + .filter((section) => section.features.length > 0) + ) + .filter((column) => column.length > 0) + }, [platformCategories]) + const hasConfigChanges = useMemo(() => { if (!viewingGroup || !editingConfig) return false const original = viewingGroup.config @@ -436,7 +481,14 @@ export function AccessControl() { return a.name.localeCompare(b.name) }) }, []) - const allProviderIds = useMemo(() => getAllProviderIds(), []) + const { data: blacklistedProvidersData } = useBlacklistedProviders({ enabled: showConfigModal }) + + const allProviderIds = useMemo(() => { + const allIds = getAllProviderIds() + const blacklist = blacklistedProvidersData?.blacklistedProviders ?? [] + if (blacklist.length === 0) return allIds + return allIds.filter((id) => !blacklist.includes(id.toLowerCase())) + }, [blacklistedProvidersData]) const filteredProviders = useMemo(() => { if (!providerSearchTerm.trim()) return allProviderIds @@ -450,6 +502,16 @@ export function AccessControl() { return allBlocks.filter((b) => b.name.toLowerCase().includes(query)) }, [allBlocks, integrationSearchTerm]) + const filteredCoreBlocks = useMemo(() => { + return filteredBlocks.filter((block) => block.category === 'blocks') + }, [filteredBlocks]) + + const filteredToolBlocks = useMemo(() => { + return filteredBlocks + .filter((block) => block.category === 'tools' || block.category === 'triggers') + .sort((a, b) => a.name.localeCompare(b.name)) + }, [filteredBlocks]) + const orgMembers = useMemo(() => { return organization?.members || [] }, [organization]) @@ -677,7 +739,7 @@ export function AccessControl() { const availableMembersToAdd = useMemo(() => { const existingMemberUserIds = new Set(members.map((m) => m.userId)) - return orgMembers.filter((m: any) => !existingMemberUserIds.has(m.userId)) + return orgMembers.filter((m) => !existingMemberUserIds.has(m.userId)) }, [orgMembers, members]) if (isLoading) { @@ -841,249 +903,259 @@ export function AccessControl() { } }} > - + Configure Permissions - + Model Providers Blocks Platform - - -
-
-
- - setProviderSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- + + +
+
+ + setProviderSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
-
- {filteredProviders.map((providerId) => { - const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon - const providerName = - PROVIDER_DEFINITIONS[providerId]?.name || - providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - return ( -
- toggleProvider(providerId)} - /> -
- {ProviderIcon && } -
- {providerName} -
+
+ }} + > + {editingConfig?.allowedModelProviders === null || + allProviderIds.every((id) => + editingConfig?.allowedModelProviders?.includes(id) + ) + ? 'Deselect All' + : 'Select All'} +
-
- - - - -
-
-
- - setIntegrationSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- -
-
- {filteredBlocks.map((block) => { - const BlockIcon = block.icon - return ( -
- toggleIntegration(block.type)} - /> -
- {BlockIcon && ( - - )} -
- {block.name} +
+ {filteredProviders.map((providerId) => { + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const checkboxId = `provider-${providerId}` + return ( + + ) + })} +
+ + + +
+
+ + setIntegrationSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+
- -
- - - -
-
-
- - setPlatformSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> +
+ {filteredCoreBlocks.length > 0 && ( +
+ + Core Blocks + +
+ {filteredCoreBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( + + ) + })} +
- -
-
- {Object.entries(platformCategories).map(([category, features]) => ( -
- - {category} - -
- {features.map((feature) => ( -
+ )} + {filteredToolBlocks.length > 0 && ( +
+ + Tools + +
+ {filteredToolBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( +
- ))} -
+ {BlockIcon && ( + + )} +
+ {block.name} + + ) + })}
- ))} +
+ )} +
+ + + +
+
+ + setPlatformSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+ +
+
+ {platformCategoryColumns.map((column, columnIndex) => ( +
+ {column.map(({ category, features }) => ( +
+ + {category} + +
+ {features.map((feature) => ( + + ))} +
+
+ ))} +
+ ))}
- -
+ + {expanded && ( -
-
- Resource - - {formatResourceType(entry.resourceType)} - {entry.resourceId && ( - ({entry.resourceId}) - )} - -
- {entry.resourceName && ( +
+
- Name - {entry.resourceName} + Resource + + {formatResourceType(entry.resourceType)} + {entry.resourceId && ( + ({entry.resourceId}) + )} +
- )} -
- Actor - - {entry.actorName || 'Unknown'} - {entry.actorEmail && ( - ({entry.actorEmail}) - )} - -
- {entry.description && ( -
- Description - {entry.description} -
- )} - {entry.metadata != null && - Object.keys(entry.metadata as Record).length > 0 ? ( + {entry.resourceName && ( +
+ Name + {entry.resourceName} +
+ )}
- Details -
-                {JSON.stringify(entry.metadata, null, 2)}
-              
+ Actor + + {entry.actorName || 'Unknown'} + {entry.actorEmail && ( + ({entry.actorEmail}) + )} +
- ) : null} + {entry.description && ( +
+ + Description + + {entry.description} +
+ )} + {metadataEntries.map(([key, value]) => ( +
+ + {formatMetadataLabel(key)} + +
{renderMetadataValue(value)}
+
+ ))} +
)}
@@ -178,7 +290,7 @@ export function AuditLogs() { return (
-
+
@@ -205,7 +317,7 @@ export function AuditLogs() { value={dateRange} onChange={setDateRange} placeholder='Date range' - size='md' + size='sm' />
-
- - Timestamp - - - Event - - - Description - - - Actor - -
+
+
+ Timestamp + Event + Description + Actor +
-
- {isLoading ? ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- - - - -
- ))} -
- ) : allEntries.length === 0 ? ( -
- {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'} -
- ) : ( -
- {allEntries.map((entry) => ( - - ))} - {hasNextPage && ( -
- -
- )} -
- )} +
+ {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+ + + + +
+
+ ))} +
+ ) : allEntries.length === 0 ? ( +
+ {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'} +
+ ) : ( +
+ {allEntries.map((entry) => ( + + ))} + {hasNextPage && ( +
+ +
+ )} +
+ )} +
) diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts index 2dfa1592ea4..37c20ffb7fb 100644 --- a/apps/sim/ee/sso/hooks/sso.ts +++ b/apps/sim/ee/sso/hooks/sso.ts @@ -14,8 +14,8 @@ export const ssoKeys = { /** * Fetch SSO providers */ -async function fetchSSOProviders() { - const response = await fetch('/api/auth/sso/providers') +async function fetchSSOProviders(signal: AbortSignal) { + const response = await fetch('/api/auth/sso/providers', { signal }) if (!response.ok) { throw new Error('Failed to fetch SSO providers') } @@ -25,12 +25,17 @@ async function fetchSSOProviders() { /** * Hook to fetch SSO providers */ -export function useSSOProviders() { +interface UseSSOProvidersOptions { + enabled?: boolean +} + +export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) { return useQuery({ queryKey: ssoKeys.providers(), - queryFn: fetchSSOProviders, - staleTime: 5 * 60 * 1000, // 5 minutes + queryFn: ({ signal }) => fetchSSOProviders(signal), + staleTime: 5 * 60 * 1000, placeholderData: keepPreviousData, + enabled, }) } diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index a6946772c9a..fa28f3d85b8 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { Loader2, X } from 'lucide-react' import Image from 'next/image' -import { Button, Input, Label, Switch } from '@/components/emcn' +import { Button, Input, Label } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' import { HEX_COLOR_REGEX } from '@/lib/branding' @@ -79,6 +79,22 @@ interface ColorInputProps { function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorInputProps) { const isValidHex = !value || HEX_COLOR_REGEX.test(value) + const handleChange = useCallback( + (e: React.ChangeEvent) => { + let v = e.target.value.trim() + if (v && !v.startsWith('#')) { + v = `#${v}` + } + v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') + onChange(v.slice(0, 7)) + }, + [onChange] + ) + + const handleFocus = useCallback((e: React.FocusEvent) => { + e.target.select() + }, []) + return (
@@ -92,7 +108,8 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
onChange(e.target.value)} + onChange={handleChange} + onFocus={handleFocus} placeholder={placeholder} className={cn( 'h-[36px] font-mono text-[13px]', @@ -154,7 +171,6 @@ export function WhitelabelingSettings() { const [documentationUrl, setDocumentationUrl] = useState('') const [termsUrl, setTermsUrl] = useState('') const [privacyUrl, setPrivacyUrl] = useState('') - const [hidePoweredBySim, setHidePoweredBySim] = useState(false) const [logoUrl, setLogoUrl] = useState(null) const [wordmarkUrl, setWordmarkUrl] = useState(null) const [formInitialized, setFormInitialized] = useState(false) @@ -172,7 +188,6 @@ export function WhitelabelingSettings() { setDocumentationUrl(savedSettings.documentationUrl ?? '') setTermsUrl(savedSettings.termsUrl ?? '') setPrivacyUrl(savedSettings.privacyUrl ?? '') - setHidePoweredBySim(savedSettings.hidePoweredBySim ?? false) setLogoUrl(savedSettings.logoUrl ?? null) setWordmarkUrl(savedSettings.wordmarkUrl ?? null) setFormInitialized(true) @@ -222,7 +237,6 @@ export function WhitelabelingSettings() { documentationUrl: documentationUrl || null, termsUrl: termsUrl || null, privacyUrl: privacyUrl || null, - hidePoweredBySim, } try { @@ -246,7 +260,6 @@ export function WhitelabelingSettings() { documentationUrl, termsUrl, privacyUrl, - hidePoweredBySim, ]) if (isBillingEnabled) { @@ -496,21 +509,6 @@ export function WhitelabelingSettings() {
-
- Advanced -
-
- - Hide "Powered by Sim" branding - - - Removes the Sim logo from deployed chats and forms. - -
- -
-
-