Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 857d520

Browse files
authored
feat/dotcom: use Enterprise Portal for Cody Gateway usage (#63653)
Closes https://linear.app/sourcegraph/issue/CORE-211 See https://linear.app/sourcegraph/issue/CORE-100 for a higher-level view - this is the first proof-of-concept for achieving our migration strategy to extract Enterprise subscription data out of dotcom while retaining the existing UI until a future project ships a dedicated Enterprise Portal UI (https://linear.app/sourcegraph/project/kr-p-enterprise-portal-user-interface-dadd5ff28bd8). The integration uses generated ConnectRPC client code + `react-query`, the latter of which has already been used elsewhere for SSC integrations. This is partly supported by https://github.com/connectrpc/connect-query-es which offers mostly-first-class integration with `react-query`, but I had to do some fenangling to provide the query clients directly as I can't get the React provider thing to work. The ConnectRPC clients point to the proxies introduced in https://github.com/sourcegraph/sourcegraph/pull/63652 which authenticates the requests for Enterprise Portal, until we ship https://linear.app/sourcegraph/project/kr-p1-streamlined-role-assignment-via-sams-and-entitle-2f118b3f9d4c/overview ## Test plan ### Local First, `sg start dotcom` Choose a subscription you have locally. Use `psql -d sourcegraph` to connect to local database, then: ``` sourcegraph=# delete from product_licenses where product_subscription_id = '<local subscription ID>'; DELETE 1 sourcegraph=# update product_subscriptions set id = '58b95c21-c2d0-4b4b-8b15-bf1b926d3557' where id = '<local subscription ID>'; UPDATE 1 ``` Now annoyingly the UI will break because there is no license, we need: ```gql query getGraphQLID { dotcom { productSubscription(uuid:"58b95c21-c2d0-4b4b-8b15-bf1b926d3557") { id # graphQL ID } } } mutation createLicense { dotcom { generateProductLicenseForSubscription(productSubscriptionID:"<graphQLID>", license:{ tags:["dev"] userCount:100 expiresAt:1814815397 }) { id } } } ``` This effectively lets us have a "pretend S2" subscription locally. Visiting the subscription page now at https://sourcegraph.test:3443/site-admin/dotcom/product/subscriptions/58b95c21-c2d0-4b4b-8b15-bf1b926d3557 ![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/1e77d77d-8032-436b-ab1d-393b34e8e4b5) The data matches the "real" data currently at https://sourcegraph.com/site-admin/dotcom/product/subscriptions/58b95c21-c2d0-4b4b-8b15-bf1b926d3557 ### Against dotcom ``` sg start web-standalone ``` follow https://www.loom.com/share/6cb3b3ca475b4b9392aa4b11938e76e6?sid=6cd1a689-d75d-4133-bcff-b0c7d25b23f1 and then check out some product subscriptions
1 parent d47b4cc commit 857d520

14 files changed

+1043
-135
lines changed

.eslintrc.js

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const config = {
3030
'typedoc.js',
3131
'client/web/dev/**/*',
3232
'graphql-schema-linter.config.js',
33+
// Generated code
34+
'client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**',
3335
],
3436
extends: ['@sourcegraph/eslint-config', 'plugin:storybook/recommended'],
3537
env: {

.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,6 @@ dev/linearhooks/internal/lineargql/schema.graphql
7070
# This is an embedded external minified library and should not be modified
7171
internal/appliance/web/static/script/htmx.min.js
7272
internal/appliance/web/static/script/bootstrap.bundle.min.js
73+
74+
# Generated code
75+
client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**

client/shared/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@
3030
"@sourcegraph/wildcard": "workspace:*"
3131
},
3232
"sideEffects": true
33-
}
33+
}

client/web/BUILD.bazel

