Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Add configured repos table to the Codecov AI tab #3303

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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
122 changes: 108 additions & 14 deletions src/pages/CodecovAIPage/CodecovAIPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { render, screen } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, render, screen } from '@testing-library/react'
import { graphql, HttpResponse } from 'msw2'
import { setupServer } from 'msw2/node'
import { MemoryRouter, Route } from 'react-router-dom'

import { ThemeContextProvider } from 'shared/ThemeContext'
Expand All @@ -17,17 +20,75 @@ vi.mock('shared/featureFlags', async () => {
}
})

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: true,
suspense: true,
},
},
})

const server = setupServer()

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<ThemeContextProvider>
<MemoryRouter initialEntries={['/gh/codecov/test-repo/bundles/new']}>
<Route path="/:provider/:owner/:repo/bundles/new">{children}</Route>
</MemoryRouter>
</ThemeContextProvider>
<QueryClientProvider client={queryClient}>
<ThemeContextProvider>
<MemoryRouter initialEntries={['/gh/codecov/']}>
<Route path="/:provider/:owner/">{children}</Route>
</MemoryRouter>
</ThemeContextProvider>
</QueryClientProvider>
)

beforeAll(() => {
server.listen()
})

afterEach(() => {
queryClient.clear()
server.resetHandlers()
})

afterEach(() => {
cleanup()
vi.clearAllMocks()
queryClient.clear()
})

afterAll(() => {
server.close()
})

describe('CodecovAIPage', () => {
function setup(
aiFeaturesEnabled = false,
aiEnabledRepos = ['repo-1', 'repo-2']
) {
server.use(
graphql.query('GetCodecovAIAppInstallInfo', (info) => {
return HttpResponse.json({
data: {
owner: {
aiFeaturesEnabled,
},
},
})
}),
graphql.query('GetCodecovAIInstalledRepos', (info) => {
return HttpResponse.json({
data: {
owner: {
aiEnabledRepos,
},
},
})
})
)
}
beforeEach(() => {
mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: true })
setup()
})

it('renders top section', async () => {
Expand Down Expand Up @@ -55,7 +116,7 @@ describe('CodecovAIPage', () => {

it('renders the install button', async () => {
render(<CodecovAIPage />, { wrapper })
const buttonEl = screen.getByRole('link', { name: /Install Codecov AI/i })
const buttonEl = await screen.findByText(/Install Codecov AI/i)
expect(buttonEl).toBeInTheDocument()
})

Expand Down Expand Up @@ -107,15 +168,48 @@ describe('CodecovAIPage', () => {
const docLink = await screen.findByText(/Visit our guide/)
expect(docLink).toBeInTheDocument()
})
})

describe('flag is off', () => {
it('does not render page', async () => {
mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: false })
describe('AI features are enabled and configured', () => {
beforeEach(() => {
setup(true)
mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: true })
})

it('does not render install link', () => {
setup(true)
render(<CodecovAIPage />, { wrapper })
const topSection = screen.queryByText(/Install Codecov AI/)
expect(topSection).not.toBeInTheDocument()
})

it('renders list of repos', async () => {
render(<CodecovAIPage />, { wrapper })

const repo1Link = await screen.findByText(/repo-1/)
expect(repo1Link).toBeInTheDocument()
const repo2Link = await screen.findByText(/repo-2/)
expect(repo2Link).toBeInTheDocument()
})

describe('No repos returned', () => {
it('renders install link', async () => {
setup(true, [])
render(<CodecovAIPage />, { wrapper })
const buttonEl = await screen.findByText(/Install Codecov AI/i)
expect(buttonEl).toBeInTheDocument()
})
})
})

describe('flag is off', () => {
it('does not render page', async () => {
setup(true)
mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: false })

render(<CodecovAIPage />, { wrapper })
render(<CodecovAIPage />, { wrapper })

const topSection = screen.queryByText(/Codecov AI is a/)
expect(topSection).not.toBeInTheDocument()
const topSection = screen.queryByText(/Codecov AI is a/)
expect(topSection).not.toBeInTheDocument()
})
})
})
14 changes: 12 additions & 2 deletions src/pages/CodecovAIPage/CodecovAIPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Redirect, useParams } from 'react-router-dom'

import { useCodecovAIInstallation } from 'services/codecovAI/useCodecovAIInstallation'
import { useFlags } from 'shared/featureFlags'

