Skip to content

Commit

Permalink
[license-checks] Revoke license keys from UI (#53220)
Browse files Browse the repository at this point in the history
## Description

Add button to revoke license key from the UI directly. This is useful if
we ever need to revoke a license key (and we do not need to go to the DB
to do it).

## Screenshots

### Before
![Screenshot 2023-06-09 at 11 45
38](https://github.com/sourcegraph/sourcegraph/assets/9974711/e1240d3f-a580-4ecf-ae0a-e6d9a877ee4e)
![Screenshot 2023-06-09 at 11 46
21](https://github.com/sourcegraph/sourcegraph/assets/9974711/0d7468e6-f3ae-49fc-92e7-78b7e16ac606)

### After
<img width="934" alt="Screenshot 2023-06-09 at 12 00 29"
src="https://github.com/sourcegraph/sourcegraph/assets/9974711/ade7bc69-ffba-4845-af3a-4e260522412c">
<img width="925" alt="Screenshot 2023-06-09 at 12 00 42"
src="https://github.com/sourcegraph/sourcegraph/assets/9974711/0e70577d-6564-409f-9a05-0c3f536eabab">



## Test plan

Tested locally + some unit tests
  • Loading branch information
kopancek authored and ErikaRS committed Jun 22, 2023
1 parent 841128a commit 723afa2
Show file tree
Hide file tree
Showing 18 changed files with 408 additions and 216 deletions.
2 changes: 1 addition & 1 deletion client/web/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const TEST_SHOW_MORE_PAGINATION_QUERY = gql`
}
`

const TestComponent = () => {
const TestComponent = ({ skip = false }) => {
const { connection, fetchMore, hasNextPage } = useShowMorePagination<
TestShowMorePaginationQueryResult,
TestShowMorePaginationQueryVariables,
Expand All @@ -51,6 +51,7 @@ const TestComponent = () => {
},
options: {
useURL: true,
skip,
},
})

Expand Down Expand Up @@ -137,10 +138,14 @@ describe('useShowMorePagination', () => {
},
]

const renderWithMocks = async (mocks: MockedResponse<TestShowMorePaginationQueryResult>[], route = '/') => {
const renderWithMocks = async (
mocks: MockedResponse<TestShowMorePaginationQueryResult>[],
route = '/',
skip = false
) => {
const renderResult = renderWithBrandedContext(
<MockedTestProvider mocks={mocks}>
<TestComponent />
<TestComponent skip={skip} />
</MockedTestProvider>,
{ route }
)
Expand Down Expand Up @@ -171,6 +176,14 @@ describe('useShowMorePagination', () => {

const cursorMocks = generateMockCursorResponses(mockResultNodes)

it('does not fetch anything if skip is true', async () => {
const queries = await renderWithMocks(cursorMocks, '/', true)
expect(queries.queryByText('repo-A')).not.toBeInTheDocument()
expect(queries.queryByText('repo-C')).not.toBeInTheDocument()
expect(queries.queryByText('repo-D')).not.toBeInTheDocument()
expect(queries.queryByText('Total count')).not.toBeInTheDocument()
})

it('renders correct result', async () => {
const queries = await renderWithMocks(cursorMocks)
expect(queries.getAllByRole('listitem').length).toBe(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ interface UseShowMorePaginationConfig<TResult> {
// workaround for existing APIs where after may already be in use for
// another field.
useAlternateAfterCursor?: boolean
/** Skip the query if this condition is true */
skip?: boolean
}

interface UseShowMorePaginationParameters<TResult, TVariables, TData> {
Expand Down Expand Up @@ -131,6 +133,7 @@ export const useShowMorePagination = <TResult, TVariables extends {}, TData>({
...initialControls,
},
notifyOnNetworkStatusChange: true, // Ensures loading state is updated on `fetchMore`
skip: options?.skip,
fetchPolicy: options?.fetchPolicy,
onCompleted: options?.onCompleted,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mdiCheckCircle, mdiCloseCircle, mdiShieldRemove } from '@mdi/js'
import classNames from 'classnames'

import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { Icon, Text } from '@sourcegraph/wildcard'
import { Icon, Label } from '@sourcegraph/wildcard'

import { ProductLicenseFields } from '../../../graphql-operations'
import { isProductLicenseExpired } from '../../../productSubscription/helpers'
Expand Down Expand Up @@ -37,15 +37,15 @@ export const ProductLicenseValidity: React.FunctionComponent<
license: ProductLicenseFields
className?: string
}>
> = ({ license: { info, revokedAt }, className = '' }) => {
> = ({ license: { info, revokedAt, revokeReason }, className = '' }) => {
const expiresAt = info?.expiresAt ?? 0
const isExpired = isProductLicenseExpired(expiresAt)
const isRevoked = !!revokedAt
const timestamp = revokedAt ?? expiresAt
const timestampSuffix = isExpired || isRevoked ? 'ago' : 'remaining'

return (
<Text className={className}>
<div className={className}>
<Icon
svgPath={getIcon(isExpired, isRevoked)}
aria-hidden={true}
Expand All @@ -56,6 +56,12 @@ export const ProductLicenseValidity: React.FunctionComponent<
/>
<strong>{getText(isExpired, isRevoked)}</strong> (
<Timestamp date={timestamp} noAbout={true} noAgo={true} /> {timestampSuffix})
</Text>
{!isExpired && isRevoked && revokeReason && (
<div className="mt-1 d-flex">
<Label>Reason to revoke</Label>
<span className="ml-3">{revokeReason}</span>
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useEffect, useState } from 'react'

import { useSearchParams } from 'react-router-dom'

import { Container, H2, Text } from '@sourcegraph/wildcard'

import {
ConnectionContainer,
ConnectionError,
ConnectionLoading,
ConnectionList,
SummaryContainer,
ConnectionSummary,
ShowMoreButton,
ConnectionForm,
} from '../../../../components/FilteredConnection/ui'
import { PageTitle } from '../../../../components/PageTitle'
import { eventLogger } from '../../../../tracking/eventLogger'

import { useQueryProductLicensesConnection } from './backend'
import { SiteAdminProductLicenseNode } from './SiteAdminProductLicenseNode'

interface Props {}

const SEARCH_PARAM_KEY = 'query'

/**
* Displays the product licenses that have been created on Sourcegraph.com.
*/
export const SiteAdminLicenseKeyLookupPage: React.FunctionComponent<React.PropsWithChildren<Props>> = () => {
useEffect(() => eventLogger.logPageView('SiteAdminLicenseKeyLookup'), [])

const [searchParams, setSearchParams] = useSearchParams()

const [search, setSearch] = useState<string>(searchParams.get(SEARCH_PARAM_KEY) ?? '')

const { loading, hasNextPage, fetchMore, refetchAll, connection, error } = useQueryProductLicensesConnection(
search,
20
)

useEffect(() => {
const query = search?.trim() ?? ''
searchParams.set(SEARCH_PARAM_KEY, query)
setSearchParams(searchParams)
}, [search, searchParams, setSearchParams])

return (
<div className="site-admin-product-subscriptions-page">
<PageTitle title="Product subscriptions" />
<H2>License key lookup</H2>
<Text>Find matching licenses and their associated product subscriptions.</Text>
<ConnectionContainer>
{error && <ConnectionError errors={[error.message]} />}
{loading && !connection && <ConnectionLoading />}
<ConnectionForm
inputValue={search}
onInputChange={event => {
const search = event.target.value
setSearch(search)
}}
inputPlaceholder="Search product licenses..."
/>
<div className="text-muted mb-2">
<small>Enter a partial license key to find matches.</small>
</div>
{search && (
<Container>
<ConnectionList
as="ul"
className="list-group list-group-flush mb-0"
aria-label="Subscription licenses"
>
{connection?.nodes?.length === 0 && (
<div className="text-center">No matching license key found.</div>
)}

{connection?.nodes?.map(node => (
<SiteAdminProductLicenseNode
key={node.id}
node={node}
showSubscription={true}
onRevokeCompleted={refetchAll}
/>
))}
</ConnectionList>
</Container>
)}
{connection && (
<SummaryContainer className="mt-2">
<ConnectionSummary
first={15}
centered={true}
connection={connection}
noun="product license"
pluralNoun="product licenses"
hasNextPage={hasNextPage}
noSummaryIfAllNodesVisible={true}
/>
{hasNextPage && <ShowMoreButton centered={true} onClick={fetchMore} />}
</SummaryContainer>
)}
</ConnectionContainer>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ describe('SiteAdminProductLicenseNode', () => {
test('active', () => {
expect(
renderWithBrandedContext(
<MockedTestProvider mocks={[]}>
<MockedTestProvider>
<SiteAdminProductLicenseNode
node={{
createdAt: '2020-01-01',
id: 'l1',
licenseKey: 'lk1',
version: 1,
revokedAt: null,
revokeReason: null,
siteID: null,
info: {
__typename: 'ProductLicenseInfo',
Expand All @@ -44,6 +45,9 @@ describe('SiteAdminProductLicenseNode', () => {
},
}}
showSubscription={true}
onRevokeCompleted={function (): void {
throw new Error('Function not implemented.')
}}
/>
</MockedTestProvider>
).asFragment()
Expand All @@ -53,33 +57,39 @@ describe('SiteAdminProductLicenseNode', () => {
test('inactive', () => {
expect(
renderWithBrandedContext(
<SiteAdminProductLicenseNode
node={{
createdAt: '2020-01-01',
id: 'l1',
licenseKey: 'lk1',
version: 1,
revokedAt: null,
siteID: null,
info: {
__typename: 'ProductLicenseInfo',
expiresAt: '2021-01-01',
productNameWithBrand: 'NB',
tags: ['a'],
userCount: 123,
salesforceSubscriptionID: null,
salesforceOpportunityID: null,
},
subscription: {
id: 'id1',
account: null,
name: 's',
activeLicense: { id: 'l0' },
urlForSiteAdmin: '/s',
},
}}
showSubscription={true}
/>
<MockedTestProvider>
<SiteAdminProductLicenseNode
node={{
createdAt: '2020-01-01',
id: 'l1',
licenseKey: 'lk1',
version: 1,
revokedAt: null,
revokeReason: null,
siteID: null,
info: {
__typename: 'ProductLicenseInfo',
expiresAt: '2021-01-01',
productNameWithBrand: 'NB',
tags: ['a'],
userCount: 123,
salesforceSubscriptionID: null,
salesforceOpportunityID: null,
},
subscription: {
id: 'id1',
account: null,
name: 's',
activeLicense: { id: 'l0' },
urlForSiteAdmin: '/s',
},
}}
showSubscription={true}
onRevokeCompleted={function (): void {
throw new Error('Function not implemented.')
}}
/>
</MockedTestProvider>
).asFragment()
).toMatchSnapshot()
})
Expand Down
Loading

0 comments on commit 723afa2

Please sign in to comment.