mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Support for projects pagination part 1 (#38298)
* init * Infinite scrolling pagination for projects in table view * Infinite scrolling pagination for projects in cards view * Shift searching projects to URL state + to server side * Shift status filter to URL state * opt to use local storage query for view moe * update wildcard project route * add created at value, and shift filter status to server side * Nit * final nit * Address comments * Fix * Prettier * Raise page limit to 96
This commit is contained in:
1 parent
b78f56d5c1
commit
dabfaa8972
16 files changed
+397
-275
No files matched your search
@@ -52,6 +52,26 @@ export const NoFilterResults = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingTableRow = () => (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-32"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-16"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-24"></Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
|
||||
export const LoadingTableView = () => {
|
||||
return (
|
||||
<Card>
|
||||
@@ -67,23 +87,7 @@ export const LoadingTableView = () => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-32"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-16"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-24"></Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<LoadingTableRow key={i} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -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 (
|
||||
<li className="list-none">
|
||||
<li className="list-none h-min">
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
className="h-44 !px-0 group pt-5 pb-0"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ProjectInfo } from 'data/projects/projects-query'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
|
||||
export const inferProjectStatus = (project: ProjectInfo) => {
|
||||
export const inferProjectStatus = (projectStatus: string) => {
|
||||
let status = undefined
|
||||
switch (project.status) {
|
||||
switch (projectStatus) {
|
||||
case PROJECT_STATUS.ACTIVE_HEALTHY:
|
||||
status = 'isHealthy'
|
||||
break
|
||||
|
||||
@@ -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<HTMLDivElement | HTMLUListElement>) => {
|
||||
if (isLoadingProjects || isFetchingNextPage || !isAtBottom(event)) return
|
||||
fetchNextPage()
|
||||
}
|
||||
|
||||
if (isErrorPermissions) {
|
||||
return (
|
||||
<AlertError
|
||||
@@ -139,9 +158,10 @@ export const ProjectList = ({
|
||||
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="flex-1 min-h-0 overflow-y-auto mb-8" onScroll={handleScroll}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{/* [Joshen] Ideally we can figure out sticky table headers here */}
|
||||
<TableHeader className="[&>tr>th]:sticky [&>tr>th]:top-0">
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
@@ -149,6 +169,11 @@ export const ProjectList = ({
|
||||
<TableHead>Region</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
</TableRow>
|
||||
<TableRow className="!border-b-0">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<LoadingLine loading={isFetching} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{noResultsFromStatusFilter ? (
|
||||
@@ -156,7 +181,7 @@ export const ProjectList = ({
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<NoFilterResults
|
||||
filterStatus={filterStatus}
|
||||
resetFilterStatus={resetFilterStatus}
|
||||
resetFilterStatus={() => setFilterStatus([])}
|
||||
className="border-0"
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -168,22 +193,26 @@ export const ProjectList = ({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredProjectsByStatus?.map((project) => (
|
||||
<ProjectTableRow
|
||||
key={project.ref}
|
||||
project={project}
|
||||
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
|
||||
resourceWarnings={resourceWarnings?.find(
|
||||
(resourceWarning) => 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) => (
|
||||
<ProjectTableRow
|
||||
key={project.ref}
|
||||
project={project}
|
||||
organization={organization}
|
||||
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
|
||||
resourceWarnings={resourceWarnings?.find(
|
||||
(resourceWarning) => 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 && <LoadingTableRow />}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -194,14 +223,24 @@ export const ProjectList = ({
|
||||
return (
|
||||
<>
|
||||
{noResultsFromStatusFilter ? (
|
||||
<NoFilterResults filterStatus={filterStatus} resetFilterStatus={resetFilterStatus} />
|
||||
<NoFilterResults
|
||||
filterStatus={filterStatus}
|
||||
resetFilterStatus={() => setFilterStatus([])}
|
||||
/>
|
||||
) : noResultsFromSearch ? (
|
||||
<NoSearchResults searchString={search} />
|
||||
) : (
|
||||
<ul className="w-full mx-auto grid grid-cols-1 gap-2 md:gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredProjectsByStatus?.map((project) => (
|
||||
<ul
|
||||
onScroll={handleScroll}
|
||||
className={cn(
|
||||
'min-h-0 overflow-auto w-full mx-auto ',
|
||||
'grid grid-cols-1 gap-2 md:gap-4 overflow-y-scroll',
|
||||
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 pb-6'
|
||||
)}
|
||||
>
|
||||
{sortedProjects?.map((project) => (
|
||||
<ProjectCard
|
||||
key={makeRandomString(5)}
|
||||
key={project.ref}
|
||||
project={project}
|
||||
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
|
||||
resourceWarnings={resourceWarnings?.find(
|
||||
@@ -215,6 +254,7 @@ export const ProjectList = ({
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{hasNextPage && [...Array(2)].map((_, i) => <ShimmeringCard key={i} />)}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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 (
|
||||
<TableRow
|
||||
className="cursor-pointer hover:bg-surface-200"
|
||||
@@ -54,7 +58,7 @@ export const ProjectTableRow = ({
|
||||
<p className="font-medium">{name}</p>
|
||||
<p className="text-xs text-foreground-lighter">ID: {projectRef}</p>
|
||||
</div>
|
||||
{(isGithubIntegrated || isVercelIntegrated || isBranchingEnabled) && (
|
||||
{(isGithubIntegrated || isVercelIntegrated) && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isVercelIntegrated && (
|
||||
<div className="w-fit p-1 border rounded-md flex items-center text-black dark:text-white">
|
||||
@@ -91,7 +95,14 @@ export const ProjectTableRow = ({
|
||||
<TableCell>
|
||||
<div className="w-fit">
|
||||
{project.status !== 'INACTIVE' ? (
|
||||
<ComputeBadgeWrapper project={project} />
|
||||
<ComputeBadgeWrapper
|
||||
project={{
|
||||
ref: project.ref,
|
||||
organization_slug: organization?.slug,
|
||||
cloud_provider: infraInformation?.cloud_provider,
|
||||
infra_compute_size: infraInformation?.infra_compute_size,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-foreground-light">-</span>
|
||||
)}
|
||||
@@ -99,7 +110,7 @@ export const ProjectTableRow = ({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="lowercase text-sm text-foreground-light">
|
||||
{project.cloud_provider} | {project.region || 'N/A'}
|
||||
{infraInformation?.cloud_provider} | {project.region || 'N/A'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -47,15 +59,26 @@ export const HomePageActions = ({
|
||||
placeholder="Search for a project"
|
||||
icon={<Search size={12} />}
|
||||
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 && (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<X />}
|
||||
onClick={() => setSearch('')}
|
||||
className="p-0 h-5 w-5"
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
|
||||
<Popover_Shadcn_>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type={filterStatus.length !== 2 ? 'secondary' : 'dashed'}
|
||||
type={filterStatus.length === 0 ? 'dashed' : 'secondary'}
|
||||
className="h-[26px] w-[26px]"
|
||||
icon={<Filter />}
|
||||
/>
|
||||
@@ -73,10 +96,12 @@ export const HomePageActions = ({
|
||||
<Checkbox_Shadcn_
|
||||
id={key}
|
||||
name={key}
|
||||
checked={filterStatus.includes(key)}
|
||||
checked={filterStatus.length === 0 || filterStatus.includes(key)}
|
||||
onCheckedChange={() => {
|
||||
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 = ({
|
||||
</div>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
|
||||
{isFetchingProjects && <Loader2 className="animate-spin" size={14} />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -75,7 +75,7 @@ export const PageLayout = ({
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-full flex flex-col items-stretch">
|
||||
<div className={cn('w-full min-h-full flex flex-col items-stretch', className)}>
|
||||
<ScaffoldContainer
|
||||
size={size}
|
||||
className={cn(
|
||||
@@ -83,8 +83,7 @@ export const PageLayout = ({
|
||||
size === 'full' &&
|
||||
(isCompact ? 'max-w-none !px-6 border-b pt-4' : 'max-w-none pt-6 !px-10 border-b'),
|
||||
size !== 'full' && (isCompact ? 'pt-4' : 'pt-12'),
|
||||
navigationItems.length === 0 && size === 'full' && (isCompact ? 'pb-4' : 'pb-8'),
|
||||
className
|
||||
navigationItems.length === 0 && size === 'full' && (isCompact ? 'pb-4' : 'pb-8')
|
||||
)}
|
||||
>
|
||||
{/* Header section */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string>
|
||||
) {
|
||||
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<ReturnType<typeof getOrganizationProjects>>
|
||||
export type OrgProjectsInfiniteError = ResponseError
|
||||
|
||||
export const useOrgProjectsInfiniteQuery = <TData = OrgProjectsInfiniteData>(
|
||||
{
|
||||
slug,
|
||||
limit = DEFAULT_LIMIT,
|
||||
sort = 'name_asc',
|
||||
search,
|
||||
statuses = [],
|
||||
}: GetOrgProjectsInfiniteVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseInfiniteQueryOptions<OrgProjectsInfiniteData, OrgProjectsInfiniteError, TData> = {}
|
||||
) => {
|
||||
const { profile } = useProfile()
|
||||
return useInfiniteQuery<OrgProjectsInfiniteData, OrgProjectsInfiniteError, TData>(
|
||||
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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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<HTMLDivElement>): boolean => {
|
||||
export const isAtBottom = ({ currentTarget }: UIEvent<HTMLElement>): boolean => {
|
||||
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string[]>([
|
||||
PROJECT_STATUS.ACTIVE_HEALTHY,
|
||||
PROJECT_STATUS.INACTIVE,
|
||||
])
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid')
|
||||
|
||||
useAutoProjectsPrefetch()
|
||||
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection isFullWidth>
|
||||
<ScaffoldContainer className="flex-grow flex">
|
||||
<ScaffoldSection isFullWidth className="flex-grow pb-0">
|
||||
{disableAccessMfa ? (
|
||||
<Admonition type="note" title={`The organization "${org?.name}" has MFA enforced`}>
|
||||
<p className="!m-0">
|
||||
@@ -40,23 +30,11 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
</p>
|
||||
</Admonition>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<HomePageActions
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
filterStatus={filterStatus}
|
||||
setFilterStatus={setFilterStatus}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
showViewToggle={true}
|
||||
/>
|
||||
|
||||
<ProjectList
|
||||
search={search}
|
||||
filterStatus={filterStatus}
|
||||
resetFilterStatus={() => 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
|
||||
<div className="flex flex-col gap-y-4 flex-grow h-px">
|
||||
<HomePageActions showViewToggle={true} />
|
||||
<ProjectList />
|
||||
</div>
|
||||
)}
|
||||
</ScaffoldSection>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
<ShimmeringLoader className="w-1/4" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const OrganizationErrorState = () => {
|
||||
return (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle_Shadcn_>Failed to load your Supabase organizations</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>Try refreshing the page</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
// [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<string[]>([
|
||||
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<string, string>)}`
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="h-screen flex flex-col">
|
||||
<Header />
|
||||
<div className="flex flex-col mx-auto w-full">
|
||||
<h1 className="mt-8 max-w-5xl mx-auto w-full">Select a project to continue</h1>
|
||||
<div
|
||||
className="flex-grow py-6 overflow-y-auto"
|
||||
style={{ maxHeight: 'calc(100vh - 49px - 64px)' }}
|
||||
>
|
||||
<div className="w-full max-w-5xl mx-auto flex flex-col gap-y-8">
|
||||
<HomePageActions
|
||||
hideNewProject
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
filterStatus={filterStatus}
|
||||
setFilterStatus={setFilterStatus}
|
||||
/>
|
||||
<PageLayout className="flex-grow min-h-0" title="Select a project to continue">
|
||||
<ScaffoldContainer className="flex-grow flex flex-col">
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Select_Shadcn_ value={selectedSlug} onValueChange={setSlug}>
|
||||
<SelectTrigger_Shadcn_ size="tiny" className="w-60 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xs text-foreground-light">Organization:</p>
|
||||
<SelectValue_Shadcn_ placeholder="Select an organization" />
|
||||
</div>
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_ className="col-span-8">
|
||||
{organizations.map((org) => (
|
||||
<SelectItem_Shadcn_ key={org.slug} value={org.slug} className="text-xs">
|
||||
{org.name}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
<HomePageActions hideNewProject />
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
<ScaffoldSection isFullWidth className="flex-grow pt-0 flex flex-col gap-y-4 h-px">
|
||||
{isLoadingOrganizations ? (
|
||||
<OrganizationLoadingState />
|
||||
) : isErrorOrganizations ? (
|
||||
<OrganizationErrorState />
|
||||
) : (
|
||||
<>
|
||||
{!!lastVisitedOrganization && (
|
||||
<>
|
||||
<h2 className="flex items-center gap-2">
|
||||
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
{lastVisitedOrganization.name}
|
||||
<Badge variant="default">Recently visited</Badge>
|
||||
</h2>
|
||||
<ProjectList
|
||||
search={search}
|
||||
filterStatus={filterStatus}
|
||||
organization={lastVisitedOrganization}
|
||||
rewriteHref={urlRewriterFactory(routeSlug)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{otherOrganizations.map((organization) => (
|
||||
<Fragment key={organization.id}>
|
||||
<h2 className="flex items-center gap-2">
|
||||
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
{organization.name}
|
||||
</h2>
|
||||
<ProjectList
|
||||
search={search}
|
||||
filterStatus={filterStatus}
|
||||
organization={organization}
|
||||
rewriteHref={urlRewriterFactory(routeSlug)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OrganizationLoadingState() {
|
||||
return (
|
||||
<>
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
<ShimmeringLoader className="w-1/4" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OrganizationErrorState() {
|
||||
return (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle_Shadcn_>Failed to load your Supabase organizations</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>Try refreshing the page</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
) : !!selectedOrganization ? (
|
||||
<ProjectList
|
||||
organization={selectedOrganization}
|
||||
rewriteHref={urlRewriterFactory(routeSlug)}
|
||||
/>
|
||||
) : null}
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
</PageLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user