diff --git a/apps/design-system/package.json b/apps/design-system/package.json
index e6acd2763ef0b..197bb8d13bc14 100644
--- a/apps/design-system/package.json
+++ b/apps/design-system/package.json
@@ -6,7 +6,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --turbopack --port 3003",
- "build": "pnpm run content:build && pnpm run build:registry && next build",
+ "build": "pnpm run content:build && pnpm run build:registry && next build --turbopack",
"build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"start": "next start",
"lint": "next lint",
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx
index 2d4aea160f4a1..36b0407e88d6b 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx
@@ -6,7 +6,6 @@ import * as z from 'zod'
import { useParams } from 'common'
import { useCreateDestinationPipelineMutation } from 'data/replication/create-destination-pipeline-mutation'
-import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-mutation'
import { useReplicationDestinationByIdQuery } from 'data/replication/destination-by-id-query'
import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query'
import { useReplicationPublicationsQuery } from 'data/replication/publications-query'
@@ -21,9 +20,6 @@ import {
AccordionContent_Shadcn_,
AccordionItem_Shadcn_,
AccordionTrigger_Shadcn_,
- Alert_Shadcn_,
- AlertDescription_Shadcn_,
- AlertTitle_Shadcn_,
Button,
Form_Shadcn_,
FormControl_Shadcn_,
@@ -43,7 +39,6 @@ import {
SheetSection,
SheetTitle,
TextArea_Shadcn_,
- WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import NewPublicationPanel from './NewPublicationPanel'
@@ -60,7 +55,6 @@ const FormSchema = z.object({
datasetId: z.string().min(1, 'Dataset id is required'),
serviceAccountKey: z.string().min(1, 'Service account key is required'),
publicationName: z.string().min(1, 'Publication is required'),
- maxSize: z.number().min(1, 'Max Size must be greater than 0').int().optional(),
maxFillMs: z.number().min(1, 'Max Fill milliseconds should be greater than 0').int().optional(),
maxStalenessMins: z.number().nonnegative().optional(),
})
@@ -90,9 +84,6 @@ export const DestinationPanel = ({
const editMode = !!existingDestination
const [publicationPanelVisible, setPublicationPanelVisible] = useState(false)
- const { mutateAsync: createTenantSource, isLoading: creatingTenantSource } =
- useCreateTenantSourceMutation()
-
const { mutateAsync: createDestinationPipeline, isLoading: creatingDestinationPipeline } =
useCreateDestinationPipelineMutation({
onSuccess: () => form.reset(defaultValues),
@@ -129,7 +120,6 @@ export const DestinationPanel = ({
// For now, the password will always be set as empty for security reasons.
serviceAccountKey: destinationData?.config?.big_query?.service_account_key ?? '',
publicationName: pipelineData?.config.publication_name ?? '',
- maxSize: pipelineData?.config?.batch?.max_size,
maxFillMs: pipelineData?.config?.batch?.max_fill_ms,
maxStalenessMins: destinationData?.config?.big_query?.max_staleness_mins,
}),
@@ -162,9 +152,8 @@ export const DestinationPanel = ({
}
const batchConfig: any = {}
- if (!!data.maxSize) batchConfig.maxSize = data.maxSize
if (!!data.maxFillMs) batchConfig.maxFillMs = data.maxFillMs
- const hasBothBatchFields = Object.keys(batchConfig).length === 2
+ const hasBatchFields = Object.keys(batchConfig).length > 0
await updateDestinationPipeline({
destinationId: existingDestination.destinationId,
@@ -174,7 +163,7 @@ export const DestinationPanel = ({
destinationConfig: { bigQuery: bigQueryConfig },
pipelineConfig: {
publicationName: data.publicationName,
- ...(hasBothBatchFields ? { batch: batchConfig } : {}),
+ ...(hasBatchFields ? { batch: batchConfig } : {}),
},
sourceId,
})
@@ -209,9 +198,8 @@ export const DestinationPanel = ({
}
const batchConfig: any = {}
- if (!!data.maxSize) batchConfig.maxSize = data.maxSize
if (!!data.maxFillMs) batchConfig.maxFillMs = data.maxFillMs
- const hasBothBatchFields = Object.keys(batchConfig).length === 2
+ const hasBatchFields = Object.keys(batchConfig).length > 0
const { pipeline_id: pipelineId } = await createDestinationPipeline({
projectRef,
@@ -220,7 +208,7 @@ export const DestinationPanel = ({
sourceId,
pipelineConfig: {
publicationName: data.publicationName,
- ...(hasBothBatchFields ? { batch: batchConfig } : {}),
+ ...(hasBatchFields ? { batch: batchConfig } : {}),
},
})
// Set request status only right before starting, then fire and close
@@ -235,11 +223,6 @@ export const DestinationPanel = ({
}
}
- const onEnableReplication = async () => {
- if (!projectRef) return console.error('Project ref is required')
- await createTenantSource({ projectRef })
- }
-
useEffect(() => {
if (editMode && destinationData && pipelineData) {
form.reset(defaultValues)
@@ -253,7 +236,7 @@ export const DestinationPanel = ({
}
}, [visible, defaultValues, form])
- return sourceId ? (
+ return (
<>
@@ -399,31 +382,6 @@ export const DestinationPanel = ({
Advanced Settings
- (
-
-
- {
- const val = e.target.value
- field.onChange(val === '' ? undefined : Number(val))
- }}
- placeholder="Leave empty for default"
- />
-
-
- )}
- />
+
setPublicationPanelVisible(false)}
/>
>
- ) : (
-
-
-
-
- Create a new destination
-
-
-
-
-
- {/* Pricing to be decided yet */}
- Enabling replication will cost additional $xx.xx
-
-
-
-
-
- Enable replication
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-
)
}
diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
index c1c133f3f0111..604b908c1d762 100644
--- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
@@ -1,19 +1,21 @@
+import { useQueryClient } from '@tanstack/react-query'
import { Plus, Search } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
-import AlertError from 'components/ui/AlertError'
+import { AlertError } from 'components/ui/AlertError'
+import { DocsButton } from 'components/ui/DocsButton'
import { useReplicationDestinationsQuery } from 'data/replication/destinations-query'
+import { replicationKeys } from 'data/replication/keys'
+import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query'
import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query'
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
-import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query'
-import { replicationKeys } from 'data/replication/keys'
-import { useQueryClient } from '@tanstack/react-query'
import { Button, cn, Input_Shadcn_ } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { DestinationPanel } from './DestinationPanel'
import { DestinationRow } from './DestinationRow'
+import { EnableReplicationModal } from './EnableReplicationModal'
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
export const Destinations = () => {
@@ -26,11 +28,13 @@ export const Destinations = () => {
error: sourcesError,
isLoading: isSourcesLoading,
isError: isSourcesError,
+ isSuccess: isSourcesSuccess,
} = useReplicationSourcesQuery({
projectRef,
})
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
+ const replicationNotEnabled = isSourcesSuccess && !sourceId
const {
data: destinationsData,
@@ -52,7 +56,7 @@ export const Destinations = () => {
projectRef,
})
- const anyDestinations = isDestinationsSuccess && destinationsData.destinations.length > 0
+ const hasDestinations = isDestinationsSuccess && destinationsData.destinations.length > 0
const filteredDestinations =
filterString.length === 0
@@ -103,9 +107,11 @@ export const Destinations = () => {
/>
- } onClick={() => setShowNewDestinationPanel(true)}>
- Add destination
-
+ {!!sourceId && (
+ } onClick={() => setShowNewDestinationPanel(true)}>
+ Add destination
+
+ )}
@@ -119,7 +125,21 @@ export const Destinations = () => {
/>
)}
- {anyDestinations ? (
+ {replicationNotEnabled ? (
+
+
+
Run analysis on your data via integrations with Replication
+
+ Enable replication on your project to send data to your first destination
+
+
+
+
+ {/* [Joshen] Placeholder for when we have documentation */}
+
+
+
+ ) : hasDestinations ? (
Name,
@@ -147,7 +167,7 @@ export const Destinations = () => {
/>
)
})}
- >
+ />
) : (
!isSourcesLoading &&
!isDestinationsLoading &&
@@ -180,7 +200,7 @@ export const Destinations = () => {
{!isSourcesLoading &&
!isDestinationsLoading &&
filteredDestinations.length === 0 &&
- anyDestinations && (
+ hasDestinations && (
No destinations match "{filterString}"
diff --git a/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx b/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx
new file mode 100644
index 0000000000000..c84b826ab4ba6
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx
@@ -0,0 +1,78 @@
+import { useState } from 'react'
+import { toast } from 'sonner'
+
+import { useParams } from 'common'
+import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-mutation'
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ DialogTrigger,
+} from 'ui'
+import { Admonition } from 'ui-patterns'
+
+export const EnableReplicationModal = () => {
+ const { ref: projectRef } = useParams()
+ const [open, setOpen] = useState(false)
+
+ const { mutateAsync: createTenantSource, isLoading: creatingTenantSource } =
+ useCreateTenantSourceMutation({
+ onSuccess: () => {
+ toast.success('Replication has been successfully enabled!')
+ setOpen(false)
+ },
+ onError: (error) => {
+ toast.error(`Failed to enable replication: ${error.message}`)
+ },
+ })
+
+ const onEnableReplication = async () => {
+ if (!projectRef) return console.error('Project ref is required')
+ await createTenantSource({ projectRef })
+ }
+
+ return (
+
+
+
+ Enable replication
+
+
+
+
+ Confirm to enable Replication
+
+
+
+
+
+ This feature is in active development and may change as we gather feedback.
+ Availability and behavior can evolve while in Alpha.
+
+
+ Pricing has not been finalized yet. You can enable replication now; we’ll announce
+ pricing later and notify you before any charges apply.
+
+
+
+
+
+ Cancel
+
+
+ Enable replication
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx b/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx
index 10d003463461e..d09575245629d 100644
--- a/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx
+++ b/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx
@@ -52,6 +52,26 @@ export const NoFilterResults = ({
)
}
+export const LoadingTableRow = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
+
export const LoadingTableView = () => {
return (
@@ -67,23 +87,7 @@ export const LoadingTableView = () => {
{[...Array(3)].map((_, i) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
))}
diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx
index 5ae3a2958f279..650cb768cbd7a 100644
--- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx
+++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx
@@ -1,19 +1,19 @@
import { Github } from 'lucide-react'
+import InlineSVG from 'react-inlinesvg'
import CardButton from 'components/ui/CardButton'
import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
import type { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
-import type { ProjectInfo } from 'data/projects/projects-query'
+import { OrgProject } from 'data/projects/projects-infinite-query'
import type { ResourceWarning } from 'data/usage/resource-warnings-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
-import InlineSVG from 'react-inlinesvg'
import { inferProjectStatus } from './ProjectCard.utils'
import { ProjectCardStatus } from './ProjectCardStatus'
export interface ProjectCardProps {
- project: ProjectInfo
+ project: OrgProject
rewriteHref?: string
githubIntegration?: IntegrationProjectConnection
vercelIntegration?: IntegrationProjectConnection
@@ -28,20 +28,20 @@ export const ProjectCard = ({
resourceWarnings,
}: ProjectCardProps) => {
const { name, ref: projectRef } = project
- const desc = `${project.cloud_provider} | ${project.region}`
+ const infraInformation = project.databases.find((x) => x.identifier === project.ref)
+ const desc = `${infraInformation?.cloud_provider} | ${project.region}`
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
'project_homepage:show_instance_size',
])
- const isBranchingEnabled = project.preview_branch_refs?.length > 0
const isGithubIntegrated = githubIntegration !== undefined
const isVercelIntegrated = vercelIntegration !== undefined
const githubRepository = githubIntegration?.metadata.name ?? undefined
- const projectStatus = inferProjectStatus(project)
+ const projectStatus = inferProjectStatus(project.status)
return (
-
+
{
+export const inferProjectStatus = (projectStatus: string) => {
let status = undefined
- switch (project.status) {
+ switch (projectStatus) {
case PROJECT_STATUS.ACTIVE_HEALTHY:
status = 'isHealthy'
break
diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx
index 4ce7a0a194ced..bb204454298c4 100644
--- a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx
+++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx
@@ -1,47 +1,85 @@
+import { UIEvent, useMemo } from 'react'
+
+import { useDebounce } from '@uidotdev/usehooks'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import NoSearchResults from 'components/ui/NoSearchResults'
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
-import { useProjectsQuery } from 'data/projects/projects-query'
+import { useOrgProjectsInfiniteQuery } from 'data/projects/projects-infinite-query'
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
+import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
-import { makeRandomString } from 'lib/helpers'
+import { isAtBottom } from 'lib/helpers'
+import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'
import type { Organization } from 'types'
-import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
-import { LoadingCardView, LoadingTableView, NoFilterResults, NoProjectsState } from './EmptyStates'
+import {
+ Card,
+ cn,
+ LoadingLine,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from 'ui'
+import {
+ LoadingCardView,
+ LoadingTableRow,
+ LoadingTableView,
+ NoFilterResults,
+ NoProjectsState,
+} from './EmptyStates'
import { ProjectCard } from './ProjectCard'
import { ProjectTableRow } from './ProjectTableRow'
+import { ShimmeringCard } from './ShimmeringCard'
export interface ProjectListProps {
organization?: Organization
rewriteHref?: (projectRef: string) => string
- search?: string
- filterStatus?: string[]
- resetFilterStatus?: () => void
- viewMode?: 'grid' | 'table'
}
-export const ProjectList = ({
- search = '',
- organization: organization_,
- rewriteHref,
- filterStatus,
- resetFilterStatus,
- viewMode = 'grid',
-}: ProjectListProps) => {
+export const ProjectList = ({ organization: organization_, rewriteHref }: ProjectListProps) => {
+ const { slug: urlSlug } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
+
+ const [search] = useQueryState('search', parseAsString.withDefault(''))
+ const debouncedSearch = useDebounce(search, 500)
+
+ const [filterStatus, setFilterStatus] = useQueryState(
+ 'status',
+ parseAsArrayOf(parseAsString, ',').withDefault([])
+ )
+ const [viewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')
+
const organization = organization_ ?? selectedOrganization
+ const slug = organization?.slug ?? urlSlug
const {
data,
+ error: projectsError,
isLoading: isLoadingProjects,
isSuccess: isSuccessProjects,
isError: isErrorProjects,
- error: projectsError,
- } = useProjectsQuery()
- const allProjects = data?.projects ?? []
+ isFetching,
+ isFetchingNextPage,
+ hasNextPage,
+ fetchNextPage,
+ } = useOrgProjectsInfiniteQuery(
+ {
+ slug,
+ search: search.length === 0 ? search : debouncedSearch,
+ statuses: filterStatus,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+ const orgProjects =
+ useMemo(() => data?.pages.flatMap((page) => page.projects), [data?.pages]) || []
const {
isLoading: _isLoadingPermissions,
@@ -54,42 +92,18 @@ export const ProjectList = ({
const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
const { data: connections } = useGitHubConnectionsQuery({ organizationId: organization?.id })
- const orgProjects = allProjects.filter((x) => x.organization_slug === organization?.slug)
const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
- const hasFilterStatusApplied = filterStatus !== undefined && filterStatus.length !== 2
+ const isEmpty =
+ debouncedSearch.length === 0 &&
+ filterStatus.length === 0 &&
+ (!orgProjects || orgProjects.length === 0)
+ const sortedProjects = [...(orgProjects || [])].sort((a, b) => a.name.localeCompare(b.name))
+
const noResultsFromSearch =
- search.length > 0 &&
- isSuccessProjects &&
- orgProjects.filter((project) => {
- return (
- project.name.toLowerCase().includes(search.toLowerCase()) ||
- project.ref.includes(search.toLowerCase())
- )
- }).length === 0
+ debouncedSearch.length > 0 && isSuccessProjects && orgProjects.length === 0
const noResultsFromStatusFilter =
- hasFilterStatusApplied &&
- isSuccessProjects &&
- orgProjects.filter((project) => filterStatus.includes(project.status)).length === 0
-
- const isEmpty = !orgProjects || orgProjects.length === 0
- const sortedProjects = [...(orgProjects || [])].sort((a, b) => a.name.localeCompare(b.name))
- const filteredProjects =
- search.length > 0
- ? sortedProjects.filter((project) => {
- return (
- project.name.toLowerCase().includes(search.toLowerCase()) ||
- project.ref.includes(search.toLowerCase())
- )
- })
- : sortedProjects
-
- const filteredProjectsByStatus =
- filterStatus !== undefined
- ? filterStatus.length === 2
- ? filteredProjects
- : filteredProjects.filter((project) => filterStatus.includes(project.status))
- : filteredProjects
+ filterStatus.length > 0 && isSuccessProjects && orgProjects.length === 0
const githubConnections = connections?.map((connection) => ({
id: String(connection.id),
@@ -111,6 +125,11 @@ export const ProjectList = ({
?.filter((integration) => integration.integration.name === 'Vercel')
.flatMap((integration) => integration.connections)
+ const handleScroll = (event: UIEvent) => {
+ if (isLoadingProjects || isFetchingNextPage || !isAtBottom(event)) return
+ fetchNextPage()
+ }
+
if (isErrorPermissions) {
return (
+
-
+ {/* [Joshen] Ideally we can figure out sticky table headers here */}
+
Project
Status
@@ -149,6 +169,11 @@ export const ProjectList = ({
Region
Created
+
+
+
+
+
{noResultsFromStatusFilter ? (
@@ -156,7 +181,7 @@ export const ProjectList = ({
setFilterStatus([])}
className="border-0"
/>
@@ -168,22 +193,26 @@ export const ProjectList = ({
) : (
- filteredProjectsByStatus?.map((project) => (
- resourceWarning.project === project.ref
- )}
- githubIntegration={githubConnections?.find(
- (connection) => connection.supabase_project_ref === project.ref
- )}
- vercelIntegration={vercelConnections?.find(
- (connection) => connection.supabase_project_ref === project.ref
- )}
- />
- ))
+ <>
+ {sortedProjects?.map((project) => (
+ resourceWarning.project === project.ref
+ )}
+ githubIntegration={githubConnections?.find(
+ (connection) => connection.supabase_project_ref === project.ref
+ )}
+ vercelIntegration={vercelConnections?.find(
+ (connection) => connection.supabase_project_ref === project.ref
+ )}
+ />
+ ))}
+ {hasNextPage && }
+ >
)}
@@ -194,14 +223,24 @@ export const ProjectList = ({
return (
<>
{noResultsFromStatusFilter ? (
-
+ setFilterStatus([])}
+ />
) : noResultsFromSearch ? (
) : (
-
- {filteredProjectsByStatus?.map((project) => (
+
+ {sortedProjects?.map((project) => (
))}
+ {hasNextPage && [...Array(2)].map((_, i) => )}
)}
>
diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx
index 812fc067d81e0..30563af30bb53 100644
--- a/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx
+++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectTableRow.tsx
@@ -4,16 +4,18 @@ import InlineSVG from 'react-inlinesvg'
import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
import type { IntegrationProjectConnection } from 'data/integrations/integrations.types'
-import type { ProjectInfo } from 'data/projects/projects-query'
+import { OrgProject } from 'data/projects/projects-infinite-query'
import type { ResourceWarning } from 'data/usage/resource-warnings-query'
import { BASE_PATH } from 'lib/constants'
+import { Organization } from 'types'
import { TableCell, TableRow } from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { inferProjectStatus } from './ProjectCard.utils'
import { ProjectCardStatus } from './ProjectCardStatus'
export interface ProjectTableRowProps {
- project: ProjectInfo
+ project: OrgProject
+ organization?: Organization
rewriteHref?: string
githubIntegration?: IntegrationProjectConnection
vercelIntegration?: IntegrationProjectConnection
@@ -22,6 +24,7 @@ export interface ProjectTableRowProps {
export const ProjectTableRow = ({
project,
+ organization,
rewriteHref,
githubIntegration,
vercelIntegration,
@@ -29,14 +32,15 @@ export const ProjectTableRow = ({
}: ProjectTableRowProps) => {
const router = useRouter()
const { name, ref: projectRef } = project
- const projectStatus = inferProjectStatus(project)
+ const projectStatus = inferProjectStatus(project.status)
const url = rewriteHref ?? `/project/${project.ref}`
- const isBranchingEnabled = project.preview_branch_refs?.length > 0
const isGithubIntegrated = githubIntegration !== undefined
const isVercelIntegrated = vercelIntegration !== undefined
const githubRepository = githubIntegration?.metadata.name ?? undefined
+ const infraInformation = project.databases.find((x) => x.identifier === project.ref)
+
return (
{name}
ID: {projectRef}
- {(isGithubIntegrated || isVercelIntegrated || isBranchingEnabled) && (
+ {(isGithubIntegrated || isVercelIntegrated) && (
{isVercelIntegrated && (
@@ -91,7 +95,14 @@ export const ProjectTableRow = ({
{project.status !== 'INACTIVE' ? (
-
+
) : (
-
)}
@@ -99,7 +110,7 @@ export const ProjectTableRow = ({
- {project.cloud_provider} | {project.region || 'N/A'}
+ {infraInformation?.cloud_provider} | {project.region || 'N/A'}
diff --git a/apps/studio/components/interfaces/HomePageActions.tsx b/apps/studio/components/interfaces/HomePageActions.tsx
index 7ca095608665a..64058004de517 100644
--- a/apps/studio/components/interfaces/HomePageActions.tsx
+++ b/apps/studio/components/interfaces/HomePageActions.tsx
@@ -1,9 +1,13 @@
-import { Filter, Grid, List, Plus, Search } from 'lucide-react'
+import { Filter, Grid, List, Loader2, Plus, Search, X } from 'lucide-react'
import Link from 'next/link'
-import { useParams } from 'common'
+import { useDebounce } from '@uidotdev/usehooks'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { useOrgProjectsInfiniteQuery } from 'data/projects/projects-infinite-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { PROJECT_STATUS } from 'lib/constants'
+import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'
import {
Button,
Checkbox_Shadcn_,
@@ -17,29 +21,37 @@ import {
import { Input } from 'ui-patterns/DataInputs/Input'
interface HomePageActionsProps {
- search: string
- filterStatus: string[]
+ slug?: string
hideNewProject?: boolean
- viewMode?: 'grid' | 'table'
showViewToggle?: boolean
- setSearch: (value: string) => void
- setFilterStatus: (value: string[]) => void
- setViewMode?: (value: 'grid' | 'table') => void
}
export const HomePageActions = ({
- search,
- filterStatus,
+ slug: _slug,
hideNewProject = false,
- viewMode,
showViewToggle = false,
- setSearch,
- setFilterStatus,
- setViewMode,
}: HomePageActionsProps) => {
- const { slug } = useParams()
+ const { slug: urlSlug } = useParams()
const projectCreationEnabled = useIsFeatureEnabled('projects:create')
+ const slug = _slug ?? urlSlug
+ const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
+ const debouncedSearch = useDebounce(search, 500)
+ const [filterStatus, setFilterStatus] = useQueryState(
+ 'status',
+ parseAsArrayOf(parseAsString, ',').withDefault([])
+ )
+ const [viewMode, setViewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')
+
+ const { isFetching: isFetchingProjects } = useOrgProjectsInfiniteQuery(
+ {
+ slug,
+ search: search.length === 0 ? search : debouncedSearch,
+ statuses: filterStatus,
+ },
+ { keepPreviousData: true }
+ )
+
return (
@@ -47,15 +59,26 @@ export const HomePageActions = ({
placeholder="Search for a project"
icon={
}
size="tiny"
- className="w-64 pl-8 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
+ className="w-32 md:w-64 pl-8 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
value={search}
onChange={(event) => setSearch(event.target.value)}
+ actions={[
+ search && (
+
}
+ onClick={() => setSearch('')}
+ className="p-0 h-5 w-5"
+ />
+ ),
+ ]}
/>
}
/>
@@ -73,10 +96,12 @@ export const HomePageActions = ({
{
if (filterStatus.includes(key)) {
setFilterStatus(filterStatus.filter((y) => y !== key))
+ } else if (filterStatus.length === 1) {
+ setFilterStatus([])
} else {
setFilterStatus(filterStatus.concat([key]))
}
@@ -100,6 +125,8 @@ export const HomePageActions = ({
+
+ {isFetchingProjects &&
}
diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx
index 9361bd4dce92f..99a094feedc11 100644
--- a/apps/studio/components/layouts/DefaultLayout.tsx
+++ b/apps/studio/components/layouts/DefaultLayout.tsx
@@ -1,13 +1,12 @@
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
-import { LOCAL_STORAGE_KEYS } from 'common'
-import { useParams } from 'common'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AppBannerWrapper } from 'components/interfaces/App'
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
import { Sidebar } from 'components/interfaces/Sidebar'
-import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
+import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy'
import { useAppStateSnapshot } from 'state/app-state'
import { SidebarProvider } from 'ui'
import { LayoutHeader } from './ProjectLayout/LayoutHeader'
diff --git a/apps/studio/components/layouts/PageLayout/PageLayout.tsx b/apps/studio/components/layouts/PageLayout/PageLayout.tsx
index e9144545a7ec4..033a2da2e4cc6 100644
--- a/apps/studio/components/layouts/PageLayout/PageLayout.tsx
+++ b/apps/studio/components/layouts/PageLayout/PageLayout.tsx
@@ -75,7 +75,7 @@ export const PageLayout = ({
const router = useRouter()
return (
-
+
{/* Header section */}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index 269790306d708..ad60d7afc75b8 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -394,7 +394,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
void
+ onNewChat: () => void
onCloseAssistant: () => void
showMetadataWarning: boolean
updatedOptInSinceMCP: boolean
@@ -19,7 +19,7 @@ interface AIAssistantHeaderProps {
export const AIAssistantHeader = ({
isChatLoading,
- onClearMessages,
+ onNewChat,
onCloseAssistant,
showMetadataWarning,
updatedOptInSinceMCP,
@@ -54,22 +54,22 @@ export const AIAssistantHeader = ({
}
- onClick={() => setIsOptInModalOpen(true)}
+ icon={ }
+ onClick={onNewChat}
className="h-7 w-7 p-0"
disabled={isChatLoading}
- tooltip={{
- content: { side: 'bottom', text: 'Permission settings' },
- }}
+ tooltip={{ content: { side: 'bottom', text: 'New chat' } }}
/>
}
- onClick={onClearMessages}
+ icon={ }
+ onClick={() => setIsOptInModalOpen(true)}
className="h-7 w-7 p-0"
disabled={isChatLoading}
- tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }}
+ tooltip={{
+ content: { side: 'bottom', text: 'Permission settings' },
+ }}
/>
['all-projects'] as const,
+ infiniteListByOrg: (
+ slug: string | undefined,
+ params: {
+ limit: number
+ sort?: 'name_asc' | 'name_desc' | 'created_asc' | 'created_desc'
+ search?: string
+ statuses?: string[]
+ }
+ ) => ['all-projects', slug, params] as const,
status: (projectRef: string | undefined) => ['project', projectRef, 'status'] as const,
types: (projectRef: string | undefined) => ['project', projectRef, 'types'] as const,
detail: (projectRef: string | undefined) => ['project', projectRef, 'detail'] as const,
diff --git a/apps/studio/data/projects/projects-infinite-query.ts b/apps/studio/data/projects/projects-infinite-query.ts
new file mode 100644
index 0000000000000..356cf8df0da31
--- /dev/null
+++ b/apps/studio/data/projects/projects-infinite-query.ts
@@ -0,0 +1,87 @@
+import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
+
+import { components } from 'api-types'
+import { get, handleError } from 'data/fetchers'
+import { useProfile } from 'lib/profile'
+import { ResponseError } from 'types'
+import { projectKeys } from './keys'
+
+// [Joshen] Try to keep this value a multiple of 6 (common denominator of 2 and 3) to fit the cards view
+// So that the last row will always be a full row of cards while there's a next page
+// API max rows is 100, I'm just choosing 96 here as the highest value thats a multiple of 6
+const DEFAULT_LIMIT = 96
+
+interface GetOrgProjectsInfiniteVariables {
+ slug?: string
+ limit?: number
+ sort?: 'name_asc' | 'name_desc' | 'created_asc' | 'created_desc'
+ search?: string
+ page?: number
+ statuses?: string[]
+}
+
+export type OrgProject = components['schemas']['OrganizationProjectsResponse']['projects'][number]
+
+async function getOrganizationProjects(
+ {
+ slug,
+ limit = DEFAULT_LIMIT,
+ page = 0,
+ sort = 'name_asc',
+ search: _search = '',
+ statuses: _statuses = [],
+ }: GetOrgProjectsInfiniteVariables,
+ signal?: AbortSignal,
+ headers?: Record
+) {
+ if (!slug) throw new Error('Slug is required')
+
+ const offset = page * limit
+ const search = _search.length === 0 ? undefined : _search
+ const statuses = _statuses.length === 0 ? undefined : _statuses.join(',')
+
+ const { data, error } = await get('/platform/organizations/{slug}/projects', {
+ params: { path: { slug }, query: { limit, offset, sort, search, statuses } },
+ signal,
+ headers,
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+export type OrgProjectsInfiniteData = Awaited>
+export type OrgProjectsInfiniteError = ResponseError
+
+export const useOrgProjectsInfiniteQuery = (
+ {
+ slug,
+ limit = DEFAULT_LIMIT,
+ sort = 'name_asc',
+ search,
+ statuses = [],
+ }: GetOrgProjectsInfiniteVariables,
+ {
+ enabled = true,
+ ...options
+ }: UseInfiniteQueryOptions = {}
+) => {
+ const { profile } = useProfile()
+ return useInfiniteQuery(
+ projectKeys.infiniteListByOrg(slug, { limit, sort, search, statuses }),
+ ({ signal, pageParam }) =>
+ getOrganizationProjects({ slug, limit, page: pageParam, sort, search, statuses }, signal),
+ {
+ enabled: enabled && profile !== undefined && typeof slug !== 'undefined',
+ getNextPageParam(lastPage, pages) {
+ const page = pages.length
+ const currentTotalCount = page * limit
+ const totalCount = lastPage.pagination.count
+
+ if (currentTotalCount >= totalCount) return undefined
+ return page
+ },
+ ...options,
+ }
+ )
+}
diff --git a/apps/studio/data/replication/create-destination-pipeline-mutation.ts b/apps/studio/data/replication/create-destination-pipeline-mutation.ts
index ce9e285b09258..2189dd8cd0691 100644
--- a/apps/studio/data/replication/create-destination-pipeline-mutation.ts
+++ b/apps/studio/data/replication/create-destination-pipeline-mutation.ts
@@ -22,7 +22,6 @@ export type CreateDestinationPipelineParams = {
pipelineConfig: {
publicationName: string
batch?: {
- maxSize: number
maxFillMs: number
}
}
@@ -60,7 +59,6 @@ async function createDestinationPipeline(
...(batch
? {
batch: {
- max_size: batch.maxSize,
max_fill_ms: batch.maxFillMs,
},
}
diff --git a/apps/studio/data/replication/update-destination-pipeline-mutation.ts b/apps/studio/data/replication/update-destination-pipeline-mutation.ts
index c96719e1df390..2bd6830a8e5a1 100644
--- a/apps/studio/data/replication/update-destination-pipeline-mutation.ts
+++ b/apps/studio/data/replication/update-destination-pipeline-mutation.ts
@@ -24,7 +24,6 @@ export type UpdateDestinationPipelineParams = {
pipelineConfig: {
publicationName: string
batch?: {
- maxSize: number
maxFillMs: number
}
}
@@ -64,7 +63,6 @@ async function updateDestinationPipeline(
publication_name: publicationName,
...(batch && {
batch: {
- max_size: batch.maxSize,
max_fill_ms: batch.maxFillMs,
},
}),
diff --git a/apps/studio/lib/ai/tool-filter.test.ts b/apps/studio/lib/ai/tool-filter.test.ts
index 3dae04865f221..c419c119e47d1 100644
--- a/apps/studio/lib/ai/tool-filter.test.ts
+++ b/apps/studio/lib/ai/tool-filter.test.ts
@@ -35,6 +35,7 @@ describe('tool allowance by opt-in level', () => {
list_policies: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
// Log tools
get_advisors: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
+ get_logs: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
} as unknown as ToolSet
const filtered = filterToolsByOptInLevel(mockTools, optInLevel as any)
@@ -61,6 +62,7 @@ describe('tool allowance by opt-in level', () => {
expect(tools).not.toContain('list_extensions')
expect(tools).not.toContain('list_edge_functions')
expect(tools).not.toContain('list_branches')
+ expect(tools).not.toContain('get_logs')
expect(tools).not.toContain('execute_sql')
})
@@ -76,6 +78,7 @@ describe('tool allowance by opt-in level', () => {
expect(tools).toContain('list_policies')
expect(tools).toContain('search_docs')
expect(tools).not.toContain('get_advisors')
+ expect(tools).not.toContain('get_logs')
expect(tools).not.toContain('execute_sql')
})
@@ -91,6 +94,7 @@ describe('tool allowance by opt-in level', () => {
expect(tools).toContain('list_policies')
expect(tools).toContain('search_docs')
expect(tools).toContain('get_advisors')
+ expect(tools).toContain('get_logs')
expect(tools).not.toContain('execute_sql')
})
@@ -106,6 +110,7 @@ describe('tool allowance by opt-in level', () => {
expect(tools).toContain('list_policies')
expect(tools).toContain('search_docs')
expect(tools).toContain('get_advisors')
+ expect(tools).toContain('get_logs')
expect(tools).not.toContain('execute_sql')
})
})
@@ -125,6 +130,7 @@ describe('filterToolsByOptInLevel', () => {
search_docs: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
// Log tools
get_advisors: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
+ get_logs: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
// Unknown tool - should be filtered out entirely
some_other_tool: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
} as unknown as ToolSet
@@ -180,6 +186,7 @@ describe('filterToolsByOptInLevel', () => {
'list_branches',
'list_policies',
'get_advisors',
+ 'get_logs',
])
})
@@ -193,13 +200,14 @@ describe('filterToolsByOptInLevel', () => {
'list_branches',
'list_policies',
'get_advisors',
+ 'get_logs',
])
})
it('should stub log tools for schema opt-in level', async () => {
const tools = filterToolsByOptInLevel(mockTools, 'schema')
- await expectStubsFor(tools, ['get_advisors'])
+ await expectStubsFor(tools, ['get_advisors', 'get_logs'])
})
// No execute_sql tool, so nothing additional to stub for schema_and_log opt-in level
diff --git a/apps/studio/lib/ai/tool-filter.ts b/apps/studio/lib/ai/tool-filter.ts
index 2a32b8638b3f3..33d5f07c7fa0d 100644
--- a/apps/studio/lib/ai/tool-filter.ts
+++ b/apps/studio/lib/ai/tool-filter.ts
@@ -25,6 +25,7 @@ export const toolSetValidationSchema = z.record(
'list_branches',
'search_docs',
'get_advisors',
+ 'get_logs',
// Local tools
'display_query',
@@ -105,6 +106,7 @@ export const TOOL_CATEGORY_MAP: Record = {
// Log tools - MCP and local
get_advisors: TOOL_CATEGORIES.LOG,
+ get_logs: TOOL_CATEGORIES.LOG,
}
/**
diff --git a/apps/studio/lib/ai/tools/fallback-tools.ts b/apps/studio/lib/ai/tools/fallback-tools.ts
index 4b6783d1a6b9b..cb59c733c91ba 100644
--- a/apps/studio/lib/ai/tools/fallback-tools.ts
+++ b/apps/studio/lib/ai/tools/fallback-tools.ts
@@ -370,8 +370,10 @@ export const getFallbackTools = ({
)
: []
+ const dataArray = Array.isArray(data) ? data : []
+
// Filter functions by requested schemas
- const filteredFunctions = data.filter((func) => schemas.includes(func.schema))
+ const filteredFunctions = dataArray.filter((func) => schemas.includes(func.schema))
const formattedFunctions = filteredFunctions
.map(
diff --git a/apps/studio/lib/helpers.ts b/apps/studio/lib/helpers.ts
index 2fab08b83d579..505b4712eeac2 100644
--- a/apps/studio/lib/helpers.ts
+++ b/apps/studio/lib/helpers.ts
@@ -3,7 +3,7 @@ export { default as uuidv4 } from './uuid'
import { UIEvent } from 'react'
import type { TablesData } from '../data/tables/tables-query'
-export const isAtBottom = ({ currentTarget }: UIEvent): boolean => {
+export const isAtBottom = ({ currentTarget }: UIEvent): boolean => {
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight
}
diff --git a/apps/studio/pages/org/[slug]/index.tsx b/apps/studio/pages/org/[slug]/index.tsx
index 4ea440053677c..77159c82864ab 100644
--- a/apps/studio/pages/org/[slug]/index.tsx
+++ b/apps/studio/pages/org/[slug]/index.tsx
@@ -1,5 +1,3 @@
-import { useState } from 'react'
-
import { useIsMFAEnabled } from 'common'
import { ProjectList } from 'components/interfaces/Home/ProjectList/ProjectList'
import { HomePageActions } from 'components/interfaces/HomePageActions'
@@ -10,7 +8,6 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { InlineLink } from 'components/ui/InlineLink'
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { PROJECT_STATUS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Admonition } from 'ui-patterns'
@@ -19,18 +16,11 @@ const ProjectsPage: NextPageWithLayout = () => {
const isUserMFAEnabled = useIsMFAEnabled()
const disableAccessMfa = org?.organization_requires_mfa && !isUserMFAEnabled
- const [search, setSearch] = useState('')
- const [filterStatus, setFilterStatus] = useState([
- PROJECT_STATUS.ACTIVE_HEALTHY,
- PROJECT_STATUS.INACTIVE,
- ])
- const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid')
-
useAutoProjectsPrefetch()
return (
-
-
+
+
{disableAccessMfa ? (
@@ -40,23 +30,11 @@ const ProjectsPage: NextPageWithLayout = () => {
) : (
-
-
-
-
setFilterStatus(['ACTIVE_HEALTHY', 'INACTIVE'])}
- viewMode={viewMode}
- />
+ // [Joshen] Very odd, but the h-px here is required for ProjectList to have a max
+ // height based on the remaining space that it can grow to
+
)}
diff --git a/apps/studio/pages/project/_/[[...routeSlug]].tsx b/apps/studio/pages/project/_/[[...routeSlug]].tsx
index 9da13856dd818..b4c1e96940a32 100644
--- a/apps/studio/pages/project/_/[[...routeSlug]].tsx
+++ b/apps/studio/pages/project/_/[[...routeSlug]].tsx
@@ -1,18 +1,28 @@
-import { partition } from 'lodash'
-import { AlertTriangleIcon, Boxes } from 'lucide-react'
+import { AlertTriangleIcon } from 'lucide-react'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
-import { Fragment, useMemo, useState } from 'react'
+import { useEffect, useState } from 'react'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common'
import { ProjectList } from 'components/interfaces/Home/ProjectList/ProjectList'
import { HomePageActions } from 'components/interfaces/HomePageActions'
+import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
+import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
-import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
-import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge } from 'ui'
+import { BASE_PATH } from 'lib/constants'
+import {
+ Alert_Shadcn_,
+ AlertDescription_Shadcn_,
+ AlertTitle_Shadcn_,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
const Header = () => {
@@ -32,6 +42,26 @@ const Header = () => {
)
}
+const OrganizationLoadingState = () => {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+const OrganizationErrorState = () => {
+ return (
+
+
+ Failed to load your Supabase organizations
+ Try refreshing the page
+
+ )
+}
+
// [Joshen] I'd say we don't do route validation here, this page will act more
// like a proxy to the project specific pages, and we let those pages handle
// any route validation logic instead
@@ -40,24 +70,22 @@ const GenericProjectPage: NextPage = () => {
const router = useRouter()
const { routeSlug, ...queryParams } = router.query
- const [search, setSearch] = useState('')
- const [filterStatus, setFilterStatus] = useState([
- PROJECT_STATUS.ACTIVE_HEALTHY,
- PROJECT_STATUS.INACTIVE,
- ])
-
const [lastVisitedOrgSlug] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
)
+ const [selectedSlug, setSlug] = useState(lastVisitedOrgSlug)
+
const {
data: organizations = [],
+ isSuccess: isSuccessOrganizations,
isLoading: isLoadingOrganizations,
isError: isErrorOrganizations,
} = useOrganizationsQuery({
enabled: IS_PLATFORM,
})
+ const selectedOrganization = organizations.find((x) => x.slug === selectedSlug)
const query = Object.keys(queryParams).length
? `?${new URLSearchParams(queryParams as Record)}`
@@ -76,89 +104,55 @@ const GenericProjectPage: NextPage = () => {
}
}
- const [[lastVisitedOrganization], otherOrganizations] = useMemo(
- () => partition(organizations, (org) => org.slug === lastVisitedOrgSlug),
- [lastVisitedOrgSlug, organizations]
- )
+ useEffect(() => {
+ if (!!lastVisitedOrgSlug) {
+ setSlug(lastVisitedOrgSlug)
+ } else if (isSuccessOrganizations) {
+ setSlug(organizations[0].slug)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [lastVisitedOrgSlug, isSuccessOrganizations])
return (
- <>
+
-
-
Select a project to continue
-
-
-
+
+
+
+
+
+
+
+
+
+ {organizations.map((org) => (
+
+ {org.name}
+
+ ))}
+
+
+
+
+
+
{isLoadingOrganizations ? (
) : isErrorOrganizations ? (
- ) : (
- <>
- {!!lastVisitedOrganization && (
- <>
-
-
- {lastVisitedOrganization.name}
- Recently visited
-
-
- >
- )}
- {otherOrganizations.map((organization) => (
-
-
-
- {organization.name}
-
-
-
- ))}
- >
- )}
-
-
-
- >
- )
-}
-
-function OrganizationLoadingState() {
- return (
- <>
-
-
-
- >
- )
-}
-
-function OrganizationErrorState() {
- return (
-
-
- Failed to load your Supabase organizations
- Try refreshing the page
-
+ ) : !!selectedOrganization ? (
+
+ ) : null}
+
+
+
+
)
}
diff --git a/apps/studio/styles/main.scss b/apps/studio/styles/main.scss
index 90c0bdfc2f959..f7df5cab778ea 100644
--- a/apps/studio/styles/main.scss
+++ b/apps/studio/styles/main.scss
@@ -1,6 +1,4 @@
-@tailwind base;
@tailwind components;
-@tailwind utilities;
@import './../../../packages/ui/build/css/source/global.css';
@import './../../../packages/ui/build/css/themes/dark.css';
@@ -46,21 +44,6 @@
--sidebar-ring: 217.2 91.2% 59.8%;
}
-@layer utilities {
- .btn-primary {
- @apply inline-block rounded border border-green-500 bg-green-500 py-1 px-3 text-sm;
- color: #fff !important;
- font-weight: 600;
- line-height: 20px;
- text-align: center;
- }
-
- .btn-primary-hover {
- @apply bg-green-600;
- cursor: pointer;
- }
-}
-
html,
body,
#__next,
@@ -161,14 +144,6 @@ input.is-invalid {
@apply placeholder:text-red-600;
}
-input[type='submit'] {
- @apply btn-primary;
-}
-
-input[type='submit']:hover {
- @apply btn-primary-hover;
-}
-
input::placeholder {
@apply text-foreground-lighter;
}
diff --git a/apps/studio/styles/typography.scss b/apps/studio/styles/typography.scss
index b284618ace54e..55a5ca29acef7 100644
--- a/apps/studio/styles/typography.scss
+++ b/apps/studio/styles/typography.scss
@@ -1,5 +1,4 @@
@tailwind base;
-@tailwind components;
@tailwind utilities;
@layer base {
diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json
index 00a3f742a9793..acf53e60d3baa 100644
--- a/apps/ui-library/package.json
+++ b/apps/ui-library/package.json
@@ -6,7 +6,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3004",
- "build": "pnpm run content:build && pnpm run build:registry && pnpm run build:llms && next build",
+ "build": "pnpm run content:build && pnpm run build:registry && pnpm run build:llms && next build --turbopack",
"build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --cache --write registry.json && rimraf -G public/r && shadcn build && tsx scripts/clean-registry.ts",
"build:llms": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-llms-txt.ts",
"start": "next start",
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 7204239e4ad15..525b4f2561311 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -5142,12 +5142,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
@@ -5170,12 +5165,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
@@ -8015,12 +8005,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
@@ -8075,12 +8060,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
@@ -9429,12 +9409,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
@@ -9457,12 +9432,7 @@ export interface components {
* @description Maximum fill time in milliseconds
* @example 200
*/
- max_fill_ms: number
- /**
- * @description Maximum batch size
- * @example 5000
- */
- max_size: number
+ max_fill_ms?: number
}
/**
* @description Publication name
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index db05c8dd204b5..3572600e27315 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -6,6 +6,7 @@ export const LOCAL_STORAGE_KEYS = {
`supabase-ai-assistant-state-${projectRef}`,
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
+ PROJECTS_VIEW: 'projects-view',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d7c926d80b79..6e29c270a35ea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,8 +7,8 @@ settings:
catalogs:
default:
'@supabase/auth-js':
- specifier: 2.71.1-rc.1
- version: 2.71.1-rc.1
+ specifier: 2.72.0-rc.11
+ version: 2.72.0-rc.11
'@supabase/realtime-js':
specifier: ^2.11.3
version: 2.11.3
@@ -41,7 +41,7 @@ catalogs:
version: 6.3.5
overrides:
- '@supabase/supabase-js>@supabase/auth-js': 2.71.1-rc.1
+ '@supabase/supabase-js>@supabase/auth-js': 2.72.0-rc.11
'@tanstack/directive-functions-plugin>vite': ^6.2.7
'@tanstack/react-start-plugin>vite': ^6.2.7
esbuild: ^0.25.2
@@ -794,7 +794,7 @@ importers:
version: 7.5.0
'@supabase/auth-js':
specifier: 'catalog:'
- version: 2.71.1-rc.1
+ version: 2.72.0-rc.11
'@supabase/mcp-server-supabase':
specifier: ^0.4.4
version: 0.4.4(supports-color@8.1.1)
@@ -1857,7 +1857,7 @@ importers:
dependencies:
'@supabase/auth-js':
specifier: 'catalog:'
- version: 2.71.1-rc.1
+ version: 2.72.0-rc.11
'@supabase/supabase-js':
specifier: 'catalog:'
version: 2.49.3
@@ -8041,8 +8041,8 @@ packages:
resolution: {integrity: sha512-Cq3KKe+G1o7PSBMbmrgpT2JgBeyH2THHr3RdIX2MqF7AnBuspIMgtZ3ktcCgP7kZsTMvnmWymr7zZCT1zeWbMw==}
engines: {node: '>=12.16'}
- '@supabase/auth-js@2.71.1-rc.1':
- resolution: {integrity: sha512-tgSFLO19e8Ig7eQON6c02DoyfV3OLEG709UqinbFExvRKB4z1yHBwwaMbkZtoKcu395AFVtekcHtoM9TEmixoQ==}
+ '@supabase/auth-js@2.72.0-rc.11':
+ resolution: {integrity: sha512-7C0xC6ZXbIB8GAoO6saWNMRhKox9OeYvMmpfPJyxFiECg9z78SMhMotiyI3U1Ws47iGiaDonEUnuxDuHAG04nQ==}
'@supabase/functions-js@2.4.4':
resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==}
@@ -26091,7 +26091,7 @@ snapshots:
'@stripe/stripe-js@7.5.0': {}
- '@supabase/auth-js@2.71.1-rc.1':
+ '@supabase/auth-js@2.72.0-rc.11':
dependencies:
'@supabase/node-fetch': 2.6.15
@@ -26248,7 +26248,7 @@ snapshots:
'@supabase/supabase-js@2.49.3':
dependencies:
- '@supabase/auth-js': 2.71.1-rc.1
+ '@supabase/auth-js': 2.72.0-rc.11
'@supabase/functions-js': 2.4.4
'@supabase/node-fetch': 2.6.15
'@supabase/postgrest-js': 1.19.2
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 086ef363e3135..cf0cf483d42e5 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -5,7 +5,7 @@ packages:
catalog:
'@types/node': ^22.0.0
- '@supabase/auth-js': 2.71.1-rc.1
+ '@supabase/auth-js': 2.72.0-rc.11
'@supabase/supabase-js': ^2.47.14
'@supabase/realtime-js': ^2.11.3
next: ^15.5.2