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

chore: Migrate useSelfHostedSeatsConfig to TS Query V5 #3580

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
44 changes: 27 additions & 17 deletions src/layouts/Header/components/SeatDetails/SeatDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
QueryClientProvider as QueryClientProviderV5,
QueryClient as QueryClientV5,
} from '@tanstack/react-queryV5'
import { render, screen } from '@testing-library/react'
import { graphql, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { Suspense } from 'react'
import { MemoryRouter, Route } from 'react-router-dom'

import SeatDetails from './SeatDetails'
Expand All @@ -14,37 +19,43 @@ const mockData = {
}

const mockUndefinedSeats = {
config: {},
config: null,
}

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const queryClientV5 = new QueryClientV5({
defaultOptions: { queries: { retry: false } },
})

const wrapper: ({
initialEntries,
}: {
initialEntries?: string
}) => React.FC<React.PropsWithChildren> =
({ initialEntries = '/gh' }) =>
({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialEntries]}>
<Route path="/:provider" exact>
{children}
</Route>
</MemoryRouter>
</QueryClientProvider>
<QueryClientProviderV5 client={queryClientV5}>
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialEntries]}>
<Route path="/:provider" exact>
<Suspense fallback={<div>Loading</div>}>{children}</Suspense>
</Route>
</MemoryRouter>
</QueryClientProvider>
</QueryClientProviderV5>
)

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

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

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

