Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { ArrowDownNarrowWide, ArrowDownWideNarrow } from 'lucide-react'

import {
Button,
DropdownMenu,
Expand All @@ -24,6 +23,7 @@ interface SortDropdownProps {
improvedSearchEnabled: boolean
}

/** [Joshen] To refactor to use the SortDropdown component in components/ui */
export const SortDropdown = ({
specificFilterColumn,
sortColumn,
Expand Down
77 changes: 66 additions & 11 deletions apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useMemo } from 'react'

import { keepPreviousData } from '@tanstack/react-query'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
Expand All @@ -13,12 +11,30 @@ 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 { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'
import { parseAsArrayOf, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useMemo } from 'react'
import type { Organization } from 'types'
import { Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import {
Card,
cn,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableHeadSort,
TableRow,
} from 'ui'

import { LoadingCardView, LoadingTableView, NoProjectsState } from './EmptyStates'
import { LoadMoreRows } from './LoadMoreRow'
import { ProjectCard } from './ProjectCard'
import {
getNextProjectListSortForColumn,
getProjectListAriaSort,
PROJECT_LIST_SORT_VALUES,
toTableHeadSortValue,
} from './ProjectListSort.utils'
import { ProjectTableRow } from './ProjectTableRow'

export interface ProjectListProps {
Expand All @@ -37,6 +53,10 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
'status',
parseAsArrayOf(parseAsString, ',').withDefault([])
)
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral(PROJECT_LIST_SORT_VALUES).withDefault('name_asc')
)
const [viewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')

const organization = organization_ ?? selectedOrganization
Expand All @@ -54,6 +74,7 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
} = useOrgProjectsInfiniteQuery(
{
slug,
sort,
search: search.length === 0 ? search : debouncedSearch,
statuses: filterStatus,
},
Expand Down Expand Up @@ -81,14 +102,14 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
debouncedSearch.length === 0 &&
filterStatus.length === 0 &&
(!orgProjects || orgProjects.length === 0)
const sortedProjects = [...(orgProjects || [])].sort((a, b) => a.name.localeCompare(b.name))

const noResultsFromSearch =
debouncedSearch.length > 0 && isSuccessProjects && orgProjects.length === 0
const noResultsFromStatusFilter =
filterStatus.length > 0 && isSuccessProjects && orgProjects.length === 0

const noResults = noResultsFromStatusFilter || noResultsFromSearch
const tableHeadSortValue = toTableHeadSortValue(sort)

const githubConnections = connections?.map((connection) => ({
id: String(connection.id),
Expand Down Expand Up @@ -143,18 +164,52 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
{/* [Joshen] Ideally we can figure out sticky table headers here */}
<TableHeader>
<TableRow>
<TableHead className={cn(noResults && 'text-foreground-muted')}>Project</TableHead>
<TableHead
className={cn(noResults && 'text-foreground-muted')}
aria-sort={getProjectListAriaSort(sort)}
>
<TableHeadSort
column="name"
currentSort={tableHeadSortValue}
onSortChange={() => {
const sortValue = sort.includes('created')
? 'name_asc'
: getNextProjectListSortForColumn(sort)
setSort(sortValue)
}}
className={cn(noResults && 'text-foreground-muted')}
>
Project
</TableHeadSort>
</TableHead>
<TableHead className={cn(noResults && 'text-foreground-muted')}>Status</TableHead>
<TableHead className={cn(noResults && 'text-foreground-muted')}>Compute</TableHead>
<TableHead className={cn(noResults && 'text-foreground-muted')}>Region</TableHead>
<TableHead className={cn(noResults && 'text-foreground-muted')}>Created</TableHead>
<TableHead
className={cn(noResults && 'text-foreground-muted')}
aria-sort={getProjectListAriaSort(sort)}
>
<TableHeadSort
column="created"
currentSort={tableHeadSortValue}
onSortChange={() => {
const sortValue = sort.includes('name')
? 'created_asc'
: getNextProjectListSortForColumn(sort)
setSort(sortValue)
}}
className={cn(noResults && 'text-foreground-muted')}
>
Created
</TableHeadSort>
</TableHead>
<TableHead className={cn(noResults && 'text-foreground-muted')} />
</TableRow>
</TableHeader>
<TableBody>
{noResultsFromStatusFilter ? (
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={5}>
<TableCell colSpan={6}>
<NoSearchResults
withinTableCell
label={
Expand All @@ -169,13 +224,13 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
</TableRow>
) : noResultsFromSearch ? (
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={5}>
<TableCell colSpan={6}>
<NoSearchResults searchString={search} withinTableCell />
</TableCell>
</TableRow>
) : (
<>
{sortedProjects?.map((project) => (
{orgProjects?.map((project) => (
<ProjectTableRow
key={project.ref}
project={project}
Expand Down Expand Up @@ -230,7 +285,7 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
)}
>
{sortedProjects?.map((project) => (
{orgProjects?.map((project) => (
<ProjectCard
key={project.ref}
slug={slug}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const PROJECT_LIST_SORT_VALUES = [
'name_asc',
'name_desc',
'created_desc',
'created_asc',
] as const

export type ProjectListSort = (typeof PROJECT_LIST_SORT_VALUES)[number]

export const toTableHeadSortValue = (sort: ProjectListSort) => sort.replace('_', ':')

export const getNextProjectListSortForColumn = (currentSort: ProjectListSort): ProjectListSort => {
if (currentSort.includes('asc')) return currentSort.replace('asc', 'desc') as ProjectListSort
if (currentSort.includes('desc')) return currentSort.replace('desc', 'asc') as ProjectListSort
return 'name_asc'
}

export const getProjectListAriaSort = (
currentSort: ProjectListSort
): 'ascending' | 'descending' | 'none' => {
if (currentSort.includes('asc')) return 'ascending'
if (currentSort.includes('desc')) return 'descending'
return 'none'
}
48 changes: 34 additions & 14 deletions apps/studio/components/interfaces/HomePageActions.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { keepPreviousData } from '@tanstack/react-query'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
PROJECT_LIST_SORT_VALUES,
type ProjectListSort,
} from 'components/interfaces/Home/ProjectList/ProjectListSort.utils'
import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { PROJECT_STATUS } from 'lib/constants'
import { Grid, List, Loader2, Plus, Search, X } from 'lucide-react'
import Link from 'next/link'
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'
import { parseAsArrayOf, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { Button, ToggleGroup, ToggleGroupItem } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'

import { FilterPopover } from '../ui/FilterPopover'
import { SortDropdown } from '../ui/SortDropdown'

interface HomePageActionsProps {
slug?: string
hideNewProject?: boolean
showViewToggle?: boolean
}

export const HomePageActions = ({
slug: _slug,
hideNewProject = false,
showViewToggle = false,
}: HomePageActionsProps) => {
export const HomePageActions = ({ slug: _slug, hideNewProject = false }: HomePageActionsProps) => {
const { slug: urlSlug } = useParams()
const projectCreationEnabled = useIsFeatureEnabled('projects:create')

Expand All @@ -35,27 +35,38 @@ export const HomePageActions = ({
'status',
parseAsArrayOf(parseAsString, ',').withDefault([])
)
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral(PROJECT_LIST_SORT_VALUES).withDefault('name_asc')
)
const [viewMode, setViewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')

const [filterStatusStorage, setFilterStatusStorage, { isSuccess }] = useLocalStorageQuery<
string[]
>(LOCAL_STORAGE_KEYS.PROJECTS_FILTER, [])
const [filterStatusStorage, setFilterStatusStorage, { isSuccess: isSuccessFilterStatusStorage }] =
useLocalStorageQuery<string[]>(LOCAL_STORAGE_KEYS.PROJECTS_FILTER, [])

const [sortStorage, setSortStorage, { isSuccess: isSuccessSortStorage }] =
useLocalStorageQuery<ProjectListSort>(LOCAL_STORAGE_KEYS.PROJECTS_SORT, 'name_asc')

const { isFetching: isFetchingProjects } = useOrgProjectsInfiniteQuery(
{
slug,
sort,
search: search.length === 0 ? search : debouncedSearch,
statuses: filterStatus,
},
{ placeholderData: keepPreviousData }
)

useEffect(() => {
if (isSuccess && !!urlSlug) setFilterStatus(filterStatusStorage)
}, [filterStatusStorage, isSuccess, urlSlug, setFilterStatus])
if (isSuccessFilterStatusStorage && !!slug) setFilterStatus(filterStatusStorage)
}, [filterStatusStorage, isSuccessFilterStatusStorage, setFilterStatus, slug])

useEffect(() => {
if (isSuccessSortStorage && slug) setSort(sortStorage)
}, [sortStorage, isSuccessSortStorage, setSort, slug])

return (
<div className="flex items-center justify-between">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<Input
placeholder="Search for a project"
Expand Down Expand Up @@ -91,11 +102,20 @@ export const HomePageActions = ({
onSaveFilters={(options) => setFilterStatusStorage(options)}
/>

<SortDropdown
options={[
{ label: 'name', value: 'name' },
{ label: 'creation date', value: 'created' },
]}
value={sort}
setValue={(val) => setSortStorage(val as ProjectListSort)}
/>

{isFetchingProjects && <Loader2 className="animate-spin" size={14} />}
</div>

<div className="flex items-center gap-2">
{showViewToggle && viewMode && setViewMode && (
{viewMode && setViewMode && (
<ToggleGroup
type="single"
size="sm"
Expand Down
61 changes: 61 additions & 0 deletions apps/studio/components/ui/SortDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ArrowDownNarrowWide, ArrowDownWideNarrow } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ui'

type SortOption = {
label: string
value: string
}

interface SortDropdownProps {
options: SortOption[]
value: string
setValue: (value: string) => void
}

export const SortDropdown = ({ options, value, setValue }: SortDropdownProps) => {
const [sortColumn, sortOrder] = value.split('_')
const columnLabel = options.find((x) => x.value === sortColumn)?.label

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={sortOrder === 'desc' ? <ArrowDownWideNarrow /> : <ArrowDownNarrowWide />}
>
Sorted by {columnLabel ?? sortColumn}
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent className="w-44" align="start">
<DropdownMenuRadioGroup value={value} onValueChange={setValue}>
{options.map((option) => {
return (
<DropdownMenuSub key={option.value}>
<DropdownMenuSubTrigger>Sort by {option.label}</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value={`${option.value}_asc`}>
Ascending
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value={`${option.value}_desc`}>
Descending
</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
)
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
3 changes: 2 additions & 1 deletion apps/studio/data/auth/auth-config-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { get, handleError } from 'data/fetchers'
import { IS_PLATFORM } from 'lib/constants'
import { useCallback } from 'react'
import type { ResponseError, UseCustomQueryOptions } from 'types'

import { authKeys } from './keys'

export type AuthConfigVariables = {
Expand Down Expand Up @@ -40,7 +41,7 @@ export const useAuthConfigQuery = <TData = ProjectAuthConfigData>(
useQuery<ProjectAuthConfigData, ProjectAuthConfigError, TData>({
queryKey: authKeys.authConfig(projectRef),
queryFn: ({ signal }) => getProjectAuthConfig({ projectRef }, signal),
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined',
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined' && projectRef !== '_',
...options,
})

Expand Down
4 changes: 2 additions & 2 deletions apps/studio/data/config/project-storage-config-query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query'

import { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import { IS_PLATFORM } from 'lib/constants'
import type { ResponseError, UseCustomQueryOptions } from 'types'

import { configKeys } from './keys'

export type ProjectStorageConfigVariables = {
Expand Down Expand Up @@ -48,7 +48,7 @@ export const useProjectStorageConfigQuery = <TData = ProjectStorageConfigData>(
useQuery<ProjectStorageConfigData, ProjectStorageConfigError, TData>({
queryKey: configKeys.storage(projectRef),
queryFn: ({ signal }) => getProjectStorageConfig({ projectRef }, signal),
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined',
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined' && projectRef !== '_',
...options,
})

Expand Down
Loading
Loading