Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function ShowcaseWithExplore({ prompt }: ShowcaseWithExploreProps) {
<div className='relative'>
<IntegrationsShowcase />
<Chip
active
rightIcon={ArrowRight}
onClick={() => {
storeCuratedPrompt(prompt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
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 { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'

const logger = createLogger('BYOKKeyManager')
Expand Down Expand Up @@ -278,21 +279,13 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
const Icon = provider.icon

return (
<div key={provider.id} className='flex items-center justify-between gap-2.5'>
<div className='flex min-w-0 items-center gap-2.5'>
<div className='flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
<Icon className='size-5' />
</div>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<span className='truncate text-[14px] text-[var(--text-body)]'>{provider.name}</span>
<span className='truncate text-[12px] text-[var(--text-muted)]'>
{provider.description}
</span>
</div>
</div>

{renderActions(provider)}
</div>
<SettingsResourceRow
key={provider.id}
icon={<Icon />}
title={provider.name}
description={provider.description}
trailing={renderActions(provider)}
/>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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 { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import {
type CredentialSet,
Expand Down Expand Up @@ -632,68 +633,48 @@ export function CredentialSets() {
<div className='flex flex-col gap-4.5'>
{filteredInvitations.length > 0 && (
<SettingsSection label='Pending Invitations'>
<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-2'>
{filteredInvitations.map((invitation) => (
<div
<SettingsResourceRow
key={invitation.invitationId}
className='flex items-center justify-between rounded-lg p-2 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex items-center gap-2.5'>
<div className='flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
{getProviderIcon(invitation.providerId)}
</div>
<div className='flex flex-col'>
<span className='text-[14px] text-[var(--text-body)]'>
{invitation.credentialSetName}
</span>
<span className='text-[12px] text-[var(--text-muted)]'>
{invitation.organizationName}
</span>
</div>
</div>
<Chip
variant='primary'
onClick={() => handleAcceptInvitation(invitation.token)}
disabled={acceptInvitation.isPending}
>
{acceptInvitation.isPending ? 'Accepting...' : 'Accept'}
</Chip>
</div>
icon={getProviderIcon(invitation.providerId)}
title={invitation.credentialSetName}
description={invitation.organizationName}
trailing={
<Chip
variant='primary'
onClick={() => handleAcceptInvitation(invitation.token)}
disabled={acceptInvitation.isPending}
>
{acceptInvitation.isPending ? 'Accepting...' : 'Accept'}
</Chip>
}
/>
))}
</div>
</SettingsSection>
)}

{filteredMemberships.length > 0 && (
<SettingsSection label='My Memberships'>
<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-2'>
{filteredMemberships.map((membership) => (
<div
<SettingsResourceRow
key={membership.membershipId}
className='flex items-center justify-between rounded-lg p-2 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex items-center gap-2.5'>
<div className='flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
{getProviderIcon(membership.providerId)}
</div>
<div className='flex flex-col'>
<span className='text-[14px] text-[var(--text-body)]'>
{membership.credentialSetName}
</span>
<span className='text-[12px] text-[var(--text-muted)]'>
{membership.organizationName}
</span>
</div>
</div>
<Chip
onClick={() =>
handleLeave(membership.credentialSetId, membership.credentialSetName)
}
disabled={leaveCredentialSet.isPending}
>
Leave
</Chip>
</div>
icon={getProviderIcon(membership.providerId)}
title={membership.credentialSetName}
description={membership.organizationName}
trailing={
<Chip
onClick={() =>
handleLeave(membership.credentialSetId, membership.credentialSetName)
}
disabled={leaveCredentialSet.isPending}
>
Leave
</Chip>
}
/>
))}
</div>
</SettingsSection>
Expand All @@ -709,26 +690,14 @@ export function CredentialSets() {
No polling groups created yet
</div>
) : (
<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-2'>
{filteredOwnedSets.map((set) => (
<div
<SettingsResourceRow
key={set.id}
className='flex items-center justify-between rounded-lg p-2 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex items-center gap-2.5'>
<div className='flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
{getProviderIcon(set.providerId)}
</div>
<div className='flex flex-col'>
<span className='text-[14px] text-[var(--text-body)]'>
{set.name}
</span>
<span className='text-[12px] text-[var(--text-muted)]'>
{set.memberCount} member{set.memberCount !== 1 ? 's' : ''}
</span>
</div>
</div>
<div className='flex items-center gap-1'>
icon={getProviderIcon(set.providerId)}
title={set.name}
description={`${set.memberCount} member${set.memberCount !== 1 ? 's' : ''}`}
trailing={
<RowActionsMenu
label='Group actions'
actions={[
Expand All @@ -741,8 +710,8 @@ export function CredentialSets() {
},
]}
/>
</div>
</div>
}
/>
))}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useCallback, useMemo, useState } from 'react'
import { Button, ChipInput, ChipModalTabs } from '@sim/emcn'
import { Chip, ChipInput, ChipModalTabs } from '@sim/emcn'
import { Folder, Search, Workflow } from '@sim/emcn/icons'
import { toError } from '@sim/utils/errors'
import { formatDate } from '@sim/utils/formatting'
Expand All @@ -21,6 +21,7 @@ import {
} 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 { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { useFolders, useRestoreFolder } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge'
import { useRestoreTable, useTablesList } from '@/hooks/queries/tables'
Expand Down Expand Up @@ -82,7 +83,7 @@ const SORT_OPTIONS: ColumnOption[] = [
{ id: 'type', label: 'Type' },
]

const ICON_CLASS = 'size-[14px]'
const ICON_CLASS = 'size-5'

const RESOURCE_TYPE_TO_MOTHERSHIP: Partial<
Record<Exclude<ResourceType, 'all'>, MothershipResourceType>
Expand Down Expand Up @@ -464,45 +465,40 @@ export function RecentlyDeleted() {
const isRestored = restoredItems.has(resource.id)

return (
<div
<SettingsResourceRow
key={resource.id}
className='flex items-center gap-2.5 rounded-lg p-2 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<ResourceIcon resource={resource} />

<div className='flex min-w-0 flex-1 flex-col'>
<span className='truncate font-medium text-[var(--text-primary)] text-small'>
{resource.name}
</span>
<span className='text-[var(--text-muted)] text-small'>
icon={<ResourceIcon resource={resource} />}
title={resource.name}
description={
<>
{TYPE_LABEL[resource.type]}
{' \u00b7 '}
Deleted {formatDate(resource.deletedAt)}
</span>
</div>

{isRestoring ? (
<Button variant='primary' size='sm' disabled className='shrink-0'>
Restoring...
</Button>
) : isRestored ? (
<div className='flex shrink-0 items-center gap-2'>
<span className='text-[var(--text-muted)] text-small'>Restored</span>
<Button variant='primary' size='sm' onClick={() => handleView(resource)}>
View
</Button>
</div>
) : (
<Button
variant='primary'
size='sm'
onClick={() => void handleRestore(resource)}
className='shrink-0'
>
Restore
</Button>
)}
</div>
</>
}
trailing={
isRestoring ? (
<Chip variant='primary' disabled className='shrink-0'>
Restoring...
</Chip>
) : isRestored ? (
<div className='flex shrink-0 items-center gap-2'>
<span className='text-[var(--text-muted)] text-small'>Restored</span>
<Chip variant='primary' onClick={() => handleView(resource)}>
View
</Chip>
</div>
) : (
<Chip
variant='primary'
onClick={() => void handleRestore(resource)}
className='shrink-0'
>
Restore
</Chip>
)
}
/>
)
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SettingsResourceRow } from './settings-resource-row'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ReactNode } from 'react'

/**
* The canonical settings "resource row": a rounded-bordered icon tile, a
* title + muted description text block, and an optional trailing slot
* (action chips, a {@link RowActionsMenu}, a status label, etc.).
*
* Single source of truth for the credential-style row shared by the BYOK key
* manager, credential sets, and recently-deleted lists — never re-derive the
* tile/text chrome per consumer. The tile force-sizes any `<svg>`/`<img>` it
* contains to 20px, so callers pass their raw icon node without pre-sizing it.
*/
interface SettingsResourceRowProps {
/** Icon node centered in the tile; any `<svg>`/`<img>` is normalized to 20px. */
icon: ReactNode
/** Primary line — truncates. */
title: ReactNode
/** Secondary muted line — truncates. */
description?: ReactNode
/** Trailing element pinned to the row's end (chips, actions menu, status). */
trailing?: ReactNode
}

const TILE_CLASS =
'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)] [&_img]:size-5 [&_svg]:size-5'

export function SettingsResourceRow({
icon,
title,
description,
trailing,
}: SettingsResourceRowProps) {
return (
<div className='flex items-center justify-between gap-2.5'>
<div className='flex min-w-0 items-center gap-2.5'>
<div className={TILE_CLASS}>{icon}</div>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<span className='truncate text-[14px] text-[var(--text-body)]'>{title}</span>
{description != null && (
<span className='truncate text-[12px] text-[var(--text-muted)]'>{description}</span>
)}
</div>
</div>
{trailing}
</div>
)
}
3 changes: 2 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ export const allNavigationItems: NavigationItem[] = [
{
id: 'data-retention',
label: 'Data retention',
description: 'Control data retention windows and PII redaction.',
description:
'Control data retention windows and PII redaction. Workspaces without an override inherit the organization defaults.',
icon: Database,
section: 'enterprise',
requiresHosted: true,
Expand Down
Loading
Loading