diff --git a/.claude/rules/sim-settings-pages.md b/.claude/rules/sim-settings-pages.md
new file mode 100644
index 00000000000..671d76f4c57
--- /dev/null
+++ b/.claude/rules/sim-settings-pages.md
@@ -0,0 +1,123 @@
+---
+paths:
+ - "apps/sim/app/workspace/*/settings/**"
+ - "apps/sim/ee/**/components/**"
+---
+
+# Settings Pages
+
+Every settings page renders through the shared **`SettingsPanel`** primitive
+(`@/app/workspace/[workspaceId]/settings/components/settings-panel`). It owns the
+page chrome so pages never hand-roll it: a fixed header bar (right-aligned
+actions), a scroll region, and a centered `max-w-[48rem]` content column led by a
+**title + description that come from navigation metadata**. Pages render only
+their body.
+
+Do NOT hand-roll any of these in a settings page — they are the panel's job:
+
+- `
` shell
+- the header bar (`flex flex-shrink-0 … px-[16px] pt-[8.5px] pb-[8.5px]`)
+- the scroll container (`min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]`)
+- the content column (`mx-auto … max-w-[48rem] … gap-7`)
+- a title block (`
` + ` `)
+- the page-level search input
+
+## Canonical page shape
+
+```tsx
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
+
+return (
+
+ Create
+
+ }
+ search={{ value: searchTerm, onChange: setSearchTerm, placeholder: 'Search …' }}
+ >
+ {/* body only — sections, lists, forms */}
+
+)
+```
+
+When the page has modal/dialog siblings, wrap them with the panel in a fragment:
+
+```tsx
+return (
+ <>
+ {body}
+
+ >
+)
+```
+
+## `SettingsPanel` props
+
+- `actions?: ReactNode` — right-aligned header chips. Wrap multiple in a fragment;
+ the slot reserves the 30px chip height even when empty, so vertical rhythm is
+ identical across pages. Conditional actions are fine: `actions={canManage && }`.
+- `search?: { value; onChange: (value: string) => void; placeholder?; disabled? }` —
+ renders the canonical search field directly below the title. Pass `setSearchTerm`
+ straight to `onChange`. Use this for a standalone search; if search shares a row
+ with other controls (sort, filters, a date picker), render that whole row in
+ `children` instead and omit the prop.
+- `title?` / `description?` — overrides for the nav-driven defaults. **Only** for a
+ detail sub-view that needs a different heading; normal pages never pass these.
+- `scrollContainerRef?: React.Ref` — forwards a ref to the scroll
+ region (e.g. programmatic scroll-to-bottom).
+- `contentClassName?` — layout/spacing only; reach for it rarely. Prefer the
+ default `gap-7`.
+
+## Title + description live in navigation metadata
+
+`apps/sim/app/workspace/[workspaceId]/settings/navigation.ts` is the single source
+of truth. Every `NavigationItem` carries a one-line `description`; `SettingsPanel`
+resolves both via `getSettingsSectionMeta(section)` and the
+`SettingsSectionProvider` the settings shell wraps around the active section.
+
+Adding a new settings page:
+
+1. Add the `SettingsSection` id + a `NavigationItem` (with `label` **and**
+ `description`) in `navigation.ts`. Keep descriptions verb-first, one line,
+ ~40–55 chars, in the product voice (see `.claude/rules/constitution.md`).
+2. Render the component inside the shell's `effectiveSection` switch in
+ `settings/[section]/settings.tsx`.
+3. Build the component body inside `` — no shell, no title block.
+
+## Other shared settings primitives (do not re-roll these)
+
+- **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical
+ muted status message. `variant='fill'` (default) centers in the available
+ height (empty list, or a not-entitled/loading gate); `variant='inline'` sits in
+ flow (a search "no results"). Never hand-roll
+ ``
+ or `
`. It owns the `--text-muted` + `text-sm`
+ tokens, so it also keeps these messages consistent across pages.
+- **`RowActionsMenu`** (`…/components/row-actions-menu`) — the trailing `...`
+ actions menu for a list row. Pass `label` (aria-label) and
+ `actions: RowAction[]` (`{ label, onSelect, destructive?, disabled? }`); the
+ component renders the canonical flush `...` trigger + `DropdownMenuContent`.
+ Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never
+ hand-roll the `` + `` trigger per page.
+
+## Detail sub-views (the one exception)
+
+A drill-down view reached from a list row (selected MCP server, workflow MCP
+server, credential set, permission group) keeps its **own** chrome because it
+needs a left-aligned back button (``), which the panel
+header (right-actions only) does not model. Leave those returns as hand-rolled
+shells; only the list/main view uses `SettingsPanel`. Gate/early-return states
+(not-entitled, loading, upgrade prompts) also stay as-is.
+
+## Audit checklist
+
+A settings page is design-system-clean when:
+
+- [ ] Its main return is a `` (or `<>……>` with modal siblings) — no hand-rolled shell/header/scroll/column.
+- [ ] It renders **no** hand-rolled ``/description title block — the title comes from nav metadata.
+- [ ] Header chips are in `actions`; a standalone search is in the `search` prop.
+- [ ] Its `NavigationItem` has an accurate, consistent-length `description`.
+- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
+- [ ] No business logic, handlers, or conditional rendering changed by the migration.
+- [ ] `tsc`, `biome`, and the page's tests pass.
diff --git a/.claude/skills/add-settings-page/SKILL.md b/.claude/skills/add-settings-page/SKILL.md
new file mode 100644
index 00000000000..9e5d7b5671c
--- /dev/null
+++ b/.claude/skills/add-settings-page/SKILL.md
@@ -0,0 +1,57 @@
+---
+name: add-settings-page
+description: Add a new Sim settings page, or audit existing settings pages for design-system compliance with the shared SettingsPanel layout. Use when creating a settings tab, or when asked to check/clean up settings pages so they match the design system (consistent title, header, search, spacing).
+---
+
+# Settings Page (add / audit)
+
+Sim settings pages all render through the shared **`SettingsPanel`** primitive,
+which owns the page chrome and renders a nav-driven title + description. The full
+convention lives in `.claude/rules/sim-settings-pages.md` — read it first; this
+skill is the procedure.
+
+Key paths:
+- Layout primitive: `apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx`
+- Nav metadata (titles + descriptions): `apps/sim/app/workspace/[workspaceId]/settings/navigation.ts`
+- Section switch + provider: `apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx`
+- Pages: `apps/sim/app/workspace/[workspaceId]/settings/components//.tsx` and EE pages under `apps/sim/ee//components/`
+
+## Mode A — Add a new settings page
+
+1. **Navigation.** In `navigation.ts`: add the id to the `SettingsSection` union,
+ then a `NavigationItem` with `label` AND a one-line `description` (verb-first,
+ ~40–55 chars, product voice per `.claude/rules/constitution.md`). Place it in
+ the right `section` group and set any gating flags (`requiresHosted`,
+ `requiresEnterprise`, etc.).
+2. **Wire the switch.** Add the component to the `effectiveSection` render switch
+ in `settings/[section]/settings.tsx` (lazy `dynamic(...)` like its siblings).
+3. **Build the body inside `SettingsPanel`.** Never hand-roll the shell, header
+ bar, scroll region, content column, or title block. Put header buttons in
+ `actions`, a standalone search in `search={{ value, onChange, placeholder }}`,
+ and the page content as `children`. Modals go beside the panel inside a `<>`.
+4. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write `.
+
+## Mode B — Audit existing settings pages
+
+For each page component, confirm the checklist in `.claude/rules/sim-settings-pages.md`:
+
+1. Find hand-rolled shells that should be `SettingsPanel`:
+ `git grep -n "flex h-full flex-col bg-\[var(--bg)\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
+ — every match should be either `settings-panel.tsx`, a **detail sub-view**
+ (has a `` back button), or an entitlement/loading
+ **gate** early-return. Anything else is a page that still needs migrating.
+2. Find hand-rolled title blocks (should be 0 outside detail views):
+ `git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
+3. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
+ accurate `description` of consistent length with its peers.
+4. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
+ move header chips to `actions`, the standalone search to `search`, delete the
+ `` title block, replace the three closing `
` (column/scroll/shell)
+ with ``, and keep modal siblings in a `<>` fragment. Do NOT
+ touch handlers, state, queries, conditional rendering, or detail/gate returns.
+ Drop per-page `gap-*`/`pt-*` on the content column in favor of the panel default.
+5. Remove now-unused imports (`ChipInput`/`Search`) ONLY after grepping that
+ they are not still used elsewhere in the file (e.g. by a detail view).
+6. **Verify the whole sweep:** `tsc --noEmit`, `biome check` on every touched
+ file, and run the affected pages' tests. Diff each file against the base and
+ confirm the change is purely structural before shipping.
diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx
index ec84c16702e..109c9b2cf45 100644
--- a/apps/docs/components/icons.tsx
+++ b/apps/docs/components/icons.tsx
@@ -2760,10 +2760,10 @@ export function TinybirdIcon(props: SVGProps
) {
export function ThriveIcon(props: SVGProps) {
return (
-
+
)
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
index b194b2bf359..288c0d146a3 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 { usePostHog } from 'posthog-js/react'
import { useSession } from '@/lib/auth/auth-client'
import { captureEvent } from '@/lib/posthog/client'
import { General } from '@/app/workspace/[workspaceId]/settings/components/general/general'
+import { SettingsSectionProvider } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import {
isBillingEnabled,
@@ -105,7 +106,6 @@ export function SettingsPage({ section }: SettingsPageProps) {
const posthog = usePostHog()
const isAdminRole = session?.user?.role === 'admin'
- // The Subscription tab was replaced by Billing; redirect legacy links there.
const normalizedSection: SettingsSection =
(section as string) === 'subscription' ? 'billing' : section
const effectiveSection =
@@ -125,29 +125,31 @@ export function SettingsPage({ section }: SettingsPageProps) {
}, [effectiveSection, sessionLoading, posthog])
return (
-
- {effectiveSection === 'general' &&
}
- {effectiveSection === 'secrets' &&
}
- {effectiveSection === 'credential-sets' &&
}
- {effectiveSection === 'access-control' &&
}
- {effectiveSection === 'audit-logs' &&
}
- {effectiveSection === 'apikeys' &&
}
- {isBillingEnabled && effectiveSection === 'billing' &&
}
- {effectiveSection === 'teammates' &&
}
- {isBillingEnabled && effectiveSection === 'organization' &&
}
- {effectiveSection === 'sso' &&
}
- {effectiveSection === 'data-retention' &&
}
- {effectiveSection === 'data-drains' &&
}
- {effectiveSection === 'whitelabeling' &&
}
- {effectiveSection === 'byok' &&
}
- {effectiveSection === 'copilot' &&
}
- {effectiveSection === 'mcp' &&
}
- {effectiveSection === 'custom-tools' &&
}
- {effectiveSection === 'workflow-mcp-servers' &&
}
- {effectiveSection === 'inbox' &&
}
- {effectiveSection === 'recently-deleted' &&
}
- {effectiveSection === 'admin' &&
}
- {effectiveSection === 'mothership' &&
}
-
+
+
+ {effectiveSection === 'general' &&
}
+ {effectiveSection === 'secrets' &&
}
+ {effectiveSection === 'credential-sets' &&
}
+ {effectiveSection === 'access-control' &&
}
+ {effectiveSection === 'audit-logs' &&
}
+ {effectiveSection === 'apikeys' &&
}
+ {isBillingEnabled && effectiveSection === 'billing' &&
}
+ {effectiveSection === 'teammates' &&
}
+ {isBillingEnabled && effectiveSection === 'organization' &&
}
+ {effectiveSection === 'sso' &&
}
+ {effectiveSection === 'data-retention' &&
}
+ {effectiveSection === 'data-drains' &&
}
+ {effectiveSection === 'whitelabeling' &&
}
+ {effectiveSection === 'byok' &&
}
+ {effectiveSection === 'copilot' &&
}
+ {effectiveSection === 'mcp' &&
}
+ {effectiveSection === 'custom-tools' &&
}
+ {effectiveSection === 'workflow-mcp-servers' &&
}
+ {effectiveSection === 'inbox' &&
}
+ {effectiveSection === 'recently-deleted' &&
}
+ {effectiveSection === 'admin' &&
}
+ {effectiveSection === 'mothership' &&
}
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
index 444d6f573bf..bca74138132 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
@@ -12,6 +12,8 @@ import {
adminParsers,
adminUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/components/admin/search-params'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
useAdminUsers,
useBanUser,
@@ -155,306 +157,292 @@ export function Admin() {
impersonatingUserId,
])
return (
-
-
-
-
-
-
Super admin mode
-
+
+
+ Super admin mode
+
+
+
+ {settings?.superUserModeEnabled && (
+ <>
+
+
+
Mothership Environment
+
+ Default uses the configured Sim agent URL.
+
+
+
+ handleMothershipEnvironmentChange(value as MothershipEnvironment)
+ }
+ placeholder='Select environment'
disabled={updateSetting.isPending}
- onCheckedChange={handleSuperUserModeToggle}
+ options={MOTHERSHIP_ENV_OPTIONS}
/>
+ >
+ )}
+
- {settings?.superUserModeEnabled && (
- <>
-
-
-
- Mothership Environment
-
-
- Default uses the configured Sim agent URL.
-
-
-
- handleMothershipEnvironmentChange(value as MothershipEnvironment)
- }
- placeholder='Select environment'
- disabled={updateSetting.isPending}
- options={MOTHERSHIP_ENV_OPTIONS}
- />
-
- >
- )}
-
+
-
+
+
+ Import a workflow by ID along with its associated copilot chats.
+
+
+ {
+ setWorkflowId(e.target.value)
+ importWorkflow.reset()
+ }}
+ placeholder='Enter workflow ID'
+ disabled={importWorkflow.isPending}
+ />
+
+ {importWorkflow.isPending ? 'Importing...' : 'Import'}
+
+
+ {importWorkflow.error && (
+
{importWorkflow.error.message}
+ )}
+ {importWorkflow.isSuccess && (
+
+ Workflow imported successfully (new ID: {importWorkflow.data.newWorkflowId},{' '}
+ {importWorkflow.data.copilotChatsImported ?? 0} copilot chats imported)
+
+ )}
+
-
-
- Import a workflow by ID along with its associated copilot chats.
-
-
- {
- setWorkflowId(e.target.value)
- importWorkflow.reset()
- }}
- placeholder='Enter workflow ID'
- disabled={importWorkflow.isPending}
- />
-
- {importWorkflow.isPending ? 'Importing...' : 'Import'}
-
-
- {importWorkflow.error && (
-
{importWorkflow.error.message}
- )}
- {importWorkflow.isSuccess && (
-
- Workflow imported successfully (new ID: {importWorkflow.data.newWorkflowId},{' '}
- {importWorkflow.data.copilotChatsImported ?? 0} copilot chats imported)
-
- )}
-
+
-
+
+
User Management
+
+ setSearchInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
+ placeholder='Search by email or paste a user ID...'
+ className='min-w-0 flex-1'
+ />
+
+ {usersLoading ? 'Searching...' : 'Search'}
+
+
-
-
User Management
-
- setSearchInput(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
- placeholder='Search by email or paste a user ID...'
- className='min-w-0 flex-1'
- />
-
- {usersLoading ? 'Searching...' : 'Search'}
-
-
+ {usersError && (
+
+ {getErrorMessage(usersError, 'Failed to fetch users')}
+
+ )}
- {usersError && (
-
- {getErrorMessage(usersError, 'Failed to fetch users')}
-
- )}
+ {(setUserRole.error ||
+ banUser.error ||
+ unbanUser.error ||
+ impersonateUser.error ||
+ impersonationGuardError) && (
+
+ {impersonationGuardError ||
+ (setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error)
+ ?.message ||
+ 'Action failed. Please try again.'}
+
+ )}
- {(setUserRole.error ||
- banUser.error ||
- unbanUser.error ||
- impersonateUser.error ||
- impersonationGuardError) && (
-
- {impersonationGuardError ||
- (setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error)
- ?.message ||
- 'Action failed. Please try again.'}
-
- )}
+ {searchQuery.length > 0 && usersData && (
+ <>
+
+
+ Name
+ Email
+ Role
+ Status
+ Actions
+
- {searchQuery.length > 0 && usersData && (
- <>
-
-
- Name
- Email
- Role
- Status
- Actions
-
+ {usersData.users.length === 0 && (
+
No users found.
+ )}
- {usersData.users.length === 0 && (
-
- No users found.
-
+ {usersData.users.map((u) => (
+
(
-
+
+
+ {u.name || '—'}
+
+
{u.email}
+
+
+ {u.role || 'user'}
+
+
+
+ {u.banned ? (
+ Banned
+ ) : (
+ Active
)}
- >
-
-
- {u.name || '—'}
-
-
- {u.email}
-
-
-
- {u.role || 'user'}
-
-
-
- {u.banned ? (
- Banned
- ) : (
- Active
- )}
-
-
- {u.id !== session?.user?.id && (
- <>
- handleImpersonate(u.id)}
- disabled={pendingUserIds.has(u.id)}
- >
- {impersonatingUserId === u.id ||
- (impersonateUser.isPending &&
- (impersonateUser.variables as { userId?: string } | undefined)
- ?.userId === u.id)
- ? 'Switching...'
- : 'Impersonate'}
-
- {
- setUserRole.reset()
- setUserRole.mutate({
- userId: u.id,
- role: u.role === 'admin' ? 'user' : 'admin',
- })
- }}
- disabled={pendingUserIds.has(u.id)}
- >
- {u.role === 'admin' ? 'Demote' : 'Promote'}
-
- {u.banned ? (
- {
- unbanUser.reset()
- unbanUser.mutate({ userId: u.id })
- }}
- disabled={pendingUserIds.has(u.id)}
- >
- Unban
-
- ) : (
- {
- if (banUserId === u.id) {
- setBanUserId(null)
- setBanReason('')
- } else {
- setBanUserId(u.id)
- setBanReason('')
- }
- }}
- disabled={pendingUserIds.has(u.id)}
- >
- {banUserId === u.id ? 'Cancel' : 'Ban'}
-
- )}
- >
- )}
-
-
- {banUserId === u.id && !u.banned && (
-
- setBanReason(e.target.value)}
- placeholder='Reason (optional)'
- className='flex-1'
- />
+
+
+ {u.id !== session?.user?.id && (
+ <>
handleImpersonate(u.id)}
+ disabled={pendingUserIds.has(u.id)}
+ >
+ {impersonatingUserId === u.id ||
+ (impersonateUser.isPending &&
+ (impersonateUser.variables as { userId?: string } | undefined)
+ ?.userId === u.id)
+ ? 'Switching...'
+ : 'Impersonate'}
+
+ {
- banUser.reset()
- banUser.mutate(
- {
- userId: u.id,
- ...(banReason.trim() ? { banReason: banReason.trim() } : {}),
- },
- {
- onSuccess: () => {
- setBanUserId(null)
- setBanReason('')
- },
- }
- )
+ setUserRole.reset()
+ setUserRole.mutate({
+ userId: u.id,
+ role: u.role === 'admin' ? 'user' : 'admin',
+ })
}}
disabled={pendingUserIds.has(u.id)}
>
- Confirm Ban
+ {u.role === 'admin' ? 'Demote' : 'Promote'}
-
+ {u.banned ? (
+ {
+ unbanUser.reset()
+ unbanUser.mutate({ userId: u.id })
+ }}
+ disabled={pendingUserIds.has(u.id)}
+ >
+ Unban
+
+ ) : (
+ {
+ if (banUserId === u.id) {
+ setBanUserId(null)
+ setBanReason('')
+ } else {
+ setBanUserId(u.id)
+ setBanReason('')
+ }
+ }}
+ disabled={pendingUserIds.has(u.id)}
+ >
+ {banUserId === u.id ? 'Cancel' : 'Ban'}
+
+ )}
+ >
)}
-
- ))}
-
-
- {totalPages > 1 && (
-
-
- Page {currentPage} of {totalPages} ({usersData.total} users)
-
-
- setAdminParams((prev) => ({
- offset: Math.max(0, prev.offset - PAGE_SIZE),
- }))
- }
- disabled={usersOffset === 0 || usersLoading}
- >
- Previous
-
+
+ {banUserId === u.id && !u.banned && (
+
+ setBanReason(e.target.value)}
+ placeholder='Reason (optional)'
+ className='flex-1'
+ />
- setAdminParams((prev) => ({ offset: prev.offset + PAGE_SIZE }))
- }
- disabled={
- usersOffset + PAGE_SIZE >= (usersData?.total ?? 0) || usersLoading
- }
+ variant='primary'
+ className='h-[28px] px-3 text-caption'
+ onClick={() => {
+ banUser.reset()
+ banUser.mutate(
+ {
+ userId: u.id,
+ ...(banReason.trim() ? { banReason: banReason.trim() } : {}),
+ },
+ {
+ onSuccess: () => {
+ setBanUserId(null)
+ setBanReason('')
+ },
+ }
+ )
+ }}
+ disabled={pendingUserIds.has(u.id)}
>
- Next
+ Confirm Ban
-
- )}
- >
+ )}
+
+ ))}
+
+
+ {totalPages > 1 && (
+
+
+ Page {currentPage} of {totalPages} ({usersData.total} users)
+
+
+
+ setAdminParams((prev) => ({
+ offset: Math.max(0, prev.offset - PAGE_SIZE),
+ }))
+ }
+ disabled={usersOffset === 0 || usersLoading}
+ >
+ Previous
+
+ setAdminParams((prev) => ({ offset: prev.offset + PAGE_SIZE }))}
+ disabled={usersOffset + PAGE_SIZE >= (usersData?.total ?? 0) || usersLoading}
+ >
+ Next
+
+
+
)}
-
-
+ >
+ )}
-
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx
index abbc6927713..8c388d89a48 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx
@@ -5,23 +5,12 @@ import { createLogger } from '@sim/logger'
import { formatDate } from '@sim/utils/formatting'
import { Info, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
-import {
- Chip,
- ChipConfirmModal,
- ChipInput,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Search,
- Switch,
- Tooltip,
- toast,
-} from '@/components/emcn'
+import { Chip, ChipConfirmModal, Switch, Tooltip, toast } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import {
type ApiKey,
@@ -54,27 +43,13 @@ interface ApiKeyRowMenuProps {
function ApiKeyRowMenu({ keyName, onDelete, canDelete = true }: ApiKeyRowMenuProps) {
return (
-
-
-
-
-
-
-
- copyKeyName(keyName)}>Copy name
-
- Delete
-
-
-
+ copyKeyName(keyName) },
+ { label: 'Delete', destructive: true, disabled: !canDelete, onSelect: onDelete },
+ ]}
+ />
)
}
@@ -87,7 +62,6 @@ export function ApiKeys() {
const userPermissions = useUserPermissionsContext()
const canManageWorkspaceKeys = userPermissions.canAdmin
- // React Query hooks
const {
data: apiKeysData,
isLoading: isLoadingKeys,
@@ -98,7 +72,6 @@ export function ApiKeys() {
const deleteApiKeyMutation = useDeleteApiKey()
const updateSettingsMutation = useUpdateWorkspaceApiKeySettings()
- // Extract data from queries
const workspaceKeys = apiKeysData?.workspaceKeys || []
const personalKeys = apiKeysData?.personalKeys || []
const conflicts = apiKeysData?.conflicts || []
@@ -107,7 +80,6 @@ export function ApiKeys() {
const allowPersonalApiKeys =
workspaceSettingsData?.settings?.workspace?.allowPersonalApiKeys ?? true
- // Local UI state
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [deleteKey, setDeleteKey] = useState
(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
@@ -151,7 +123,6 @@ export function ApiKeys() {
})
} catch (error) {
logger.error('Error deleting API key:', { error })
- // Refetch to restore correct state in case of error
refetchApiKeys()
}
}
@@ -162,11 +133,14 @@ export function ApiKeys() {
}
return (
-
- {/* Fixed header bar */}
-
-
-
+ <>
+
Create API Key
-
-
-
- {/* Scrollable content */}
-
-
- {/* Search Input */}
-
setSearchTerm(e.target.value)}
- />
-
- {/* Key list */}
- {isLoading ? null : personalKeys.length === 0 && workspaceKeys.length === 0 ? (
-
- Click "Create API Key" above to get started
-
- ) : (
-
- {/* Workspace section */}
- {!searchTerm.trim() ? (
-
- {workspaceKeys.length === 0 ? (
-
- No workspace API keys yet
-
- ) : (
-
- {workspaceKeys.map((key) => (
-
-
-
-
- {key.name}
-
-
- (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
-
-
-
- {key.displayKey || key.key}
-
-
-
{
- setDeleteKey(key)
- setShowDeleteDialog(true)
- }}
- canDelete={canManageWorkspaceKeys}
- />
-
- ))}
-
- )}
-
- ) : filteredWorkspaceKeys.length > 0 ? (
-
+ }
+ >
+ {isLoading ? null : personalKeys.length === 0 && workspaceKeys.length === 0 ? (
+ Click "Create API Key" above to get started
+ ) : (
+
+ {!searchTerm.trim() ? (
+
+ {workspaceKeys.length === 0 ? (
+ No workspace API keys yet
+ ) : (
- {filteredWorkspaceKeys.map(({ key }) => (
+ {workspaceKeys.map((key) => (
@@ -265,112 +190,138 @@ export function ApiKeys() {
))}
-
- ) : null}
+ )}
+
+ ) : filteredWorkspaceKeys.length > 0 ? (
+
+
+ {filteredWorkspaceKeys.map(({ key }) => (
+
+
+
+
+ {key.name}
+
+
+ (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
+
+
+
+ {key.displayKey || key.key}
+
+
+
{
+ setDeleteKey(key)
+ setShowDeleteDialog(true)
+ }}
+ canDelete={canManageWorkspaceKeys}
+ />
+
+ ))}
+
+
+ ) : null}
- {/* Personal section */}
- {(!searchTerm.trim() || filteredPersonalKeys.length > 0) && (
-
-
- {filteredPersonalKeys.map(({ key }) => {
- const isConflict = conflicts.includes(key.name)
- return (
-
-
-
-
-
- {key.name}
-
-
- (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
-
-
-
- {key.displayKey || key.key}
-
+ {(!searchTerm.trim() || filteredPersonalKeys.length > 0) && (
+
+
+ {filteredPersonalKeys.map(({ key }) => {
+ const isConflict = conflicts.includes(key.name)
+ return (
+
+
+
+
+
+ {key.name}
+
+
+ (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
+
-
{
- setDeleteKey(key)
- setShowDeleteDialog(true)
- }}
- />
+
+ {key.displayKey || key.key}
+
- {isConflict && (
-
- Workspace API key with the same name overrides this. Rename your
- personal key to use it.
-
- )}
+
{
+ setDeleteKey(key)
+ setShowDeleteDialog(true)
+ }}
+ />
- )
- })}
-
-
- )}
+ {isConflict && (
+
+ Workspace API key with the same name overrides this. Rename your
+ personal key to use it.
+
+ )}
+
+ )
+ })}
+
+
+ )}
- {/* Show message when search has no results across both sections */}
- {searchTerm.trim() &&
- filteredPersonalKeys.length === 0 &&
- filteredWorkspaceKeys.length === 0 &&
- (personalKeys.length > 0 || workspaceKeys.length > 0) && (
-
- No API keys found matching "{searchTerm}"
-
- )}
-
- )}
+ {searchTerm.trim() &&
+ filteredPersonalKeys.length === 0 &&
+ filteredWorkspaceKeys.length === 0 &&
+ (personalKeys.length > 0 || workspaceKeys.length > 0) && (
+
+ No API keys found matching "{searchTerm}"
+
+ )}
+
+ )}
- {/* Allow Personal API Keys Toggle */}
- {!isLoading && canManageWorkspaceKeys && (
-
-
-
-
-
- Allow personal API keys
-
-
-
-
-
-
-
-
- Allow collaborators to create and use their own keys with billing charged to
- them.
-
-
-
- {isLoadingSettings ? null : (
-
{
- try {
- await updateSettingsMutation.mutateAsync({
- workspaceId,
- allowPersonalApiKeys: checked,
- })
- } catch (error) {
- logger.error('Error updating workspace settings:', { error })
- }
- }}
- />
- )}
+ {!isLoading && canManageWorkspaceKeys && (
+
+
+
+
+
+ Allow personal API keys
+
+
+
+
+
+
+
+
+ Allow collaborators to create and use their own keys with billing charged to
+ them.
+
+
-
-
- )}
-
-
+ {isLoadingSettings ? null : (
+ {
+ try {
+ await updateSettingsMutation.mutateAsync({
+ workspaceId,
+ allowPersonalApiKeys: checked,
+ })
+ } catch (error) {
+ logger.error('Error updating workspace settings:', { error })
+ }
+ }}
+ />
+ )}
+
+
+
+ )}
+
- {/* Create API Key Modal */}
- {/* Delete Confirmation Dialog */}
{
@@ -406,6 +356,6 @@ export function ApiKeys() {
pendingLabel: 'Deleting...',
}}
/>
-
+ >
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
index 3b800edaba3..36dde10900e 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx
@@ -38,6 +38,7 @@ import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { UsageLimitField } from '@/app/workspace/[workspaceId]/settings/components/billing/components/usage-limit-field/usage-limit-field'
import { getSubscriptionPermissions } from '@/app/workspace/[workspaceId]/settings/components/billing/subscription-permissions'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import {
useBillingUsageNotifications,
@@ -388,8 +389,6 @@ export function Billing() {
url: invoice.hostedInvoiceUrl ?? invoice.invoicePdf,
}))
- // Org admins (and solo users managing their own billing) can edit; everyone
- // else sees the same controls rendered read-only / disabled rather than hidden.
const canManageBilling = permissions.canEditUsageLimit
const showUsageLimit = !subscription.isFree && !subscription.isEnterprise
const showOnDemand = hasUsablePaidAccess && !subscription.isEnterprise
@@ -405,215 +404,196 @@ export function Billing() {
: usageLimitData.minimumLimit
return (
-
-
-
-
-
-
Billing
-
- Manage your plan, pricing, and invoices.
-
-
-
-
-
-
-
-
- {planName} plan
-
- {priceText}
-
+
+
+
+
+
+
- {!subscription.isEnterprise &&
- (canManageBilling ? (
-
- Explore plans
-
- ) : (
-
- Explore plans
-
- ))}
+
+ {planName} plan
+ {priceText}
+
+
+ {!subscription.isEnterprise &&
+ (canManageBilling ? (
+
+ Explore plans
+
+ ) : (
+
+ Explore plans
+
+ ))}
+
- {showUsageLimit && (
-
+ )}
+
+ {showOnDemand && (
+
+
+
+ Allow usage to go past included usage
+
+
+
+
+ )}
+
+ {!subscription.isFree && !subscription.isEnterprise && (
+
+
+
+ Email me when I reach 80% usage
+
+ {
+ if (value !== billingUsageNotificationsEnabled) {
+ updateGeneralSetting.mutate({
+ key: 'billingUsageNotificationsEnabled',
+ value,
+ })
+ }
+ }}
/>
- )}
+
+
+ )}
- {showOnDemand && (
-
+ {(subscription.isPaid || subscription.isEnterprise) && (
+
+
+ {periodEnd && (
- Allow usage to go past included usage
+ {isCancelledAtPeriodEnd ? 'Access until' : 'Next billing date'}
+
+
+ {new Date(periodEnd).toLocaleDateString()}
-
-
- )}
+ )}
+
+
+ Payment method
+
+ Manage in Stripe
+
+
- {!subscription.isFree && !subscription.isEnterprise && (
-
+ {!subscription.isEnterprise && (
- Email me when I reach 80% usage
+ {isCancelledAtPeriodEnd ? 'Subscription canceled' : 'Cancel subscription'}
- {
- if (value !== billingUsageNotificationsEnabled) {
- updateGeneralSetting.mutate({
- key: 'billingUsageNotificationsEnabled',
- value,
- })
- }
- }}
- />
-
-
- )}
-
- {(subscription.isPaid || subscription.isEnterprise) && (
-
-
- {periodEnd && (
-
-
- {isCancelledAtPeriodEnd ? 'Access until' : 'Next billing date'}
-
-
- {new Date(periodEnd).toLocaleDateString()}
-
-
- )}
-
-
- Payment method
+ {isCancelledAtPeriodEnd ? (
- Manage in Stripe
+ Restore
-
-
- {!subscription.isEnterprise && (
-
-
- {isCancelledAtPeriodEnd ? 'Subscription canceled' : 'Cancel subscription'}
-
- {isCancelledAtPeriodEnd ? (
-
- Restore
-
- ) : (
-
- Cancel
-
- )}
-
- )}
-
-
- )}
-
- {!subscription.isFree && invoices.length > 0 && (
-
-
- {invoices.map((invoice) => {
- const rowClassName =
- 'flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors'
- const rowContent = (
- <>
-
- {invoice.date}
-
-
- {invoice.badge.label}
-
-
- {invoice.amount}
-
-
- >
- )
-
- return invoice.url ? (
-
- {rowContent}
-
- ) : (
-
- {rowContent}
-
- )
- })}
-
- {invoicesData?.hasMore && (
-
- View all
-
+ Cancel
+
)}
-
- )}
-
-
-
+ )}
+
+
+ )}
+
+ {!subscription.isFree && invoices.length > 0 && (
+
+
+ {invoices.map((invoice) => {
+ const rowClassName =
+ 'flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors'
+ const rowContent = (
+ <>
+
+ {invoice.date}
+
+
+ {invoice.badge.label}
+
+
+ {invoice.amount}
+
+
+ >
+ )
+
+ return invoice.url ? (
+
+ {rowContent}
+
+ ) : (
+
+ {rowContent}
+
+ )
+ })}
+
+ {invoicesData?.hasMore && (
+
+ View all
+
+ )}
+
+
+ )}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx
index 10b8902f805..23e8837e451 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx
@@ -22,6 +22,7 @@ import {
} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field'
import { BYOKProviderKeysModal } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal'
import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
const logger = createLogger('BYOKKeyManager')
@@ -323,9 +324,9 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
))}
) : showNoResults ? (
-
+
No providers found matching "{searchTerm}"
-
+
) : sections ? (
{sections.map((section) => {
@@ -382,7 +383,6 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
: `This key will be used for all ${editingMeta?.name} requests in this workspace. Your key is encrypted and stored securely.`}
- {/* Hidden decoy fields to prevent browser autofill */}
-
-
- {
- await upsertKey.mutateAsync({
- workspaceId,
- providerId: providerId as BYOKProviderId,
- apiKey,
- keyId,
- name,
- })
- }}
- onDeleteKey={async (providerId, keyId) => {
- await deleteKey.mutateAsync({
- workspaceId,
- providerId: providerId as BYOKProviderId,
- keyId,
- })
- }}
- />
-
-
-
+
+ {
+ await upsertKey.mutateAsync({
+ workspaceId,
+ providerId: providerId as BYOKProviderId,
+ apiKey,
+ keyId,
+ name,
+ })
+ }}
+ onDeleteKey={async (providerId, keyId) => {
+ await deleteKey.mutateAsync({
+ workspaceId,
+ providerId: providerId as BYOKProviderId,
+ keyId,
+ })
+ }}
+ />
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx
index 00366d171ca..7082bbcba29 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx
@@ -1,25 +1,22 @@
'use client'
import { useMemo, useState } from 'react'
-// import { useParams } from 'next/navigation'
import { createLogger } from '@sim/logger'
import { formatDate } from '@sim/utils/formatting'
import { Plus } from 'lucide-react'
import {
Chip,
ChipConfirmModal,
- ChipInput,
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
- Search,
SecretReveal,
- // Switch,
} from '@/components/emcn'
-// import { useMcpServers, useUpdateMcpServer } from '@/hooks/queries/mcp'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
type CopilotKey,
useCopilotKeys,
@@ -33,29 +30,11 @@ const logger = createLogger('CopilotSettings')
* Copilot Keys management component for handling API keys used with the Copilot feature.
* Provides functionality to create, view, and delete copilot API keys.
*/
-// function McpServerSkeleton() {
-// return (
-//
-// )
-// }
-
export function Copilot() {
- // const params = useParams()
- // const workspaceId = params.workspaceId as string
-
const { data: keys = [], isLoading } = useCopilotKeys()
const generateKey = useGenerateCopilotKey()
const deleteKeyMutation = useDeleteCopilotKey()
- // const { data: mcpServers = [], isLoading: mcpLoading } = useMcpServers(workspaceId)
- // const updateServer = useUpdateMcpServer()
-
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
const [newKey, setNewKey] = useState
(null)
@@ -65,20 +44,6 @@ export function Copilot() {
const [searchTerm, setSearchTerm] = useState('')
const [createError, setCreateError] = useState(null)
- // const enabledServers = mcpServers.filter((s) => s.enabled)
-
- // const handleToggleCopilot = async (serverId: string, enabled: boolean) => {
- // try {
- // await updateServer.mutateAsync({
- // workspaceId,
- // serverId,
- // updates: { copilotEnabled: enabled },
- // })
- // } catch (error) {
- // logger.error('Failed to toggle MCP server for Mothership', { error })
- // }
- // }
-
const filteredKeys = useMemo(() => {
if (!searchTerm.trim()) return keys
const term = searchTerm.toLowerCase()
@@ -140,116 +105,63 @@ export function Copilot() {
return (
<>
-
- {/* Fixed header bar */}
-
-
-
- {
- setIsCreateDialogOpen(true)
- setCreateError(null)
- }}
- disabled={isLoading}
- >
- Create API Key
-
-
-
-
- {/* Scrollable Content */}
-
-
- {/* MCP Tools Section — uncomment when ready to allow users to toggle MCP servers for Mothership
-
-
- MCP Tools
-
- {mcpLoading ? (
-
-
-
-
- ) : enabledServers.length === 0 ? (
-
- No MCP servers configured. Add servers in the MCP Tools tab.
-
- ) : (
-
- {enabledServers.map((server) => (
-
-
-
{server.name}
-
- {server.toolCount ?? 0} tool{server.toolCount === 1 ? '' : 's'}
-
-
-
handleToggleCopilot(server.id, checked)}
- />
-
- ))}
-
- )}
-
- */}
-
- {/* Search Input */}
-
setSearchTerm(e.target.value)}
- />
-
- {/* Keys List */}
- {isLoading ? null : showEmptyState ? (
-
- Click "Create API Key" above to get started
-
- ) : (
-
- {filteredKeys.map((key) => (
-
-
-
-
- {key.name || 'Unnamed Key'}
-
-
- (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
-
-
-
- {key.displayKey}
-
-
-
{
- setDeleteKey(key)
- setShowDeleteDialog(true)
- }}
- >
- Delete
-
-
- ))}
- {showNoResults && (
-
- No API keys found matching "{searchTerm}"
+
{
+ setIsCreateDialogOpen(true)
+ setCreateError(null)
+ }}
+ disabled={isLoading}
+ >
+ Create API Key
+
+ }
+ >
+ {isLoading ? null : showEmptyState ? (
+ Click "Create API Key" above to get started
+ ) : (
+
+ {filteredKeys.map((key) => (
+
+
+
+
+ {key.name || 'Unnamed Key'}
+
+
+ (last used: {formatLastUsed(key.lastUsed).toLowerCase()})
+
- )}
+
{key.displayKey}
+
+
{
+ setDeleteKey(key)
+ setShowDeleteDialog(true)
+ }}
+ >
+ Delete
+
+ ))}
+ {showNoResults && (
+
+ No API keys found matching "{searchTerm}"
+
)}
-
-
+ )}
+
- {/* Create API Key Dialog */}
- {/* New API Key Dialog */}
{
@@ -329,7 +240,6 @@ export function Copilot() {
/>
- {/* Delete Confirmation Dialog */}
{
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx
index 6f5f05477d3..8664283de10 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx
@@ -13,21 +13,13 @@ import {
ButtonGroupItem,
Chip,
ChipConfirmModal,
- ChipInput,
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
type FileInputOptions,
- MoreHorizontal,
- Search,
TagInput,
type TagItem,
} from '@/components/emcn'
@@ -39,6 +31,9 @@ import { getProviderDisplayName, type PollingProvider } from '@/lib/credential-s
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { getUserColor } from '@/lib/workspaces/colors'
import { getUserRole } from '@/lib/workspaces/organization'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import {
type CredentialSet,
@@ -522,26 +517,17 @@ export function CredentialSets() {
-
-
-
-
-
-
-
- handleRemoveMember(member.id)}
- disabled={removeMember.isPending}
- >
- Remove
-
-
-
+ handleRemoveMember(member.id),
+ },
+ ]}
+ />
)
@@ -580,41 +566,30 @@ export function CredentialSets() {
-
-
-
-
-
-
-
- handleResendInvitation(invitation.id, email)}
- disabled={
- resendingInvitations.has(invitation.id) ||
- (resendCooldowns[invitation.id] ?? 0) > 0
- }
- >
- {resendingInvitations.has(invitation.id)
+
- handleCancelInvitation(invitation.id)}
- disabled={cancellingInvitations.has(invitation.id)}
- >
- {cancellingInvitations.has(invitation.id)
+ : 'Resend',
+ disabled:
+ resendingInvitations.has(invitation.id) ||
+ (resendCooldowns[invitation.id] ?? 0) > 0,
+ onSelect: () => handleResendInvitation(invitation.id, email),
+ },
+ {
+ label: cancellingInvitations.has(invitation.id)
? 'Cancelling...'
- : 'Cancel'}
-
-
-
+ : 'Cancel',
+ destructive: true,
+ disabled: cancellingInvitations.has(invitation.id),
+ onSelect: () => handleCancelInvitation(invitation.id),
+ },
+ ]}
+ />
)
@@ -630,177 +605,154 @@ export function CredentialSets() {
return (
<>
-
-
-
-
- {canManageCredentialSets && (
- setShowCreateModal(true)}>
- Create Group
-
- )}
-
-
+
setShowCreateModal(true)}>
+ Create Group
+
+ )
+ }
+ >
+
+ {hasNoContent && !canManageCredentialSets ? (
+
+ You're not a member of any polling groups yet. When someone invites you, it will
+ appear here.
+
+ ) : hasNoResults ? (
+
+ No results found matching "{searchTerm}"
+
+ ) : (
+
+ {filteredInvitations.length > 0 && (
+
+
+ {filteredInvitations.map((invitation) => (
+
+
+
+ {getProviderIcon(invitation.providerId)}
+
+
+
+ {invitation.credentialSetName}
+
+
+ {invitation.organizationName}
+
+
+
+
handleAcceptInvitation(invitation.token)}
+ disabled={acceptInvitation.isPending}
+ >
+ {acceptInvitation.isPending ? 'Accepting...' : 'Accept'}
+
+
+ ))}
+
+
+ )}
-
-
-
setSearchTerm(e.target.value)}
- />
-
-
- {hasNoContent && !canManageCredentialSets ? (
-
- You're not a member of any polling groups yet. When someone invites you, it will
- appear here.
-
- ) : hasNoResults ? (
-
- No results found matching "{searchTerm}"
-
- ) : (
-
- {filteredInvitations.length > 0 && (
-
-
- {filteredInvitations.map((invitation) => (
-
-
-
- {getProviderIcon(invitation.providerId)}
-
-
-
- {invitation.credentialSetName}
-
-
- {invitation.organizationName}
-
-
-
-
handleAcceptInvitation(invitation.token)}
- disabled={acceptInvitation.isPending}
- >
- {acceptInvitation.isPending ? 'Accepting...' : 'Accept'}
-
+ {filteredMemberships.length > 0 && (
+
+
+ {filteredMemberships.map((membership) => (
+
+
+
+ {getProviderIcon(membership.providerId)}
- ))}
+
+
+ {membership.credentialSetName}
+
+
+ {membership.organizationName}
+
+
+
+
+ handleLeave(membership.credentialSetId, membership.credentialSetName)
+ }
+ disabled={leaveCredentialSet.isPending}
+ >
+ Leave
+
-
- )}
+ ))}
+
+
+ )}
- {filteredMemberships.length > 0 && (
-
+ {canManageCredentialSets &&
+ (filteredOwnedSets.length > 0 ||
+ ownedSetsLoading ||
+ (!searchTerm.trim() && ownedSets.length === 0)) && (
+
+ {ownedSetsLoading ? null : !searchTerm.trim() && ownedSets.length === 0 ? (
+
+ No polling groups created yet
+
+ ) : (
- {filteredMemberships.map((membership) => (
+ {filteredOwnedSets.map((set) => (
- {getProviderIcon(membership.providerId)}
+ {getProviderIcon(set.providerId)}
- {membership.credentialSetName}
+ {set.name}
- {membership.organizationName}
+ {set.memberCount} member{set.memberCount !== 1 ? 's' : ''}
-
- handleLeave(
- membership.credentialSetId,
- membership.credentialSetName
- )
- }
- disabled={leaveCredentialSet.isPending}
- >
- Leave
-
+
+ setViewingSet(set) },
+ {
+ label: 'Delete',
+ destructive: true,
+ disabled: deletingSetIds.has(set.id),
+ onSelect: () => handleDeleteClick(set),
+ },
+ ]}
+ />
+
))}
-
- )}
-
- {canManageCredentialSets &&
- (filteredOwnedSets.length > 0 ||
- ownedSetsLoading ||
- (!searchTerm.trim() && ownedSets.length === 0)) && (
-
- {ownedSetsLoading ? null : !searchTerm.trim() && ownedSets.length === 0 ? (
-
- No polling groups created yet
-
- ) : (
-
- {filteredOwnedSets.map((set) => (
-
-
-
- {getProviderIcon(set.providerId)}
-
-
-
- {set.name}
-
-
- {set.memberCount} member{set.memberCount !== 1 ? 's' : ''}
-
-
-
-
-
-
-
-
-
-
-
- setViewingSet(set)}>
- Details
-
- handleDeleteClick(set)}
- disabled={deletingSetIds.has(set.id)}
- >
- Delete
-
-
-
-
-
- ))}
-
- )}
-
)}
-
- )}
+
+ )}
-
+ )}
-
+
-
-
-
-
-
setShowAddForm(true)}
- disabled={isLoading}
- >
- Add Tool
-
+
setShowAddForm(true)}
+ disabled={isLoading}
+ >
+ Add Tool
+
+ }
+ >
+ {error ? (
+
+
+ {getErrorMessage(error, 'Failed to load tools')}
+
-
-
-
-
-
setSearchTerm(e.target.value)}
- disabled={isLoading}
- />
-
- {error ? (
-
-
- {getErrorMessage(error, 'Failed to load tools')}
-
-
- ) : isLoading ? null : showEmptyState ? (
-
- Click "Add Tool" above to get started
-
- ) : (
-
- {filteredTools.map((tool) => (
-
-
-
- {tool.title || 'Unnamed Tool'}
-
- {tool.schema?.function?.description && (
-
- {tool.schema.function.description}
-
- )}
-
-
-
-
-
-
-
-
-
- setEditingTool(tool.id)}>
- Edit
-
- handleDeleteClick(tool.id)}
- disabled={deletingTools.has(tool.id)}
- >
- Delete
-
-
-
-
-
- ))}
- {showNoResults && (
-
- No tools found matching "{searchTerm}"
-
- )}
+ ) : isLoading ? null : showEmptyState ? (
+
Click "Add Tool" above to get started
+ ) : (
+
+ {filteredTools.map((tool) => (
+
+
+
+ {tool.title || 'Unnamed Tool'}
+
+ {tool.schema?.function?.description && (
+
+ {tool.schema.function.description}
+
+ )}
+
+
+ setEditingTool(tool.id) },
+ {
+ label: 'Delete',
+ destructive: true,
+ disabled: deletingTools.has(tool.id),
+ onSelect: () => handleDeleteClick(tool.id),
+ },
+ ]}
+ />
+
+ ))}
+ {showNoResults && (
+
+ No tools found matching "{searchTerm}"
+
)}
-
-
+ )}
+
- {/* Fixed header bar */}
-
-
- {isHosted && (
- window.open('/?home', '_blank', 'noopener,noreferrer')}>
- Home Page
-
- )}
-
- {!isAuthDisabled && (
-
- Sign out
- setShowResetPasswordModal(true)}>Reset password
-
- )}
-
-
- {/* Scrollable content */}
-
-
-
-
-
-
-
- handleKeyboardActivation(event, handleProfilePictureClick)
- }
- >
- {(() => {
- if (imageUrl) {
- return (
-
- )
- }
+ <>
+
+ {isHosted && (
+ window.open('/?home', '_blank', 'noopener,noreferrer')}>
+ Home Page
+
+ )}
+ {!isAuthDisabled && (
+ <>
+ Sign out
+ setShowResetPasswordModal(true)}>Reset password
+ >
+ )}
+ >
+ }
+ >
+
+
+
+
+
handleKeyboardActivation(event, handleProfilePictureClick)}
+ >
+ {(() => {
+ if (imageUrl) {
return (
-
- {getInitials(profile?.name) || ''}
-
+
)
- })()}
-
- {isUploadingProfilePicture ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- {isEditingName ? (
- <>
-
-
- {name || ' '}
-
- setName(e.target.value)}
- onKeyDown={handleKeyDown}
- onBlur={handleInputBlur}
- className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-base outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
- maxLength={100}
- disabled={updateProfile.isPending}
- autoComplete='off'
- autoCorrect='off'
- autoCapitalize='off'
- spellCheck='false'
- />
-
-
-
-
- >
+ }
+ return (
+
+ {getInitials(profile?.name) || ''}
+
+ )
+ })()}
+
+ {isUploadingProfilePicture ? (
+
) : (
- <>
-
{profile?.name || ''}
-
setIsEditingName(true)}
- aria-label='Edit name'
- >
-
-
- >
+
)}
-
{profile?.email || ''}
+
- {uploadError &&
{uploadError}
}
-
-
-
-
-
-
-
Theme
-
-
+
+
+ {isEditingName ? (
+ <>
+
+
+ {name || ' '}
+
+ setName(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleInputBlur}
+ className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-base outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
+ maxLength={100}
+ disabled={updateProfile.isPending}
+ autoComplete='off'
+ autoCorrect='off'
+ autoCapitalize='off'
+ spellCheck='false'
+ />
+
+
+
+
+ >
+ ) : (
+ <>
+
{profile?.name || ''}
+
setIsEditingName(true)}
+ aria-label='Edit name'
+ >
+
+
+ >
+ )}
+
{profile?.email || ''}
-
-
+ {uploadError &&
{uploadError}
}
+
+
+
+
+
+
-
-
-
Auto-connect on drop
-
-
-
-
-
- Automatically connect blocks when dropped near each other
-
-
-
-
-
+ Timezone
+
+
+
-
-
-
Canvas error notifications
-
-
-
-
-
- Show error popups on blocks when a workflow run fails
-
-
-
-
-
+
+
+
Auto-connect on drop
+
+
+
+
+
+ Automatically connect blocks when dropped near each other
+
+
+
+
+
-
-
Snap to grid
-
-
-
+
+
+
Canvas error notifications
+
+
+
+
+
+ Show error popups on blocks when a workflow run fails
+
+
+
+
+
-
-
Show canvas controls
-
+ Snap to grid
+
+
+
- {isTrainingEnabled && (
-
- Training controls
-
-
- )}
+
+ Show canvas controls
+
-
-
-
+ {isTrainingEnabled && (
- Allow anonymous telemetry
+ Training controls
-
- We use OpenTelemetry to collect anonymous usage data to improve Sim. You can opt-out
- at any time.
-
+ )}
+
+
+
+
+
+
+ Allow anonymous telemetry
+
-
-
-
+
+ We use OpenTelemetry to collect anonymous usage data to improve Sim. You can opt-out
+ at any time.
+
+
+
+
- {/* Password Reset Confirmation Modal */}
-
+ >
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx
index 7f87e271258..f734ffd5a72 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx
@@ -5,6 +5,7 @@ import { formatRelativeTime } from '@sim/utils/formatting'
import { ArrowRight, Paperclip } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Badge, ChipInput, ChipSelect, Search } from '@/components/emcn'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import type { InboxTaskItem } from '@/hooks/queries/inbox'
import { useInboxConfig, useInboxTasks } from '@/hooks/queries/inbox'
@@ -89,15 +90,15 @@ export function InboxTaskList() {
{isLoading ? null : filteredTasks.length === 0 ? (
searchTerm.trim() ? (
-
+
{`No tasks matching "${searchTerm}"`}
-
+
) : (
-
+
{config?.address
? `No email tasks yet. Send an email to ${config.address} to get started.`
: 'No email tasks yet.'}
-
+
)
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox.tsx
index 83cffbf0979..728c01c7017 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox.tsx
@@ -9,6 +9,7 @@ import {
InboxSettingsTab,
InboxTaskList,
} from '@/app/workspace/[workspaceId]/settings/components/inbox/components'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
import { useInboxConfig } from '@/hooks/queries/inbox'
@@ -59,25 +60,21 @@ export function Inbox() {
}
return (
-
-
-
-
+
+
- {config?.enabled && (
- <>
-
+ {config?.enabled && (
+ <>
+
-
-
- Email tasks received by this workspace.
-
-
-
- >
- )}
-
-
-
+
+
+ Email tasks received by this workspace.
+
+
+
+ >
+ )}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
index c468f1e8e4e..a1f60a9e2fc 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
@@ -6,21 +6,7 @@ import { getErrorMessage } from '@sim/utils/errors'
import { ChevronDown, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import { useQueryState } from 'nuqs'
-import {
- Badge,
- Button,
- Chip,
- ChipConfirmModal,
- ChipInput,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Search,
- Tooltip,
-} from '@/components/emcn'
+import { Badge, Button, Chip, ChipConfirmModal, Tooltip } from '@/components/emcn'
import { ArrowLeft } from '@/components/emcn/icons'
import { requestJson } from '@/lib/api/client/request'
import { getWorkflowStateContract } from '@/lib/api/contracts/workflows'
@@ -36,6 +22,9 @@ import {
mcpServerIdParam,
mcpServerIdUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/[section]/search-params'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { useMcpOauthPopup } from '@/hooks/mcp/use-mcp-oauth-popup'
import {
@@ -126,27 +115,13 @@ function ServerListItem({
-
-
-
-
-
-
-
- Details
-
- Delete
-
-
-
+
)
@@ -632,70 +607,59 @@ export function MCP() {
return (
<>
-
-
-
-
-
setShowAddModal(true)}
- disabled={serversLoading}
- >
- Add Server
-
+
setShowAddModal(true)}
+ disabled={serversLoading}
+ >
+ Add Server
+
+ }
+ >
+ {error ? (
+
+
+ {getErrorMessage(error, 'Failed to load MCP servers')}
+
-
-
-
-
-
setSearchTerm(e.target.value)}
- />
-
- {error ? (
-
-
- {getErrorMessage(error, 'Failed to load MCP servers')}
-
-
- ) : serversLoading ? null : !hasServers ? (
-
- Click "Add Server" above to get started
-
- ) : (
-
- {filteredServers.map((server) => {
- if (!server?.id) return null
- const tools = toolsByServer[server.id] || []
- const isLoadingTools = toolsLoading || toolsFetching
-
- return (
-
handleRemoveServer(server.id)}
- onViewDetails={() => handleViewDetails(server.id)}
- />
- )
- })}
- {showNoResults && (
-
- No servers found matching "{searchTerm}"
-
- )}
-
+ ) : serversLoading ? null : !hasServers ? (
+ Click "Add Server" above to get started
+ ) : (
+
+ {filteredServers.map((server) => {
+ if (!server?.id) return null
+ const tools = toolsByServer[server.id] || []
+ const isLoadingTools = toolsLoading || toolsFetching
+
+ return (
+ handleRemoveServer(server.id)}
+ onViewDetails={() => handleViewDetails(server.id)}
+ />
+ )
+ })}
+ {showNoResults && (
+
+ No servers found matching "{searchTerm}"
+
)}
-
-
+ )}
+
{isEmpty ? (
- {emptyText}
+ {emptyText}
) : (
{children}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx
index 4cdf68f9e54..a2eab8cfb6c 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx
@@ -15,6 +15,8 @@ import {
mothershipParsers,
mothershipUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/components/mothership/search-params'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
type MothershipByokKey,
type MothershipEnv,
@@ -98,79 +100,68 @@ export function Mothership() {
const [end, setEnd] = useState(defaults.end)
return (
-
-
-
- {/* Environment selector */}
+
+
+
+ Environment
+ setMothershipParams({ env: value as MothershipEnv })}
+ placeholder='Select environment'
+ options={ENV_OPTIONS}
+ />
+
+
+
+ {TABS.map((tab) => (
+ setMothershipParams({ tab: tab.id })}
+ className={cn(
+ 'relative px-3 py-2 font-medium text-sm transition-colors',
+ activeTab === tab.id
+ ? 'text-[var(--text-primary)]'
+ : 'text-[var(--text-tertiary)] hover-hover:hover:text-[var(--text-secondary)]'
+ )}
+ >
+ {tab.label}
+ {activeTab === tab.id && (
+
+ )}
+
+ ))}
+
+
+
- Environment
- setMothershipParams({ env: value as MothershipEnv })}
- placeholder='Select environment'
- options={ENV_OPTIONS}
+ From
+ setStart(e.target.value)}
/>
-
- {/* Tab bar */}
-
- {TABS.map((tab) => (
- setMothershipParams({ tab: tab.id })}
- className={cn(
- 'relative px-3 py-2 font-medium text-sm transition-colors',
- activeTab === tab.id
- ? 'text-[var(--text-primary)]'
- : 'text-[var(--text-tertiary)] hover-hover:hover:text-[var(--text-secondary)]'
- )}
- >
- {tab.label}
- {activeTab === tab.id && (
-
- )}
-
- ))}
-
-
- {/* Time range (shared across tabs) */}
-
-
- From
- setStart(e.target.value)}
- />
-
-
- To
- setEnd(e.target.value)}
- />
-
+
+ To
+ setEnd(e.target.value)} />
+
-
+
- {activeTab === 'overview' && (
-
- )}
- {activeTab === 'licenses' &&
}
- {activeTab === 'byok' &&
}
-
+ {activeTab === 'overview' && (
+
+ )}
+ {activeTab === 'licenses' &&
}
+ {activeTab === 'byok' &&
}
-
+
)
}
-/* ─── BYOK Tab ─── */
-
function ByokTab() {
const params = useParams()
const workspaceId = (params?.workspaceId as string) || ''
@@ -203,8 +194,6 @@ function ByokTab() {
)
}
-/* ─── Overview Tab ─── */
-
function OverviewTab({
environment,
start,
@@ -227,7 +216,6 @@ function OverviewTab({
return (
- {/* Summary cards */}
- {/* User breakdown */}
User Breakdown
{breakdownLoading && (
@@ -311,7 +298,6 @@ function OverviewTab({
)}
- {/* Recent requests */}
Recent Requests ({requests?.count ?? '…'})
{requestsLoading && (
@@ -387,8 +373,6 @@ function OverviewTab({
)
}
-/* ─── Licenses Tab ─── */
-
function LicensesTab({ environment }: { environment: MothershipEnv }) {
const { data, isLoading, refetch } = useMothershipLicenses(environment)
const generateLicense = useGenerateLicense(environment)
@@ -484,9 +468,7 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
Created
{data.licenses.length === 0 && (
-
- No licenses found.
-
+
No licenses found.
)}
{data.licenses.map(
(lic: {
@@ -519,8 +501,6 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
)
}
-/* ─── Shared components ─── */
-
function StatCard({
label,
value,
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
index a9dfb740da6..8dc3b308813 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
@@ -19,6 +19,8 @@ import {
recentlyDeletedParsers,
recentlyDeletedUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/search-params'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { useFolders, useRestoreFolder } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge'
import { useRestoreTable, useTablesList } from '@/hooks/queries/tables'
@@ -406,111 +408,105 @@ export function RecentlyDeleted() {
}
return (
-
-
-
-
- setSearchTerm(e.target.value)}
- disabled={isLoading}
- className='min-w-0 flex-1'
- />
- {
- const sort = (RECENTLY_DELETED_SORT_COLUMNS as readonly string[]).includes(column)
- ? (column as RecentlyDeletedSortColumn)
- : DEFAULT_RECENTLY_DELETED_SORT_COLUMN
- setRecentlyDeletedFilters({ sort, dir: direction })
- },
- onClear: () =>
- setRecentlyDeletedFilters({
- sort: DEFAULT_RECENTLY_DELETED_SORT_COLUMN,
- dir: DEFAULT_RECENTLY_DELETED_SORT_DIRECTION,
- }),
- }}
- />
-
-
-
({ value: tab.id, label: tab.label }))}
- value={activeTab}
- onChange={(v) => setRecentlyDeletedFilters({ tab: v as RecentlyDeletedTab })}
- />
-
- {error ? (
-
-
- {toError(error).message || 'Failed to load deleted items'}
-
-
- ) : isLoading ? null : filtered.length === 0 ? (
- showNoResults ? (
-
- {`No items found matching \u201c${urlSearchTerm}\u201d`}
-
- ) : (
-
- No deleted items
+
+
+ setSearchTerm(e.target.value)}
+ disabled={isLoading}
+ className='min-w-0 flex-1'
+ />
+ {
+ const sort = (RECENTLY_DELETED_SORT_COLUMNS as readonly string[]).includes(column)
+ ? (column as RecentlyDeletedSortColumn)
+ : DEFAULT_RECENTLY_DELETED_SORT_COLUMN
+ setRecentlyDeletedFilters({ sort, dir: direction })
+ },
+ onClear: () =>
+ setRecentlyDeletedFilters({
+ sort: DEFAULT_RECENTLY_DELETED_SORT_COLUMN,
+ dir: DEFAULT_RECENTLY_DELETED_SORT_DIRECTION,
+ }),
+ }}
+ />
+
+
+ ({ value: tab.id, label: tab.label }))}
+ value={activeTab}
+ onChange={(v) => setRecentlyDeletedFilters({ tab: v as RecentlyDeletedTab })}
+ />
+
+ {error ? (
+
+
+ {toError(error).message || 'Failed to load deleted items'}
+
+
+ ) : isLoading ? null : filtered.length === 0 ? (
+ showNoResults ? (
+
+ {`No items found matching \u201c${urlSearchTerm}\u201d`}
+
+ ) : (
+ No deleted items
+ )
+ ) : (
+
+ {filtered.map((resource) => {
+ const isRestoring = restoringIds.has(resource.id)
+ const isRestored = restoredItems.has(resource.id)
+
+ return (
+
+
+
+
+
+ {resource.name}
+
+
+ {TYPE_LABEL[resource.type]}
+ {' \u00b7 '}
+ Deleted {formatDate(resource.deletedAt)}
+
+
+
+ {isRestoring ? (
+
+ Restoring...
+
+ ) : isRestored ? (
+
+ Restored
+ handleView(resource)}>
+ View
+
+
+ ) : (
+
void handleRestore(resource)}
+ className='shrink-0'
+ >
+ Restore
+
+ )}
)
- ) : (
-
- {filtered.map((resource) => {
- const isRestoring = restoringIds.has(resource.id)
- const isRestored = restoredItems.has(resource.id)
-
- return (
-
-
-
-
-
- {resource.name}
-
-
- {TYPE_LABEL[resource.type]}
- {' \u00b7 '}
- Deleted {formatDate(resource.deletedAt)}
-
-
-
- {isRestoring ? (
-
- Restoring...
-
- ) : isRestored ? (
-
- Restored
- handleView(resource)}>
- View
-
-
- ) : (
-
void handleRestore(resource)}
- className='shrink-0'
- >
- Restore
-
- )}
-
- )
- })}
-
- )}
+ })}
-
-
+ )}
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/index.ts
new file mode 100644
index 00000000000..750a8f684de
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/index.ts
@@ -0,0 +1 @@
+export { type RowAction, RowActionsMenu } from './row-actions-menu'
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx
new file mode 100644
index 00000000000..288ccd1109f
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx
@@ -0,0 +1,57 @@
+import {
+ chipVariants,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ MoreHorizontal,
+} from '@/components/emcn'
+import { cn } from '@/lib/core/utils/cn'
+
+export interface RowAction {
+ label: string
+ onSelect: () => void
+ /** Renders in the error color (e.g. Delete). */
+ destructive?: boolean
+ disabled?: boolean
+}
+
+interface RowActionsMenuProps {
+ /** Accessible label for the trigger, e.g. `API key actions`. */
+ label: string
+ actions: RowAction[]
+ /** Layout-only classes for the trigger button (e.g. a left margin). */
+ triggerClassName?: string
+}
+
+/**
+ * Canonical trailing `...` actions menu for a settings list row. Mirrors the
+ * Teammates / Secrets / API-key row menus so every list row behaves identically.
+ */
+export function RowActionsMenu({ label, actions, triggerClassName }: RowActionsMenuProps) {
+ return (
+
+
+
+
+
+
+
+ {actions.map((action) => (
+
+ {action.label}
+
+ ))}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx
index d1c25230b8f..bbec6f9f8ca 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx
@@ -5,19 +5,7 @@ import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { useQueryClient } from '@tanstack/react-query'
import { useParams, useRouter } from 'next/navigation'
-import {
- Chip,
- ChipInput,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Search,
- Tooltip,
- toast,
-} from '@/components/emcn'
+import { Chip, ChipInput, Tooltip, toast } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import {
clearPendingCredentialCreateRequest,
@@ -27,7 +15,10 @@ import {
} from '@/lib/credentials/client-state'
import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import { SecretValueField } from '@/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { isValidEnvVarName } from '@/executor/constants'
import { useWorkspaceCredentials, type WorkspaceCredential } from '@/hooks/queries/credentials'
import {
@@ -67,28 +58,15 @@ interface SecretRowMenuProps {
*/
function SecretRowMenu({ onCopyName, onViewDetails, onDelete }: SecretRowMenuProps) {
return (
-
-
-
-
-
-
-
- {onViewDetails && (
- View details
- )}
- Copy name
- {onDelete && (
-
- Delete
-
- )}
-
-
+
)
}
@@ -944,36 +922,39 @@ export function SecretsManager() {
return (
<>
-
- {/* Hidden honeypot inputs to prevent browser autofill */}
-
-
-
-
-
+
+
+
+
+
- {/* Fixed header bar */}
-
-
-
+
{hasChanges && (
Discard
@@ -997,110 +978,87 @@ export function SecretsManager() {
{isListSaving ? 'Saving...' : 'Save'}
)}
-
-
-
- {/* Scrollable content */}
-
-
- {/* Search */}
-
setSearchTerm(e.target.value)}
- name='env_search_field'
- autoComplete='off'
- autoCapitalize='off'
- spellCheck='false'
- readOnly
- onFocus={(e) => e.target.removeAttribute('readOnly')}
- />
-
- {/* Secrets grid */}
- {!isLoading && (
-
- {(!searchTerm.trim() ||
- filteredWorkspaceEntries.length > 0 ||
- filteredNewWorkspaceRows.length > 0) && (
-
- Workspace
-
-
- {(searchTerm.trim()
- ? filteredWorkspaceEntries
- : Object.entries(workspaceVars)
- ).map(([key, value]) => {
- const cred = workspaceEnvKeyToCredential.get(key)
- const canEditRow = cred?.role === 'admin'
- return (
-
- )
- })}
- {canCreateWorkspaceSecret &&
- (searchTerm.trim()
- ? filteredNewWorkspaceRows
- : newWorkspaceRows.map((row, index) => ({ row, originalIndex: index }))
- ).map(({ row, originalIndex }) => (
-
- ))}
-
-
- )}
+ >
+ }
+ >
+ {!isLoading && (
+
+ {(!searchTerm.trim() ||
+ filteredWorkspaceEntries.length > 0 ||
+ filteredNewWorkspaceRows.length > 0) && (
+
+ Workspace
+
+
+ {(searchTerm.trim()
+ ? filteredWorkspaceEntries
+ : Object.entries(workspaceVars)
+ ).map(([key, value]) => {
+ const cred = workspaceEnvKeyToCredential.get(key)
+ const canEditRow = cred?.role === 'admin'
+ return (
+
+ )
+ })}
+ {canCreateWorkspaceSecret &&
+ (searchTerm.trim()
+ ? filteredNewWorkspaceRows
+ : newWorkspaceRows.map((row, index) => ({ row, originalIndex: index }))
+ ).map(({ row, originalIndex }) => (
+
+ ))}
+
+
+ )}
- {(!searchTerm.trim() || filteredEnvVars.length > 0) && (
-
- Personal
-
-
- {filteredEnvVars.map(({ envVar, originalIndex }) => (
-
- {renderEnvVarRow(envVar, originalIndex)}
-
- ))}
-
-
- )}
- {searchTerm.trim() &&
- filteredEnvVars.length === 0 &&
- filteredWorkspaceEntries.length === 0 &&
- filteredNewWorkspaceRows.length === 0 &&
- (envVars.length > 0 ||
- Object.keys(workspaceVars).length > 0 ||
- newWorkspaceRows.length > 0) && (
-
- No secrets found matching “{searchTerm}”
+ {(!searchTerm.trim() || filteredEnvVars.length > 0) && (
+
+ Personal
+
+
+ {filteredEnvVars.map(({ envVar, originalIndex }) => (
+
+ {renderEnvVarRow(envVar, originalIndex)}
- )}
-
+ ))}
+
+
)}
+ {searchTerm.trim() &&
+ filteredEnvVars.length === 0 &&
+ filteredWorkspaceEntries.length === 0 &&
+ filteredNewWorkspaceRows.length === 0 &&
+ (envVars.length > 0 ||
+ Object.keys(workspaceVars).length > 0 ||
+ newWorkspaceRows.length > 0) && (
+
+ No secrets found matching “{searchTerm}”
+
+ )}
-
-
+ )}
+
+ {children}
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/index.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/index.ts
new file mode 100644
index 00000000000..4535b2c8949
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/index.ts
@@ -0,0 +1 @@
+export { SettingsPanel, SettingsSectionProvider } from './settings-panel'
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx
new file mode 100644
index 00000000000..0b38d3646db
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-panel/settings-panel.tsx
@@ -0,0 +1,124 @@
+'use client'
+
+import { createContext, type ReactNode, useContext } from 'react'
+import { ChipInput, Search } from '@/components/emcn'
+import { cn } from '@/lib/core/utils/cn'
+import {
+ getSettingsSectionMeta,
+ type SettingsSection,
+} from '@/app/workspace/[workspaceId]/settings/navigation'
+
+const SettingsSectionContext = createContext
(null)
+
+/**
+ * Provides the active settings section to descendants so `SettingsPanel` can
+ * resolve its title/description from navigation metadata. Set once by the
+ * settings shell with the resolved (post-redirect) section.
+ */
+export function SettingsSectionProvider({
+ section,
+ children,
+}: {
+ section: SettingsSection
+ children: ReactNode
+}) {
+ return (
+ {children}
+ )
+}
+
+function useSettingsSection(): SettingsSection | null {
+ return useContext(SettingsSectionContext)
+}
+
+interface SettingsPanelSearch {
+ value: string
+ onChange: (value: string) => void
+ placeholder?: string
+ disabled?: boolean
+}
+
+interface SettingsPanelProps {
+ /** Body content rendered below the header in the centered content column. */
+ children: ReactNode
+ /** Right-aligned controls in the fixed header bar (e.g. a Create/Invite chip). */
+ actions?: ReactNode
+ /** Overrides the nav-driven title (e.g. for a detail sub-view). */
+ title?: string
+ /** Overrides the nav-driven description. */
+ description?: string
+ /** Extra classes for the content column (layout/spacing only, e.g. a tighter `gap-*`). */
+ contentClassName?: string
+ /** Ref forwarded to the scroll region (e.g. for programmatic scroll-to-bottom). */
+ scrollContainerRef?: React.Ref
+ /**
+ * Renders the canonical search field directly below the title. Omit on pages
+ * with no search, or that pair search with extra controls (render that row in
+ * `children` instead).
+ */
+ search?: SettingsPanelSearch
+}
+
+/**
+ * Standard chrome for a settings page: a fixed header bar (right-aligned
+ * `actions`), a scroll region, and a centered content column led by the page
+ * title + description. The title/description come from the active section's
+ * navigation metadata by default, and can be overridden for sub-views.
+ *
+ * Pages render only their body as `children`; they no longer hand-roll the
+ * shell, header bar, or title block.
+ */
+export function SettingsPanel({
+ children,
+ actions,
+ title,
+ description,
+ contentClassName,
+ scrollContainerRef,
+ search,
+}: SettingsPanelProps) {
+ const section = useSettingsSection()
+ const meta = section ? getSettingsSectionMeta(section) : null
+ const resolvedTitle = title ?? meta?.label
+ const resolvedDescription = description ?? meta?.description
+
+ return (
+
+
+
+
+ {(resolvedTitle || resolvedDescription) && (
+
+ {resolvedTitle && (
+
{resolvedTitle}
+ )}
+ {resolvedDescription && (
+
{resolvedDescription}
+ )}
+
+ )}
+ {search && (
+
search.onChange(event.target.value)}
+ disabled={search.disabled}
+ autoComplete='off'
+ className='w-full'
+ />
+ )}
+ {children}
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx
index ca12bbced71..33d2cf2efb5 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx
@@ -4,18 +4,7 @@ import { useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/predicates'
import { getErrorMessage } from '@sim/utils/errors'
-import {
- ChipDropdown,
- ChipInput,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Search,
- toast,
-} from '@/components/emcn'
+import { ChipDropdown, ChipInput, Search, toast } from '@/components/emcn'
import {
type OrgRole,
type PermissionType,
@@ -33,6 +22,10 @@ import {
MemberRow,
MemberSection,
} from '@/app/workspace/[workspaceId]/settings/components/member-list'
+import {
+ type RowAction,
+ RowActionsMenu,
+} from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
import {
ManageCreditsModal,
type ManageCreditsTarget,
@@ -118,15 +111,8 @@ export function OrganizationMemberLists({
const isActiveSearch = q.length > 0
- const buildActionsMenu = (children: React.ReactNode) => (
-
-
-
-
-
-
- {children}
-
+ const buildActionsMenu = (actions: RowAction[]) => (
+
)
const renderOrgMemberRow = (member: RosterMember) => {
@@ -169,69 +155,63 @@ export function OrganizationMemberLists({
/>
)
}
- menu={buildActionsMenu(
- <>
- copyToClipboard(member.email)}>
- Copy email
-
- {!isOwner && (
-
- setCreditsTarget({
- userId: member.userId,
- name: member.name,
- email: member.email,
- })
- }
- >
- Manage Credits
-
- )}
- {canRemove && (
-
- onRemoveMember({
- id: member.memberId,
- role: member.role,
- user: {
- id: member.userId,
- name: member.name,
- email: member.email,
- image: member.image,
- },
- })
- }
- >
- Remove
-
- )}
- {isSelf && isOwner && onTransferOwnership && (
- onTransferOwnership()}>
- Transfer ownership
-
- )}
- {isSelf && !isOwner && (
-
- onRemoveMember({
- id: member.memberId,
- role: member.role,
- user: {
- id: member.userId,
+ menu={buildActionsMenu([
+ { label: 'Copy email', onSelect: () => copyToClipboard(member.email) },
+ ...(!isOwner
+ ? [
+ {
+ label: 'Manage Credits',
+ onSelect: () =>
+ setCreditsTarget({
+ userId: member.userId,
name: member.name,
email: member.email,
- image: member.image,
- },
- })
- }
- >
- Leave organization
-
- )}
- >
- )}
+ }),
+ },
+ ]
+ : []),
+ ...(canRemove
+ ? [
+ {
+ label: 'Remove',
+ destructive: true,
+ onSelect: () =>
+ onRemoveMember({
+ id: member.memberId,
+ role: member.role,
+ user: {
+ id: member.userId,
+ name: member.name,
+ email: member.email,
+ image: member.image,
+ },
+ }),
+ },
+ ]
+ : []),
+ ...(isSelf && isOwner && onTransferOwnership
+ ? [{ label: 'Transfer ownership', onSelect: () => onTransferOwnership() }]
+ : []),
+ ...(isSelf && !isOwner
+ ? [
+ {
+ label: 'Leave organization',
+ destructive: true,
+ onSelect: () =>
+ onRemoveMember({
+ id: member.memberId,
+ role: member.role,
+ user: {
+ id: member.userId,
+ name: member.name,
+ email: member.email,
+ image: member.image,
+ },
+ }),
+ },
+ ]
+ : []),
+ ])}
/>
)
}
@@ -248,32 +228,24 @@ export function OrganizationMemberLists({
image={invitation.inviteeImage}
status='Invite pending'
roleControl={roleControl}
- menu={buildActionsMenu(
- <>
- copyToClipboard(invitation.email)}>
- Copy email
-
-
- resendInvitation
- .mutateAsync({ invitationId: invitation.id, orgId: organizationId })
- .catch((error) => logger.error('Failed to resend invitation', { error }))
- }
- >
- Resend invite
-
-
- cancelInvitation
- .mutateAsync({ invitationId: invitation.id, orgId: organizationId })
- .catch((error) => logger.error('Failed to revoke invitation', { error }))
- }
- >
- Revoke invite
-
- >
- )}
+ menu={buildActionsMenu([
+ { label: 'Copy email', onSelect: () => copyToClipboard(invitation.email) },
+ {
+ label: 'Resend invite',
+ onSelect: () =>
+ resendInvitation
+ .mutateAsync({ invitationId: invitation.id, orgId: organizationId })
+ .catch((error) => logger.error('Failed to resend invitation', { error })),
+ },
+ {
+ label: 'Revoke invite',
+ destructive: true,
+ onSelect: () =>
+ cancelInvitation
+ .mutateAsync({ invitationId: invitation.id, orgId: organizationId })
+ .catch((error) => logger.error('Failed to revoke invitation', { error })),
+ },
+ ])}
/>
)
@@ -346,30 +318,26 @@ export function OrganizationMemberLists({
/>
}
- menu={buildActionsMenu(
- <>
- copyToClipboard(member.email)}>
- Copy email
-
- {canRemoveFromWorkspace && (
-
- removeWorkspaceMember
- .mutateAsync({ userId: member.userId, workspaceId, organizationId })
- .catch((error) => {
- logger.error('Failed to remove workspace member', { error })
- toast.error("Couldn't remove member", {
- description: getErrorMessage(error, 'Please try again in a moment.'),
- })
- })
- }
- >
- Remove from workspace
-
- )}
- >
- )}
+ menu={buildActionsMenu([
+ { label: 'Copy email', onSelect: () => copyToClipboard(member.email) },
+ ...(canRemoveFromWorkspace
+ ? [
+ {
+ label: 'Remove from workspace',
+ destructive: true,
+ onSelect: () =>
+ removeWorkspaceMember
+ .mutateAsync({ userId: member.userId, workspaceId, organizationId })
+ .catch((error) => {
+ logger.error('Failed to remove workspace member', { error })
+ toast.error("Couldn't remove member", {
+ description: getErrorMessage(error, 'Please try again in a moment.'),
+ })
+ }),
+ },
+ ]
+ : []),
+ ])}
/>
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
index 7088804bc38..a6c1b6f6e5b 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
@@ -7,6 +7,7 @@ import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateSlug, isAdminOrOwner, type Member } from '@/lib/workspaces/organization'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
NoOrganizationView,
OrganizationInviteModal,
@@ -74,9 +75,6 @@ export function TeamManagement() {
const adminOrOwner = isAdminOrOwner(organization, session?.user?.email)
const totalSeats = organizationBillingData?.data?.totalSeats ?? 0
- // Seats are consumed only by accepted members (seat count is reconciled to the
- // member count on accept/removal). Pending invites are surfaced separately and
- // do not count as used until they are accepted.
const usedSeats = organizationBillingData?.data?.members?.length ?? 0
const reservedSeats = organizationBillingData?.data?.usedSeats ?? 0
const pendingSeats = Math.max(0, reservedSeats - usedSeats)
@@ -313,10 +311,9 @@ export function TeamManagement() {
}
return (
-
-
-
-
-
-
-
Organization
-
- Manage members and their access across every workspace in your organization.
-
-
-
-
-
-
-
-
+ }
+ >
+
+
+
+
-
+ >
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx
index ac17449041c..8b915775821 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx
@@ -4,20 +4,7 @@ import { useCallback, useMemo, useState } from 'react'
import { getErrorMessage } from '@sim/utils/errors'
import { useQueryClient } from '@tanstack/react-query'
import { useParams, useRouter } from 'next/navigation'
-import {
- Chip,
- ChipDropdown,
- ChipInput,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Plus,
- Search,
- toast,
-} from '@/components/emcn'
+import { Chip, ChipDropdown, Plus, toast } from '@/components/emcn'
import {
RoleLockTooltip,
type WorkspaceRoleSource,
@@ -29,6 +16,8 @@ import {
MemberRow,
MemberSection,
} from '@/app/workspace/[workspaceId]/settings/components/member-list'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
import {
@@ -174,10 +163,14 @@ export function Teammates() {
}
return (
-
-
-
-
-
-
Teammates
-
- Manage your teammates in this workspace.
-
-
-
-
- setSearchTerm(e.target.value)}
- className='flex-1'
- />
-
-
-
- {filteredTeammates.map((teammate) => (
- {
- const lockReason = teammate.isPending
- ? null
- : workspaceRoleLockReason(teammate.roleSource)
- return (
-
- handleRoleChange(teammate, role as WorkspacePermission)}
- options={ROLE_OPTIONS}
- matchTriggerWidth={false}
- disabled={
- teammate.isPending ||
- !canManage ||
- teammate.userId === viewer?.userId ||
- lockReason !== null
- }
- />
-
- )
- })()}
- menu={
-
-
-
-
-
-
-
- copyToClipboard(teammate.email)}>
- Copy email
-
- {canManage && teammate.isPending && (
- <>
- {
+ }
+ >
+
+ {filteredTeammates.map((teammate) => (
+ {
+ const lockReason = teammate.isPending
+ ? null
+ : workspaceRoleLockReason(teammate.roleSource)
+ return (
+
+ handleRoleChange(teammate, role as WorkspacePermission)}
+ options={ROLE_OPTIONS}
+ matchTriggerWidth={false}
+ disabled={
+ teammate.isPending ||
+ !canManage ||
+ teammate.userId === viewer?.userId ||
+ lockReason !== null
+ }
+ />
+
+ )
+ })()}
+ menu={
+ copyToClipboard(teammate.email),
+ },
+ ...(canManage && teammate.isPending
+ ? [
+ {
+ label: 'Resend invite',
+ onSelect: () => {
if (teammate.invitationId) {
resendInvitation.mutate({
invitationId: teammate.invitationId,
workspaceId,
})
}
- }}
- >
- Resend invite
-
- {
+ },
+ },
+ {
+ label: 'Copy invite link',
+ onSelect: () => {
if (teammate.invitationId && teammate.token) {
copyToClipboard(
buildInviteLink(teammate.invitationId, teammate.token)
)
}
- }}
- >
- Copy invite link
-
- {
+ },
+ },
+ {
+ label: 'Revoke invite',
+ destructive: true,
+ onSelect: () => {
if (teammate.invitationId) {
cancelInvitation.mutate({
invitationId: teammate.invitationId,
workspaceId,
})
}
- }}
- >
- Revoke invite
-
- >
- )}
- {canManage && !teammate.isPending && teammate.userId !== viewer?.userId && (
- {
- if (teammate.userId) {
- removeMember.mutate(
- { userId: teammate.userId, workspaceId },
- {
- onError: (error) => {
- toast.error("Couldn't remove teammate", {
- description: getErrorMessage(
- error,
- 'Please try again in a moment.'
- ),
- })
- },
- }
- )
- }
- }}
- >
- Remove
-
- )}
-
-
- }
- />
- ))}
-
-
-
+ },
+ },
+ ]
+ : []),
+ ...(canManage && !teammate.isPending && teammate.userId !== viewer?.userId
+ ? [
+ {
+ label: 'Remove',
+ destructive: true,
+ onSelect: () => {
+ if (teammate.userId) {
+ removeMember.mutate(
+ { userId: teammate.userId, workspaceId },
+ {
+ onError: (error) => {
+ toast.error("Couldn't remove teammate", {
+ description: getErrorMessage(
+ error,
+ 'Please try again in a moment.'
+ ),
+ })
+ },
+ }
+ )
+ }
+ },
+ },
+ ]
+ : []),
+ ]}
+ />
+ }
+ />
+ ))}
+
+
-
+ >
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx
index e3dc1996b57..8eb0681115e 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx
@@ -23,18 +23,15 @@ import {
ChipSelect,
Code,
type ComboboxOption,
- chipVariants,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
Label,
- MoreHorizontal,
Tooltip,
} from '@/components/emcn'
-import { ArrowLeft, Search } from '@/components/emcn/icons'
+import { ArrowLeft } from '@/components/emcn/icons'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { CreateWorkflowMcpServerModal } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/components'
import { useApiKeys } from '@/hooks/queries/api-keys'
import { useCreateMcpServer } from '@/hooks/queries/mcp'
@@ -72,7 +69,6 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
const updateToolMutation = useUpdateWorkflowMcpTool()
const updateServerMutation = useUpdateWorkflowMcpServer()
- // API Keys - for "Create API key" link
const { data: apiKeysData } = useApiKeys(workspaceId)
const { data: workspaceSettingsData } = useWorkspaceSettings(workspaceId)
const userPermissions = useUserPermissionsContext()
@@ -260,7 +256,6 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
return `claude mcp add "${safeName}" --url "${mcpServerUrl}" --header "X-API-Key:$SIM_API_KEY"`
}
- // Cursor supports direct URL configuration (no mcp-remote needed)
if (client === 'cursor') {
const cursorConfig = isPublic
? { url: mcpServerUrl }
@@ -269,7 +264,6 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
return JSON.stringify({ mcpServers: { [safeName]: cursorConfig } }, null, 2)
}
- // Claude Desktop and VS Code still use mcp-remote (stdio transport)
const mcpRemoteArgs = isPublic
? ['-y', 'mcp-remote', mcpServerUrl]
: ['-y', 'mcp-remote', mcpServerUrl, '--header', 'X-API-Key:$SIM_API_KEY']
@@ -465,29 +459,18 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
-
-
-
-
-
-
-
- setToolToView(tool)}>
- Edit
-
- setToolToDelete(tool)}
- disabled={deleteToolMutation.isPending}
- >
- Remove
-
-
-
+ setToolToView(tool) },
+ {
+ label: 'Remove',
+ destructive: true,
+ disabled: deleteToolMutation.isPending,
+ onSelect: () => setToolToDelete(tool),
+ },
+ ]}
+ />
))}
@@ -984,103 +967,81 @@ export function WorkflowMcpServers() {
return (
<>
-
-
-
-
- setShowAddModal(true)}
- disabled={isLoading}
- >
- Add Server
-
-
-
-
-
-
-
setSearchTerm(e.target.value)}
- />
-
-
- {error ? (
-
-
- {getErrorMessage(error, 'Failed to load MCP servers')}
-
-
- ) : isLoading ? null : !hasServers ? (
-
- Click "Add Server" above to get started
-
- ) : (
-
- {filteredServers.map((server) => {
- const count = server.toolCount || 0
- const toolsLabel = `${count} tool${count !== 1 ? 's' : ''}`
- const isDeleting = deletingServers.has(server.id)
- return (
-
-
-
-
- {server.name}
-
- {server.isPublic && (
-
- Public
-
- )}
-
-
- {toolsLabel}
-
-
-
-
-
-
-
-
-
-
- setSelectedServerId(server.id)}>
- Details
-
- setServerToDelete(server)}
- disabled={isDeleting}
- >
- Delete
-
-
-
-
+
setShowAddModal(true)}
+ disabled={isLoading}
+ >
+ Add Server
+
+ }
+ >
+
+ {error ? (
+
+
+ {getErrorMessage(error, 'Failed to load MCP servers')}
+
+
+ ) : isLoading ? null : !hasServers ? (
+
+ Click "Add Server" above to get started
+
+ ) : (
+
+ {filteredServers.map((server) => {
+ const count = server.toolCount || 0
+ const toolsLabel = `${count} tool${count !== 1 ? 's' : ''}`
+ const isDeleting = deletingServers.has(server.id)
+ return (
+
+
+
+
+ {server.name}
+
+ {server.isPublic && (
+
+ Public
+
+ )}
- )
- })}
- {showNoResults && (
-
- No servers found matching "{searchTerm}"
+
{toolsLabel}
- )}
-
+
+ setSelectedServerId(server.id) },
+ {
+ label: 'Delete',
+ destructive: true,
+ disabled: isDeleting,
+ onSelect: () => setServerToDelete(server),
+ },
+ ]}
+ />
+
+
+ )
+ })}
+ {showNoResults && (
+
+ No servers found matching "{searchTerm}"
+
)}
-
+ )}
-
+
section: NavigationSection
hideWhenBillingDisabled?: boolean
@@ -96,10 +98,17 @@ export const sectionConfig: { key: NavigationSection; title: string }[] = [
]
export const allNavigationItems: NavigationItem[] = [
- { id: 'general', label: 'General', icon: Settings, section: 'account' },
+ {
+ id: 'general',
+ label: 'General',
+ description: 'Manage your profile, appearance, and preferences.',
+ icon: Settings,
+ section: 'account',
+ },
{
id: 'access-control',
label: 'Access control',
+ description: 'Manage permission groups across your organization.',
icon: ShieldCheck,
section: 'enterprise',
requiresHosted: true,
@@ -109,6 +118,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'audit-logs',
label: 'Audit logs',
+ description: 'Review activity and changes across your organization.',
icon: ClipboardList,
section: 'enterprise',
requiresHosted: true,
@@ -118,6 +128,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'billing',
label: 'Billing',
+ description: 'Manage your plan, pricing, and invoices.',
icon: ClipboardList,
section: 'subscription',
hideWhenBillingDisabled: true,
@@ -125,26 +136,59 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'teammates',
label: 'Teammates',
+ description: 'Manage your teammates in this workspace.',
icon: User,
section: 'subscription',
},
{
id: 'organization',
label: 'Organization',
+ description: "Manage your organization's members and seats.",
icon: Users,
section: 'subscription',
hideWhenBillingDisabled: true,
requiresHosted: true,
requiresTeam: true,
},
- { id: 'secrets', label: 'Secrets', icon: Key, section: 'account' },
- { id: 'custom-tools', label: 'Custom tools', icon: Wrench, section: 'tools' },
- { id: 'mcp', label: 'MCP tools', icon: McpIcon, section: 'tools' },
- { id: 'apikeys', label: 'Sim API keys', icon: TerminalWindow, section: 'system' },
- { id: 'workflow-mcp-servers', label: 'MCP servers', icon: Server, section: 'system' },
+ {
+ id: 'secrets',
+ label: 'Secrets',
+ description: 'Store environment variables for your workflows.',
+ icon: Key,
+ section: 'account',
+ },
+ {
+ id: 'custom-tools',
+ label: 'Custom tools',
+ description: 'Create and manage custom tools for your agents.',
+ icon: Wrench,
+ section: 'tools',
+ },
+ {
+ id: 'mcp',
+ label: 'MCP tools',
+ description: 'Connect MCP servers and use their tools in workflows.',
+ icon: McpIcon,
+ section: 'tools',
+ },
+ {
+ id: 'apikeys',
+ label: 'Sim API keys',
+ description: 'Create and manage API keys for the Sim API.',
+ icon: TerminalWindow,
+ section: 'system',
+ },
+ {
+ id: 'workflow-mcp-servers',
+ label: 'MCP servers',
+ description: 'Expose your workflows as tools on an MCP server.',
+ icon: Server,
+ section: 'system',
+ },
{
id: 'byok',
label: 'BYOK',
+ description: 'Bring your own model-provider API keys.',
icon: KeySquare,
section: 'system',
requiresHosted: true,
@@ -152,6 +196,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'copilot',
label: 'Chat keys',
+ description: 'Manage the model-provider keys that power Chat.',
icon: HexSimple,
section: 'system',
requiresHosted: true,
@@ -159,6 +204,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'inbox',
label: 'Sim mailer',
+ description: 'Trigger and process workflows from incoming email.',
icon: Send,
section: 'system',
requiresMax: true,
@@ -171,15 +217,23 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'credential-sets' as const,
label: 'Email polling',
+ description: 'Share email-polling credentials across your team.',
icon: Mail,
section: 'system' as const,
},
]
: []),
- { id: 'recently-deleted', label: 'Recently deleted', icon: TrashOutline, section: 'system' },
+ {
+ id: 'recently-deleted',
+ label: 'Recently deleted',
+ description: 'Restore items deleted in the last 30 days.',
+ icon: TrashOutline,
+ section: 'system',
+ },
{
id: 'sso',
label: 'Single sign-on',
+ description: 'Configure single sign-on for your organization.',
icon: LogIn,
section: 'enterprise',
requiresHosted: true,
@@ -189,6 +243,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'data-retention',
label: 'Data retention',
+ description: 'Control data retention windows and PII redaction.',
icon: Database,
section: 'enterprise',
requiresHosted: true,
@@ -198,6 +253,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'data-drains',
label: 'Data drains',
+ description: 'Stream your logs and events to external destinations.',
icon: Upload,
section: 'enterprise',
requiresHosted: true,
@@ -207,6 +263,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'whitelabeling',
label: 'Whitelabeling',
+ description: 'Customize your workspace branding and appearance.',
icon: Palette,
section: 'enterprise',
requiresHosted: true,
@@ -216,6 +273,7 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'admin',
label: 'Admin',
+ description: 'Superuser administration and workspace tools.',
icon: Lock,
section: 'superuser',
requiresAdminRole: true,
@@ -223,8 +281,21 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'mothership',
label: 'Mothership',
+ description: 'Internal Sim operations and license management.',
icon: Server,
section: 'superuser',
requiresAdminRole: true,
},
]
+
+/**
+ * Title + description for a settings section, the single source of truth used by
+ * `SettingsPanel` to render the page header. Falls back to `null` for sections
+ * that are gated off (callers render no title in that case).
+ */
+export function getSettingsSectionMeta(
+ section: SettingsSection
+): { label: string; description: string } | null {
+ const item = allNavigationItems.find((navItem) => navItem.id === section)
+ return item ? { label: item.label, description: item.description } : null
+}
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 3566c77add1..834daee7fdd 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -2760,10 +2760,10 @@ export function TinybirdIcon(props: SVGProps) {
export function ThriveIcon(props: SVGProps) {
return (
-
+
)
diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx
index 6ac886fec28..0af913eb34b 100644
--- a/apps/sim/ee/access-control/components/access-control.tsx
+++ b/apps/sim/ee/access-control/components/access-control.tsx
@@ -7,7 +7,6 @@ import { useParams } from 'next/navigation'
import {
Checkbox,
Chip,
- ChipInput,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -16,9 +15,10 @@ import {
ChipModalHeader,
ChipTag,
Label,
- Search,
} from '@/components/emcn'
import { getEnv, isTruthy } from '@/lib/core/config/env'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { GroupDetail } from '@/ee/access-control/components/group-detail'
import { WorkspaceSelect } from '@/ee/access-control/components/workspace-select'
@@ -133,11 +133,11 @@ export function AccessControl() {
if (!canManage) {
return (
-
+
{!organizationId
? "Access Control applies to organization workspaces. This workspace isn't part of an organization."
: 'Only organization admins on Enterprise plans can manage Access Control settings.'}
-
+
)
}
@@ -158,85 +158,66 @@ export function AccessControl() {
return (
<>
-
-
-
-
- setShowCreateModal(true)}>
- Create Group
-
-
-
-
-
-
-
-
Access Control
-
- Manage permission groups across every workspace in your organization.
-
-
-
-
-
setSearchTerm(e.target.value)}
- className='flex-1'
- />
+ setShowCreateModal(true)}>
+ Create Group
+
+ }
+ >
+
+ {permissionGroups.length === 0 ? (
+
+ No permission groups yet. Click "Create Group" to get started.
+
+ ) : filteredGroups.length === 0 ? (
+
+ No groups found matching "{searchTerm}"
+
+ ) : (
+
+ {filteredGroups.map((group) => (
+
setSelectedGroupId(group.id)}
+ className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
+ >
+
+
+
+ {group.name}
+
+ {group.isDefault && (
+
+ Default
+
+ )}
+
+
+ {group.isDefault
+ ? 'Everyone in the organization'
+ : `${
+ group.memberCount === 0
+ ? 'All members'
+ : `${group.memberCount} member${group.memberCount === 1 ? '' : 's'}`
+ } · ${group.workspaces.length} workspace${
+ group.workspaces.length === 1 ? '' : 's'
+ }`}
+
+
+
+
+ ))}
-
-
- {permissionGroups.length === 0 ? (
-
- No permission groups yet. Click "Create Group" to get started.
-
- ) : filteredGroups.length === 0 ? (
-
- No groups found matching "{searchTerm}"
-
- ) : (
-
- {filteredGroups.map((group) => (
-
setSelectedGroupId(group.id)}
- className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
- >
-
-
-
- {group.name}
-
- {group.isDefault && (
-
- Default
-
- )}
-
-
- {group.isDefault
- ? 'Everyone in the organization'
- : `${
- group.memberCount === 0
- ? 'All members'
- : `${group.memberCount} member${group.memberCount === 1 ? '' : 's'}`
- } · ${group.workspaces.length} workspace${
- group.workspaces.length === 1 ? '' : 's'
- }`}
-
-
-
-
- ))}
-
- )}
-
-
-
-
+ )}
+
+
-
-
-
-
-
-
- handleRemoveMember(member.id)}
- >
- Remove
-
-
-
+ handleRemoveMember(member.id),
+ destructive: true,
+ },
+ ]}
+ />
}
/>
))}
diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
index 304a3817afe..bda2338b792 100644
--- a/apps/sim/ee/audit-logs/components/audit-logs.tsx
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -19,6 +19,8 @@ import { cn } from '@/lib/core/utils/cn'
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
import { formatDateShort } from '@/app/workspace/[workspaceId]/logs/utils'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { RESOURCE_TYPE_OPTIONS } from '@/ee/audit-logs/constants'
import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs'
import type { TimeRange } from '@/stores/logs/filters/types'
@@ -357,102 +359,94 @@ export function AuditLogs() {
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
return (
-
-
-
- {/* Search + filter bar */}
-
-
setSearchTerm(e.target.value)}
- />
-
-
-
- {
- if (!isOpen) {
- if (dateRangeAppliedRef.current) {
- dateRangeAppliedRef.current = false
- } else {
- handleDatePickerCancel()
- }
- }
- }}
- startDate={customStartDate}
- endDate={customEndDate}
- onRangeChange={handleDateRangeApply}
- onCancel={handleDatePickerCancel}
- />
-
-
-
-
-
+
+
+
setSearchTerm(e.target.value)}
+ />
+
+
+
+ {
+ if (!isOpen) {
+ if (dateRangeAppliedRef.current) {
+ dateRangeAppliedRef.current = false
+ } else {
+ handleDatePickerCancel()
+ }
+ }
+ }}
+ startDate={customStartDate}
+ endDate={customEndDate}
+ onRangeChange={handleDateRangeApply}
+ onCancel={handleDatePickerCancel}
+ />
+
+
+
+
+
- {/* Table */}
-
-
- Timestamp
- Event
- Description
- Actor
-
+
+
+ Timestamp
+ Event
+ Description
+ Actor
+
- {isLoading ? null : allEntries.length === 0 ? (
- debouncedSearch ? (
-
- No results for "{debouncedSearch}"
-
- ) : (
-
- No audit logs found
-
- )
- ) : (
-
- {allEntries.map((entry) => (
-
- ))}
- {hasNextPage && (
-
-
- {isFetchingNextPage ? 'Loading...' : 'Load more'}
-
-
- )}
+ {isLoading ? null : allEntries.length === 0 ? (
+ debouncedSearch ? (
+
+ No results for "{debouncedSearch}"
+
+ ) : (
+
No audit logs found
+ )
+ ) : (
+
+ {allEntries.map((entry) => (
+
+ ))}
+ {hasNextPage && (
+
+
+ {isFetchingNextPage ? 'Loading...' : 'Load more'}
+
)}
-
+ )}
-
+
)
}
diff --git a/apps/sim/ee/data-drains/components/data-drains-settings.tsx b/apps/sim/ee/data-drains/components/data-drains-settings.tsx
index aae623477e6..8238748660f 100644
--- a/apps/sim/ee/data-drains/components/data-drains-settings.tsx
+++ b/apps/sim/ee/data-drains/components/data-drains-settings.tsx
@@ -7,10 +7,8 @@ import { toError } from '@sim/utils/errors'
import { ChevronDown, Plus } from 'lucide-react'
import {
Badge,
- Button,
Chip,
ChipConfirmModal,
- ChipInput,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -18,12 +16,6 @@ import {
ChipModalFooter,
ChipModalHeader,
ChipSelect,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- MoreHorizontal,
- Search,
Switch,
Table,
TableBody,
@@ -38,6 +30,9 @@ import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { CADENCE_TYPES, DESTINATION_TYPES, SOURCE_TYPES } from '@/lib/data-drains/types'
import { getUserRole } from '@/lib/workspaces/organization/utils'
+import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { InfoNote } from '@/ee/components/info-note'
import { DESTINATION_FORM_REGISTRY } from '@/ee/data-drains/destinations/registry'
import {
@@ -118,45 +113,40 @@ export function DataDrainsSettings() {
if (!orgId) {
return (
-
+
Data drains are configured per organization. Join or create one to continue.
-
+
)
}
if (!canManage) {
return (
-
+
Only organization owners and admins can configure data drains.
-
+
)
}
return (
-
-
-
-
+ <>
+ setCreateOpen(true)}>
New Drain
-
-
-
-
-
+ }
+ search={{
+ value: searchTerm,
+ onChange: setSearchTerm,
+ placeholder: 'Search data drains...',
+ }}
+ >
+
Drains continuously export Sim data to your own storage on a schedule. Combine with Data
Retention to satisfy long-term compliance archives.
-
setSearchTerm(e.target.value)}
- />
-
{drainsError ? (
@@ -193,23 +183,21 @@ export function DataDrainsSettings() {
) : (
-
+
No results for "{searchTerm.trim()}"
-
+
)
) : (
-
- Click "New Drain" above to get started
-
+
Click "New Drain" above to get started
)}
-
+
{createOpen && (
setCreateOpen(false)} />
)}
-
+ >
)
}
@@ -304,22 +292,14 @@ function DrainRow({ drain, organizationId, expanded, onToggleExpand }: DrainRowP
/>
e.stopPropagation()}>
-
-
-
-
-
-
-
-
- Run now
-
- Test connection
-
- Delete
-
-
-
+
{expanded && (
diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx
index a72ca616a09..a39addd8daa 100644
--- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx
+++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx
@@ -34,6 +34,8 @@ import {
SUPPORTED_PII_ENTITIES,
} from '@/lib/guardrails/pii-entities'
import { getUserRole } from '@/lib/workspaces/organization/utils'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import {
useOrganizationRetention,
@@ -426,8 +428,6 @@ export function DataRetentionSettings() {
const [overrides, setOverrides] = useState
([])
const [modal, setModal] = useState(null)
const [showUnsaved, setShowUnsaved] = useState(false)
- // Org the form was hydrated for; re-hydrate when the active org switches so
- // saves don't target the new org with the previous org's config.
const hydratedOrgRef = useRef(null)
useEffect(() => {
@@ -464,8 +464,6 @@ export function DataRetentionSettings() {
const modalChanged =
modal !== null && normalizePolicyDraft(modal.draft) !== normalizePolicyDraft(modal.original)
- // PII-only rows are only surfaced when redaction is enabled — the route
- // rejects PII writes while the flag is off, so such rows couldn't be deleted.
const overrideWorkspaceIds = Array.from(
new Set([
...overrides.map((o) => o.workspaceId),
@@ -647,8 +645,6 @@ export function DataRetentionSettings() {
const ids = draft.workspaceIds
if (ids.length === 0) return
- // Clear the workspaces this edit previously owned plus the new selection,
- // so deselecting a workspace removes its override instead of orphaning it.
const clearIds = new Set([...modal.original.workspaceIds, ...ids])
const nextOverrides = overrides.filter((o) => !clearIds.has(o.workspaceId))
const nextPiiOverrides = piiOverrides.filter((p) => !clearIds.has(p.workspaceId))
@@ -681,8 +677,6 @@ export function DataRetentionSettings() {
async function removeCurrentOverride() {
if (!modal || modal.draft.isOrgDefault) return
- // Remove the override(s) this row originally owned, regardless of any
- // unsaved changes to the workspace multi-select in the open modal.
const idSet = new Set(modal.original.workspaceIds)
try {
await persistSnapshot({
@@ -705,41 +699,34 @@ export function DataRetentionSettings() {
if (!orgId) {
return (
-
+
Data retention is configured per organization. Join or create an organization to continue.
-
+
)
}
if (!data) {
- return (
-
- Failed to load data retention settings.
-
- )
+ return Failed to load data retention settings.
}
if (isBillingEnabled && !data.isEnterprise) {
return (
-
- Data retention is available on Enterprise plans only.
-
+ Data retention is available on Enterprise plans only.
)
}
if (!canManage) {
return (
-
+
Only organization owners and admins can configure data retention settings.
-
+
)
}
return (
-
-
-
-
+ <>
+
Add override
-
-
-
-
-
-
-
- Workspaces without an override inherit the organization defaults.
-
-
+ }
+ >
+
+
+
+ Workspaces without an override inherit the organization defaults.
+
+
+
+
+
+
+ Organization
+
+
+ Default
+
+
+
+ {orgRowSummary()}
+
+
+
+
+ {overrideWorkspaceIds.map((workspaceId) => (
openEditOverride(workspaceId)}
className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
>
-
-
- Organization
-
-
- Default
-
-
+
+ {workspaceName(workspaceId)}
+
- {orgRowSummary()}
+ {overrideRowSummary(workspaceId)}
- {overrideWorkspaceIds.map((workspaceId) => (
-
openEditOverride(workspaceId)}
- className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
- >
-
-
- {workspaceName(workspaceId)}
-
-
- {overrideRowSummary(workspaceId)}
-
-
-
-
- ))}
-
+ ))}
-
-
-
+
+
+
{modal && (
-
+ >
)
}
diff --git a/apps/sim/ee/sso/components/sso-settings.tsx b/apps/sim/ee/sso/components/sso-settings.tsx
index 048cd0a3c96..c7c0e5be19b 100644
--- a/apps/sim/ee/sso/components/sso-settings.tsx
+++ b/apps/sim/ee/sso/components/sso-settings.tsx
@@ -23,6 +23,8 @@ import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getUserRole } from '@/lib/workspaces/organization/utils'
+import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
+import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants'
import { useConfigureSSO, useSSOProviders } from '@/ee/sso/hooks/sso'
import { useOrganizations } from '@/hooks/queries/organization'
@@ -137,33 +139,33 @@ export function SSO() {
if (isBillingEnabled) {
if (!activeOrganization) {
return (
-
+
You must be part of an organization to configure Single Sign-On.
-
+
)
}
if (!hasEnterprisePlan) {
return (
-
+
Single Sign-On is available on Enterprise plans only.
-
+
)
}
if (!canManageSSO) {
return (
-
+
Only organization owners and admins can configure Single Sign-On settings.
-
+
)
}
} else {
if (activeOrganization && !canManageSSO) {
return (
-
+
Only organization owners and admins can configure Single Sign-On settings.
-
+
)
}
if (
@@ -173,9 +175,9 @@ export function SSO() {
providers.length > 0
) {
return (
-
+
Only the user who configured SSO can manage these settings.
-
+
)
}
}
@@ -433,74 +435,65 @@ export function SSO() {
const providerCallbackUrl = `${getBaseUrl()}/api/auth/${existingProvider.providerType === 'saml' ? 'sso/saml2/callback' : 'sso/callback'}/${existingProvider.providerId}`
return (
-
-
-
-
-
- {existingProvider.providerId}
-
-
-
-
- {existingProvider.providerType.toUpperCase()}
-
-
-
-
- {existingProvider.domain}
-
-
-
-
- {existingProvider.issuer}
-
-
-
-
- copyToClipboard(providerCallbackUrl)}
- className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]'
- aria-label='Copy callback URL'
- >
- {copied ? (
-
- ) : (
-
- )}
-
- }
- />
-
- Configure this in your identity provider
-
-
-
+
+ Edit
+
+ }
+ >
+
+
+ {existingProvider.providerId}
+
+
+
+
+ {existingProvider.providerType.toUpperCase()}
+
+
+
+
+ {existingProvider.domain}
+
+
+
+
+ {existingProvider.issuer}
+
+
+
+
+ copyToClipboard(providerCallbackUrl)}
+ className='size-6 p-0 text-[var(--text-icon)] hover:text-[var(--text-primary)]'
+ aria-label='Copy callback URL'
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+
+ Configure this in your identity provider
+
+
-
+
)
}
return (
-