Skip to content

Commit

Permalink
[BUBO-30] Get list of files from GitHub (#33)
Browse files Browse the repository at this point in the history
* get list of files from github

* add error handlers and zerialization

* remove GithubFileSelect component in main page

* remove unused import

* throw ServerError in requireGithubAuth

* use Button component

* move getting account to requireGitHubAuth

* type safety with enable
d option

* separate components for select files and select repo

* disable query if provider is not github

* update GitHubProvider scope

* fix checkbox state and change github sign in button label

* remove extra space

* filter file tree

* useSession in useQuery

* export type GithubRepository
  • Loading branch information
icedevera committed May 7, 2024
1 parent 5e1d4b4 commit b054e4e
Show file tree
Hide file tree
Showing 11 changed files with 894 additions and 2 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
"@lukemorales/query-key-factory": "^1.3.4",
"@meshsdk/core": "github:stargazerlabs/mesh-core",
"@meshsdk/react": "1.1.10-beta.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
Expand All @@ -44,6 +47,7 @@
"next": "14.2.3",
"next-auth": "^4.24.7",
"nodemailer": "^6.9.13",
"octokit": "^3.2.0",
"postgres": "^3.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
113 changes: 113 additions & 0 deletions src/components/sections/GitHub/GithubFileSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client'

import {signIn, useSession} from 'next-auth/react'
import {CheckedState} from '@radix-ui/react-checkbox'

import {Button} from '@/components/ui/Button'
import {useGetRepoFiles} from '@/lib/queries/github/getGitHub'
import {
GetRepoFilesParams,
GetRepoFilesResponse,
} from '@/server/actions/github/getGithub'
import {Checkbox} from '@/components/ui/Checkbox'
import {ScrollArea} from '@/components/ui/ScrollArea'

type GithubFileSelectProps = {
selectedRepo: GetRepoFilesParams | null
selectedFiles: string[]
onSelectFiles: (files: string[]) => void
}

const GithubFileSelect = ({
selectedRepo,
selectedFiles,
onSelectFiles,
}: GithubFileSelectProps) => {
const session = useSession()

const {data: repoFilesData, isLoading: repoFilesIsLoading} = useGetRepoFiles(
selectedRepo ?? undefined,
)

if (session.data?.user.provider !== 'github') {
return (
<Button
onClick={() => signIn('github')}
className="bg-gray-800 text-white">
Sign in with Github to select files
</Button>
)
}

return (
<div className="min-w-96 rounded-md border-2 border-gray-900 p-2">
{selectedRepo === null ? (
<span>No repo selected</span>
) : (
<ScrollArea className="h-96 w-full" thumbClassname="bg-gray-400">
<div className="flex flex-col items-start gap-2">
<div className="flex flex-col">
<span className="pb-2 font-bold">Select files to include</span>
<FileTree
fileTree={repoFilesData}
isLoading={repoFilesIsLoading}
selectedFilePaths={selectedFiles}
onSelectFilePaths={onSelectFiles}
/>
</div>
</div>
</ScrollArea>
)}
</div>
)
}

type FileTreeProps = {
fileTree: GetRepoFilesResponse | undefined | null
isLoading: boolean
selectedFilePaths: string[]
onSelectFilePaths: (newFiles: string[]) => void
}

const FileTree = ({
fileTree,
isLoading,
selectedFilePaths,
onSelectFilePaths,
}: FileTreeProps) => {
if (isLoading) return <span>Loading...</span>

if (!fileTree || fileTree.length === 0) return <span>No files found</span>

const handleCheckChange = (
checked: CheckedState,
path: string | undefined,
) => {
if (!path) {
return
}

if (checked) {
onSelectFilePaths([...selectedFilePaths, path])
} else {
onSelectFilePaths(selectedFilePaths.filter((file) => file !== path))
}
}

const filteredFileTree = fileTree.filter((file) => file.path != undefined)

return filteredFileTree.map((file) => (
<div key={file.path}>
<Checkbox
id={file.path}
checked={selectedFilePaths.includes(file.path ?? '')}
onCheckedChange={(checked) => handleCheckChange(checked, file.path)}
/>
<label htmlFor={file.path} className="pl-2">
{file.path}
</label>
</div>
))
}

export default GithubFileSelect
77 changes: 77 additions & 0 deletions src/components/sections/GitHub/GithubRepoSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client'

import {ArrowLeft} from 'lucide-react'
import {signIn, useSession} from 'next-auth/react'

import {Button} from '@/components/ui/Button'
import {useGetPublicRepos} from '@/lib/queries/github/getGitHub'
import {GithubRepository} from '@/server/actions/github/getGithub'
import {ScrollArea} from '@/components/ui/ScrollArea'

type GithubRepoSelect = {
selectedRepo: GithubRepository | null
onSelectRepo: (repo: GithubRepository | null) => void
}

const GithubRepoSelect = ({selectedRepo, onSelectRepo}: GithubRepoSelect) => {
const session = useSession()

const {data: publicReposData, isLoading: publicReposIsLoading} =
useGetPublicRepos()

if (session.data?.user.provider !== 'github') {
return (
<Button
onClick={() => signIn('github')}
className="bg-gray-800 text-white">
Sign in with Github to select a repo
</Button>
)
}

return (
<div className="min-w-96 rounded-md border-2 border-gray-900 p-2">
{selectedRepo === null ? (
<div>
<span className="pb-2 font-bold">Select a repo</span>
<ScrollArea className="h-96 w-full" thumbClassname="bg-gray-400">
<div className="flex flex-col items-start gap-2">
<RepoList
repos={publicReposData}
isLoading={publicReposIsLoading}
selectRepo={(params: GithubRepository) => onSelectRepo(params)}
/>
</div>
</ScrollArea>
</div>
) : (
<div>
<Button variant="outline" onClick={() => onSelectRepo(null)}>
<ArrowLeft className="h-4 w-4" />
</Button>
<span className="pl-2 font-bold">{`${selectedRepo.owner}/${selectedRepo.name}`}</span>
</div>
)}
</div>
)
}

type RepoListProps = {
repos: GithubRepository[] | undefined
isLoading: boolean
selectRepo: (params: GithubRepository) => void
}

const RepoList = ({repos, isLoading, selectRepo}: RepoListProps) => {
if (!repos && isLoading) return <span>Loading...</span>

if (!repos || repos.length === 0) return <span>No public repos found</span>

return repos.map((repo) => (
<Button key={repo.id} onClick={() => selectRepo(repo)}>
{repo.fullName}
</Button>
))
}

export default GithubRepoSelect
28 changes: 28 additions & 0 deletions src/components/ui/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import {CheckIcon} from '@radix-ui/react-icons'

import {cn} from '@/lib/utils/client/tailwind'

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({className, ...props}, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer h-4 w-4 shrink-0 rounded-sm border shadow focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export {Checkbox}
63 changes: 63 additions & 0 deletions src/components/ui/ScrollArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client'

import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'

import {cn} from '@/lib/utils/client/tailwind'

type ScrollAreaProps = React.ComponentPropsWithoutRef<
typeof ScrollAreaPrimitive.Root
> & {
thumbClassname?: string
}

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
ScrollAreaProps
>(({className, children, thumbClassname, ...props}, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar thumbClassname={thumbClassname} />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

type ScrollBarProps = React.ComponentPropsWithoutRef<
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
> & {
thumbClassname?: string
}

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
ScrollBarProps
>(({className, orientation = 'vertical', thumbClassname, ...props}, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb
className={cn(
'relative flex-1 rounded-full bg-origin-border',
thumbClassname,
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export {ScrollArea, ScrollBar}
47 changes: 47 additions & 0 deletions src/lib/queries/github/getGitHub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {useQuery} from '@tanstack/react-query'
import {useSession} from 'next-auth/react'

import {queryKeys} from '../keys'

import {
GetRepoFilesParams,
getPublicRepos,
getRepoFiles,
} from '@/server/actions/github/getGithub'
import {useUserId} from '@/lib/hooks/useUserId'
import {withApiErrorHandler} from '@/lib/utils/common/error'

const getPublicReposQueryOptions = (userId: string | undefined) => ({
queryKey: queryKeys.gitHub.publicRepos(userId).queryKey,
queryFn: withApiErrorHandler(() => getPublicRepos()),
})

export const useGetPublicRepos = () => {
const userId = useUserId()
const session = useSession()

return useQuery({
...getPublicReposQueryOptions(userId),
enabled: !!userId && session.data?.user.provider === 'github',
})
}

const getRepoFilesQueryOptions = (params: GetRepoFilesParams | undefined) => ({
queryKey: queryKeys.gitHub.repoFiles(params).queryKey,
queryFn: withApiErrorHandler(() => {
if (!params) {
throw new Error('GetRepoFilesParams is required.')
}

return getRepoFiles(params)
}),
})

export const useGetRepoFiles = (params: GetRepoFilesParams | undefined) => {
const session = useSession()

return useQuery({
...getRepoFilesQueryOptions(params),
enabled: !!params && session.data?.user.provider === 'github',
})
}
5 changes: 5 additions & 0 deletions src/lib/queries/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {createQueryKeyStore} from '@lukemorales/query-key-factory'
import {type GetRewardsParams} from '@/server/actions/reward/getReward'
import {type GetPublicContestsParams} from '@/server/actions/contest/getContests'
import {type GetDeduplicatedFindingsParams} from '@/server/actions/deduplicatedFinding/getDeduplicatedFinding'
import {GetRepoFilesParams} from '@/server/actions/github/getGithub'

export const queryKeys = createQueryKeyStore({
users: {
Expand All @@ -18,4 +19,8 @@ export const queryKeys = createQueryKeyStore({
deduplicatedFindings: {
all: (params: GetDeduplicatedFindingsParams) => [params],
},
gitHub: {
publicRepos: (userId: string | undefined) => [userId],
repoFiles: (params: GetRepoFilesParams | undefined) => [params],
},
})

0 comments on commit b054e4e

Please sign in to comment.