afterAll(() => {
Expand All @@ -62,12 +73,10 @@ describe('SeatDetails', () => {

describe('renders component', () => {
describe('values are defined', () => {
beforeEach(() => {
setup({})
})

it('displays the number of active seats', async () => {
setup({})
render(<SeatDetails />, { wrapper: wrapper({}) })

const number = await screen.findByText('5')
expect(number).toBeInTheDocument()

Expand All @@ -76,6 +85,7 @@ describe('SeatDetails', () => {
})

it('displays the number of total seats', async () => {
setup({})
render(<SeatDetails />, { wrapper: wrapper({}) })

const number = await screen.findByText('10')
Expand Down
14 changes: 12 additions & 2 deletions src/layouts/Header/components/SeatDetails/SeatDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { useSelfHostedSeatsConfig } from 'services/selfHosted'
import { useSuspenseQuery as useSuspenseQueryV5 } from '@tanstack/react-queryV5'
import { useParams } from 'react-router'

import { SelfHostedSeatsConfigQueryOpts } from 'services/selfHosted/SelfHostedSeatsConfigQueryOpts'

interface URLParams {
provider: string
}

function SeatDetails() {
const { data: selfHostedSeats } = useSelfHostedSeatsConfig()
const { provider } = useParams<URLParams>()
const { data: selfHostedSeats } = useSuspenseQueryV5(
SelfHostedSeatsConfigQueryOpts({ provider })
)

if (!selfHostedSeats?.seatsUsed || !selfHostedSeats?.seatsLimit) {
return <p>Unable to get seat usage information</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useQueryClient } from '@tanstack/react-query'
import { useSuspenseQuery as useSuspenseQueryV5 } from '@tanstack/react-queryV5'
import { useParams } from 'react-router'

import {
useSelfHostedCurrentUser,
useSelfHostedSeatsConfig,
} from 'services/selfHosted'
import { useSelfHostedCurrentUser } from 'services/selfHosted'
import { SelfHostedSeatsConfigQueryOpts } from 'services/selfHosted/SelfHostedSeatsConfigQueryOpts'
import A from 'ui/A'
import Banner from 'ui/Banner'
import BannerContent from 'ui/Banner/BannerContent'
Expand Down Expand Up @@ -39,9 +39,12 @@ function canChangeActivation({ seatConfig, currentUser }) {
}

function ActivationBanner() {
const { provider } = useParams()
const queryClient = useQueryClient()
const { data: currentUser } = useSelfHostedCurrentUser()
const { data: seatConfig } = useSelfHostedSeatsConfig()
const { data: seatConfig } = useSuspenseQueryV5(
SelfHostedSeatsConfigQueryOpts({ provider })
)

const { canChange, displaySeatMsg } = canChangeActivation({
seatConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
QueryClientProvider as QueryClientProviderV5,
QueryClient as QueryClientV5,
} from '@tanstack/react-queryV5'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { graphql, http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { Route } from 'react-router-dom'
import { MemoryRouter } from 'react-router-dom/cjs/react-router-dom.min'
import { Suspense } from 'react'
import { MemoryRouter, Route } from 'react-router-dom'

import ActivationBanner from './ActivationBanner'

Expand All @@ -27,27 +31,38 @@ const mockUserData = {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const server = setupServer()

const queryClientV5 = new QueryClientV5({
defaultOptions: { queries: { retry: false } },
})

const wrapper = ({ children }) => (
<QueryClientProviderV5 client={queryClientV5}>
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/gh']}>
<Route path="/:provider">
<Suspense fallback={<div>Loading</div>}>{children}</Suspense>
</Route>
</MemoryRouter>
</QueryClientProvider>
</QueryClientProviderV5>
)

const server = setupServer()
beforeAll(() => {
server.listen()
})
beforeEach(() => {

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

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

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/gh']}>
<Route path="/:provider">{children}</Route>
</MemoryRouter>
</QueryClientProvider>
)

describe('ActivationBanner', () => {
function setup(
{ overrideUserData = {}, overrideSeatData = {} } = {
Expand All @@ -58,14 +73,14 @@ describe('ActivationBanner', () => {
const user = userEvent.setup()

let restUsersCurrent = { ...mockUserData, ...overrideUserData }
const querySeats = { ...mockSeatData, ...overrideSeatData }
const querySeats = { ...mockSeatData.config, ...overrideSeatData }

server.use(
http.get('/internal/users/current', () => {
return HttpResponse.json(restUsersCurrent)
}),
graphql.query('Seats', () => {
return HttpResponse.json({ data: querySeats })
return HttpResponse.json({ data: { config: querySeats } })
}),
http.patch('/internal/users/current', async (info) => {
const { activated } = await info.request.json()
Expand All @@ -83,9 +98,8 @@ describe('ActivationBanner', () => {
}

describe('rendering banner header', () => {
beforeEach(() => setup())

it('renders header content', async () => {
setup()
render(<ActivationBanner />, { wrapper })

const heading = await screen.findByText('Activation Status')
Expand All @@ -95,9 +109,8 @@ describe('ActivationBanner', () => {

describe('rendering banner content', () => {
describe('user is activated', () => {
beforeEach(() => setup({ overrideUserData: { activated: true } }))

it('displays user is activated', async () => {
setup({ overrideUserData: { activated: true } })
render(<ActivationBanner />, { wrapper })

const activated = await screen.findByText('You are currently activated')
Expand All @@ -107,9 +120,8 @@ describe('ActivationBanner', () => {

describe('user is not activated', () => {
describe('org has free seats', () => {
beforeEach(() => setup({ overrideSeatData: { activated: false } }))

it('displays user is not activated', async () => {
setup({ overrideSeatData: { activated: false } })
render(<ActivationBanner />, { wrapper })

const activated = await screen.findByText(
Expand All @@ -120,14 +132,11 @@ describe('ActivationBanner', () => {
})

describe('org does not have free seats', () => {
beforeEach(() =>
it('displays org out of seat message', async () => {
setup({
overrideSeatData: { seatsUsed: 10, seatsLimit: 10 },
overrideUserData: { activated: false },
})
)

it('displays org out of seat message', async () => {
render(<ActivationBanner />, { wrapper })

const noSeatMsg = await screen.findByText(
Expand All @@ -137,6 +146,10 @@ describe('ActivationBanner', () => {
})

it('sets toggle to disabled', async () => {
setup({
overrideSeatData: { seatsUsed: 10, seatsLimit: 10 },
overrideUserData: { activated: false },
})
render(<ActivationBanner />, { wrapper })

const button = await screen.findByRole('button')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useMutation } from '@tanstack/react-query'
import { useQueryClient as useQueryClientV5 } from '@tanstack/react-queryV5'
import { useParams } from 'react-router-dom'

import { SelfHostedSeatsConfigQueryOpts } from 'services/selfHosted/SelfHostedSeatsConfigQueryOpts'
import Api from 'shared/api'

export const useSelfActivationMutation = ({ queryClient, canChange }) => {
const { provider } = useParams()
const queryClientV5 = useQueryClientV5()

return useMutation({
mutationFn: (activated) => {
Expand All @@ -18,43 +21,51 @@ export const useSelfActivationMutation = ({ queryClient, canChange }) => {
},
onMutate: async (activated) => {
await queryClient.cancelQueries(['SelfHostedCurrentUser'])
await queryClient.cancelQueries(['Seats'])
await queryClientV5.cancelQueries({
queryKey: SelfHostedSeatsConfigQueryOpts({ provider }).queryKey,
})

const prevUser = queryClient.getQueryData(['SelfHostedCurrentUser'])
const prevSeat = queryClient.getQueryData(['Seats'])
const prevSeat = queryClientV5.getQueryData(
SelfHostedSeatsConfigQueryOpts({ provider }).queryKey
)

if (canChange) {
queryClient.setQueryData(['SelfHostedCurrentUser'], (user) => ({
...user,
activated,
}))

queryClient.setQueryData(['Seats'], (seats) => {
const seatsUsed = seats?.data?.config?.seatsUsed

return {
data: {
config: {
...seats?.data?.config,
seatsUsed: activated ? seatsUsed + 1 : seatsUsed - 1,
queryClientV5.setQueryData(
SelfHostedSeatsConfigQueryOpts({ provider }).queryKey,
(seats) => {
const seatsUsed = seats?.data?.config?.seatsUsed
return {
data: {
config: {
...seats?.data?.config,
seatsUsed: activated ? seatsUsed + 1 : seatsUsed - 1,
},
},
},
}
}
})
)
}

return {
prevUser,
prevSeat,
}
return { prevUser, prevSeat }
},
onError: (_err, _activated, context) => {
queryClient.setQueryData(['SelfHostedCurrentUser'], context.prevUser)
queryClient.setQueryData(['Seats'], context.prevSeat)
queryClientV5.setQueryData(
SelfHostedSeatsConfigQueryOpts({ provider }).queryKey,
context.prevSeat
)
},
onSettled: () => {
queryClient.invalidateQueries(['SelfHostedCurrentUser'])
queryClient.invalidateQueries(['Seats'])
queryClientV5.invalidateQueries(
SelfHostedSeatsConfigQueryOpts({ provider }).queryKey
Copy link
Contributor

Choose a reason for hiding this comment

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

just making sure - is this provider key used in the lookup case-sensitive at all? Just in case that misses hits between like GITHUB, github, .. Oh and I guess also the shorthand like gh.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes it would be case sensitive, however the data fetching query key is also case sensitive so they would align on that level and ensure that the correct value in the cache gets cleared.

)
},
})
}
Loading
Loading