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 && ( +
      + + {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

      -
      -
      - + + + +
      + + +
      +

      Organization:

      + +
      +
      + + {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',