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 @@ -24,12 +24,22 @@ import { CreateApiKeyModal } from './components'

const logger = createLogger('ApiKeys')

/** Stable empty references so memoized derivations don't re-run while data loads. */
const EMPTY_KEYS: ApiKey[] = []
const EMPTY_KEY_NAMES: string[] = []

/** Copies an API key's name and confirms with a toast. */
function copyKeyName(name: string) {
void navigator.clipboard.writeText(name)
toast.success('Copied name to clipboard')
}

/** Formats an API key's last-used timestamp, or "Never" when unused. */
function formatLastUsed(dateString?: string | null): string {
if (!dateString) return 'Never'
return formatDate(new Date(dateString))
}

interface ApiKeyRowMenuProps {
keyName: string
onDelete: () => void
Expand Down Expand Up @@ -73,9 +83,9 @@ export function ApiKeys() {
const deleteApiKeyMutation = useDeleteApiKey()
const updateSettingsMutation = useUpdateWorkspaceApiKeySettings()

const workspaceKeys = apiKeysData?.workspaceKeys || []
const personalKeys = apiKeysData?.personalKeys || []
const conflicts = apiKeysData?.conflicts || []
const workspaceKeys = apiKeysData?.workspaceKeys ?? EMPTY_KEYS
const personalKeys = apiKeysData?.personalKeys ?? EMPTY_KEYS
const conflicts = apiKeysData?.conflicts ?? EMPTY_KEY_NAMES
const isLoading = isLoadingKeys || isLoadingSettings

const allowPersonalApiKeys =
Expand All @@ -90,21 +100,27 @@ export function ApiKeys() {
const createButtonDisabled = isLoading || (!allowPersonalApiKeys && !canManageWorkspaceKeys)

const filteredWorkspaceKeys = useMemo(() => {
if (!searchTerm.trim()) {
return workspaceKeys.map((key, index) => ({ key, originalIndex: index }))
const term = searchTerm.trim().toLowerCase()
const result: { key: ApiKey; originalIndex: number }[] = []
for (let index = 0; index < workspaceKeys.length; index++) {
const key = workspaceKeys[index]
if (term === '' || key.name.toLowerCase().includes(term)) {
result.push({ key, originalIndex: index })
}
}
return workspaceKeys
.map((key, index) => ({ key, originalIndex: index }))
.filter(({ key }) => key.name.toLowerCase().includes(searchTerm.toLowerCase()))
return result
}, [workspaceKeys, searchTerm])

const filteredPersonalKeys = useMemo(() => {
if (!searchTerm.trim()) {
return personalKeys.map((key, index) => ({ key, originalIndex: index }))
const term = searchTerm.trim().toLowerCase()
const result: { key: ApiKey; originalIndex: number }[] = []
for (let index = 0; index < personalKeys.length; index++) {
const key = personalKeys[index]
if (term === '' || key.name.toLowerCase().includes(term)) {
result.push({ key, originalIndex: index })
}
}
return personalKeys
.map((key, index) => ({ key, originalIndex: index }))
.filter(({ key }) => key.name.toLowerCase().includes(searchTerm.toLowerCase()))
return result
}, [personalKeys, searchTerm])

const handleDeleteKey = async () => {
Expand All @@ -128,11 +144,6 @@ export function ApiKeys() {
}
}

const formatLastUsed = (dateString?: string | null) => {
if (!dateString) return 'Never'
return formatDate(new Date(dateString))
}

const actions: SettingsAction[] = [
{
text: 'Create API key',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function CreateApiKeyModal({
type='text'
name='fakeusernameremembered'
autoComplete='username'
aria-hidden='true'
style={{
position: 'absolute',
left: '-9999px',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,23 @@ function formatInvoiceDate(createdSeconds: number): string {
})
}

/** Cached currency formatters, keyed by upper-cased ISO currency code. */
const invoiceAmountFormatters = new Map<string, Intl.NumberFormat>()

/** Resolve (and memoize) an `Intl.NumberFormat` for a currency code. */
function getInvoiceAmountFormatter(currency: string): Intl.NumberFormat {
const code = currency.toUpperCase()
let formatter = invoiceAmountFormatters.get(code)
if (!formatter) {
formatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: code })
invoiceAmountFormatters.set(code, formatter)
}
return formatter
}

/** Format a minor-unit (e.g. cents) amount as a localized currency string. */
function formatInvoiceAmount(amountMinor: number, currency: string): string {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currency.toUpperCase(),
}).format(amountMinor / 100)
return getInvoiceAmountFormatter(currency).format(amountMinor / 100)
}

export function Billing() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import { useDebounce } from '@/hooks/use-debounce'
/** Delay before a usage-limit edit is auto-saved once the user stops typing. */
const AUTOSAVE_DELAY_MS = 1000

/** Static help accessory for the usage-limit header; hoisted so it's a stable reference. */
const USAGE_LIMIT_INFO = (
<Info side='top' className='text-[var(--text-muted)]'>
{
"Max usage to consume per month, set in credits — Sim's usage unit (1,000 credits = $5). By default, it's your plan's included usage, but you can set it beyond."
}
</Info>
)

interface UsageLimitFieldProps {
/** Current monthly usage limit, in dollars. */
currentLimit: number
Expand Down Expand Up @@ -111,16 +120,7 @@ export function UsageLimitField({
}, [debouncedDraft, minimumLimit, canEdit, context, organizationId, saveOrgLimit, saveUserLimit])

return (
<SettingsSection
label='Usage limit'
headerAccessory={
<Info side='top' className='text-[var(--text-muted)]'>
{
"Max usage to consume per month, set in credits — Sim's usage unit (1,000 credits = $5). By default, it's your plan's included usage, but you can set it beyond."
}
</Info>
}
>
<SettingsSection label='Usage limit' headerAccessory={USAGE_LIMIT_INFO}>
<ChipInput
type='number'
inputMode='numeric'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
strokeWidth={2}
/>
<input
aria-label='Search providers'
placeholder='Search providers...'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
Expand Down Expand Up @@ -380,6 +381,7 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
type='text'
name='fakeusernameremembered'
autoComplete='username'
aria-hidden='true'
style={{
position: 'absolute',
left: '-9999px',
Expand All @@ -391,6 +393,7 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
/>
<div className={CHIP_FIELD_SHELL}>
<input
aria-label='API Key'
type={showApiKey ? 'text' : 'password'}
value={apiKeyInput}
onChange={(e) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,19 +325,18 @@ export function BYOK() {
const workspaceId = (params?.workspaceId as string) || ''

const { data, isLoading } = useBYOKKeys(workspaceId)
const keys = data?.keys ?? []
const upsertKey = useUpsertBYOKKey()
const deleteKey = useDeleteBYOKKey()

const keysByProvider = useMemo(() => {
const grouped = new Map<string, BYOKManagerKey[]>()
for (const key of keys) {
for (const key of data?.keys ?? []) {
const providerKeys = grouped.get(key.providerId) ?? []
providerKeys.push({ id: key.id, name: key.name, maskedKey: key.maskedKey })
grouped.set(key.providerId, providerKeys)
}
return grouped
}, [keys])
}, [data?.keys])

return (
<SettingsPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {

const logger = createLogger('CopilotSettings')

/** Formats a key's last-used timestamp, falling back to "Never" when unset. */
function formatLastUsed(dateString?: string | null): string {
if (!dateString) return 'Never'
return formatDate(new Date(dateString))
}

/**
* Copilot Keys management component for handling API keys used with the Copilot feature.
* Provides functionality to create, view, and delete copilot API keys.
Expand Down Expand Up @@ -95,11 +101,6 @@ export function Copilot() {
}
}

const formatLastUsed = (dateString?: string | null) => {
if (!dateString) return 'Never'
return formatDate(new Date(dateString))
}

const hasKeys = keys.length > 0
const showEmptyState = !hasKeys
const showNoResults = searchTerm.trim() && filteredKeys.length === 0 && keys.length > 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ import { useSubscriptionData } from '@/hooks/queries/subscription'

const logger = createLogger('EmailPolling')

function getProviderIcon(providerId: string | null) {
if (providerId === 'outlook') return <OutlookIcon className='size-4' />
return <GmailIcon className='size-4' />
}

export function CredentialSets() {
const { data: session } = useSession()
const { data: organizationsData } = useOrganizations()
Expand Down Expand Up @@ -240,10 +245,13 @@ export function CredentialSets() {
}
}, [newSetName, newSetDescription, newSetProvider, activeOrganization?.id, createCredentialSet])

const validEmails = useMemo(
() => emailItems.filter((item) => item.isValid).map((item) => item.value),
[emailItems]
)
const validEmails = useMemo(() => {
const result: string[] = []
for (const item of emailItems) {
if (item.isValid) result.push(item.value)
}
return result
}, [emailItems])

const handleInviteMembers = useCallback(async () => {
if (!viewingSet?.id) return
Expand Down Expand Up @@ -367,11 +375,6 @@ export function CredentialSets() {
}
}, [deletingSet, activeOrganization?.id, deleteCredentialSet])

const getProviderIcon = (providerId: string | null) => {
if (providerId === 'outlook') return <OutlookIcon className='size-4' />
return <GmailIcon className='size-4' />
}

const activeMemberships = useMemo(
() => memberships.filter((m) => m.status === 'active'),
[memberships]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ChipModalFooter,
ChipModalHeader,
ChipSelect,
handleKeyboardActivation,
Input,
Label,
Switch,
Expand Down Expand Up @@ -293,12 +292,11 @@ export function General() {
<div className='flex flex-col gap-3'>
<div className='flex items-center gap-3'>
<div className='relative'>
<div
role='button'
tabIndex={0}
<button
type='button'
aria-label='Change profile picture'
className={`group relative flex size-9 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-full transition-all hover-hover:bg-[var(--bg)] ${!imageUrl ? 'border border-[var(--border)]' : ''}`}
onClick={handleProfilePictureClick}
onKeyDown={(event) => handleKeyboardActivation(event, handleProfilePictureClick)}
>
{(() => {
if (imageUrl) {
Expand Down Expand Up @@ -334,7 +332,7 @@ export function General() {
<Camera className='size-4 text-white' />
)}
</div>
</div>
</button>
<Input
type='file'
accept='image/png,image/jpeg,image/jpg'
Expand All @@ -357,6 +355,7 @@ export function General() {
</span>
<input
ref={inputRef}
aria-label='Your name'
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={handleKeyDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ export function InboxEnableToggle() {
const [isDisableOpen, setIsDisableOpen] = useState(false)
const [enableUsername, setEnableUsername] = useState('')

const handleToggle = useCallback(
async (checked: boolean) => {
if (checked) {
setIsEnableOpen(true)
return
}
setIsDisableOpen(true)
},
[workspaceId]
)
const handleToggle = useCallback(async (checked: boolean) => {
if (checked) {
setIsEnableOpen(true)
return
}
setIsDisableOpen(true)
}, [])

const handleDisable = useCallback(async () => {
try {
Expand All @@ -45,7 +42,7 @@ export function InboxEnableToggle() {
} catch (error) {
logger.error('Failed to disable inbox', { error })
}
}, [workspaceId])
}, [workspaceId, toggleInbox.mutateAsync])

const handleEnable = useCallback(async () => {
try {
Expand All @@ -59,7 +56,7 @@ export function InboxEnableToggle() {
} catch (error) {
logger.error('Failed to enable inbox', { error })
}
}, [workspaceId, enableUsername])
}, [workspaceId, enableUsername, toggleInbox.mutateAsync])

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function InboxSettingsTab() {
} catch (error) {
setEditAddressError(getErrorMessage(error, 'Failed to update address'))
}
}, [workspaceId, newUsername])
}, [workspaceId, newUsername, updateAddress.mutateAsync])

const handleAddSender = useCallback(async () => {
if (!newSenderEmail.trim()) return
Expand All @@ -82,7 +82,7 @@ export function InboxSettingsTab() {
} catch (error) {
setAddSenderError(getErrorMessage(error, 'Failed to add sender'))
}
}, [workspaceId, newSenderEmail, newSenderLabel])
}, [workspaceId, newSenderEmail, newSenderLabel, addSender.mutateAsync])

const handleRemoveSender = useCallback(
async (senderId: string) => {
Expand All @@ -93,7 +93,7 @@ export function InboxSettingsTab() {
setRemoveSenderError(getErrorMessage(error, 'Failed to remove sender'))
}
},
[workspaceId]
[workspaceId, removeSender.mutateAsync]
)

return (
Expand Down
Loading
Loading