diff --git a/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx b/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx
index 10d00346346..d0957524562 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 5ae3a2958f2..650cb768cbd 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 4ce7a0a194c..bb204454298 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 noResultsFromSearch =
- search.length > 0 &&
- isSuccessProjects &&
- orgProjects.filter((project) => {
- return (
- project.name.toLowerCase().includes(search.toLowerCase()) ||
- project.ref.includes(search.toLowerCase())
- )
- }).length === 0
- const noResultsFromStatusFilter =
- hasFilterStatusApplied &&
- isSuccessProjects &&
- orgProjects.filter((project) => filterStatus.includes(project.status)).length === 0
-
- const isEmpty = !orgProjects || orgProjects.length === 0
+ const isEmpty =
+ debouncedSearch.length === 0 &&
+ filterStatus.length === 0 &&
+ (!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
+ const noResultsFromSearch =
+ debouncedSearch.length > 0 && isSuccessProjects && orgProjects.length === 0
+ const noResultsFromStatusFilter =
+ 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 812fc067d81..30563af30bb 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 7ca09560866..64058004de5 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 9361bd4dce9..99a094feedc 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 e9144545a7e..033a2da2e4c 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/data/projects/keys.ts b/apps/studio/data/projects/keys.ts
index 726558d8c5c..67e929c5c72 100644
--- a/apps/studio/data/projects/keys.ts
+++ b/apps/studio/data/projects/keys.ts
@@ -1,5 +1,14 @@
export const projectKeys = {
list: () => ['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 00000000000..356cf8df0da
--- /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/lib/helpers.ts b/apps/studio/lib/helpers.ts
index 2fab08b83d5..505b4712eea 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 4ea44005367..77159c82864 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 9da13856dd8..b4c1e96940a 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 90c0bdfc2f9..f7df5cab778 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 b284618ace5..55a5ca29ace 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/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index db05c8dd204..3572600e273 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',