import CodecovAICommands from './CodecovAICommands/CodecovAICommands'
import ConfiguredRepositories from './ConfiguredRepositories/ConfiguredRepositories'
import InstallCodecovAI from './InstallCodecovAI/InstallCodecovAI'
import LearnMoreBlurb from './LearnMoreBlurb/LearnMoreBlurb'
import Tabs from './Tabs/Tabs'
Expand All @@ -14,11 +16,15 @@ interface URLParams {

const CodecovAIPage: React.FC = () => {
const { provider, owner } = useParams<URLParams>()

const { codecovAiFeaturesTab } = useFlags({
codecovAiFeaturesTab: false,
})

const { data: installationData } = useCodecovAIInstallation({
owner,
provider,
})

if (!codecovAiFeaturesTab) {
return <Redirect to={`/${provider}/${owner}`} />
}
Expand All @@ -36,7 +42,11 @@ const CodecovAIPage: React.FC = () => {
</p>
</section>
<div className="flex flex-col gap-4 pt-2 lg:w-3/5">
<InstallCodecovAI />
{installationData?.aiFeaturesEnabled ? (
<ConfiguredRepositories />
) : (
<InstallCodecovAI />
)}
<CodecovAICommands />
<LearnMoreBlurb />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
import React, { useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'

import { useCodecovAIInstalledRepos } from 'services/codecovAI/useCodecovAIInstalledRepos'
import A from 'ui/A'
import { Card } from 'ui/Card'
import Icon from 'ui/Icon'
import Spinner from 'ui/Spinner'

import InstallCodecovAI from '../InstallCodecovAI/InstallCodecovAI'

interface URLParams {
owner: string
provider: string
}

const Loader = () => (
<div className="mb-4 flex justify-center pt-4">
<Spinner />
</div>
)

const columnHelper = createColumnHelper<{ name: string }>()

function ConfiguredRepositories() {
const { owner, provider } = useParams<URLParams>()
const { data, isLoading } = useCodecovAIInstalledRepos({
owner,
provider,
})

const [isSortedAscending, setIsSortedAscending] = useState(true)

const sortRepos = () => {
setIsSortedAscending(!isSortedAscending)
}

const tableData = useMemo(() => {
if (!data?.aiEnabledRepos) return []
const sortedRepos = [...data.aiEnabledRepos].sort((a, b) =>
isSortedAscending ? a.localeCompare(b) : b.localeCompare(a)
)
return sortedRepos.map((name) => ({ name }))
}, [data?.aiEnabledRepos, isSortedAscending])

const columns = useMemo(
() => [
columnHelper.accessor('name', {
header: 'Repo Name',
cell: (info) => {
const repoName = info.getValue()
const link = `/${provider}/${owner}/${repoName}`
return (
<A href={link} to={undefined} hook={undefined} isExternal={false}>
{repoName}
</A>
)
},
}),
],
[provider, owner]
)

const table = useReactTable({
data: tableData,
columns,
getCoreRowModel: getCoreRowModel(),
})

// This should technically never happen, but render a fallback just in case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how much value this comment is giving tbh :P

if (tableData.length === 0) {
return <InstallCodecovAI />
}

return (
<div className="flex flex-col">
<Card className="mb-0 border-b-0">
<Card.Header className="border-b-0">
<Card.Title size="base">
{tableData.length} configured repositories
</Card.Title>
<p>
To install more repos, please manage your Codecov AI app on GitHub.
<br />
To uninstall the app, please go to your GitHub Apps settings.
</p>
</Card.Header>
</Card>
<div className="tableui border border-t-0">
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
scope="col"
onClick={sortRepos}
className="cursor-pointer"
>
<div className="flex gap-1">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
<span
className="text-ds-blue-darker group-hover/columnheader:opacity-100"
data-sort-direction={isSortedAscending ? 'asc' : 'desc'}
>
<Icon name="arrowUp" size="sm" />
</span>
</div>
</th>
))}
</tr>
))}
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={1}>
<Loader />
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)
}

export default ConfiguredRepositories
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const COPY_APP_INSTALL_STRING =

const InstallCodecovAI: React.FC = () => {
const { theme } = useThemeContext()

const isDarkMode = theme === Theme.DARK
const githubImage = loginProviderImage('Github', !isDarkMode)

Expand Down
Loading
Loading