+7
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,9 @@ ts_project(
10561056
"src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx",
10571057
"src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx",
10581058
"src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts",
1059+
"src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts",
1060+
"src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/codyaccess-CodyAccessService_connectquery.ts",
1061+
"src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/codyaccess_pb.ts",
10591062
"src/enterprise/site-admin/dotcom/productSubscriptions/plandata.ts",
10601063
"src/enterprise/site-admin/dotcom/productSubscriptions/testUtils.ts",
10611064
"src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts",
@@ -1754,6 +1757,7 @@ ts_project(
17541757
":node_modules/@types/node",
17551758
":node_modules/mermaid",
17561759
"//:node_modules/@apollo/client",
1760+
"//:node_modules/@bufbuild/protobuf",
17571761
"//:node_modules/@codemirror/commands",
17581762
"//:node_modules/@codemirror/lang-json",
17591763
"//:node_modules/@codemirror/lang-markdown",
@@ -1762,6 +1766,9 @@ ts_project(
17621766
"//:node_modules/@codemirror/search",
17631767
"//:node_modules/@codemirror/state",
17641768
"//:node_modules/@codemirror/view",
1769+
"//:node_modules/@connectrpc/connect",
1770+
"//:node_modules/@connectrpc/connect-query",
1771+
"//:node_modules/@connectrpc/connect-web",
17651772
"//:node_modules/@date-fns/utc",
17661773
"//:node_modules/@graphiql/react",
17671774
"//:node_modules/@lezer/common", #keep

client/web/src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx

+38-52
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import React, { useCallback, useState } from 'react'
22

3+
import type { ConnectError } from '@connectrpc/connect'
34
import { mdiPencil, mdiTrashCan } from '@mdi/js'
4-
import { parseISO } from 'date-fns'
5+
import type { UseQueryResult } from '@tanstack/react-query'
56
import type { GraphQLError } from 'graphql'
67

78
import { Toggle } from '@sourcegraph/branded/src/components/Toggle'
89
import { logger } from '@sourcegraph/common'
9-
import { useMutation, useQuery } from '@sourcegraph/http-client'
10+
import { useMutation } from '@sourcegraph/http-client'
1011
import { CodyGatewayRateLimitSource } from '@sourcegraph/shared/src/graphql-operations'
11-
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
12+
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
1213
import {
1314
H3,
1415
ProductStatusBadge,
@@ -35,26 +36,20 @@ import type {
3536
Scalars,
3637
UpdateCodyGatewayConfigResult,
3738
UpdateCodyGatewayConfigVariables,
38-
CodyGatewayRateLimitUsageDatapoint,
3939
CodyGatewayRateLimitFields,
40-
DotComProductSubscriptionCodyGatewayCompletionsUsageResult,
41-
DotComProductSubscriptionCodyGatewayCompletionsUsageVariables,
42-
DotComProductSubscriptionCodyGatewayEmbeddingsUsageVariables,
43-
DotComProductSubscriptionCodyGatewayEmbeddingsUsageResult,
4440
} from '../../../../graphql-operations'
4541
import { ChartContainer } from '../../../../site-admin/analytics/components/ChartContainer'
4642

47-
import {
48-
DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_COMPLETIONS_USAGE,
49-
DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_EMBEDDINGS_USAGE,
50-
UPDATE_CODY_GATEWAY_CONFIG,
51-
} from './backend'
43+
import { UPDATE_CODY_GATEWAY_CONFIG } from './backend'
5244
import { CodyGatewayRateLimitModal } from './CodyGatewayRateLimitModal'
45+
import { useGetCodyGatewayUsage, type EnterprisePortalEnvironment } from './enterpriseportal'
46+
import type { CodyGatewayUsage_UsageDatapoint, GetCodyGatewayUsageResponse } from './enterpriseportalgen/codyaccess_pb'
5347
import { numberFormatter, prettyInterval } from './utils'
5448

5549
import styles from './CodyServicesSection.module.scss'
5650

5751
interface Props extends TelemetryV2Props {
52+
enterprisePortalEnvironment: EnterprisePortalEnvironment
5853
productSubscriptionUUID: string
5954
productSubscriptionID: Scalars['ID']
6055
currentSourcegraphAccessToken: string | null
@@ -65,6 +60,7 @@ interface Props extends TelemetryV2Props {
6560
}
6661

6762
export const CodyServicesSection: React.FunctionComponent<Props> = ({
63+
enterprisePortalEnvironment,
6864
productSubscriptionUUID,
6965
productSubscriptionID,
7066
viewerCanAdminister,
@@ -74,6 +70,9 @@ export const CodyServicesSection: React.FunctionComponent<Props> = ({
7470
codyGatewayAccess,
7571
telemetryRecorder,
7672
}) => {
73+
// TODO: Figure out strategy for what instance to target
74+
const codyGatewayUsageQuery = useGetCodyGatewayUsage(enterprisePortalEnvironment, productSubscriptionUUID)
75+
7776
const [updateCodyGatewayConfig, { loading: updateCodyGatewayConfigLoading, error: updateCodyGatewayConfigError }] =
7877
useMutation<UpdateCodyGatewayConfigResult, UpdateCodyGatewayConfigVariables>(UPDATE_CODY_GATEWAY_CONFIG)
7978

@@ -175,7 +174,7 @@ export const CodyServicesSection: React.FunctionComponent<Props> = ({
175174
/>
176175
</tbody>
177176
</table>
178-
<RateLimitUsage mode="completions" productSubscriptionUUID={productSubscriptionUUID} />
177+
<RateLimitUsage mode="completions" usageQuery={codyGatewayUsageQuery} />
179178

180179
<hr className="my-3" />
181180

@@ -201,10 +200,7 @@ export const CodyServicesSection: React.FunctionComponent<Props> = ({
201200
/>
202201
</tbody>
203202
</table>
204-
<EmbeddingsRateLimitUsage
205-
mode="embeddings"
206-
productSubscriptionUUID={productSubscriptionUUID}
207-
/>
203+
<EmbeddingsRateLimitUsage mode="embeddings" usageQuery={codyGatewayUsageQuery} />
208204
</>
209205
)}
210206

@@ -277,8 +273,8 @@ export const CodyGatewayRateLimitSourceBadge: React.FunctionComponent<{
277273
}
278274
}
279275

280-
function generateSeries(data: CodyGatewayRateLimitUsageDatapoint[]): [string, CodyGatewayRateLimitUsageDatapoint[]][] {
281-
const series: Record<string, CodyGatewayRateLimitUsageDatapoint[]> = {}
276+
function generateSeries(data: CodyGatewayUsage_UsageDatapoint[]): [string, CodyGatewayUsage_UsageDatapoint[]][] {
277+
const series: Record<string, CodyGatewayUsage_UsageDatapoint[]> = {}
282278
for (const entry of data) {
283279
if (!series[entry.model]) {
284280
series[entry.model] = []
@@ -411,17 +407,12 @@ const RateLimitRow: React.FunctionComponent<RateLimitRowProps> = ({
411407
}
412408

413409
interface RateLimitUsageProps {
414-
productSubscriptionUUID: string
410+
usageQuery: UseQueryResult<GetCodyGatewayUsageResponse, ConnectError>
415411
mode: 'completions' | 'embeddings'
416412
}
417413

418-
const RateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ productSubscriptionUUID }) => {
419-
const { data, loading, error } = useQuery<
420-
DotComProductSubscriptionCodyGatewayCompletionsUsageResult,
421-
DotComProductSubscriptionCodyGatewayCompletionsUsageVariables
422-
>(DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_COMPLETIONS_USAGE, { variables: { uuid: productSubscriptionUUID } })
423-
424-
if (loading && !data) {
414+
const RateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ usageQuery: { data, isLoading, error } }) => {
415+
if (isLoading && !data) {
425416
return (
426417
<>
427418
<H5 className="mb-2">Usage</H5>
@@ -439,8 +430,7 @@ const RateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ productS
439430
)
440431
}
441432

442-
const { codyGatewayAccess } = data!.dotcom.productSubscription
443-
433+
const usage = data?.usage
444434
return (
445435
<>
446436
<H5 className="mb-2">Usage</H5>
@@ -450,28 +440,28 @@ const RateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ productS
450440
width={width}
451441
height={200}
452442
series={[
453-
...generateSeries(codyGatewayAccess.chatCompletionsRateLimit?.usage ?? []).map(
454-
([model, data]): Series<CodyGatewayRateLimitUsageDatapoint> => ({
443+
...generateSeries(usage?.chatCompletionsUsage ?? []).map(
444+
([model, data]): Series<CodyGatewayUsage_UsageDatapoint> => ({
455445
data,
456446
getXValue(datum) {
457-
return parseISO(datum.date)
447+
return datum.time?.toDate() || new Date()
458448
},
459449
getYValue(datum) {
460-
return Number(datum.count)
450+
return Number(datum.usage)
461451
},
462452
id: 'chat-usage',
463453
name: 'Chat completions: ' + model,
464454
color: 'var(--purple)',
465455
})
466456
),
467-
...generateSeries(codyGatewayAccess.codeCompletionsRateLimit?.usage ?? []).map(
468-
([model, data]): Series<CodyGatewayRateLimitUsageDatapoint> => ({
457+
...generateSeries(data?.usage?.codeCompletionsUsage ?? []).map(
458+
([model, data]): Series<CodyGatewayUsage_UsageDatapoint> => ({
469459
data,
470460
getXValue(datum) {
471-
return parseISO(datum.date)
461+
return datum.time?.toDate() || new Date()
472462
},
473463
getYValue(datum) {
474-
return Number(datum.count)
464+
return Number(datum.usage)
475465
},
476466
id: 'code-completions-usage',
477467
name: 'Code completions: ' + model,
@@ -486,13 +476,10 @@ const RateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ productS
486476
)
487477
}
488478

489-
const EmbeddingsRateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({ productSubscriptionUUID }) => {
490-
const { data, loading, error } = useQuery<
491-
DotComProductSubscriptionCodyGatewayEmbeddingsUsageResult,
492-
DotComProductSubscriptionCodyGatewayEmbeddingsUsageVariables
493-
>(DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_EMBEDDINGS_USAGE, { variables: { uuid: productSubscriptionUUID } })
494-
495-
if (loading && !data) {
479+
const EmbeddingsRateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = ({
480+
usageQuery: { data, isLoading, error },
481+
}) => {
482+
if (isLoading && !data) {
496483
return (
497484
<>
498485
<H5 className="mb-2">Usage</H5>
@@ -510,8 +497,7 @@ const EmbeddingsRateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = (
510497
)
511498
}
512499

513-
const { codyGatewayAccess } = data!.dotcom.productSubscription
514-
500+
const usage = data?.usage
515501
return (
516502
<>
517503
<H5 className="mb-2">Usage</H5>
@@ -521,16 +507,16 @@ const EmbeddingsRateLimitUsage: React.FunctionComponent<RateLimitUsageProps> = (
521507
width={width}
522508
height={200}
523509
series={[
524-
...generateSeries(codyGatewayAccess.embeddingsRateLimit?.usage ?? []).map(
525-
([model, data]): Series<CodyGatewayRateLimitUsageDatapoint> => ({
510+
...generateSeries(usage?.embeddingsUsage ?? []).map(
511+
([model, data]): Series<CodyGatewayUsage_UsageDatapoint> => ({
526512
data,
527513
getXValue(datum) {
528-
return parseISO(datum.date)
514+
return datum.time?.toDate() || new Date()
529515
},
530516
getYValue(datum) {
531-
return Number(datum.count)
517+
return Number(datum.usage)
532518
},
533-
id: 'chat-usage',
519+
id: 'embeddings-usage',
534520
name: 'Embedded tokens: ' + model,
535521
color: 'var(--purple)',
536522
})

client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { catchError, concatMap, map, tap } from 'rxjs/operators'
77

88
import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common'
99
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
10-
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
10+
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
1111
import { Button, useEventObservable, Link, Alert, Icon, Form, Container, PageHeader } from '@sourcegraph/wildcard'
1212

1313
import type { AuthenticatedUser } from '../../../../auth'

client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'
66
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
77
import { logger } from '@sourcegraph/common'
88
import { useMutation, useQuery } from '@sourcegraph/http-client'
9-
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
9+
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
1010
import { Button, LoadingSpinner, Link, Icon, ErrorAlert, PageHeader, Container, H3, Text } from '@sourcegraph/wildcard'
1111

1212
import {
@@ -36,6 +36,7 @@ import {
3636
useProductSubscriptionLicensesConnection,
3737
} from './backend'
3838
import { CodyServicesSection } from './CodyServicesSection'
39+
import type { EnterprisePortalEnvironment } from './enterpriseportal'
3940
import { SiteAdminGenerateProductLicenseForSubscriptionForm } from './SiteAdminGenerateProductLicenseForSubscriptionForm'
4041
import { SiteAdminProductLicenseNode } from './SiteAdminProductLicenseNode'
4142
import { accessTokenPath, errorForPath, enterprisePortalID } from './utils'
@@ -122,6 +123,20 @@ export const SiteAdminProductSubscriptionPage: React.FunctionComponent<React.Pro
122123

123124
const productSubscription = data!.dotcom.productSubscription
124125

126+
/**
127+
* TODO(@robert): As part of https://linear.app/sourcegraph/issue/CORE-100,
128+
* eventually dev subscriptions will only live on Enterprise Portal dev and
129+
* prod subscriptions will only live on Enterprise Portal prod. Until we
130+
* cut over, we use license tags to determine what Enterprise Portal
131+
* environment to target.
132+
*/
133+
const enterprisePortalEnvironment: EnterprisePortalEnvironment =
134+
window.context.deployType === 'dev'
135+
? 'local'
136+
: productSubscription.activeLicense?.info?.tags?.includes('dev')
137+
? 'dev'
138+
: 'prod'
139+
125140
return (
126141
<>
127142
<div className="site-admin-product-subscription-page">
@@ -198,6 +213,7 @@ export const SiteAdminProductSubscriptionPage: React.FunctionComponent<React.Pro
198213
</Container>
199214

200215
<CodyServicesSection
216+
enterprisePortalEnvironment={enterprisePortalEnvironment}
201217
viewerCanAdminister={true}
202218
currentSourcegraphAccessToken={productSubscription.currentSourcegraphAccessToken}
203219
accessTokenError={errorForPath(error, accessTokenPath)}

client/web/src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts

-67
Original file line numberDiff line numberDiff line change
@@ -67,73 +67,6 @@ export const CODY_GATEWAY_ACCESS_FIELDS_FRAGMENT = gql`
6767
}
6868
`
6969

70-
const CODY_GATEWAY_RATE_LIMIT_USAGE_FIELDS = gql`
71-
fragment CodyGatewayRateLimitUsageFields on CodyGatewayRateLimit {
72-
usage {
73-
...CodyGatewayRateLimitUsageDatapoint
74-
}
75-
}
76-
77-
fragment CodyGatewayRateLimitUsageDatapoint on CodyGatewayUsageDatapoint {
78-
date
79-
count
80-
model
81-
}
82-
`
83-
84-
export const CODY_GATEWAY_ACCESS_COMPLETIONS_USAGE_FIELDS_FRAGMENT = gql`
85-
fragment CodyGatewayAccessCompletionsUsageFields on CodyGatewayAccess {
86-
codeCompletionsRateLimit {
87-
...CodyGatewayRateLimitUsageFields
88-
}
89-
chatCompletionsRateLimit {
90-
...CodyGatewayRateLimitUsageFields
91-
}
92-
}
93-
94-
${CODY_GATEWAY_RATE_LIMIT_USAGE_FIELDS}
95-
`
96-
97-
export const DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_COMPLETIONS_USAGE = gql`
98-
query DotComProductSubscriptionCodyGatewayCompletionsUsage($uuid: String!) {
99-
dotcom {
100-
productSubscription(uuid: $uuid) {
101-
id
102-
codyGatewayAccess {
103-
...CodyGatewayAccessCompletionsUsageFields
104-
}
105-
}
106-
}
107-
}
108-
109-
${CODY_GATEWAY_ACCESS_COMPLETIONS_USAGE_FIELDS_FRAGMENT}
110-
`
111-
112-
export const CODY_GATEWAY_ACCESS_EMBEDDINGS_USAGE_FIELDS_FRAGMENT = gql`
113-
fragment CodyGatewayAccessEmbeddingsUsageFields on CodyGatewayAccess {
114-
embeddingsRateLimit {
115-
...CodyGatewayRateLimitUsageFields
116-
}
117-
}
118-
119-
${CODY_GATEWAY_RATE_LIMIT_USAGE_FIELDS}
120-
`
121-
122-
export const DOTCOM_PRODUCT_SUBSCRIPTION_CODY_GATEWAY_EMBEDDINGS_USAGE = gql`
123-
query DotComProductSubscriptionCodyGatewayEmbeddingsUsage($uuid: String!) {
124-
dotcom {
125-
productSubscription(uuid: $uuid) {
126-
id
127-
codyGatewayAccess {
128-
...CodyGatewayAccessEmbeddingsUsageFields
129-
}
130-
}
131-
}
132-
}
133-
134-
${CODY_GATEWAY_ACCESS_EMBEDDINGS_USAGE_FIELDS_FRAGMENT}
135-
`
136-
13770
export const DOTCOM_PRODUCT_SUBSCRIPTION = gql`
13871
query DotComProductSubscription($uuid: String!) {
13972
dotcom {

0 commit comments

Comments
 (0)