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
@@ -1,11 +1,10 @@
import { noop } from 'lodash-es'
import { Check, ChevronsUpDown } from 'lucide-react'
import { useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import {
Button_Shadcn_ as Button,
cn,
Command_Shadcn_ as Command,
CommandEmpty_Shadcn_ as CommandEmpty,
CommandGroup_Shadcn_ as CommandGroup,
CommandInput_Shadcn_ as CommandInput,
CommandItem_Shadcn_ as CommandItem,
Expand All @@ -15,6 +14,8 @@ import {
PopoverTrigger_Shadcn_ as PopoverTrigger,
ScrollArea,
} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { useIntersectionObserver } from '~/hooks/useIntersectionObserver'

export interface ComboBoxOption {
id: string
Expand All @@ -28,25 +29,56 @@ export function ComboBox<Opt extends ComboBoxOption>({
name,
options,
selectedOption,
selectedDisplayName,
onSelectOption = noop,
className,
search = '',
hasNextPage = false,
isFetching = false,
isFetchingNextPage = false,
fetchNextPage,
setSearch = () => {},
useCommandSearch = true,
}: {
isLoading: boolean
disabled?: boolean
name: string
options: Opt[]
selectedOption?: string
selectedDisplayName?: string
onSelectOption?: (newValue: string) => void
className?: string
search?: string
hasNextPage?: boolean
isFetching?: boolean
isFetchingNextPage?: boolean
fetchNextPage?: () => void
setSearch?: (value: string) => void
useCommandSearch?: boolean
}) {
const [open, setOpen] = useState(false)

const selectedOptionDisplayName = options.find(
(option) => option.value === selectedOption
)?.displayName
const scrollRootRef = useRef<HTMLDivElement | null>(null)
const [sentinelRef, entry] = useIntersectionObserver({
root: scrollRootRef.current,
threshold: 0,
rootMargin: '0px',
})

useEffect(() => {
if (!isLoading && !isFetching && !isFetchingNextPage && hasNextPage && entry?.isIntersecting) {
fetchNextPage?.()
}
}, [isLoading, isFetching, isFetchingNextPage, hasNextPage, entry?.isIntersecting, fetchNextPage])

return (
<Popover open={open} onOpenChange={setOpen}>
<Popover
open={open}
onOpenChange={(value) => {
setOpen(value)
if (!value) setSearch('')
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
Expand All @@ -62,41 +94,64 @@ export function ComboBox<Opt extends ComboBoxOption>({
className
)}
>
{isLoading
? 'Loading...'
: options.length === 0
? `No ${name} found`
: selectedOptionDisplayName ?? `Select a ${name}...`}
{selectedDisplayName ??
(isLoading && options.length > 0
? 'Loading...'
: options.length === 0
? `No ${name} found`
: `Select a ${name}...`)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" side="bottom">
<Command>
<CommandInput placeholder={`Search ${name}...`} className="border-none ring-0" />
<PopoverContent className="p-0" side="bottom" align="start">
<Command shouldFilter={useCommandSearch}>
<CommandInput
placeholder={`Search ${name}...`}
className="border-none ring-0"
showResetIcon
value={search}
onValueChange={setSearch}
handleReset={() => setSearch('')}
/>
<CommandList>
<CommandEmpty>No {name} found.</CommandEmpty>
<CommandGroup>
<ScrollArea className={options.length > 10 ? 'h-[280px]' : ''}>
{options.map((option) => (
<CommandItem
key={option.id}
value={option.value}
onSelect={(selectedValue: string) => {
setOpen(false)
onSelectOption(selectedValue)
}}
className="cursor-pointer"
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedOption === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.displayName}
</CommandItem>
))}
</ScrollArea>
{isLoading ? (
<div className="px-2 py-1 flex flex-col gap-2">
<ShimmeringLoader className="w-full" />
<ShimmeringLoader className="w-4/5" />
</div>
) : (
<>
{search.length > 0 && options.length === 0 && (
<p className="text-xs text-center text-foreground-lighter py-3">
No {name}s found based on your search
</p>
)}
<ScrollArea className={options.length > 7 ? 'h-[210px]' : ''}>
{options.map((option) => (
<CommandItem
key={option.id}
value={option.value}
onSelect={(selectedValue: string) => {
setOpen(false)
onSelectOption(selectedValue)
}}
className="cursor-pointer"
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedOption === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.displayName}
</CommandItem>
))}
<div ref={sentinelRef} className="h-1 -mt-1" />
{hasNextPage && <ShimmeringLoader className="px-2 py-3" />}
</ScrollArea>
</>
)}
</CommandGroup>
</CommandList>
</Command>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import type {
Branch,
Org,
Project,
Variable,
} from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils'
import type { ProjectKeys, ProjectSettings } from '~/lib/fetch/projectApi'

import { Check, Copy } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { proxy, useSnapshot } from 'valtio'
Expand All @@ -31,11 +29,16 @@ import {
toOrgProjectValue,
} from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils'
import { useCopy } from '~/hooks/useCopy'
import { useDebounce } from '~/hooks/useDebounce'
import { useBranchesQuery } from '~/lib/fetch/branches'
import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
import { useProjectSettingsQuery, useProjectKeysQuery } from '~/lib/fetch/projectApi'
import { isProjectPaused, useProjectsQuery } from '~/lib/fetch/projects'
import { useSupavisorConfigQuery, type SupavisorConfigData } from '~/lib/fetch/pooler'
import { useProjectKeysQuery, useProjectSettingsQuery } from '~/lib/fetch/projectApi'
import {
isProjectPaused,
ProjectInfoInfinite,
useProjectsInfiniteQuery,
} from '~/lib/fetch/projects-infinite'
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'

Expand Down Expand Up @@ -67,8 +70,8 @@ type VariableDataState =

const projectsStore = proxy({
selectedOrg: null as Org | null,
selectedProject: null as Project | null,
setSelectedOrgProject: (org: Org | null, project: Project | null) => {
selectedProject: null as ProjectInfoInfinite | null,
setSelectedOrgProject: (org: Org | null, project: ProjectInfoInfinite | null) => {
projectsStore.selectedOrg = org
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_ORG, org?.id.toString())

Expand All @@ -90,18 +93,31 @@ function OrgProjectSelector() {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()

const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 500)

const { selectedOrg, selectedProject, setSelectedOrgProject } = useSnapshot(projectsStore)

const {
data: organizations,
isPending: organizationsIsPending,
isError: organizationsIsError,
} = useOrganizationsQuery({ enabled: isLoggedIn })

const {
data: projects,
data: projectsData,
isPending: projectsIsPending,
isError: projectsIsError,
} = useProjectsQuery({ enabled: isLoggedIn })
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useProjectsInfiniteQuery(
{ search: search.length === 0 ? search : debouncedSearch },
{ enabled: isLoggedIn }
)
const projects =
useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || []

const anyIsPending = organizationsIsPending || projectsIsPending
const anyIsError = organizationsIsError || projectsIsError
Expand Down Expand Up @@ -141,7 +157,7 @@ function OrgProjectSelector() {
const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT)

let storedOrg: Org | undefined
let storedProject: Project | undefined
let storedProject: ProjectInfoInfinite | undefined
if (storedMaybeOrgId && storedMaybeProjectRef) {
storedOrg = organizations!.find((org) => org.id === Number(storedMaybeOrgId))
storedProject = projects!.find((project) => project.ref === storedMaybeProjectRef)
Expand All @@ -167,6 +183,11 @@ function OrgProjectSelector() {
stateSummary === 'loggedIn.dataSuccess.hasNoData'
}
options={formattedData}
selectedDisplayName={
selectedOrg && selectedProject
? toDisplayNameOrgProject(selectedOrg, selectedProject)
: undefined
}
selectedOption={
selectedOrg && selectedProject ? toOrgProjectValue(selectedOrg, selectedProject) : undefined
}
Expand All @@ -181,6 +202,13 @@ function OrgProjectSelector() {
setSelectedOrgProject(org, project)
}
}}
search={search}
isFetching={isFetching}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
setSearch={setSearch}
useCommandSearch={false}
/>
)
}
Expand All @@ -190,6 +218,7 @@ function BranchSelector() {
const isLoggedIn = useIsLoggedIn()

const { selectedProject, selectedBranch, setSelectedBranch } = useSnapshot(projectsStore)
const [branchSearch, setBranchSearch] = useState('')

const projectPaused = isProjectPaused(selectedProject)
const hasBranches = selectedProject?.is_branch_enabled ?? false
Expand Down Expand Up @@ -253,7 +282,10 @@ function BranchSelector() {
stateSummary === 'loggedIn.branches.dataSuccess.noData'
}
options={formattedData}
selectedDisplayName={selectedBranch?.name}
selectedOption={selectedBranch ? toBranchValue(selectedBranch) : undefined}
search={branchSearch}
setSearch={setBranchSearch}
onSelectOption={(option) => {
const [branchId] = fromBranchValue(option)
if (branchId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { BranchesData } from '~/lib/fetch/branches'
import type { OrganizationsData } from '~/lib/fetch/organizations'
import type { ProjectsData } from '~/lib/fetch/projects'
import { ProjectInfoInfinite } from '~/lib/fetch/projects-infinite'

export type Org = OrganizationsData[number]
export type Project = ProjectsData[number]
export type Branch = BranchesData[number]

export type Variable = 'url' | 'publishable' | 'anon' | 'sessionPooler'
Expand Down Expand Up @@ -39,11 +38,17 @@ type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}

export function toDisplayNameOrgProject(org: DeepReadonly<Org>, project: DeepReadonly<Project>) {
export function toDisplayNameOrgProject(
org: DeepReadonly<Org>,
project: DeepReadonly<ProjectInfoInfinite>
) {
return `${org.name} / ${project.name}`
}

export function toOrgProjectValue(org: DeepReadonly<Org>, project: DeepReadonly<Project>) {
export function toOrgProjectValue(
org: DeepReadonly<Org>,
project: DeepReadonly<ProjectInfoInfinite>
) {
return escapeDoubleQuotes(
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
JSON.stringify([org.id, project.ref, removeDoubleQuotes(toDisplayNameOrgProject(org, project))])
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/_partials/auth_rate_limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 4 emails per hour as of 14th July 2023. As of 21 Oct 2023, this has been updated to <SharedData data="config">auth.rate_limits.email.inbuilt_smtp_per_hour</SharedData> emails per hour. You can only change this with your own custom SMTP setup. |
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to <SharedData data="config">auth.rate_limits.otp.requests_per_hour</SharedData> OTPs per hour. Is customizable. |
| Send OTPs or magic links | `/auth/v1/otp` | Last request | Defaults to <SharedData data="config">auth.rate_limits.otp.period</SharedData> window before a new request is allowed. Is customizable. |
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to <SharedData data="config">auth.rate_limits.signup_confirmation.period</SharedData> window before a new request is allowed. Is customizable. |
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to <SharedData data="config">auth.rate_limits.password_reset.period</SharedData> window before a new request is allowed. Is customizable. |
| Send OTPs or magic links | `/auth/v1/otp` | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.otp.period</SharedData> window before a new request is allowed to the same user. Is customizable. |
| Signup confirmation request | `/auth/v1/signup` | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.signup_confirmation.period</SharedData> window before a new request is allowed to the same user. Is customizable. |
| Password Reset Request | `/auth/v1/recover` | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.password_reset.period</SharedData> window before a new request is allowed to the same user. Is customizable. |
| Verification requests | `/auth/v1/verify` | IP Address | <SharedData data="config">auth.rate_limits.verification.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.verification.requests_burst</SharedData> requests) |
| Token refresh requests | `/auth/v1/token` | IP Address | <SharedData data="config">auth.rate_limits.token_refresh.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.token_refresh.requests_burst</SharedData> requests) |
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | <SharedData data="config">auth.rate_limits.mfa.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.verification.mfa</SharedData> requests) |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/database/extensions/pgvector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Vector similarity refers to a measure of the similarity between two related item

### Embeddings

This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation.
This is particularly useful if you're building AI applications with large language models. You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation (RAG).

## Usage

Expand Down
Loading
Loading