-
Notifications
You must be signed in to change notification settings - Fork 23
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: Handle self hosted activation statuses #3523
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
...poPage/ActivationAlert/ActivationRequiredSelfHosted/ActivationRequiredSelfHosted.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||
import { render, screen, waitFor } from '@testing-library/react' | ||
import { graphql, http, HttpResponse } from 'msw' | ||
import { setupServer } from 'msw/node' | ||
import { MemoryRouter, Route } from 'react-router-dom' | ||
import { vi } from 'vitest' | ||
|
||
import ActivationRequiredSelfHosted from './ActivationRequiredSelfHosted' | ||
|
||
const queryClient = new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
retry: false, | ||
suspense: false, | ||
}, | ||
}, | ||
}) | ||
|
||
const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => ( | ||
<QueryClientProvider client={queryClient}> | ||
<MemoryRouter initialEntries={['/gh/codecov/gazebo/new']}> | ||
<Route path="/:provider/:owner/:repo/new">{children}</Route> | ||
</MemoryRouter> | ||
</QueryClientProvider> | ||
) | ||
|
||
const server = setupServer() | ||
|
||
beforeAll(() => { | ||
server.listen({ onUnhandledRequest: 'warn' }) | ||
console.error = () => {} | ||
}) | ||
|
||
afterEach(() => { | ||
queryClient.clear() | ||
server.resetHandlers() | ||
vi.clearAllMocks() | ||
}) | ||
|
||
afterAll(() => { | ||
server.close() | ||
}) | ||
|
||
describe('ActivationRequiredSelfHosted', () => { | ||
function setup(isAdmin: boolean, seatsUsed: number, seatsLimit: number) { | ||
server.use( | ||
http.get('/internal/users/current', (info) => | ||
HttpResponse.json({ | ||
isAdmin, | ||
email: '[email protected]', | ||
name: 'Test User', | ||
ownerid: 1, | ||
username: 'testuser', | ||
activated: true, | ||
}) | ||
), | ||
graphql.query('Seats', (info) => { | ||
return HttpResponse.json({ | ||
data: { | ||
config: { | ||
seatsUsed, | ||
seatsLimit, | ||
}, | ||
}, | ||
}) | ||
}) | ||
) | ||
} | ||
describe('when user has seats left', () => { | ||
it('renders the banner with correct heading', async () => { | ||
setup(false, 0, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const bannerHeading = await screen.findByRole('heading', { | ||
name: /Activation Required/, | ||
}) | ||
expect(bannerHeading).toBeInTheDocument() | ||
}) | ||
|
||
it('renders the banner with correct description', async () => { | ||
setup(false, 1, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const description = await screen.findByText( | ||
/You have available seats, but activation is needed./ | ||
) | ||
expect(description).toBeInTheDocument() | ||
}) | ||
|
||
it('renders copy for the user', async () => { | ||
setup(false, 1, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const copy = await screen.findByText(/Contact your admin for activation./) | ||
expect(copy).toBeInTheDocument() | ||
}) | ||
|
||
it('does not render the link if the user is not an admin', async () => { | ||
setup(false, 1, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
await waitFor(() => queryClient.isFetching) | ||
await waitFor(() => !queryClient.isFetching) | ||
|
||
const link = screen.queryByRole('link', { | ||
name: /Manage members/, | ||
}) | ||
expect(link).not.toBeInTheDocument() | ||
}) | ||
|
||
it('renders the correct img', async () => { | ||
setup(false, 1, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const img = await screen.findByAltText('Forbidden') | ||
expect(img).toBeInTheDocument() | ||
expect(img).toHaveAttribute( | ||
'src', | ||
'/src/layouts/shared/NetworkErrorBoundary/assets/error-upsidedown-umbrella.svg' | ||
) | ||
}) | ||
|
||
describe('when the user is an admin', () => { | ||
it('renders the correct link', async () => { | ||
setup(true, 1, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const link = await screen.findByRole('link', { | ||
name: /Manage members/, | ||
}) | ||
expect(link).toBeInTheDocument() | ||
expect(link).toHaveAttribute('href', '/admin/gh/users') | ||
}) | ||
}) | ||
}) | ||
|
||
describe('when user has no seats left', () => { | ||
it('renders the banner with correct heading', async () => { | ||
setup(false, 10, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const bannerHeading = await screen.findByRole('heading', { | ||
name: /Activation Required/, | ||
}) | ||
expect(bannerHeading).toBeInTheDocument() | ||
}) | ||
|
||
it('renders the banner with correct description', async () => { | ||
setup(false, 10, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const description = await screen.findByText( | ||
/Your organization has utilized all available seats./ | ||
) | ||
expect(description).toBeInTheDocument() | ||
}) | ||
|
||
it('renders the correct img', async () => { | ||
setup(false, 10, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const img = await screen.findByAltText('Forbidden') | ||
expect(img).toBeInTheDocument() | ||
}) | ||
|
||
describe('renders contact sales link', () => { | ||
it('when the user is an admin', async () => { | ||
setup(true, 10, 10) | ||
render(<ActivationRequiredSelfHosted />, { wrapper }) | ||
|
||
const link = await screen.findByRole('link', { | ||
name: /Contact Sales/, | ||
}) | ||
expect(link).toBeInTheDocument() | ||
expect(link).toHaveAttribute('href', 'https://about.codecov.io/sales') | ||
}) | ||
}) | ||
}) | ||
}) |
71 changes: 71 additions & 0 deletions
71
...es/RepoPage/ActivationAlert/ActivationRequiredSelfHosted/ActivationRequiredSelfHosted.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import upsideDownUmbrella from 'layouts/shared/NetworkErrorBoundary/assets/error-upsidedown-umbrella.svg' | ||
import { | ||
useSelfHostedCurrentUser, | ||
useSelfHostedSeatsConfig, | ||
} from 'services/selfHosted' | ||
import A from 'ui/A' | ||
import Button from 'ui/Button' | ||
|
||
function SeatsLimitReached() { | ||
return ( | ||
<div className="flex flex-col items-center justify-center gap-8 bg-ds-gray-primary pb-28 pt-12 text-center"> | ||
<img src={upsideDownUmbrella} alt="Forbidden" className="w-36" /> | ||
<div className="flex w-2/5 flex-col gap-1"> | ||
<h1 className="text-2xl">Activation Required</h1> | ||
<p>Your organization has utilized all available seats.</p> | ||
<div className="mt-5"> | ||
<A | ||
to={{ pageName: 'sales' }} | ||
isExternal | ||
hook="contact-sales-self-hosted" | ||
> | ||
Contact Sales | ||
</A>{' '} | ||
to increase your seat count. | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
function SeatsAvailable({ isAdmin }: { isAdmin: boolean }) { | ||
return ( | ||
<div className="flex flex-col items-center justify-center gap-8 bg-ds-gray-primary pb-28 pt-12 text-center"> | ||
<img src={upsideDownUmbrella} alt="Forbidden" className="w-36" /> | ||
<div className="flex w-2/5 flex-col gap-1"> | ||
<h1 className="text-2xl">Activation Required</h1> | ||
<p>You have available seats, but activation is needed.</p> | ||
</div> | ||
{isAdmin ? ( | ||
<Button | ||
to={{ pageName: 'users' }} | ||
disabled={undefined} | ||
hook={undefined} | ||
variant="primary" | ||
> | ||
Manage members | ||
</Button> | ||
) : ( | ||
<p>Contact your admin for activation.</p> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
function ActivationRequiredSelfHosted() { | ||
const { data } = useSelfHostedCurrentUser() | ||
const { data: selfHostedSeats } = useSelfHostedSeatsConfig() | ||
|
||
const hasSelfHostedSeats = | ||
selfHostedSeats?.seatsUsed && | ||
selfHostedSeats?.seatsLimit && | ||
selfHostedSeats?.seatsUsed < selfHostedSeats?.seatsLimit | ||
|
||
return hasSelfHostedSeats ? ( | ||
<SeatsAvailable isAdmin={data?.isAdmin ?? false} /> | ||
) : ( | ||
<SeatsLimitReached /> | ||
) | ||
} | ||
|
||
export default ActivationRequiredSelfHosted |
1 change: 1 addition & 0 deletions
1
src/pages/RepoPage/ActivationAlert/ActivationRequiredSelfHosted/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './ActivationRequiredSelfHosted' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we pull this into a variable? It's pretty long but identical to line 33 so it would be easier to know at a glance that the two components look the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing