From 6e828b0a14d34bdd4b5de3d24094c3b45869d97c Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Wed, 14 Aug 2024 08:32:22 -0700 Subject: [PATCH] feat/dotcom: use Enterprise Portal for all subscriptions UI (#64115) Overview Loom: 1. https://www.loom.com/share/988465de1c3a4caaa08fc6b22ffb74e5?sid=860f28cd-4dfb-4758-9bd9-d2f485ceb317 2. Announcement: https://www.loom.com/share/8560388e0a4a404caf67d908820ed0d0?sid=6482815f-ac60-4251-95cc-307f527a60dd tl;dr ![image](https://github.com/user-attachments/assets/542c5b64-5729-4a6a-91b2-e0373abb35fa) ![image](https://github.com/user-attachments/assets/9cc41a80-2ff1-4845-a290-823df242cd2e) ![image](https://github.com/user-attachments/assets/47c9a400-d091-416f-a85c-f1dcc93d880d) TODO: - [x] Fix the "edit instance type" input - [x] ~Subscription view: reload on changing the Enterprise Portal environment selector~ -> https://linear.app/sourcegraph/issue/CORE-245/dotcom-subscriptions-ui-enterprise-portal-instance-selector-doesnt, use a jank reload for now - [x] CI Closes https://linear.app/sourcegraph/issue/CORE-100/enterprise-portal-migrate-away-from-dotcom-db-as-source-of-truth ## Test plan Manual testing for UI via `sg start dotcom` -> https://sourcegraph.test:3443/site-admin/dotcom/product/subscriptions The PRs this PR is stacked on top of implement testing of the backend capabilities --- client/web/BUILD.bazel | 12 +- .../productSubscriptions/AccountName.tsx | 24 - .../ProductLicenseValidity.tsx | 32 +- .../ProductSubscriptionLabel.tsx | 17 +- .../ProductSubscriptionNode.tsx | 60 - .../ProductLicenseInfoDescription.tsx | 17 - .../customers/SiteAdminCustomersPage.tsx | 107 - .../CodyServicesSection.tsx | 21 +- .../EnterprisePortalEnvSelector.tsx | 47 + .../EnterprisePortalEnvWarning.tsx | 40 + .../InstanceTypeBadge.tsx | 38 + ...SiteAdminCreateProductSubscriptionPage.tsx | 406 ++-- ...ProductLicenseForSubscriptionForm.test.tsx | 28 - ...erateProductLicenseForSubscriptionForm.tsx | 308 +-- .../SiteAdminLicenseKeyLookupPage.tsx | 216 +- .../SiteAdminProductLicenseNode.test.tsx | 95 - .../SiteAdminProductLicenseNode.tsx | 166 +- ...teAdminProductSubscriptionNode.module.scss | 9 + .../SiteAdminProductSubscriptionNode.tsx | 97 +- .../SiteAdminProductSubscriptionPage.tsx | 806 ++++++-- .../SiteAdminProductSubscriptionsPage.tsx | 214 +- .../SiteAdminProductLicenseNode.test.tsx.snap | 449 ----- .../dotcom/productSubscriptions/backend.ts | 275 --- .../productSubscriptions/enterpriseportal.ts | 160 +- ...tions-SubscriptionsService_connectquery.ts | 176 ++ .../enterpriseportalgen/subscriptions_pb.ts | 1794 +++++++++++++++++ .../dotcom/productSubscriptions/utils.ts | 8 - client/web/src/site-admin/routes.tsx | 9 - client/web/src/site-admin/sidebaritems.ts | 5 - .../internal/database/importer/importer.go | 2 +- cmd/enterprise-portal/service/config.go | 2 +- .../subscriptions/v1/buf.gen.yaml | 8 + 32 files changed, 3846 insertions(+), 1802 deletions(-) delete mode 100644 client/web/src/enterprise/dotcom/productSubscriptions/AccountName.tsx delete mode 100644 client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionNode.tsx delete mode 100644 client/web/src/enterprise/productSubscription/ProductLicenseInfoDescription.tsx delete mode 100644 client/web/src/enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage.tsx create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvSelector.tsx create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvWarning.tsx create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/InstanceTypeBadge.tsx delete mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.test.tsx delete mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductLicenseNode.test.tsx create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.module.scss delete mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/__snapshots__/SiteAdminProductLicenseNode.test.tsx.snap delete mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions-SubscriptionsService_connectquery.ts create mode 100644 client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb.ts diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index cd5710db508..3119a574ef8 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -701,10 +701,8 @@ ts_project( "src/enterprise/codeintel/sort.ts", "src/enterprise/codeintel/useCodeIntel.ts", "src/enterprise/codeintel/useSearchBasedCodeIntel.ts", - "src/enterprise/dotcom/productSubscriptions/AccountName.tsx", "src/enterprise/dotcom/productSubscriptions/ProductLicenseValidity.tsx", "src/enterprise/dotcom/productSubscriptions/ProductSubscriptionLabel.tsx", - "src/enterprise/dotcom/productSubscriptions/ProductSubscriptionNode.tsx", "src/enterprise/dotcom/productSubscriptions/features.ts", "src/enterprise/embed/EmbeddedWebApp.tsx", "src/enterprise/embed/OpenNewTabAnchorLink.tsx", @@ -981,7 +979,6 @@ ts_project( "src/enterprise/productSubscription/ExpirationDate.tsx", "src/enterprise/productSubscription/LicenseGenerationKeyWarning.tsx", "src/enterprise/productSubscription/ProductCertificate.tsx", - "src/enterprise/productSubscription/ProductLicenseInfoDescription.tsx", "src/enterprise/productSubscription/ProductLicenseTags.tsx", "src/enterprise/productSubscription/TrueUpStatusSummary.tsx", "src/enterprise/rbac/SiteAdminRolesPage.tsx", @@ -1025,9 +1022,11 @@ ts_project( "src/enterprise/site-admin/UserManagement/backend.ts", "src/enterprise/site-admin/UserManagement/components/RoleAssignmentModal.tsx", "src/enterprise/site-admin/backend.ts", - "src/enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/CodyGatewayRateLimitModal.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx", + "src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvSelector.tsx", + "src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvWarning.tsx", + "src/enterprise/site-admin/dotcom/productSubscriptions/InstanceTypeBadge.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminLicenseKeyLookupPage.tsx", @@ -1035,10 +1034,11 @@ ts_project( "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx", - "src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/codyaccess-CodyAccessService_connectquery.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/codyaccess_pb.ts", + "src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions-SubscriptionsService_connectquery.ts", + "src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/plandata.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/testUtils.ts", "src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts", @@ -1944,8 +1944,6 @@ ts_project( "src/enterprise/repo/settings/RepoSettingsLogsPage.test.tsx", "src/enterprise/repo/settings/utils.test.ts", "src/enterprise/searchContexts/SearchContextsList.test.tsx", - "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.test.tsx", - "src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductLicenseNode.test.tsx", "src/enterprise/site-admin/dotcom/productSubscriptions/utils.test.ts", "src/enterprise/user/settings/auth/UserSettingsPermissionsPage.test.tsx", "src/featureFlags/lib/parseUrlOverrideFeatureFlags.test.ts", diff --git a/client/web/src/enterprise/dotcom/productSubscriptions/AccountName.tsx b/client/web/src/enterprise/dotcom/productSubscriptions/AccountName.tsx deleted file mode 100644 index c6cd542b947..00000000000 --- a/client/web/src/enterprise/dotcom/productSubscriptions/AccountName.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' - -import { Link } from '@sourcegraph/wildcard' - -import type { ProductLicenseSubscriptionAccount } from '../../../graphql-operations' -import { userURL } from '../../../user' - -/** - * Displays the account name as a link. - */ -export const AccountName: React.FunctionComponent< - React.PropsWithChildren<{ - account: Pick | null - link?: string - }> -> = ({ account, link }) => - account ? ( - <> - {account.username}{' '} - {account.displayName && `(${account.displayName})`} - - ) : ( - (Account deleted) - ) diff --git a/client/web/src/enterprise/dotcom/productSubscriptions/ProductLicenseValidity.tsx b/client/web/src/enterprise/dotcom/productSubscriptions/ProductLicenseValidity.tsx index 8fb9b2a0a58..f7d0624fade 100644 --- a/client/web/src/enterprise/dotcom/productSubscriptions/ProductLicenseValidity.tsx +++ b/client/web/src/enterprise/dotcom/productSubscriptions/ProductLicenseValidity.tsx @@ -6,8 +6,12 @@ import classNames from 'classnames' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { Icon, Label } from '@sourcegraph/wildcard' -import type { ProductLicenseFields } from '../../../graphql-operations' import { isProductLicenseExpired } from '../../../productSubscription/helpers' +import { + EnterpriseSubscriptionLicenseCondition_Status, + type EnterpriseSubscriptionLicenseKey_Info, + type EnterpriseSubscriptionLicenseCondition, +} from '../../site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb' const getIcon = (isExpired: boolean, isRevoked: boolean): string => { if (isExpired) { @@ -46,32 +50,36 @@ const getText = (isExpired: boolean, isRevoked: boolean): string => { */ export const ProductLicenseValidity: React.FunctionComponent< React.PropsWithChildren<{ - license: ProductLicenseFields + licenseInfo: EnterpriseSubscriptionLicenseKey_Info | undefined + licenseConditions: EnterpriseSubscriptionLicenseCondition[] variant?: 'icon-only' | 'no-icon' className?: string }> -> = ({ license: { info, revokedAt, revokeReason }, variant, className = '' }) => { - const expiresAt = info?.expiresAt ?? 0 +> = ({ licenseInfo: info, licenseConditions: conditions, variant, className = '' }) => { + const expiresAt = info?.expireTime?.toDate() ?? 0 const isExpired = isProductLicenseExpired(expiresAt) - const isRevoked = !!revokedAt - const timestamp = revokedAt ?? expiresAt - const timestampSuffix = isExpired || isRevoked ? 'ago' : 'remaining' + + const revoked = conditions.find( + condition => condition.status === EnterpriseSubscriptionLicenseCondition_Status.REVOKED + ) + const timestamp = revoked?.lastTransitionTime?.toDate() ?? expiresAt + const timestampSuffix = isExpired || revoked ? 'ago' : 'remaining' if (variant === 'icon-only') { return (
- +
) } return (
- {variant !== 'no-icon' && } - {getText(isExpired, isRevoked)}, {' '} + {variant !== 'no-icon' && } + {getText(isExpired, !!revoked)}, {' '} {timestampSuffix} - {!isExpired && isRevoked && revokeReason && ( + {revoked?.message && ( <> - {revokeReason} + {revoked.message} )}
diff --git a/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionLabel.tsx b/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionLabel.tsx index d972b3d061a..3e3b8270ab0 100644 --- a/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionLabel.tsx +++ b/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionLabel.tsx @@ -1,6 +1,5 @@ import React from 'react' -import type { ProductSubscriptionFields, SiteAdminProductSubscriptionFields } from '../../../graphql-operations' import { formatUserCount } from '../../../productSubscription/helpers' /** @@ -9,18 +8,20 @@ import { formatUserCount } from '../../../productSubscription/helpers' */ export const ProductSubscriptionLabel: React.FunctionComponent< React.PropsWithChildren<{ - productSubscription: ProductSubscriptionFields | SiteAdminProductSubscriptionFields + productName?: string + userCount?: bigint className?: string }> -> = ({ productSubscription, className = '' }) => ( +> = ({ productName, userCount, className = '' }) => ( - {productSubscription.activeLicense?.info ? ( - <> - {productSubscription.activeLicense.info.productNameWithBrand} ( - {formatUserCount(productSubscription.activeLicense.info.userCount)}) - + {productName && userCount ? ( + <>{productSubscriptionLabel(productName, userCount)} ) : ( No plan selected )} ) + +export function productSubscriptionLabel(productName?: string, userCount?: bigint): string { + return `${productName} (${formatUserCount(Number(userCount))})` +} diff --git a/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionNode.tsx b/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionNode.tsx deleted file mode 100644 index 42c7984cc43..00000000000 --- a/client/web/src/enterprise/dotcom/productSubscriptions/ProductSubscriptionNode.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react' - -import { gql } from '@sourcegraph/http-client' -import { Link } from '@sourcegraph/wildcard' - -import type { ProductSubscriptionFields } from '../../../graphql-operations' - -import { ProductSubscriptionLabel } from './ProductSubscriptionLabel' - -export const productSubscriptionFragment = gql` - fragment ProductSubscriptionFields on ProductSubscription { - id - name - account { - id - username - displayName - } - activeLicense { - licenseKey - info { - productNameWithBrand - tags - userCount - expiresAt - } - } - createdAt - isArchived - url - } -` - -export const ProductSubscriptionNodeHeader: React.FunctionComponent> = () => ( - - - ID - Plan - - -) - -export interface ProductSubscriptionNodeProps { - node: ProductSubscriptionFields -} - -export const ProductSubscriptionNode: React.FunctionComponent< - React.PropsWithChildren -> = ({ node }) => ( - - - - {node.name} - - - - - - -) diff --git a/client/web/src/enterprise/productSubscription/ProductLicenseInfoDescription.tsx b/client/web/src/enterprise/productSubscription/ProductLicenseInfoDescription.tsx deleted file mode 100644 index 1d27c744ca1..00000000000 --- a/client/web/src/enterprise/productSubscription/ProductLicenseInfoDescription.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' - -import { H3 } from '@sourcegraph/wildcard' - -import type { ProductLicenseInfoFields } from '../../graphql-operations' -import { formatUserCount } from '../../productSubscription/helpers' - -export const ProductLicenseInfoDescription: React.FunctionComponent< - React.PropsWithChildren<{ - licenseInfo: ProductLicenseInfoFields - className?: string - }> -> = ({ licenseInfo, className = '' }) => ( -

- {licenseInfo.productNameWithBrand} ({formatUserCount(licenseInfo.userCount)}) -

-) diff --git a/client/web/src/enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage.tsx b/client/web/src/enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage.tsx deleted file mode 100644 index e539394ef25..00000000000 --- a/client/web/src/enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect, useMemo } from 'react' - -import { type Observable, Subject } from 'rxjs' -import { map } from 'rxjs/operators' - -import { createAggregateError } from '@sourcegraph/common' -import { gql } from '@sourcegraph/http-client' -import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' -import { H2 } from '@sourcegraph/wildcard' - -import { queryGraphQL } from '../../../../backend/graphql' -import { FilteredConnection } from '../../../../components/FilteredConnection' -import { PageTitle } from '../../../../components/PageTitle' -import type { CustomerFields, CustomersResult, CustomersVariables } from '../../../../graphql-operations' -import { userURL } from '../../../../user' -import { AccountName } from '../../../dotcom/productSubscriptions/AccountName' - -const siteAdminCustomerFragment = gql` - fragment CustomerFields on User { - id - username - displayName - } -` - -interface SiteAdminCustomerNodeProps { - node: CustomerFields -} - -/** - * Displays a customer in a connection in the site admin area. - */ -const SiteAdminCustomerNode: React.FunctionComponent> = ({ - node, -}) => ( -
  • -
    - - - -
    -
  • -) - -interface Props extends TelemetryV2Props {} - -/** - * Displays a list of customers associated with user accounts on Sourcegraph.com. - */ -export const SiteAdminProductCustomersPage: React.FunctionComponent> = props => { - useEffect(() => props.telemetryRecorder.recordEvent('admin.customers', 'view'), [props.telemetryRecorder]) - - const updates = useMemo(() => new Subject(), []) - const nodeProps: Pick> = {} - - return ( -
    - -
    -

    Customers

    -
    - > - > - className="list-group list-group-flush mt-3" - noun="customer" - pluralNoun="customers" - queryConnection={queryCustomers} - nodeComponent={SiteAdminCustomerNode} - nodeComponentProps={nodeProps} - noSummaryIfAllNodesVisible={true} - updates={updates} - /> -
    - ) -} - -function queryCustomers(args: Partial): Observable { - return queryGraphQL( - gql` - query Customers($first: Int, $query: String) { - users(first: $first, query: $query) { - nodes { - ...CustomerFields - } - totalCount - pageInfo { - hasNextPage - } - } - } - ${siteAdminCustomerFragment} - `, - { - first: args.first, - query: args.query, - } - ).pipe( - map(({ data, errors }) => { - if (!data?.users || (errors && errors.length > 0)) { - throw createAggregateError(errors) - } - return data.users - }) - ) -} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx index 66586dcfe92..095f5edbbb8 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/CodyServicesSection.tsx @@ -9,7 +9,6 @@ import { logger } from '@sourcegraph/common' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { H3, - ProductStatusBadge, Container, Text, H4, @@ -112,21 +111,31 @@ export const CodyServicesSection: React.FunctionComponent = ({ telemetryRecorder, ]) + const header =

    Cody services

    + if (getCodyGatewayAccessLoading && !codyGatewayAccessResponse) { - return + return ( + <> + {header} + + + ) } if (getCodyGatewayAccessError) { - return + return ( + <> + {header} + + + ) } const { access: codyGatewayAccess } = codyGatewayAccessResponse! return ( <> -

    - Cody services -

    + {header} <>
    diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvSelector.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvSelector.tsx new file mode 100644 index 00000000000..34053bfac28 --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvSelector.tsx @@ -0,0 +1,47 @@ +import React from 'react' + +import { mdiInformationOutline } from '@mdi/js' + +import { Icon, Select, Tooltip } from '@sourcegraph/wildcard' + +import type { EnterprisePortalEnvironment } from './enterpriseportal' + +interface Props { + env: EnterprisePortalEnvironment | undefined + setEnv: (env: EnterprisePortalEnvironment) => void +} + +export const EnterprisePortalEnvSelector: React.FunctionComponent = ({ env, setEnv }) => ( + +) + +export function getDefaultEnterprisePortalEnv(): EnterprisePortalEnvironment { + return window.context.deployType === 'dev' ? 'local' : 'prod' +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvWarning.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvWarning.tsx new file mode 100644 index 00000000000..5b51325dfe5 --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/EnterprisePortalEnvWarning.tsx @@ -0,0 +1,40 @@ +import { Alert, Text } from '@sourcegraph/wildcard' + +import type { EnterprisePortalEnvironment } from './enterpriseportal' + +export interface EnterprisePortalEnvWarningProps { + env: EnterprisePortalEnvironment + /** + * For example, 'creating a subscription' - this will be inserted into the + * warning text. + */ + actionText: string + + className?: string +} + +/** + * Displays a warning about the user's action if the selected env is not 'prod'. + */ +export const EnterprisePortalEnvWarning: React.FunctionComponent = ({ + env, + actionText, + className, +}) => { + if (env === 'prod') { + return null + } + return ( + + + You are {actionText} for the {env} Enterprise Portal deployment. Everything you do here + will only be visible to non-production environments that connect to this specific Enterprise Portal + deployment. + + + If you are {actionText} for a customer, select Production from the "Enterprise Portal" + dropdown in the top right + + + ) +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/InstanceTypeBadge.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/InstanceTypeBadge.tsx new file mode 100644 index 00000000000..4c84f87b97f --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/InstanceTypeBadge.tsx @@ -0,0 +1,38 @@ +import { Badge, type BadgeVariantType } from '@sourcegraph/wildcard' + +import { EnterpriseSubscriptionInstanceType } from './enterpriseportalgen/subscriptions_pb' + +export interface InstanceTypeBadgeProps { + instanceType: EnterpriseSubscriptionInstanceType + className?: string +} + +/** + * Displays instance type in a cute badge with relevant tooltips. + */ +export const InstanceTypeBadge: React.FunctionComponent = ({ instanceType, className }) => { + let variant: BadgeVariantType = 'outlineSecondary' + let tooltip = '' + switch (instanceType) { + case EnterpriseSubscriptionInstanceType.INTERNAL: { + tooltip = 'This subscription is for Sourcegraph-internal instances.' + break + } + case EnterpriseSubscriptionInstanceType.PRIMARY: { + variant = 'primary' + tooltip = "This subscription is for a customer's primary, production Sourcegraph instance." + break + } + case EnterpriseSubscriptionInstanceType.SECONDARY: { + variant = 'info' + tooltip = + "This subscription is for a customer's secondary Sourcegraph instance, such as one for staging new releases." + break + } + } + return ( + + {EnterpriseSubscriptionInstanceType[instanceType]} + + ) +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx index 1771685e184..3b5a6dfe5ee 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx @@ -1,118 +1,35 @@ -import React, { useCallback, useEffect } from 'react' +import React, { useEffect, useMemo, useState } from 'react' -import { mdiPlus } from '@mdi/js' -import { Navigate } from 'react-router-dom' -import { merge, of, type Observable } from 'rxjs' -import { catchError, concatMap, map, tap } from 'rxjs/operators' +import { QueryClientProvider } from '@tanstack/react-query' +import { Navigate, useSearchParams } from 'react-router-dom' -import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common' -import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' -import { Button, useEventObservable, Link, Alert, Icon, Form, Container, PageHeader } from '@sourcegraph/wildcard' +import { + Alert, + Form, + Container, + PageHeader, + useForm, + Select, + ErrorAlert, + getDefaultInputProps, + useField, + createRequiredValidator, + composeValidators, + Input, + Text, + Label, + type ValidationResult, +} from '@sourcegraph/wildcard' import type { AuthenticatedUser } from '../../../../auth' -import { mutateGraphQL, queryGraphQL } from '../../../../backend/graphql' -import { FilteredConnection } from '../../../../components/FilteredConnection' +import { LoaderButton } from '../../../../components/LoaderButton' import { PageTitle } from '../../../../components/PageTitle' -import type { - CreateProductSubscriptionVariables, - ProductSubscriptionAccountsResult, - ProductSubscriptionAccountsVariables, - ProductSubscriptionAccountFields, - CreateProductSubscriptionResult, -} from '../../../../graphql-operations' -interface UserCreateSubscriptionNodeProps extends TelemetryV2Props { - /** - * The user to display in this list item. - */ - node: ProductSubscriptionAccountFields - authenticatedUser: AuthenticatedUser -} - -const createProductSubscription = ( - args: CreateProductSubscriptionVariables -): Observable => - mutateGraphQL( - gql` - mutation CreateProductSubscription($accountID: ID!) { - dotcom { - createProductSubscription(accountID: $accountID) { - urlForSiteAdmin - uuid - } - } - } - `, - args - ).pipe( - map(dataOrThrowErrors), - map(data => data.dotcom.createProductSubscription) - ) - -const UserCreateSubscriptionNode: React.FunctionComponent> = ( - props: UserCreateSubscriptionNodeProps -) => { - const [onSubmit, createdSubscription] = useEventObservable( - useCallback( - ( - submits: Observable> - ): Observable< - CreateProductSubscriptionResult['dotcom']['createProductSubscription'] | 'saving' | ErrorLike - > => - submits.pipe( - tap(event => event.preventDefault()), - tap(() => props.telemetryRecorder.recordEvent('admin.productSubscriptions', 'create')), - concatMap(() => - merge( - of('saving' as const), - createProductSubscription({ accountID: props.node.id }).pipe( - catchError(error => [asError(error)]) - ) - ) - ) - ), - [props.node.id, props.telemetryRecorder] - ) - ) - - return ( - <> - {createdSubscription && - createdSubscription !== 'saving' && - !isErrorLike(createdSubscription) && - createdSubscription.urlForSiteAdmin && ( - - )} -
  • -
    -
    - {props.node.username} -
    -
    -
    - -
    -
    -
    - {isErrorLike(createdSubscription) && {createdSubscription.message}} - {createdSubscription && - createdSubscription !== 'saving' && - !isErrorLike(createdSubscription) && - !createdSubscription.urlForSiteAdmin && ( - No subscription URL available (only accessible to site admins) - )} -
  • - - ) -} +import { type EnterprisePortalEnvironment, useCreateEnterpriseSubscription, queryClient } from './enterpriseportal' +import { getDefaultEnterprisePortalEnv, EnterprisePortalEnvSelector } from './EnterprisePortalEnvSelector' +import { EnterprisePortalEnvWarning } from './EnterprisePortalEnvWarning' +import { EnterpriseSubscriptionInstanceType } from './enterpriseportalgen/subscriptions_pb' interface Props extends TelemetryV2Props { authenticatedUser: AuthenticatedUser @@ -125,51 +42,244 @@ interface Props extends TelemetryV2Props { */ export const SiteAdminCreateProductSubscriptionPage: React.FunctionComponent< React.PropsWithChildren -> = props => { +> = props => ( + + + +) + +interface FormData { + displayName: string + salesforceSubscriptionID: string + instanceDomain: string + instanceType: EnterpriseSubscriptionInstanceType + message: string +} + +const QUERY_PARAM_ENV = 'env' + +const DISPLAY_NAME_VALIDATOR = createRequiredValidator( + 'Brief, human-friendly, globally unique name for this subscription is required. This can be changed later.' +) + +const MESSAGE_VALIDATOR = createRequiredValidator('A message about the creation of this subscription is required.') + +const SALESFORCE_SUBSCRIPTION_ID_VALIDATOR: (value: string | undefined) => ValidationResult = value => { + if (!value) { + return // not required + } + if (!value.startsWith('a1a')) { + return 'Salesforce subscription ID must start with "a1a"' + } + if (value.length < 17) { + return 'Salesforce subscription ID must be 17 characters long' + } + return +} + +const Page: React.FunctionComponent> = props => { useEffect(() => props.telemetryRecorder.recordEvent('admin.productSubscriptions.create', 'view')) + + const [searchParams, setSearchParams] = useSearchParams() + const [env, setEnv] = useState( + (searchParams.get(QUERY_PARAM_ENV) as EnterprisePortalEnvironment) || getDefaultEnterprisePortalEnv() + ) + useEffect(() => { + searchParams.set(QUERY_PARAM_ENV, env) + setSearchParams(searchParams) + }, [env, setSearchParams, searchParams]) + + const { mutateAsync: createSubscription, error, data: createdSubscription } = useCreateEnterpriseSubscription(env) + + const { + formAPI, + ref: formRef, + handleSubmit, + } = useForm({ + initialValues: { + displayName: '', + message: '', + salesforceSubscriptionID: '', + instanceDomain: '', + instanceType: EnterpriseSubscriptionInstanceType.PRIMARY, + }, + onSubmit: async ({ + message, + displayName, + instanceDomain, + salesforceSubscriptionID, + instanceType, + }: FormData) => { + props.telemetryRecorder.recordEvent('admin.productSubscriptions', 'create') + await createSubscription({ + message, + subscription: { + displayName, + instanceDomain, + instanceType, + salesforce: salesforceSubscriptionID + ? { + subscriptionId: salesforceSubscriptionID, + } + : undefined, + }, + }) + }, + }) + + const displayName = useField({ + name: 'displayName', + formApi: formAPI, + validators: { + sync: DISPLAY_NAME_VALIDATOR, + }, + }) + + const message = useField({ + name: 'message', + formApi: formAPI, + validators: { + sync: MESSAGE_VALIDATOR, + }, + }) + + const instanceType = useField({ + name: 'instanceType', + formApi: formAPI, + }) + + const salesforceSubscriptionID = useField({ + name: 'salesforceSubscriptionID', + formApi: formAPI, + validators: { + sync: useMemo( + () => + composeValidators([ + value => { + if (instanceType.input.value !== EnterpriseSubscriptionInstanceType.INTERNAL && !value) { + return 'Salesforce subscription ID is required for non-internal instances.' + } + return + }, + SALESFORCE_SUBSCRIPTION_ID_VALIDATOR, + ]), + [instanceType.input.value] + ), + }, + }) + + const instanceDomain = useField({ + name: 'instanceDomain', + formApi: formAPI, + }) + + // A subscription was created, navigate to the management page + if (createdSubscription?.subscription) { + return ( + + ) + } + return (
    - - + + } + /> - - {...props} - className="list-group list-group-flush" - noun="user" - pluralNoun="users" - queryConnection={queryAccounts} - nodeComponent={UserCreateSubscriptionNode} - nodeComponentProps={props} - /> + {error && } + + + You are creating an Enterprise subscription for a SINGLE Sourcegraph instance. + Customers with multiple Sourcegraph instances should have a separate subscription for each.{' '} + Each subscription should only have licenses for a SINGLE Sourcegraph instance. + + + The Salesforce subscription ID can be set to link multiple Enterprise subscriptions + corresponding to a single customer. + + + + +
    + + + + + + +
    ) } - -function queryAccounts( - args: Partial -): Observable { - return queryGraphQL( - gql` - query ProductSubscriptionAccounts($first: Int, $query: String) { - users(first: $first, query: $query) { - nodes { - ...ProductSubscriptionAccountFields - } - totalCount - pageInfo { - hasNextPage - } - } - } - fragment ProductSubscriptionAccountFields on User { - id - username - } - `, - args - ).pipe( - map(dataOrThrowErrors), - map(data => data.users) - ) -} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.test.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.test.tsx deleted file mode 100644 index fe63acd291b..00000000000 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { noop } from 'lodash' -import { describe, expect, test } from 'vitest' - -import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' -import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' -import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing' - -import { SiteAdminGenerateProductLicenseForSubscriptionForm } from './SiteAdminGenerateProductLicenseForSubscriptionForm' -import { mockLicense } from './testUtils' - -describe('SiteAdminGenerateProductLicenseForSubscriptionForm', () => { - test('renders', () => { - expect( - renderWithBrandedContext( - - - - ).baseElement - ).toMatchSnapshot() - }) -}) diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.tsx index 2593013a396..c978af02f3c 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminGenerateProductLicenseForSubscriptionForm.tsx @@ -1,14 +1,12 @@ import React, { useState, useCallback } from 'react' +import { Timestamp } from '@bufbuild/protobuf' import { UTCDate } from '@date-fns/utc' -import { mdiChatQuestionOutline } from '@mdi/js' import classNames from 'classnames' import { addDays, endOfDay } from 'date-fns' import { noop } from 'lodash' -import { useMutation } from '@sourcegraph/http-client' -import type { Scalars } from '@sourcegraph/shared/src/graphql-operations' -import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' +import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { Alert, Button, @@ -23,23 +21,18 @@ import { Text, Checkbox, H4, - Icon, - Tooltip, Label, + Container, } from '@sourcegraph/wildcard' import { Collapsible } from '../../../../components/Collapsible' import { LoaderButton } from '../../../../components/LoaderButton' -import type { - GenerateProductLicenseForSubscriptionResult, - GenerateProductLicenseForSubscriptionVariables, - ProductLicenseFields, - ProductLicenseInfoFields, -} from '../../../../graphql-operations' import { ExpirationDate } from '../../../productSubscription/ExpirationDate' import { hasUnknownTags, ProductLicenseTags, UnknownTagWarning } from '../../../productSubscription/ProductLicenseTags' -import { GENERATE_PRODUCT_LICENSE } from './backend' +import { type EnterprisePortalEnvironment, useCreateEnterpriseSubscriptionLicense } from './enterpriseportal' +import { EnterprisePortalEnvWarning } from './EnterprisePortalEnvWarning' +import type { EnterpriseSubscription, EnterpriseSubscriptionLicense } from './enterpriseportalgen/subscriptions_pb' import { ALL_PLANS, TAG_AIR_GAPPED, @@ -52,32 +45,21 @@ import { import styles from './SiteAdminGenerateProductLicenseForSubscriptionForm.module.scss' -// TODO: Maybe a field for a custom comment on what instance the key is for? -// In accordance with: -// Add trial and instance:test or instance:whatever_name_is_appropriate tags, so that we can identify which license keys are test and which are not -// from https://handbook.sourcegraph.com/departments/technical-success/ce/process/license_keys. - -// TODO: Add MAU switch. - -interface License extends Omit { - info: Omit | null -} interface Props extends TelemetryV2Props { - subscriptionID: Scalars['ID'] - subscriptionAccount: string - latestLicense: License | undefined + env: EnterprisePortalEnvironment + subscription: EnterpriseSubscription + latestLicense: EnterpriseSubscriptionLicense | undefined onGenerate: () => void onCancel: () => void } interface FormData { + message: string /** Comma-separated additional license tags. */ tags: string - customer: string - salesforceSubscriptionID: string salesforceOpportunityID: string plan: string - userCount: number + userCount: bigint expiresAt: Date trueUp: boolean trial: boolean @@ -87,31 +69,31 @@ interface FormData { codeInsights: boolean } -const getEmptyFormData = (account: string, latestLicense: License | undefined): FormData => { +const getEmptyFormData = (latestLicense: EnterpriseSubscriptionLicense | undefined): FormData => { + const licenseData = latestLicense?.license?.value const formData: FormData = { + message: '', tags: '', - customer: account, - salesforceSubscriptionID: latestLicense?.info?.salesforceSubscriptionID ?? '', - salesforceOpportunityID: latestLicense?.info?.salesforceOpportunityID ?? '', - plan: latestLicense?.info?.tags.find(tag => tag.startsWith('plan:'))?.slice('plan:'.length) ?? '', - userCount: latestLicense?.info?.userCount ?? 1, + salesforceOpportunityID: licenseData?.info?.salesforceOpportunityId ?? '', + plan: licenseData?.info?.tags.find(tag => tag.startsWith('plan:'))?.slice('plan:'.length) ?? '', + userCount: licenseData?.info?.userCount ?? BigInt(1), expiresAt: endOfDay(new UTCDate(UTCDate.now())), - trial: latestLicense?.info?.tags.includes(TAG_TRIAL.tagValue) ?? false, - trueUp: latestLicense?.info?.tags.includes(TAG_TRUEUP.tagValue) ?? false, - airGapped: latestLicense?.info?.tags.includes(TAG_AIR_GAPPED.tagValue) ?? false, - batchChanges: latestLicense?.info?.tags.includes(TAG_BATCH_CHANGES.tagValue) ?? false, - codeInsights: latestLicense?.info?.tags.includes(TAG_CODE_INSIGHTS.tagValue) ?? false, - disableTelemetry: latestLicense?.info?.tags.includes(TAG_DISABLE_TELEMETRY_EXPORT.tagValue) ?? false, + trial: licenseData?.info?.tags.includes(TAG_TRIAL.tagValue) ?? false, + trueUp: licenseData?.info?.tags.includes(TAG_TRUEUP.tagValue) ?? false, + airGapped: licenseData?.info?.tags.includes(TAG_AIR_GAPPED.tagValue) ?? false, + batchChanges: licenseData?.info?.tags.includes(TAG_BATCH_CHANGES.tagValue) ?? false, + codeInsights: licenseData?.info?.tags.includes(TAG_CODE_INSIGHTS.tagValue) ?? false, + disableTelemetry: licenseData?.info?.tags.includes(TAG_DISABLE_TELEMETRY_EXPORT.tagValue) ?? false, } - if (latestLicense?.info) { + if (licenseData?.info) { // Based on the tag-less formData created above, generate the list of tags to add. // We then only add additional tags for the things that aren't yet expressed, // to avoid duplicates and let the specific flags on form data handle addition // of their tag values. const presentTags = getTagsFromFormData(formData) formData.tags = - latestLicense?.info?.tags + licenseData?.info?.tags .filter(tag => !tag.startsWith('plan:') && !tag.startsWith('customer:') && !presentTags.includes(tag)) .join(',') ?? '' } @@ -136,7 +118,6 @@ const tagsFromString = (tagString: string): string[] => const getTagsFromFormData = (formData: FormData): string[] => Array.from( new Set([ - `customer:${formData.customer}`, ...(formData.plan ? [`plan:${formData.plan}`] : []), ...(formData.trueUp && ALL_PLANS.find(other => other.label === formData.plan)?.additionalTags?.some( @@ -162,9 +143,6 @@ const getTagsForTelemetry = (formData: FormData): { [key: string]: number } => ( disableTelemetry: formData.disableTelemetry ? 1 : 0, }) -const HANDBOOK_INFO_URL = - 'https://handbook.sourcegraph.com/ce/license_keys#how-to-create-a-license-key-for-a-new-prospect-or-new-customer' - /** * Displays a form to generate a new product license for a product subscription. * @@ -172,12 +150,12 @@ const HANDBOOK_INFO_URL = */ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionComponent< React.PropsWithChildren -> = ({ latestLicense, subscriptionID, subscriptionAccount, onGenerate, onCancel, telemetryRecorder }) => { +> = ({ env, latestLicense, subscription, onGenerate, onCancel, telemetryRecorder }) => { const labelId = 'generateLicense' const [hasAcknowledgedInfo, setHasAcknowledgedInfo] = useState(false) - const [formData, setFormData] = useState(getEmptyFormData(subscriptionAccount, latestLicense)) + const [formData, setFormData] = useState(getEmptyFormData(latestLicense)) const onPlanChange = useCallback>( event => setFormData(formData => ({ ...formData, plan: event.target.value })), @@ -189,17 +167,18 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC event => setFormData(formData => ({ ...formData, [key]: event.target.value })), [key] ) - const onCustomerChange = useOnChange('customer') - const onSFSubscriptionIDChange = useOnChange('salesforceSubscriptionID') + const onMessageChange = useOnChange('message') const onSFOpportunityIDChange = useOnChange('salesforceOpportunityID') + const [sfOpportunityIDError, setSFOpportunityIDError] = useState(undefined) + const onTagsChange = useCallback>( event => setFormData(formData => ({ ...formData, tags: event.target.value || '' })), [] ) const onUserCountChange = useCallback>( - event => setFormData(formData => ({ ...formData, userCount: event.target.valueAsNumber })), + event => setFormData(formData => ({ ...formData, userCount: BigInt(event.target.valueAsNumber || 0) })), [] ) @@ -259,7 +238,7 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC const date: Date = dateRegex.test(event.target.value) ? new UTCDate(event.target.value) - : getEmptyFormData(subscriptionAccount, latestLicense).expiresAt + : getEmptyFormData(latestLicense).expiresAt const expiresAt = endOfDay(new UTCDate(date)) @@ -268,31 +247,10 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC expiresAt, })) }, - [subscriptionAccount, latestLicense] + [latestLicense] ) - const [generateLicense, { loading, error }] = useMutation< - GenerateProductLicenseForSubscriptionResult['dotcom']['generateProductLicenseForSubscription'], - GenerateProductLicenseForSubscriptionVariables - >(GENERATE_PRODUCT_LICENSE, { - variables: { - productSubscriptionID: subscriptionID, - license: { - tags: getTagsFromFormData(formData), - userCount: formData.userCount, - expiresAt: Math.floor(formData.expiresAt.getTime() / 1000), - salesforceSubscriptionID: - formData.salesforceSubscriptionID.trim().length > 0 - ? formData.salesforceSubscriptionID.trim() - : undefined, - salesforceOpportunityID: - formData.salesforceOpportunityID.trim().length > 0 - ? formData.salesforceOpportunityID.trim() - : undefined, - }, - }, - onCompleted: onGenerate, - }) + const { mutate: generateLicense, isPending: isLoading, error } = useCreateEnterpriseSubscriptionLicense(env) const onSubmit = useCallback( event => { @@ -300,16 +258,74 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC telemetryRecorder.recordEvent('admin.productSubscription.license', 'generate', { metadata: getTagsForTelemetry(formData), }) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - generateLicense() + generateLicense( + { + message: formData.message, + license: { + subscriptionId: subscription.id, + license: { + // We only support creating old-school license keys + case: 'key', + value: { + info: { + tags: getTagsFromFormData(formData), + userCount: formData.userCount, + expireTime: Timestamp.fromDate(formData.expiresAt), + salesforceOpportunityId: + formData.salesforceOpportunityID.trim().length > 0 + ? formData.salesforceOpportunityID.trim() + : undefined, + }, + }, + }, + }, + }, + { + onSuccess: onGenerate, + } + ) }, - [formData, telemetryRecorder, generateLicense] + [formData, telemetryRecorder, generateLicense, subscription, onGenerate] ) const tags = useDebounce(tagsFromString(formData.tags), 300) const selectedPlan = formData.plan ? ALL_PLANS.find(plan => plan.label === formData.plan) : undefined + const infoAlerts = ( + <> + + + Each subscription must map to exactly ONE Sourcegraph instance.{' '} + + DO NOT create licenses used by multiple Sourcegraph instances within a single subscription + {' '} + - instead, create a NEW subscription with the appropriate Salesforce subscription ID and a relevant + display name. + + + Existing licenses can be re-linked to a new subscription by reaching out to{' '} + + #discuss-core-services + + . + + + + + More documentation can be found in the{' '} + + "Customer License Key Management" Notion page + + . + + + ) + return (

    Generate new Sourcegraph license - {hasAcknowledgedInfo && ( - <> - {' '} - - - - - - - )}

    {error && } {!hasAcknowledgedInfo && ( <> - - Please read the{' '} - - guide for how to create a license key for a new prospect or new customer. - - + + {infoAlerts} @@ -350,6 +356,7 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC {hasAcknowledgedInfo && ( <> + {infoAlerts}
    + - - - { + onSFOpportunityIDChange(event) + const { value } = event.target + if (!value) { + setSFOpportunityIDError(undefined) + return + } + + if (!value.startsWith('006')) { + setSFOpportunityIDError( + 'Salesforce opportunity ID must start with "006"' + ) + return + } + if (value.length < 17) { + setSFOpportunityIDError( + 'Salesforce opportunity ID must be longer than 17 characters' + ) + return + } + + // No problems + setSFOpportunityIDError(undefined) + }} /> } /> - + Additional information}> Note that specifying tags manually is no longer required and the - form should handle all options. + form should handle all options. For example, the{' '} + customer: tag is + automatically added on license creation.
    Only use this if you know what you're doing!
    - All the tags are displayed at the end of the form as well. + All final tags are displayed at the end of the form as well. } className="mt-2" @@ -604,24 +626,30 @@ export const SiteAdminGenerateProductLicenseForSubscriptionForm: React.FunctionC ))}
    -
    -

    Final License Details

    - - Please double check that the license tags and user count are correct before - generating the license. The license cannot be modified once generated. - -
    - {hasUnknownTags(tags) && } + +

    Final License Details

    - + Please double check that the license tags and user count are correct before + generating the license. The license cannot be modified once generated. -
    +
    + {hasUnknownTags(tags) && } + + + +
    + )}
    - {!node?.revokedAt && !isProductLicenseExpired(node?.info?.expiresAt ?? 0) && ( + {!revoked && !isProductLicenseExpired(info?.expireTime?.toDate() ?? 0) && ( )} @@ -127,52 +167,46 @@ export const SiteAdminProductLicenseNode: React.FunctionComponent<
    - {uuid} + + {node.id} +
    - {node.version} + {licenseKey.infoVersion}
    - {node.version > 1 && ( + {licenseKey.infoVersion > 1 && ( <> -
    - - - {node.siteID ?? Unused} - -
    -
    - - - {node.info?.salesforceSubscriptionID ?? ( - Unused - )} - -
    - {node.info?.salesforceOpportunityID ?? ( - Unused + {info?.salesforceOpportunityId ? ( + + {info.salesforceOpportunityId} + + ) : ( + Not set )}
    )} - {node.info && node.info.tags.length > 0 && ( + {info && info.tags.length > 0 && ( <> - {hasUnknownTags(node.info.tags) && } + {hasUnknownTags(info.tags) && } )}
    diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.module.scss b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.module.scss new file mode 100644 index 00000000000..f3658d57a5c --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.module.scss @@ -0,0 +1,9 @@ +.row { + background-color: var(--code-bg); + border-bottom: 1px solid var(--border-color-2); + + td { + padding-top: 1rem; + padding-bottom: 1rem; + } +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.tsx index 42ba4b9702b..110757de24d 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionNode.tsx @@ -1,29 +1,30 @@ import * as React from 'react' -import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' -import { LinkOrSpan } from '@sourcegraph/wildcard' +import { Badge, LinkOrSpan } from '@sourcegraph/wildcard' -import type { SiteAdminProductSubscriptionFields } from '../../../../graphql-operations' -import { AccountName } from '../../../dotcom/productSubscriptions/AccountName' -import { ProductSubscriptionLabel } from '../../../dotcom/productSubscriptions/ProductSubscriptionLabel' -import { ProductLicenseTags } from '../../../productSubscription/ProductLicenseTags' +import type { EnterprisePortalEnvironment } from './enterpriseportal' +import { + type EnterpriseSubscription, + EnterpriseSubscriptionCondition_Status, +} from './enterpriseportalgen/subscriptions_pb' +import { InstanceTypeBadge } from './InstanceTypeBadge' -import { enterprisePortalID } from './utils' +import styles from './SiteAdminProductSubscriptionNode.module.scss' export const SiteAdminProductSubscriptionNodeHeader: React.FunctionComponent> = () => ( - ID - Customer - Plan - Expiration - Tags + Display name + Salesforce subscription + Instance type + Instance domain ) export interface SiteAdminProductSubscriptionNodeProps { - node: SiteAdminProductSubscriptionFields + env: EnterprisePortalEnvironment + node: EnterpriseSubscription } /** @@ -31,32 +32,44 @@ export interface SiteAdminProductSubscriptionNodeProps { */ export const SiteAdminProductSubscriptionNode: React.FunctionComponent< React.PropsWithChildren -> = ({ node }) => ( - - - - {enterprisePortalID(node.uuid)} - - - - - - - - - - {node.activeLicense?.info ? ( - - ) : ( - None - )} - - - {node.activeLicense?.info && node.activeLicense.info.tags.length > 0 ? ( - - ) : ( - None - )} - - -) +> = ({ env, node }) => { + const archived = node.conditions.find( + condition => condition.status === EnterpriseSubscriptionCondition_Status.ARCHIVED + ) + + return ( + + + {archived && ( + + Archived + + )} + + {node.displayName} + + + + {node?.salesforce?.subscriptionId ? ( + {node?.salesforce?.subscriptionId} + ) : ( + Not set + )} + + + {node?.instanceType ? ( + + ) : ( + Not set + )} + + + {node?.instanceDomain ? ( + {node?.instanceDomain} + ) : ( + Not set + )} + + + ) +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx index fc89764079f..f664b2effba 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx @@ -1,46 +1,69 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { mdiPlus } from '@mdi/js' -import { QueryClientProvider } from '@tanstack/react-query' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import type { ConnectError } from '@connectrpc/connect' +import { mdiInformationOutline, mdiCircle, mdiPlus, mdiPencil } from '@mdi/js' +import { QueryClientProvider, type UseQueryResult } from '@tanstack/react-query' +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { logger } from '@sourcegraph/common' -import { useMutation, useQuery } from '@sourcegraph/http-client' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' -import { Button, Container, ErrorAlert, H3, Icon, Link, LoadingSpinner, PageHeader, Text } from '@sourcegraph/wildcard' +import { + Alert, + Button, + Container, + ErrorAlert, + H3, + Icon, + Link, + LoadingSpinner, + PageHeader, + Text, + Tooltip, +} from '@sourcegraph/wildcard' +import { Collapsible } from '../../../../components/Collapsible' import { ConnectionContainer, ConnectionError, ConnectionList, ConnectionLoading, - ConnectionSummary, - ShowMoreButton, - SummaryContainer, } from '../../../../components/FilteredConnection/ui' import { PageTitle } from '../../../../components/PageTitle' +import { Timeline, type TimelineStage } from '../../../../components/Timeline' import { useScrollToLocationHash } from '../../../../components/useScrollToLocationHash' -import type { - ArchiveProductSubscriptionResult, - ArchiveProductSubscriptionVariables, - DotComProductSubscriptionResult, - DotComProductSubscriptionVariables, -} from '../../../../graphql-operations' -import { AccountName } from '../../../dotcom/productSubscriptions/AccountName' -import { ProductSubscriptionLabel } from '../../../dotcom/productSubscriptions/ProductSubscriptionLabel' +import { isProductLicenseExpired } from '../../../../productSubscription/helpers' +import { + ProductSubscriptionLabel, + productSubscriptionLabel, +} from '../../../dotcom/productSubscriptions/ProductSubscriptionLabel' import { LicenseGenerationKeyWarning } from '../../../productSubscription/LicenseGenerationKeyWarning' -import { - ARCHIVE_PRODUCT_SUBSCRIPTION, - DOTCOM_PRODUCT_SUBSCRIPTION, - useProductSubscriptionLicensesConnection, -} from './backend' import { CodyServicesSection } from './CodyServicesSection' -import { queryClient, type EnterprisePortalEnvironment } from './enterpriseportal' +import { + queryClient, + useArchiveEnterpriseSubscription, + useGetEnterpriseSubscription, + useListEnterpriseSubscriptionLicenses, + useUpdateEnterpriseSubscription, + type EnterprisePortalEnvironment, +} from './enterpriseportal' +import { EnterprisePortalEnvSelector, getDefaultEnterprisePortalEnv } from './EnterprisePortalEnvSelector' +import { EnterprisePortalEnvWarning } from './EnterprisePortalEnvWarning' +import { + type EnterpriseSubscriptionCondition, + type EnterpriseSubscriptionLicenseCondition, + EnterpriseSubscriptionCondition_Status, + EnterpriseSubscriptionLicenseType, + type ListEnterpriseSubscriptionLicensesResponse, + EnterpriseSubscriptionLicenseCondition_Status, + type EnterpriseSubscriptionLicenseKey, + EnterpriseSubscriptionInstanceType, + type EnterpriseSubscriptionLicense, +} from './enterpriseportalgen/subscriptions_pb' +import { InstanceTypeBadge } from './InstanceTypeBadge' import { SiteAdminGenerateProductLicenseForSubscriptionForm } from './SiteAdminGenerateProductLicenseForSubscriptionForm' import { SiteAdminProductLicenseNode } from './SiteAdminProductLicenseNode' -import { enterprisePortalID } from './utils' interface Props extends TelemetryV2Props {} @@ -50,224 +73,488 @@ export const SiteAdminProductSubscriptionPage: React.FunctionComponent ) +const QUERY_PARAM_ENV = 'env' + /** * Displays a product subscription in the site admin area. */ const Page: React.FunctionComponent> = ({ telemetryRecorder }) => { const navigate = useNavigate() - const { subscriptionUUID = '' } = useParams<{ subscriptionUUID: string }>() + const { subscriptionUUID: paramSubscriptionUUID = '' } = useParams<{ subscriptionUUID: string }>() useEffect(() => telemetryRecorder.recordEvent('admin.productSubscription', 'view'), [telemetryRecorder]) + const [searchParams, setSearchParams] = useSearchParams() + const [env, setEnv] = useState( + (searchParams.get(QUERY_PARAM_ENV) as EnterprisePortalEnvironment) || getDefaultEnterprisePortalEnv() + ) + useEffect(() => { + const currentEnv = searchParams.get(QUERY_PARAM_ENV) as EnterprisePortalEnvironment + + searchParams.set(QUERY_PARAM_ENV, env) + setSearchParams(searchParams) + + // HACK: env state doesn't propagate to hooks correctly, so conditionally + // reload the page. + // Required until we fix https://linear.app/sourcegraph/issue/CORE-245 + if (env !== currentEnv) { + window.location.reload() + return + } + }, [env, setSearchParams, searchParams]) + + const { data, isFetching: isLoading, error, refetch } = useGetEnterpriseSubscription(env, paramSubscriptionUUID) + const [showGenerate, setShowGenerate] = useState(false) - const { data, loading, error, refetch } = useQuery< - DotComProductSubscriptionResult, - DotComProductSubscriptionVariables - >(DOTCOM_PRODUCT_SUBSCRIPTION, { - variables: { uuid: subscriptionUUID }, - errorPolicy: 'all', - }) + const licenses = useListEnterpriseSubscriptionLicenses( + env, + [ + { + filter: { + case: 'subscriptionId', + value: paramSubscriptionUUID, + }, + }, + { + filter: { + // This UI only manages old-school license keys. + case: 'type', + value: EnterpriseSubscriptionLicenseType.KEY, + }, + }, + ], + { limit: 100, shouldLoad: !!data } + ) - const [archiveProductSubscription, { loading: archiveLoading, error: archiveError }] = useMutation< - ArchiveProductSubscriptionResult, - ArchiveProductSubscriptionVariables - >(ARCHIVE_PRODUCT_SUBSCRIPTION) + const { + mutateAsync: archiveProductSubscription, + isPending: archiveLoading, + error: archiveError, + } = useArchiveEnterpriseSubscription(env) + + const subscription = data?.subscription const onArchive = useCallback(async () => { - if (!data) { + if (!subscription) { return } - if ( - !window.confirm( - 'Do you really want to archive this product subscription? This will hide it from site admins and users.\n\nHowever, it does NOT:\n\n- invalidate the license key\n- refund payment or cancel billing\n\nYou must manually do those things.' - ) - ) { + const reason = window.prompt( + 'Do you really want to PERMANENTLY archive this subscription? All licenses associated with this subscription will be PERMANENTLY revoked, it will no longer be available for various Sourcegraph services, and changes can no longer be made to this subscription.\n\nHowever, it does NOT refund payment or cancel billing for you.\n\nEnter a revocation reason to continue.' + ) + if (!reason || reason.length <= 3) { + window.alert('Aborting.') return } try { telemetryRecorder.recordEvent('admin.productSubscription', 'archive') - await archiveProductSubscription({ variables: { id: data.dotcom.productSubscription.id } }) + await archiveProductSubscription({ + reason, + subscriptionId: subscription.id, + }) navigate('/site-admin/dotcom/product/subscriptions') } catch (error) { logger.error(error) } - }, [data, archiveProductSubscription, navigate, telemetryRecorder]) + }, [subscription, archiveProductSubscription, navigate, telemetryRecorder]) const toggleShowGenerate = useCallback((): void => setShowGenerate(previousValue => !previousValue), []) - const refetchRef = useRef<(() => void) | null>(null) - const setRefetchRef = useCallback( - (refetch: (() => void) | null) => { - refetchRef.current = refetch - }, - [refetchRef] - ) + const { + mutateAsync: updateEnterpriseSubscription, + isPending: subscriptionUpdating, + error: subscriptionUpdateError, + } = useUpdateEnterpriseSubscription(env) const onLicenseUpdate = useCallback(async () => { - await refetch() - if (refetchRef.current) { - refetchRef.current() - } + await licenses.refetch() setShowGenerate(false) - }, [refetch, refetchRef]) + }, [licenses]) - if (loading && !data) { - return - } + const isAnythingLoading = isLoading || licenses.isLoading || subscriptionUpdating || archiveLoading - // If there's an error, simply render an error page. - if (error) { - return - } + const created = subscription?.conditions?.find( + condition => condition.status === EnterpriseSubscriptionCondition_Status.CREATED + ) + const archived = subscription?.conditions?.find( + condition => condition.status === EnterpriseSubscriptionCondition_Status.ARCHIVED + ) - const productSubscription = data!.dotcom.productSubscription - - /** - * TODO(@robert): As part of https://linear.app/sourcegraph/issue/CORE-100, - * eventually dev subscriptions will only live on Enterprise Portal dev and - * prod subscriptions will only live on Enterprise Portal prod. Until we - * cut over, we use license tags to determine what Enterprise Portal - * environment to target. - */ - const enterprisePortalEnvironment: EnterprisePortalEnvironment = - window.context.deployType === 'dev' - ? 'local' - : productSubscription.activeLicense?.info?.tags?.includes('dev') - ? 'dev' - : 'prod' + const activeLicense = getActiveLicense(licenses?.data?.licenses) return ( - <> -
    - - + + - Created + Created - } - actions={ - - } - className="mb-3" - /> - {archiveError && } + ) + } + actions={ +
    + +
    + +
    +
    + } + className="mb-3" + /> + {isAnythingLoading && } -

    Details

    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
    ID{enterprisePortalID(subscriptionUUID)}
    Current Plan - -
    Account - —{' '} - View as user -
    Salesforce Opportunity - {(!productSubscription.activeLicense || - productSubscription.activeLicense.info?.salesforceOpportunityID === null) && ( - None - )} - {productSubscription.activeLicense && - productSubscription.activeLicense.info?.salesforceOpportunityID !== null && ( - <>{productSubscription.activeLicense.info?.salesforceOpportunityID} + {archiveError && } + {subscriptionUpdateError && } + {error && } + + {archived && This subscription has been permanently archived.} + + {subscription && ( + <> +

    Details

    + + + + + + + + + + + - - - - - - -
    + Subscription ID{' '} + + + + + {subscription?.id} +
    + Display name{' '} + + + + + {subscription?.displayName ? ( + <>{subscription?.displayName} + ) : ( + Not set )} -
    Salesforce Subscription - {(!productSubscription.activeLicense || - productSubscription.activeLicense.info?.salesforceSubscriptionID === null) && ( - None - )} - {productSubscription.activeLicense && - productSubscription.activeLicense.info?.salesforceSubscriptionID !== null && ( - <>{productSubscription.activeLicense.info?.salesforceSubscriptionID} + {!archived && ( + { + const displayName = window.prompt( + 'Enter instance display name to assign:', + subscription?.displayName + ) + if (displayName === null) { + return + } + await updateEnterpriseSubscription({ + subscription: { id: subscription?.id, displayName }, + }) + }} + /> )} -
    -
    +
    + Salesforce subscription{' '} + + + + + {subscription?.salesforce?.subscriptionId ? ( + + {subscription?.salesforce?.subscriptionId} + + ) : ( + Not set + )} + {!archived && ( + { + const salesforceSubscriptionID = window.prompt( + 'Enter the Salesforce subscription ID to assign:', + subscription?.salesforce?.subscriptionId + ) + if (salesforceSubscriptionID === null) { + return + } + if (salesforceSubscriptionID === '') { + await updateEnterpriseSubscription({ + subscription: { + id: subscription?.id, + }, + updateMask: { + paths: ['salesforce.subscription_id'], + }, + }) + } else { + await updateEnterpriseSubscription({ + subscription: { + id: subscription?.id, + salesforce: { + subscriptionId: salesforceSubscriptionID, + }, + }, + }) + } + }} + /> + )} +
    + Instance domain{' '} + + + + + {subscription?.instanceDomain ? ( + + {subscription?.instanceDomain} + + ) : ( + Not set + )} + {!archived && ( + { + const instanceDomain = window.prompt( + 'Enter instance domain to assign (leave empty to unassign):', + subscription?.instanceDomain + ) + if (instanceDomain === null) { + return + } + if (instanceDomain === '') { + await updateEnterpriseSubscription({ + subscription: { + id: subscription?.id, + }, + updateMask: { + paths: ['instance_domain'], + }, + }) + } else { + await updateEnterpriseSubscription({ + subscription: { id: subscription?.id, instanceDomain }, + }) + } + }} + /> + )} +
    + Instance type{' '} + + + + + {subscription?.instanceType ? ( + + ) : ( + Not set + )} + {!archived && ( + { + const types = [ + EnterpriseSubscriptionInstanceType.PRIMARY, + EnterpriseSubscriptionInstanceType.SECONDARY, + EnterpriseSubscriptionInstanceType.INTERNAL, + ] + const instanceType = window.prompt( + `Enter an instance type to assign (one of: ${types + .map(type => EnterpriseSubscriptionInstanceType[type]) + .join(', ')}). Leave empty to unassign.` + ) + if (instanceType === null) { + return + } + if (instanceType === '') { + await updateEnterpriseSubscription({ + subscription: { + id: subscription?.id, + }, + updateMask: { paths: ['instance_type'] }, + }) + return + } + const type = types.find( + type => + EnterpriseSubscriptionInstanceType[type].toLowerCase() === + instanceType.toLowerCase() + ) + if (!type) { + window.alert(`Invalid instance type ${instanceType}`) + return + } + await updateEnterpriseSubscription({ + subscription: { + id: subscription?.id, + instanceType: type, + }, + }) + }} + /> + )} +
    + Active license{' '} + + + + + {activeLicense ? ( + <> + {' '} + - view license + + ) : ( + No active license + )} +
    +
    - + History} + titleAtStart={true} + defaultExpanded={!!archived} + className="mb-3" + > + + {subscription && licenses.data ? ( + + conditions.map(condition => ({ + licenseID: id, + license: license.value, + condition, + })) + )} + /> + ) : ( + + )} + + -

    - Licenses - -

    - - - - -
    - {showGenerate && ( +

    + Licenses + +

    + + + + + + + )} + {subscription && showGenerate && ( setShowGenerate(false)} telemetryRecorder={telemetryRecorder} /> )} - +
    + ) +} + +function getActiveLicense( + licenses: EnterpriseSubscriptionLicense[] | undefined +): EnterpriseSubscriptionLicense | undefined { + return licenses?.find( + // Exists if it is the first license, has an expiry, and expiry is before + // now + ({ license, conditions }, idx) => + idx === 0 && + license?.value?.info?.expireTime && + !isProductLicenseExpired(license?.value?.info?.expireTime?.toDate()) && + !conditions.find(condition => condition.status === EnterpriseSubscriptionLicenseCondition_Status.REVOKED) ) } interface ProductSubscriptionLicensesConnectionProps extends TelemetryV2Props { - subscriptionUUID: string + env: EnterprisePortalEnvironment + licenses: UseQueryResult toggleShowGenerate: () => void - setRefetch: (refetch: () => void) => void } const ProductSubscriptionLicensesConnection: React.FunctionComponent = ({ - subscriptionUUID, - setRefetch, + env, + licenses: { data, refetch, error, isLoading }, toggleShowGenerate, telemetryRecorder, }) => { - const { loading, hasNextPage, fetchMore, refetchAll, connection, error } = - useProductSubscriptionLicensesConnection(subscriptionUUID) - - useEffect(() => { - setRefetch(refetchAll) - }, [setRefetch, refetchAll]) - const location = useLocation() const licenseIDFromLocationHash = useMemo(() => { if (location.hash.length > 1) { @@ -280,32 +567,22 @@ const ProductSubscriptionLicensesConnection: React.FunctionComponent {error && } - {loading && !connection && } + {isLoading && !data && } - {connection?.nodes?.map(node => ( + {data?.licenses?.map(node => ( ))} - {connection && ( - - } - /> - {hasNextPage && } - - )} + {data?.licenses?.length === 0 && } ) } @@ -320,3 +597,136 @@ const NoProductLicense: React.FunctionComponent<{ ) + +interface ConditionsTimelineProps { + subscriptionConditions: EnterpriseSubscriptionCondition[] + /** + * Combined conditions of all licenses found. + */ + licensesConditions: { + licenseID: string + license: EnterpriseSubscriptionLicenseKey | undefined + condition: EnterpriseSubscriptionLicenseCondition + }[] +} + +const ConditionsTimeline: React.FunctionComponent = ({ + subscriptionConditions, + licensesConditions, +}) => { + const getSubscriptionConditionBackgroundClassName = (status: EnterpriseSubscriptionCondition_Status): string => { + switch (status) { + case EnterpriseSubscriptionCondition_Status.CREATED: { + return 'bg-success' + } + case EnterpriseSubscriptionCondition_Status.ARCHIVED: { + return 'bg-danger' + } + default: { + return 'bg-info' + } + } + } + const getLicenseConditionBackgroundClassName = (status: EnterpriseSubscriptionLicenseCondition_Status): string => { + switch (status) { + case EnterpriseSubscriptionLicenseCondition_Status.CREATED: { + return 'bg-success' + } + case EnterpriseSubscriptionLicenseCondition_Status.REVOKED: { + return 'bg-danger' + } + default: { + return 'bg-info' + } + } + } + + const allConditions: { + lastTransitionTime: Date + summary: string + details: React.ReactNode + className: string + }[] = subscriptionConditions + .map(condition => ({ + lastTransitionTime: condition.lastTransitionTime!.toDate(), + summary: `Subscription ${EnterpriseSubscriptionCondition_Status[condition.status].toLowerCase()}`, + details: ( + <> + {condition.message ? ( + <>{condition.message} + ) : ( + No details provided. + )} + + ), + className: getSubscriptionConditionBackgroundClassName(condition.status), + })) + .concat( + ...licensesConditions.map(({ licenseID, license, condition }) => ({ + lastTransitionTime: condition.lastTransitionTime!.toDate(), + summary: `License ${EnterpriseSubscriptionLicenseCondition_Status[condition.status] + .toLowerCase() + .replaceAll('_', ' ')}: ${productSubscriptionLabel( + license?.planDisplayName, + license?.info?.userCount + )}`, + details: ( + <> + {condition.message ? ( + <>{condition.message} + ) : ( + No details provided. + )} +
    + + View license {licenseID} + +
    + + ), + className: getLicenseConditionBackgroundClassName(condition.status), + })) + ) + .sort((a, b) => (a.lastTransitionTime > b.lastTransitionTime ? -1 : 1)) + + const stages = allConditions?.map( + (condition): TimelineStage => ({ + icon: , + date: condition.lastTransitionTime.toISOString(), + className: condition.className, + + text: condition.summary, + details: {condition.details}, + }) + ) + + return +} + +interface EditAttributeButtonProps { + label: string + onClick: () => Promise + refetch: () => void + disabled?: boolean +} + +const EditAttributeButton: React.FunctionComponent = ({ + label, + onClick, + refetch, + disabled, +}) => ( + +) diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx index ba0274ea0d1..635023b5b53 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage.tsx @@ -1,69 +1,211 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import type { PartialMessage } from '@bufbuild/protobuf' import { mdiPlus } from '@mdi/js' +import { QueryClientProvider } from '@tanstack/react-query' +import { useSearchParams } from 'react-router-dom' +import { useDebounce } from 'use-debounce' -import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' +import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { Button, Container, Icon, Link, PageHeader } from '@sourcegraph/wildcard' -import type { AuthenticatedUser } from '../../../../auth' -import { FilteredConnection } from '../../../../components/FilteredConnection' +import { + ConnectionContainer, + ConnectionError, + ConnectionForm, + ConnectionList, + ConnectionLoading, + SummaryContainer, +} from '../../../../components/FilteredConnection/ui' import { PageTitle } from '../../../../components/PageTitle' -import type { SiteAdminProductSubscriptionFields } from '../../../../graphql-operations' -import { queryProductSubscriptions } from './backend' +import { queryClient, useListEnterpriseSubscriptions, type EnterprisePortalEnvironment } from './enterpriseportal' +import { EnterprisePortalEnvSelector, getDefaultEnterprisePortalEnv } from './EnterprisePortalEnvSelector' +import { EnterprisePortalEnvWarning } from './EnterprisePortalEnvWarning' +import type { ListEnterpriseSubscriptionsFilter } from './enterpriseportalgen/subscriptions_pb' import { SiteAdminProductSubscriptionNode, SiteAdminProductSubscriptionNodeHeader, - type SiteAdminProductSubscriptionNodeProps, } from './SiteAdminProductSubscriptionNode' -import styles from './SiteAdminCreateProductSubscriptionPage.module.scss' - -interface Props extends TelemetryV2Props { - authenticatedUser: AuthenticatedUser -} +interface Props extends TelemetryV2Props {} /** * Displays the enterprise subscriptions (formerly known as "product subscriptions") that have been * created on Sourcegraph.com. */ -export const SiteAdminProductSubscriptionsPage: React.FunctionComponent> = ({ - authenticatedUser, - telemetryRecorder, -}) => { +export const SiteAdminProductSubscriptionsPage: React.FunctionComponent> = props => ( + + + +) + +const QUERY_PARAM_KEY = 'query' +const QUERY_PARAM_ENV = 'env' +const QUERY_PARAM_FILTER = 'filter' + +type FilterType = 'display_name' | 'sf_sub_id' + +const MAX_RESULTS = 50 + +const Page: React.FunctionComponent> = ({ telemetryRecorder }) => { useEffect(() => telemetryRecorder.recordEvent('admin.productSubscriptions', 'view'), [telemetryRecorder]) + const [searchParams, setSearchParams] = useSearchParams() + const [env, setEnv] = useState( + (searchParams.get(QUERY_PARAM_ENV) as EnterprisePortalEnvironment) || getDefaultEnterprisePortalEnv() + ) + const [query, setQuery] = useState(searchParams.get(QUERY_PARAM_KEY) ?? '') + const [filters, setFilters] = useState<{ filter: FilterType }>({ + filter: (searchParams.get(QUERY_PARAM_FILTER) as FilterType) ?? 'display_name', + }) + + useEffect(() => { + const currentEnv = searchParams.get(QUERY_PARAM_ENV) as EnterprisePortalEnvironment + + searchParams.set(QUERY_PARAM_KEY, query?.trim() ?? '') + searchParams.set(QUERY_PARAM_FILTER, filters.filter) + searchParams.set(QUERY_PARAM_ENV, env) + setSearchParams(searchParams) + + // HACK: env state doesn't propagate to hooks correctly, so conditionally + // reload the page. + // Required until we fix https://linear.app/sourcegraph/issue/CORE-245 + if (env !== currentEnv) { + window.location.reload() + return + } + }, [query, searchParams, setSearchParams, filters, env]) + + const [debouncedQuery] = useDebounce(query, 200) + + let listFilters: PartialMessage[] = [] + // no filter without a query + if (debouncedQuery) { + switch (filters.filter) { + case 'display_name': { + listFilters = [ + { + filter: { + case: 'displayName', + value: debouncedQuery, + }, + }, + ] + break + } + case 'sf_sub_id': { + listFilters = [ + { + filter: { + case: 'salesforce', + value: { subscriptionId: debouncedQuery }, + }, + }, + ] + break + } + } + } + const { error, isFetching, data } = useListEnterpriseSubscriptions(env, listFilters, { + limit: MAX_RESULTS, + }) + return (
    - - Create Enterprise subscription - +
    + +
    + +
    +
    } className="mb-3" /> - - - listComponent="table" - listClassName="table" - contentWrapperComponent={ListContentWrapper} - noun="Enterprise subscription" - pluralNoun="Enterprise subscriptions" - queryConnection={queryProductSubscriptions} - headComponent={SiteAdminProductSubscriptionNodeHeader} - nodeComponent={SiteAdminProductSubscriptionNode} - /> - + + + + + { + setQuery(event.target.value) + }} + onFilterSelect={(filter, value) => { + if (value) { + setFilters({ ...filters, [filter.id]: value as FilterType }) + } + }} + inputPlaceholder="Enter a query to find subscriptions" + /> + + + + {error && } + + {isFetching && } + {data && ( + + + + {data?.subscriptions?.map(node => ( + + ))} + + + )} + + {data && data.subscriptions.length > 0 && ( + Showing {data.subscriptions.length} subscriptions. + )} + {data && data.subscriptions.length === 0 && ( + No subscriptions found. + )} + {data && data.subscriptions.length >= MAX_RESULTS && ( + + )} + + +
    ) } - -const ListContentWrapper: React.FunctionComponent> = ({ children }) => ( -
    {children}
    -) diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/__snapshots__/SiteAdminProductLicenseNode.test.tsx.snap b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/__snapshots__/SiteAdminProductLicenseNode.test.tsx.snap deleted file mode 100644 index 991244b2ecf..00000000000 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/__snapshots__/SiteAdminProductLicenseNode.test.tsx.snap +++ /dev/null @@ -1,449 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`SiteAdminProductLicenseNode > active 1`] = ` - -
  • -
    - - -
    -
    -
    - -

    -

    -
    - -

    - 1 -

    -
    - - - -
    -
    -
  • -
    -`; - -exports[`SiteAdminProductLicenseNode > inactive 1`] = ` - -
  • -
    - - -
    -
    -
    - -

    -

    -
    - -

    - 1 -

    -
    - - - -
    -
    -
  • -
    -`; diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts deleted file mode 100644 index 2c0f95e47ad..00000000000 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/backend.ts +++ /dev/null @@ -1,275 +0,0 @@ -import type { Observable } from 'rxjs' -import { map } from 'rxjs/operators' - -import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' - -import { queryGraphQL } from '../../../../backend/graphql' -import { - useShowMorePagination, - type UseShowMorePaginationResult, -} from '../../../../components/FilteredConnection/hooks/useShowMorePagination' -import type { - DotComProductLicensesResult, - DotComProductLicensesVariables, - ProductLicenseFields, - ProductLicensesResult, - ProductLicensesVariables, - ProductSubscriptionsDotComResult, - ProductSubscriptionsDotComVariables, -} from '../../../../graphql-operations' - -const siteAdminProductSubscriptionFragment = gql` - fragment SiteAdminProductSubscriptionFields on ProductSubscription { - id - name - uuid - account { - id - username - displayName - } - activeLicense { - id - info { - productNameWithBrand - tags - userCount - expiresAt - } - licenseKey - createdAt - } - createdAt - isArchived - urlForSiteAdmin - } -` - -export const DOTCOM_PRODUCT_SUBSCRIPTION = gql` - query DotComProductSubscription($uuid: String!) { - dotcom { - productSubscription(uuid: $uuid) { - id - name - account { - id - username - displayName - } - productLicenses { - nodes { - id - info { - tags - userCount - expiresAt - salesforceSubscriptionID - salesforceOpportunityID - } - licenseKey - createdAt - revokedAt - revokeReason - siteID - version - } - totalCount - pageInfo { - hasNextPage - } - } - activeLicense { - id - info { - productNameWithBrand - tags - userCount - expiresAt - salesforceSubscriptionID - salesforceOpportunityID - } - licenseKey - createdAt - } - currentSourcegraphAccessToken - createdAt - isArchived - url - } - } - } -` - -export const ARCHIVE_PRODUCT_SUBSCRIPTION = gql` - mutation ArchiveProductSubscription($id: ID!) { - dotcom { - archiveProductSubscription(id: $id) { - alwaysNil - } - } - } -` - -export const GENERATE_PRODUCT_LICENSE = gql` - mutation GenerateProductLicenseForSubscription($productSubscriptionID: ID!, $license: ProductLicenseInput!) { - dotcom { - generateProductLicenseForSubscription(productSubscriptionID: $productSubscriptionID, license: $license) { - id - } - } - } -` - -const siteAdminProductLicenseFragment = gql` - fragment ProductLicenseFields on ProductLicense { - id - subscription { - id - uuid - name - account { - ...ProductLicenseSubscriptionAccount - } - activeLicense { - id - } - urlForSiteAdmin - } - licenseKey - siteID - info { - ...ProductLicenseInfoFields - } - createdAt - revokedAt - revokeReason - version - } - - fragment ProductLicenseInfoFields on ProductLicenseInfo { - productNameWithBrand - tags - userCount - expiresAt - salesforceSubscriptionID - salesforceOpportunityID - } - - fragment ProductLicenseSubscriptionAccount on User { - id - username - displayName - } -` - -export const PRODUCT_LICENSES = gql` - query ProductLicenses($first: Int, $subscriptionUUID: String!) { - dotcom { - productSubscription(uuid: $subscriptionUUID) { - productLicenses(first: $first) { - nodes { - ...ProductLicenseFields - } - totalCount - pageInfo { - hasNextPage - } - } - } - } - } - ${siteAdminProductLicenseFragment} -` - -export const useProductSubscriptionLicensesConnection = ( - subscriptionUUID: string -): UseShowMorePaginationResult => - useShowMorePagination({ - query: PRODUCT_LICENSES, - variables: { - subscriptionUUID, - }, - getConnection: result => { - const { dotcom } = dataOrThrowErrors(result) - return dotcom.productSubscription.productLicenses - }, - options: { - fetchPolicy: 'cache-and-network', - }, - }) - -export function queryProductSubscriptions(args: { - first?: number | null - query?: string -}): Observable { - return queryGraphQL( - gql` - query ProductSubscriptionsDotCom($first: Int, $account: ID, $query: String) { - dotcom { - productSubscriptions(first: $first, account: $account, query: $query) { - nodes { - ...SiteAdminProductSubscriptionFields - } - totalCount - pageInfo { - hasNextPage - } - } - } - } - ${siteAdminProductSubscriptionFragment} - `, - { - first: args.first, - query: args.query, - } as Partial - ).pipe( - map(dataOrThrowErrors), - map(data => data.dotcom.productSubscriptions) - ) -} - -const QUERY_PRODUCT_LICENSES = gql` - query DotComProductLicenses($first: Int, $licenseKeySubstring: String) { - dotcom { - productLicenses(first: $first, licenseKeySubstring: $licenseKeySubstring) { - nodes { - ...ProductLicenseFields - } - totalCount - pageInfo { - hasNextPage - } - } - } - } - ${siteAdminProductLicenseFragment} -` - -export const useQueryProductLicensesConnection = ( - licenseKeySubstring: string -): UseShowMorePaginationResult => - useShowMorePagination({ - query: QUERY_PRODUCT_LICENSES, - variables: { - licenseKeySubstring, - }, - getConnection: result => { - const { dotcom } = dataOrThrowErrors(result) - return dotcom.productLicenses - }, - options: { - fetchPolicy: 'cache-and-network', - skip: !licenseKeySubstring, - }, - }) - -export const REVOKE_LICENSE = gql` - mutation RevokeLicense($id: ID!, $reason: String!) { - dotcom { - revokeLicense(id: $id, reason: $reason) { - alwaysNil - } - } - } -` diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts index c265f7c9bf6..5863e4ea350 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportal.ts @@ -2,7 +2,7 @@ import type { PartialMessage } from '@bufbuild/protobuf' import type { ConnectError, Transport } from '@connectrpc/connect' import { defaultOptions, useMutation, useQuery } from '@connectrpc/connect-query' import { createConnectTransport } from '@connectrpc/connect-web' -import { QueryClient, type UseMutationResult, type UseQueryResult } from '@tanstack/react-query' +import { QueryClient, type UseMutationResult, type UseQueryResult, keepPreviousData } from '@tanstack/react-query' import { getCodyGatewayAccess, @@ -15,6 +15,33 @@ import type { UpdateCodyGatewayAccessRequest, UpdateCodyGatewayAccessResponse, } from './enterpriseportalgen/codyaccess_pb' +import { + listEnterpriseSubscriptions, + listEnterpriseSubscriptionLicenses, + revokeEnterpriseSubscriptionLicense, + getEnterpriseSubscription, + createEnterpriseSubscriptionLicense, + createEnterpriseSubscription, + archiveEnterpriseSubscription, + updateEnterpriseSubscription, +} from './enterpriseportalgen/subscriptions-SubscriptionsService_connectquery' +import type { + ListEnterpriseSubscriptionsResponse, + ListEnterpriseSubscriptionsFilter, + ListEnterpriseSubscriptionLicensesFilter, + ListEnterpriseSubscriptionLicensesResponse, + RevokeEnterpriseSubscriptionLicenseResponse, + RevokeEnterpriseSubscriptionLicenseRequest, + GetEnterpriseSubscriptionResponse, + CreateEnterpriseSubscriptionLicenseResponse, + CreateEnterpriseSubscriptionLicenseRequest, + CreateEnterpriseSubscriptionRequest, + CreateEnterpriseSubscriptionResponse, + ArchiveEnterpriseSubscriptionResponse, + ArchiveEnterpriseSubscriptionRequest, + UpdateEnterpriseSubscriptionRequest, + UpdateEnterpriseSubscriptionResponse, +} from './enterpriseportalgen/subscriptions_pb' /** * Use a shared QueryClient defined here and explicitly provided via @@ -28,7 +55,17 @@ import type { * Portal gets its own dedicated UI: * https://linear.app/sourcegraph/project/kr-p-enterprise-portal-user-interface-dadd5ff28bd8 */ -export const queryClient = new QueryClient({ defaultOptions }) +export const queryClient = new QueryClient({ + defaultOptions: { + ...defaultOptions, + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, +}) /** * Use proxy that routes to a locally running Enterprise Portal at localhost:6081 @@ -130,3 +167,122 @@ export function useUpdateCodyGatewayAccess( transport: mustGetEnvironment(env), }) } + +export function useListEnterpriseSubscriptions( + env: EnterprisePortalEnvironment, + filters: PartialMessage[], + options: { + limit: number + } +): UseQueryResult { + return useQuery( + listEnterpriseSubscriptions, + { + filters, + pageSize: options.limit, + }, + { + transport: mustGetEnvironment(env), + placeholderData: keepPreviousData, + } + ) +} + +export function useListEnterpriseSubscriptionLicenses( + env: EnterprisePortalEnvironment, + filters: PartialMessage[], + options: { + limit: number + shouldLoad: boolean + } +): UseQueryResult { + return useQuery( + listEnterpriseSubscriptionLicenses, + { + filters, + }, + { + transport: mustGetEnvironment(env), + enabled: options.shouldLoad, + placeholderData: keepPreviousData, + } + ) +} + +export function useRevokeEnterpriseSubscriptionLicense( + env: EnterprisePortalEnvironment +): UseMutationResult< + RevokeEnterpriseSubscriptionLicenseResponse, + ConnectError, + PartialMessage, + unknown +> { + return useMutation(revokeEnterpriseSubscriptionLicense, { + transport: mustGetEnvironment(env), + }) +} + +export function useGetEnterpriseSubscription( + env: EnterprisePortalEnvironment, + subscriptionUUID: string +): UseQueryResult { + return useQuery( + getEnterpriseSubscription, + { + query: { value: subscriptionUUID, case: 'id' }, + }, + { transport: mustGetEnvironment(env) } + ) +} + +export function useCreateEnterpriseSubscriptionLicense( + env: EnterprisePortalEnvironment +): UseMutationResult< + CreateEnterpriseSubscriptionLicenseResponse, + ConnectError, + PartialMessage, + unknown +> { + return useMutation(createEnterpriseSubscriptionLicense, { + transport: mustGetEnvironment(env), + }) +} + +export function useCreateEnterpriseSubscription( + env: EnterprisePortalEnvironment +): UseMutationResult< + CreateEnterpriseSubscriptionResponse, + ConnectError, + PartialMessage, + unknown +> { + return useMutation(createEnterpriseSubscription, { + transport: mustGetEnvironment(env), + }) +} + +export function useArchiveEnterpriseSubscription( + env: EnterprisePortalEnvironment +): UseMutationResult< + ArchiveEnterpriseSubscriptionResponse, + ConnectError, + PartialMessage, + unknown +> { + return useMutation(archiveEnterpriseSubscription, { + transport: mustGetEnvironment(env), + }) +} + +export function useUpdateEnterpriseSubscription( + env: EnterprisePortalEnvironment +): UseMutationResult< + UpdateEnterpriseSubscriptionResponse, + ConnectError, + PartialMessage, + unknown +> { + return useMutation(updateEnterpriseSubscription, { + transport: mustGetEnvironment(env), + }) +} diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions-SubscriptionsService_connectquery.ts b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions-SubscriptionsService_connectquery.ts new file mode 100644 index 00000000000..0c60ead159e --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions-SubscriptionsService_connectquery.ts @@ -0,0 +1,176 @@ +// @generated by protoc-gen-connect-query v1.4.1 with parameter "target=ts" +// @generated from file subscriptions.proto (package enterpriseportal.subscriptions.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; +import { ArchiveEnterpriseSubscriptionRequest, ArchiveEnterpriseSubscriptionResponse, CreateEnterpriseSubscriptionLicenseRequest, CreateEnterpriseSubscriptionLicenseResponse, CreateEnterpriseSubscriptionRequest, CreateEnterpriseSubscriptionResponse, GetEnterpriseSubscriptionRequest, GetEnterpriseSubscriptionResponse, ListEnterpriseSubscriptionLicensesRequest, ListEnterpriseSubscriptionLicensesResponse, ListEnterpriseSubscriptionsRequest, ListEnterpriseSubscriptionsResponse, RevokeEnterpriseSubscriptionLicenseRequest, RevokeEnterpriseSubscriptionLicenseResponse, UpdateEnterpriseSubscriptionMembershipRequest, UpdateEnterpriseSubscriptionMembershipResponse, UpdateEnterpriseSubscriptionRequest, UpdateEnterpriseSubscriptionResponse } from "./subscriptions_pb.js"; + +/** + * GetEnterpriseSubscription retrieves an exact match on an Enterprise subscription. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.GetEnterpriseSubscription + */ +export const getEnterpriseSubscription = { + localName: "getEnterpriseSubscription", + name: "GetEnterpriseSubscription", + kind: MethodKind.Unary, + I: GetEnterpriseSubscriptionRequest, + O: GetEnterpriseSubscriptionResponse, + idempotency: MethodIdempotency.NoSideEffects, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * ListEnterpriseSubscriptions queries for Enterprise subscriptions. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.ListEnterpriseSubscriptions + */ +export const listEnterpriseSubscriptions = { + localName: "listEnterpriseSubscriptions", + name: "ListEnterpriseSubscriptions", + kind: MethodKind.Unary, + I: ListEnterpriseSubscriptionsRequest, + O: ListEnterpriseSubscriptionsResponse, + idempotency: MethodIdempotency.NoSideEffects, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * ListEnterpriseSubscriptionLicenses queries for licenses associated with + * Enterprise subscription licenses, with the ability to list licenses across + * all subscriptions, or just a specific subscription. + * + * Each subscription owns a collection of licenses, typically a series of + * licenses with the most recent one being a subscription's active license. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.ListEnterpriseSubscriptionLicenses + */ +export const listEnterpriseSubscriptionLicenses = { + localName: "listEnterpriseSubscriptionLicenses", + name: "ListEnterpriseSubscriptionLicenses", + kind: MethodKind.Unary, + I: ListEnterpriseSubscriptionLicensesRequest, + O: ListEnterpriseSubscriptionLicensesResponse, + idempotency: MethodIdempotency.NoSideEffects, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * CreateEnterpriseSubscription creates license for an Enterprise subscription. + * + * Not idempotent - we could implement https://google.aip.dev/155 for + * optional idempotency in the future. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.CreateEnterpriseSubscriptionLicense + */ +export const createEnterpriseSubscriptionLicense = { + localName: "createEnterpriseSubscriptionLicense", + name: "CreateEnterpriseSubscriptionLicense", + kind: MethodKind.Unary, + I: CreateEnterpriseSubscriptionLicenseRequest, + O: CreateEnterpriseSubscriptionLicenseResponse, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * RevokeEnterpriseSubscriptionLicense revokes an existing license for an + * Enterprise subscription, permanently disabling its use for features + * managed by Sourcegraph. Revocation cannot be undone. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.RevokeEnterpriseSubscriptionLicense + */ +export const revokeEnterpriseSubscriptionLicense = { + localName: "revokeEnterpriseSubscriptionLicense", + name: "RevokeEnterpriseSubscriptionLicense", + kind: MethodKind.Unary, + I: RevokeEnterpriseSubscriptionLicenseRequest, + O: RevokeEnterpriseSubscriptionLicenseResponse, + idempotency: MethodIdempotency.Idempotent, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * UpdateEnterpriseSubscription updates an existing enterprise subscription. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.UpdateEnterpriseSubscription + */ +export const updateEnterpriseSubscription = { + localName: "updateEnterpriseSubscription", + name: "UpdateEnterpriseSubscription", + kind: MethodKind.Unary, + I: UpdateEnterpriseSubscriptionRequest, + O: UpdateEnterpriseSubscriptionResponse, + idempotency: MethodIdempotency.Idempotent, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * ArchiveEnterpriseSubscriptionRequest archives an existing Enterprise + * subscription. This is a permanent operation, and cannot be undone. + * + * Archiving a subscription also immediately and permanently revokes all + * associated licenses. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.ArchiveEnterpriseSubscription + */ +export const archiveEnterpriseSubscription = { + localName: "archiveEnterpriseSubscription", + name: "ArchiveEnterpriseSubscription", + kind: MethodKind.Unary, + I: ArchiveEnterpriseSubscriptionRequest, + O: ArchiveEnterpriseSubscriptionResponse, + idempotency: MethodIdempotency.Idempotent, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * CreateEnterpriseSubscription creates an Enterprise subscription. + * + * Not idempotent - we could implement https://google.aip.dev/155 for + * optional idempotency in the future. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.CreateEnterpriseSubscription + */ +export const createEnterpriseSubscription = { + localName: "createEnterpriseSubscription", + name: "CreateEnterpriseSubscription", + kind: MethodKind.Unary, + I: CreateEnterpriseSubscriptionRequest, + O: CreateEnterpriseSubscriptionResponse, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; + +/** + * UpdateEnterpriseSubscriptionMembership updates an enterprise subscription + * membership in an authoritative manner. + * + * @generated from rpc enterpriseportal.subscriptions.v1.SubscriptionsService.UpdateEnterpriseSubscriptionMembership + */ +export const updateEnterpriseSubscriptionMembership = { + localName: "updateEnterpriseSubscriptionMembership", + name: "UpdateEnterpriseSubscriptionMembership", + kind: MethodKind.Unary, + I: UpdateEnterpriseSubscriptionMembershipRequest, + O: UpdateEnterpriseSubscriptionMembershipResponse, + idempotency: MethodIdempotency.Idempotent, + service: { + typeName: "enterpriseportal.subscriptions.v1.SubscriptionsService" + } +} as const; diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb.ts b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb.ts new file mode 100644 index 00000000000..258ff06208a --- /dev/null +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/subscriptions_pb.ts @@ -0,0 +1,1794 @@ +// @generated by protoc-gen-es v1.10.0 with parameter "target=ts" +// @generated from file subscriptions.proto (package enterpriseportal.subscriptions.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import { FieldMask, Message, proto3, protoInt64, Timestamp } from "@bufbuild/protobuf"; + +/** + * EnterpriseSubscriptionInstanceType describes what kind of Sourcegraph + * instance an Enterprise Subscription is designated for. + * + * @generated from enum enterpriseportal.subscriptions.v1.EnterpriseSubscriptionInstanceType + */ +export enum EnterpriseSubscriptionInstanceType { + /** + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Represents a primary, production instance. + * + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_PRIMARY = 1; + */ + PRIMARY = 1, + + /** + * Represents a secondary (e.g. dev or testing) instance. + * + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_SECONDARY = 2; + */ + SECONDARY = 2, + + /** + * Represents a Sourcegraph-internal instance. + * + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_INTERNAL = 3; + */ + INTERNAL = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(EnterpriseSubscriptionInstanceType) +proto3.util.setEnumType(EnterpriseSubscriptionInstanceType, "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionInstanceType", [ + { no: 0, name: "ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_UNSPECIFIED" }, + { no: 1, name: "ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_PRIMARY" }, + { no: 2, name: "ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_SECONDARY" }, + { no: 3, name: "ENTERPRISE_SUBSCRIPTION_INSTANCE_TYPE_INTERNAL" }, +]); + +/** + * EnterpriseSubscriptionLicenseType can be used to denote different types of + * licenses. + * + * @generated from enum enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseType + */ +export enum EnterpriseSubscriptionLicenseType { + /** + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * The 'license key' type is the classic licensing mechanism that Sourcegraph + * has always had. They are signed by a private key and offline-validated by + * a public key that ships with all Sourcegraph builds. + * + * Each Subscription is expected to have at most one active Sourcegraph classic + * license used by a Sourcegraph instance at a time. + * + * @generated from enum value: ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY = 1; + */ + KEY = 1, +} +// Retrieve enum metadata with: proto3.getEnumType(EnterpriseSubscriptionLicenseType) +proto3.util.setEnumType(EnterpriseSubscriptionLicenseType, "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseType", [ + { no: 0, name: "ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_UNSPECIFIED" }, + { no: 1, name: "ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY" }, +]); + +/** + * Role represents a role that can be granted to a user. + * + * @generated from enum enterpriseportal.subscriptions.v1.Role + */ +export enum Role { + /** + * @generated from enum value: ROLE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: ROLE_SUBSCRIPTION_CUSTOMER_ADMIN = 2; + */ + SUBSCRIPTION_CUSTOMER_ADMIN = 2, +} +// Retrieve enum metadata with: proto3.getEnumType(Role) +proto3.util.setEnumType(Role, "enterpriseportal.subscriptions.v1.Role", [ + { no: 0, name: "ROLE_UNSPECIFIED" }, + { no: 2, name: "ROLE_SUBSCRIPTION_CUSTOMER_ADMIN" }, +]); + +/** + * PermissionType defines a class of objects with similar characteristics. + * https://openfga.dev/docs/concepts#what-is-a-type + * + * @generated from enum enterpriseportal.subscriptions.v1.PermissionType + */ +export enum PermissionType { + /** + * @generated from enum value: PERMISSION_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: PERMISSION_TYPE_SUBSCRIPTION_CODY_ANALYTICS = 1; + */ + SUBSCRIPTION_CODY_ANALYTICS = 1, +} +// Retrieve enum metadata with: proto3.getEnumType(PermissionType) +proto3.util.setEnumType(PermissionType, "enterpriseportal.subscriptions.v1.PermissionType", [ + { no: 0, name: "PERMISSION_TYPE_UNSPECIFIED" }, + { no: 1, name: "PERMISSION_TYPE_SUBSCRIPTION_CODY_ANALYTICS" }, +]); + +/** + * PermissionRelation represents the relation between the user and the resource. + * + * @generated from enum enterpriseportal.subscriptions.v1.PermissionRelation + */ +export enum PermissionRelation { + /** + * @generated from enum value: PERMISSION_RELATION_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: PERMISSION_RELATION_VIEW = 1; + */ + VIEW = 1, +} +// Retrieve enum metadata with: proto3.getEnumType(PermissionRelation) +proto3.util.setEnumType(PermissionRelation, "enterpriseportal.subscriptions.v1.PermissionRelation", [ + { no: 0, name: "PERMISSION_RELATION_UNSPECIFIED" }, + { no: 1, name: "PERMISSION_RELATION_VIEW" }, +]); + +/** + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition + */ +export class EnterpriseSubscriptionCondition extends Message { + /** + * The time this subscription transitioned into this status. + * + * @generated from field: google.protobuf.Timestamp last_transition_time = 1; + */ + lastTransitionTime?: Timestamp; + + /** + * Status is the type of status corresponding to this condition. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition.Status status = 2; + */ + status = EnterpriseSubscriptionCondition_Status.UNSPECIFIED; + + /** + * Message is a description of the status transition and why it happened. + * + * @generated from field: string message = 3; + */ + message = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "last_transition_time", kind: "message", T: Timestamp }, + { no: 2, name: "status", kind: "enum", T: proto3.getEnumType(EnterpriseSubscriptionCondition_Status) }, + { no: 3, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionCondition { + return new EnterpriseSubscriptionCondition().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionCondition { + return new EnterpriseSubscriptionCondition().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionCondition { + return new EnterpriseSubscriptionCondition().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionCondition | PlainMessage | undefined, b: EnterpriseSubscriptionCondition | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionCondition, a, b); + } +} + +/** + * @generated from enum enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition.Status + */ +export enum EnterpriseSubscriptionCondition_Status { + /** + * @generated from enum value: STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Subscription creation status. + * + * @generated from enum value: STATUS_CREATED = 1; + */ + CREATED = 1, + + /** + * Subscription archival status. i.e. 'is_archived' + * + * @generated from enum value: STATUS_ARCHIVED = 2; + */ + ARCHIVED = 2, + + /** + * Subscription import status, from the one-time migration from the + * predecessor in-dotcom database to Enterprise Portal. + * + * @generated from enum value: STATUS_IMPORTED = 3; + */ + IMPORTED = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(EnterpriseSubscriptionCondition_Status) +proto3.util.setEnumType(EnterpriseSubscriptionCondition_Status, "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition.Status", [ + { no: 0, name: "STATUS_UNSPECIFIED" }, + { no: 1, name: "STATUS_CREATED" }, + { no: 2, name: "STATUS_ARCHIVED" }, + { no: 3, name: "STATUS_IMPORTED" }, +]); + +/** + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionSalesforceMetadata + */ +export class EnterpriseSubscriptionSalesforceMetadata extends Message { + /** + * The Salesforce subscription ID associated with this Enterprise subscription. + * + * @generated from field: string subscription_id = 1; + */ + subscriptionId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionSalesforceMetadata"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionSalesforceMetadata { + return new EnterpriseSubscriptionSalesforceMetadata().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionSalesforceMetadata { + return new EnterpriseSubscriptionSalesforceMetadata().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionSalesforceMetadata { + return new EnterpriseSubscriptionSalesforceMetadata().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionSalesforceMetadata | PlainMessage | undefined, b: EnterpriseSubscriptionSalesforceMetadata | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionSalesforceMetadata, a, b); + } +} + +/** + * EnterpriseSubscription represents a Sourcegraph Enterprise subscription. + * + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscription + */ +export class EnterpriseSubscription extends Message { + /** + * ID is the external, prefixed UUID-format identifier for this subscription + * (e.g. "es_..."). + * + * @generated from field: string id = 1; + */ + id = ""; + + /** + * Timeline of key events corresponding to this subscription. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.EnterpriseSubscriptionCondition conditions = 2; + */ + conditions: EnterpriseSubscriptionCondition[] = []; + + /** + * Display name of this subscription, e.g. "Acme, Inc." + * + * @generated from field: string display_name = 3; + */ + displayName = ""; + + /** + * The instance domain associated with this subscription, e.g. "acme.sourcegraphcloud.com". + * + * @generated from field: string instance_domain = 4; + */ + instanceDomain = ""; + + /** + * Salesforce details associated with this subscription. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionSalesforceMetadata salesforce = 5; + */ + salesforce?: EnterpriseSubscriptionSalesforceMetadata; + + /** + * The use case for the instance used in this Enterprise subscription. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionInstanceType instance_type = 6; + */ + instanceType = EnterpriseSubscriptionInstanceType.UNSPECIFIED; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscription"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "conditions", kind: "message", T: EnterpriseSubscriptionCondition, repeated: true }, + { no: 3, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "instance_domain", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "salesforce", kind: "message", T: EnterpriseSubscriptionSalesforceMetadata }, + { no: 6, name: "instance_type", kind: "enum", T: proto3.getEnumType(EnterpriseSubscriptionInstanceType) }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscription { + return new EnterpriseSubscription().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscription { + return new EnterpriseSubscription().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscription { + return new EnterpriseSubscription().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscription | PlainMessage | undefined, b: EnterpriseSubscription | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscription, a, b); + } +} + +/** + * EnterpriseSubscriptionLicenseKey is the classic offline Sourcegraph license + * key, and corresponds to ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY. + * + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey + */ +export class EnterpriseSubscriptionLicenseKey extends Message { + /** + * Version of this classic license's information schema. It is incremented + * whenever a major change is made to the shape of Info to indicate what + * fields can be expected from the information embedded in the license key. + * + * @generated from field: uint32 info_version = 1; + */ + infoVersion = 0; + + /** + * Read-only information embedded into ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey.Info info = 2; + */ + info?: EnterpriseSubscriptionLicenseKey_Info; + + /** + * The signed license key. + * + * @generated from field: string license_key = 3; + */ + licenseKey = ""; + + /** + * Generated display name representing the plan and some high-level attributes + * about the plan. + * + * @generated from field: string plan_display_name = 4; + */ + planDisplayName = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "info_version", kind: "scalar", T: 13 /* ScalarType.UINT32 */ }, + { no: 2, name: "info", kind: "message", T: EnterpriseSubscriptionLicenseKey_Info }, + { no: 3, name: "license_key", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "plan_display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionLicenseKey { + return new EnterpriseSubscriptionLicenseKey().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionLicenseKey { + return new EnterpriseSubscriptionLicenseKey().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionLicenseKey { + return new EnterpriseSubscriptionLicenseKey().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionLicenseKey | PlainMessage | undefined, b: EnterpriseSubscriptionLicenseKey | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionLicenseKey, a, b); + } +} + +/** + * Read-only information embedded into ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY. + * + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey.Info + */ +export class EnterpriseSubscriptionLicenseKey_Info extends Message { + /** + * The tags that indicate which features are activated by this license. + * + * @generated from field: repeated string tags = 1; + */ + tags: string[] = []; + + /** + * The number of users for which this product subscription is valid. + * + * @generated from field: uint64 user_count = 2; + */ + userCount = protoInt64.zero; + + /** + * The expiration date of this product license. In license data, this is + * called 'expires_at', expressed as the number of seconds since the epoch. + * + * @generated from field: google.protobuf.Timestamp expire_time = 3; + */ + expireTime?: Timestamp; + + /** + * The Salesforce subscription ID associated with this license's parent + * Enterprise Subscription. + * + * @generated from field: string salesforce_subscription_id = 4; + */ + salesforceSubscriptionId = ""; + + /** + * The Salesforce opportunity ID associated with this product license's + * creation. Opportunities in Salesforce generally lead to the creation of a + * new license key. + * + * @generated from field: string salesforce_opportunity_id = 5; + */ + salesforceOpportunityId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey.Info"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "tags", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: "user_count", kind: "scalar", T: 4 /* ScalarType.UINT64 */ }, + { no: 3, name: "expire_time", kind: "message", T: Timestamp }, + { no: 4, name: "salesforce_subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "salesforce_opportunity_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionLicenseKey_Info { + return new EnterpriseSubscriptionLicenseKey_Info().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionLicenseKey_Info { + return new EnterpriseSubscriptionLicenseKey_Info().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionLicenseKey_Info { + return new EnterpriseSubscriptionLicenseKey_Info().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionLicenseKey_Info | PlainMessage | undefined, b: EnterpriseSubscriptionLicenseKey_Info | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionLicenseKey_Info, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition + */ +export class EnterpriseSubscriptionLicenseCondition extends Message { + /** + * The time this subscription transitioned into this status. + * + * @generated from field: google.protobuf.Timestamp last_transition_time = 1; + */ + lastTransitionTime?: Timestamp; + + /** + * Status is the type of status corresponding to this condition. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition.Status status = 2; + */ + status = EnterpriseSubscriptionLicenseCondition_Status.UNSPECIFIED; + + /** + * Message is a description of the status transition and why it happened. + * + * @generated from field: string message = 3; + */ + message = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "last_transition_time", kind: "message", T: Timestamp }, + { no: 2, name: "status", kind: "enum", T: proto3.getEnumType(EnterpriseSubscriptionLicenseCondition_Status) }, + { no: 3, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionLicenseCondition { + return new EnterpriseSubscriptionLicenseCondition().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionLicenseCondition { + return new EnterpriseSubscriptionLicenseCondition().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionLicenseCondition { + return new EnterpriseSubscriptionLicenseCondition().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionLicenseCondition | PlainMessage | undefined, b: EnterpriseSubscriptionLicenseCondition | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionLicenseCondition, a, b); + } +} + +/** + * @generated from enum enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition.Status + */ +export enum EnterpriseSubscriptionLicenseCondition_Status { + /** + * @generated from enum value: STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * License creation status. + * + * @generated from enum value: STATUS_CREATED = 1; + */ + CREATED = 1, + + /** + * License revocation status, i.e. 'is_revoked' + * + * @generated from enum value: STATUS_REVOKED = 2; + */ + REVOKED = 2, +} +// Retrieve enum metadata with: proto3.getEnumType(EnterpriseSubscriptionLicenseCondition_Status) +proto3.util.setEnumType(EnterpriseSubscriptionLicenseCondition_Status, "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition.Status", [ + { no: 0, name: "STATUS_UNSPECIFIED" }, + { no: 1, name: "STATUS_CREATED" }, + { no: 2, name: "STATUS_REVOKED" }, +]); + +/** + * EnterpriseSubscriptionLicense represents a license for a Sourcegraph + * Enterprise product. Multiple licenses are associated with a single + * subscription, typically a series of licenses with the most recent one being + * a subscription's active license. + * + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense + */ +export class EnterpriseSubscriptionLicense extends Message { + /** + * ID is the external, prefixed UUID-format identifier for this license key. + * + * @generated from field: string id = 1; + */ + id = ""; + + /** + * The external, prefixed UUID-format identifier for the subscription that + * owns this license. + * + * @generated from field: string subscription_id = 2; + */ + subscriptionId = ""; + + /** + * Timeline of key events corresponding to this license. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseCondition conditions = 3; + */ + conditions: EnterpriseSubscriptionLicenseCondition[] = []; + + /** + * License data, based on the type of the license. + * + * @generated from oneof enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense.license + */ + license: { + /** + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseKey key = 4; + */ + value: EnterpriseSubscriptionLicenseKey; + case: "key"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "conditions", kind: "message", T: EnterpriseSubscriptionLicenseCondition, repeated: true }, + { no: 4, name: "key", kind: "message", T: EnterpriseSubscriptionLicenseKey, oneof: "license" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionLicense { + return new EnterpriseSubscriptionLicense().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionLicense { + return new EnterpriseSubscriptionLicense().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionLicense { + return new EnterpriseSubscriptionLicense().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionLicense | PlainMessage | undefined, b: EnterpriseSubscriptionLicense | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionLicense, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.GetEnterpriseSubscriptionRequest + */ +export class GetEnterpriseSubscriptionRequest extends Message { + /** + * Query specifies the lookup strategy for this get request. + * + * @generated from oneof enterpriseportal.subscriptions.v1.GetEnterpriseSubscriptionRequest.query + */ + query: { + /** + * Look up a subscription using its external, prefixed UUID-format identifier. + * + * @generated from field: string id = 1; + */ + value: string; + case: "id"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.GetEnterpriseSubscriptionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "query" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetEnterpriseSubscriptionRequest { + return new GetEnterpriseSubscriptionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetEnterpriseSubscriptionRequest { + return new GetEnterpriseSubscriptionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetEnterpriseSubscriptionRequest { + return new GetEnterpriseSubscriptionRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetEnterpriseSubscriptionRequest | PlainMessage | undefined, b: GetEnterpriseSubscriptionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetEnterpriseSubscriptionRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.GetEnterpriseSubscriptionResponse + */ +export class GetEnterpriseSubscriptionResponse extends Message { + /** + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscription subscription = 1; + */ + subscription?: EnterpriseSubscription; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.GetEnterpriseSubscriptionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription", kind: "message", T: EnterpriseSubscription }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetEnterpriseSubscriptionResponse { + return new GetEnterpriseSubscriptionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetEnterpriseSubscriptionResponse { + return new GetEnterpriseSubscriptionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetEnterpriseSubscriptionResponse { + return new GetEnterpriseSubscriptionResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetEnterpriseSubscriptionResponse | PlainMessage | undefined, b: GetEnterpriseSubscriptionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetEnterpriseSubscriptionResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsFilter + */ +export class ListEnterpriseSubscriptionsFilter extends Message { + /** + * @generated from oneof enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsFilter.filter + */ + filter: { + /** + * Filter by exact match on the Enterprise Subscription ID. + * + * @generated from field: string subscription_id = 1; + */ + value: string; + case: "subscriptionId"; + } | { + /** + * Return only product subscriptions with the given archival status. + * + * @generated from field: bool is_archived = 2; + */ + value: boolean; + case: "isArchived"; + } | { + /** + * Return only product subscriptions that satisfies the given permission. + * + * @generated from field: enterpriseportal.subscriptions.v1.Permission permission = 3; + */ + value: Permission; + case: "permission"; + } | { + /** + * Filter by partial match on display name. The query must be at least 3 + * characters long. + * + * @generated from field: string display_name = 4; + */ + value: string; + case: "displayName"; + } | { + /** + * Filter by exact match on Salesforce metadata. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionSalesforceMetadata salesforce = 5; + */ + value: EnterpriseSubscriptionSalesforceMetadata; + case: "salesforce"; + } | { + /** + * Filter by partial match on instance domain. The query must be a valid + * domain. + * + * @generated from field: string instance_domain = 6; + */ + value: string; + case: "instanceDomain"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsFilter"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + { no: 2, name: "is_archived", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "filter" }, + { no: 3, name: "permission", kind: "message", T: Permission, oneof: "filter" }, + { no: 4, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + { no: 5, name: "salesforce", kind: "message", T: EnterpriseSubscriptionSalesforceMetadata, oneof: "filter" }, + { no: 6, name: "instance_domain", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionsFilter { + return new ListEnterpriseSubscriptionsFilter().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionsFilter { + return new ListEnterpriseSubscriptionsFilter().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionsFilter { + return new ListEnterpriseSubscriptionsFilter().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionsFilter | PlainMessage | undefined, b: ListEnterpriseSubscriptionsFilter | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionsFilter, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsRequest + */ +export class ListEnterpriseSubscriptionsRequest extends Message { + /** + * Clients use this field to specify the maximum number of results to be + * returned by the server. The server may further constrain the maximum number + * of results returned in a single page. If the page_size is 0, the server + * will decide the number of results to be returned. + * + * See pagination concepts from https://cloud.google.com/apis/design/design_patterns#list_pagination + * + * @generated from field: int32 page_size = 1; + */ + pageSize = 0; + + /** + * The client uses this field to request a specific page of the list results. + * + * See pagination concepts from https://cloud.google.com/apis/design/design_patterns#list_pagination + * + * @generated from field: string page_token = 2; + */ + pageToken = ""; + + /** + * Filters define the lookup strategy for this list request. Multiple filters + * are treated as AND-concatenated. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsFilter filters = 3; + */ + filters: ListEnterpriseSubscriptionsFilter[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "page_size", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 2, name: "page_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "filters", kind: "message", T: ListEnterpriseSubscriptionsFilter, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionsRequest { + return new ListEnterpriseSubscriptionsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionsRequest { + return new ListEnterpriseSubscriptionsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionsRequest { + return new ListEnterpriseSubscriptionsRequest().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionsRequest | PlainMessage | undefined, b: ListEnterpriseSubscriptionsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionsRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsResponse + */ +export class ListEnterpriseSubscriptionsResponse extends Message { + /** + * This field represents the pagination token to retrieve the next page of + * results. If the value is "", it means no further results for the request. + * + * @generated from field: string next_page_token = 1; + */ + nextPageToken = ""; + + /** + * The list of subscriptions that matched the given query. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.EnterpriseSubscription subscriptions = 2; + */ + subscriptions: EnterpriseSubscription[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "next_page_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "subscriptions", kind: "message", T: EnterpriseSubscription, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionsResponse { + return new ListEnterpriseSubscriptionsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionsResponse { + return new ListEnterpriseSubscriptionsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionsResponse { + return new ListEnterpriseSubscriptionsResponse().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionsResponse | PlainMessage | undefined, b: ListEnterpriseSubscriptionsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionsResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesFilter + */ +export class ListEnterpriseSubscriptionLicensesFilter extends Message { + /** + * @generated from oneof enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesFilter.filter + */ + filter: { + /** + * Return only licenses corresponding to the given subscription ID, with the + * most recently issued licenses first. + * + * @generated from field: string subscription_id = 1; + */ + value: string; + case: "subscriptionId"; + } | { + /** + * Return only licenses of the given type. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicenseType type = 2; + */ + value: EnterpriseSubscriptionLicenseType; + case: "type"; + } | { + /** + * Return only licenses that are active. + * + * @generated from field: bool is_revoked = 3; + */ + value: boolean; + case: "isRevoked"; + } | { + /** + * Return only licenses where the signed license key contains this substring. + * Query must be at least 3 characters. + * + * MUST be used in conjunction with the type = 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' + * filter. + * + * @generated from field: string license_key_substring = 4; + */ + value: string; + case: "licenseKeySubstring"; + } | { + /** + * Return only licenses associated with this Salesforce opportunity ID. + * MUST be used in conjunction with the 'type' filter. + * + * @generated from field: string salesforce_opportunity_id = 5; + */ + value: string; + case: "salesforceOpportunityId"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesFilter"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + { no: 2, name: "type", kind: "enum", T: proto3.getEnumType(EnterpriseSubscriptionLicenseType), oneof: "filter" }, + { no: 3, name: "is_revoked", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "filter" }, + { no: 4, name: "license_key_substring", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + { no: 5, name: "salesforce_opportunity_id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "filter" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionLicensesFilter { + return new ListEnterpriseSubscriptionLicensesFilter().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionLicensesFilter { + return new ListEnterpriseSubscriptionLicensesFilter().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionLicensesFilter { + return new ListEnterpriseSubscriptionLicensesFilter().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionLicensesFilter | PlainMessage | undefined, b: ListEnterpriseSubscriptionLicensesFilter | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionLicensesFilter, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesRequest + */ +export class ListEnterpriseSubscriptionLicensesRequest extends Message { + /** + * Clients use this field to specify the maximum number of results to be + * returned by the server. The server may further constrain the maximum number + * of results returned in a single page. If the page_size is 0, the server + * will decide the number of results to be returned. + * + * See pagination concepts from https://cloud.google.com/apis/design/design_patterns#list_pagination + * + * @generated from field: int32 page_size = 1; + */ + pageSize = 0; + + /** + * The client uses this field to request a specific page of the list results. + * A zero value requests the first page. + * + * See pagination concepts from https://cloud.google.com/apis/design/design_patterns#list_pagination + * + * TODO: Create an internal pagination token type: https://protobuf.dev/programming-guides/api/#encode-opaque-data-in-strings + * + * @generated from field: string page_token = 2; + */ + pageToken = ""; + + /** + * Filters define the lookup strategy for this list request. Multiple filters + * are treated as AND-concatenated. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesFilter filters = 3; + */ + filters: ListEnterpriseSubscriptionLicensesFilter[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "page_size", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 2, name: "page_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "filters", kind: "message", T: ListEnterpriseSubscriptionLicensesFilter, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionLicensesRequest { + return new ListEnterpriseSubscriptionLicensesRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionLicensesRequest { + return new ListEnterpriseSubscriptionLicensesRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionLicensesRequest { + return new ListEnterpriseSubscriptionLicensesRequest().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionLicensesRequest | PlainMessage | undefined, b: ListEnterpriseSubscriptionLicensesRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionLicensesRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesResponse + */ +export class ListEnterpriseSubscriptionLicensesResponse extends Message { + /** + * This field represents the pagination token to retrieve the next page of + * results. If the value is "", it means no further results for the request. + * + * @generated from field: string next_page_token = 1; + */ + nextPageToken = ""; + + /** + * The list of licenses that matched the given query, sorted by the most + * recently created licenses first. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense licenses = 2; + */ + licenses: EnterpriseSubscriptionLicense[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ListEnterpriseSubscriptionLicensesResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "next_page_token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "licenses", kind: "message", T: EnterpriseSubscriptionLicense, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListEnterpriseSubscriptionLicensesResponse { + return new ListEnterpriseSubscriptionLicensesResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ListEnterpriseSubscriptionLicensesResponse { + return new ListEnterpriseSubscriptionLicensesResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ListEnterpriseSubscriptionLicensesResponse { + return new ListEnterpriseSubscriptionLicensesResponse().fromJsonString(jsonString, options); + } + + static equals(a: ListEnterpriseSubscriptionLicensesResponse | PlainMessage | undefined, b: ListEnterpriseSubscriptionLicensesResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(ListEnterpriseSubscriptionLicensesResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionLicenseRequest + */ +export class CreateEnterpriseSubscriptionLicenseRequest extends Message { + /** + * The license to create. + * + * Required attributes: + * - subscription_id (this is the parent resource, EnterpriseSubscription) + * - license + * + * `license` supports the following types. At least one must be provided: + * + * - license.key, which requires: + * - license.key.info_version + * - license.key.info.tags + * - license.key.info.user_count + * - license.key.info.expire_time + * - license.key.info.salesforce_opportunity_id + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense license = 1; + */ + license?: EnterpriseSubscriptionLicense; + + /** + * Message to associate with the license creation event. + * + * @generated from field: string message = 2; + */ + message = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionLicenseRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "license", kind: "message", T: EnterpriseSubscriptionLicense }, + { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CreateEnterpriseSubscriptionLicenseRequest { + return new CreateEnterpriseSubscriptionLicenseRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CreateEnterpriseSubscriptionLicenseRequest { + return new CreateEnterpriseSubscriptionLicenseRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CreateEnterpriseSubscriptionLicenseRequest { + return new CreateEnterpriseSubscriptionLicenseRequest().fromJsonString(jsonString, options); + } + + static equals(a: CreateEnterpriseSubscriptionLicenseRequest | PlainMessage | undefined, b: CreateEnterpriseSubscriptionLicenseRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(CreateEnterpriseSubscriptionLicenseRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionLicenseResponse + */ +export class CreateEnterpriseSubscriptionLicenseResponse extends Message { + /** + * The license that was created. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionLicense license = 1; + */ + license?: EnterpriseSubscriptionLicense; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionLicenseResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "license", kind: "message", T: EnterpriseSubscriptionLicense }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CreateEnterpriseSubscriptionLicenseResponse { + return new CreateEnterpriseSubscriptionLicenseResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CreateEnterpriseSubscriptionLicenseResponse { + return new CreateEnterpriseSubscriptionLicenseResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CreateEnterpriseSubscriptionLicenseResponse { + return new CreateEnterpriseSubscriptionLicenseResponse().fromJsonString(jsonString, options); + } + + static equals(a: CreateEnterpriseSubscriptionLicenseResponse | PlainMessage | undefined, b: CreateEnterpriseSubscriptionLicenseResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(CreateEnterpriseSubscriptionLicenseResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.RevokeEnterpriseSubscriptionLicenseRequest + */ +export class RevokeEnterpriseSubscriptionLicenseRequest extends Message { + /** + * The ID of the license to revoke. + * + * @generated from field: string license_id = 1; + */ + licenseId = ""; + + /** + * Human-readable explanation for revoking the license. + * + * @generated from field: string reason = 2; + */ + reason = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.RevokeEnterpriseSubscriptionLicenseRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "license_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "reason", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RevokeEnterpriseSubscriptionLicenseRequest { + return new RevokeEnterpriseSubscriptionLicenseRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RevokeEnterpriseSubscriptionLicenseRequest { + return new RevokeEnterpriseSubscriptionLicenseRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RevokeEnterpriseSubscriptionLicenseRequest { + return new RevokeEnterpriseSubscriptionLicenseRequest().fromJsonString(jsonString, options); + } + + static equals(a: RevokeEnterpriseSubscriptionLicenseRequest | PlainMessage | undefined, b: RevokeEnterpriseSubscriptionLicenseRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(RevokeEnterpriseSubscriptionLicenseRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.RevokeEnterpriseSubscriptionLicenseResponse + */ +export class RevokeEnterpriseSubscriptionLicenseResponse extends Message { + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.RevokeEnterpriseSubscriptionLicenseResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RevokeEnterpriseSubscriptionLicenseResponse { + return new RevokeEnterpriseSubscriptionLicenseResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RevokeEnterpriseSubscriptionLicenseResponse { + return new RevokeEnterpriseSubscriptionLicenseResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RevokeEnterpriseSubscriptionLicenseResponse { + return new RevokeEnterpriseSubscriptionLicenseResponse().fromJsonString(jsonString, options); + } + + static equals(a: RevokeEnterpriseSubscriptionLicenseResponse | PlainMessage | undefined, b: RevokeEnterpriseSubscriptionLicenseResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(RevokeEnterpriseSubscriptionLicenseResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionRequest + */ +export class UpdateEnterpriseSubscriptionRequest extends Message { + /** + * The subscription to update. + * The following fields are used to identify the membership to update: + * - id + * Multiple fields are treated as AND-concatenated. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscription subscription = 1; + */ + subscription?: EnterpriseSubscription; + + /** + * The list of fields to update, fields are specified relative to the EnterpriseSubscription. + * Updatable fields are: + * - instance_domain + * - instance_type + * - display_name + * - salesforce.subscription_id + * + * @generated from field: google.protobuf.FieldMask update_mask = 2; + */ + updateMask?: FieldMask; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription", kind: "message", T: EnterpriseSubscription }, + { no: 2, name: "update_mask", kind: "message", T: FieldMask }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateEnterpriseSubscriptionRequest { + return new UpdateEnterpriseSubscriptionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateEnterpriseSubscriptionRequest { + return new UpdateEnterpriseSubscriptionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateEnterpriseSubscriptionRequest { + return new UpdateEnterpriseSubscriptionRequest().fromJsonString(jsonString, options); + } + + static equals(a: UpdateEnterpriseSubscriptionRequest | PlainMessage | undefined, b: UpdateEnterpriseSubscriptionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(UpdateEnterpriseSubscriptionRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionResponse + */ +export class UpdateEnterpriseSubscriptionResponse extends Message { + /** + * The updated subscription. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscription subscription = 1; + */ + subscription?: EnterpriseSubscription; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription", kind: "message", T: EnterpriseSubscription }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateEnterpriseSubscriptionResponse { + return new UpdateEnterpriseSubscriptionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateEnterpriseSubscriptionResponse { + return new UpdateEnterpriseSubscriptionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateEnterpriseSubscriptionResponse { + return new UpdateEnterpriseSubscriptionResponse().fromJsonString(jsonString, options); + } + + static equals(a: UpdateEnterpriseSubscriptionResponse | PlainMessage | undefined, b: UpdateEnterpriseSubscriptionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(UpdateEnterpriseSubscriptionResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ArchiveEnterpriseSubscriptionRequest + */ +export class ArchiveEnterpriseSubscriptionRequest extends Message { + /** + * The ID of the subscription to archive. + * + * @generated from field: string subscription_id = 1; + */ + subscriptionId = ""; + + /** + * Human-readable explanation for revoking the license. + * + * @generated from field: string reason = 2; + */ + reason = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ArchiveEnterpriseSubscriptionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "reason", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ArchiveEnterpriseSubscriptionRequest { + return new ArchiveEnterpriseSubscriptionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ArchiveEnterpriseSubscriptionRequest { + return new ArchiveEnterpriseSubscriptionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ArchiveEnterpriseSubscriptionRequest { + return new ArchiveEnterpriseSubscriptionRequest().fromJsonString(jsonString, options); + } + + static equals(a: ArchiveEnterpriseSubscriptionRequest | PlainMessage | undefined, b: ArchiveEnterpriseSubscriptionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(ArchiveEnterpriseSubscriptionRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.ArchiveEnterpriseSubscriptionResponse + */ +export class ArchiveEnterpriseSubscriptionResponse extends Message { + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.ArchiveEnterpriseSubscriptionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ArchiveEnterpriseSubscriptionResponse { + return new ArchiveEnterpriseSubscriptionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ArchiveEnterpriseSubscriptionResponse { + return new ArchiveEnterpriseSubscriptionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ArchiveEnterpriseSubscriptionResponse { + return new ArchiveEnterpriseSubscriptionResponse().fromJsonString(jsonString, options); + } + + static equals(a: ArchiveEnterpriseSubscriptionResponse | PlainMessage | undefined, b: ArchiveEnterpriseSubscriptionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(ArchiveEnterpriseSubscriptionResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionRequest + */ +export class CreateEnterpriseSubscriptionRequest extends Message { + /** + * The subscription to create. + * + * Required attributes: + * - display_name + * - instance_type + * + * Optional attributes: + * - instance_domain + * - salesforce.subscription_id + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscription subscription = 1; + */ + subscription?: EnterpriseSubscription; + + /** + * Message to associate with the subscription creation event. + * + * @generated from field: string message = 2; + */ + message = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription", kind: "message", T: EnterpriseSubscription }, + { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CreateEnterpriseSubscriptionRequest { + return new CreateEnterpriseSubscriptionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CreateEnterpriseSubscriptionRequest { + return new CreateEnterpriseSubscriptionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CreateEnterpriseSubscriptionRequest { + return new CreateEnterpriseSubscriptionRequest().fromJsonString(jsonString, options); + } + + static equals(a: CreateEnterpriseSubscriptionRequest | PlainMessage | undefined, b: CreateEnterpriseSubscriptionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(CreateEnterpriseSubscriptionRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionResponse + */ +export class CreateEnterpriseSubscriptionResponse extends Message { + /** + * The created subscription. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscription subscription = 1; + */ + subscription?: EnterpriseSubscription; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.CreateEnterpriseSubscriptionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription", kind: "message", T: EnterpriseSubscription }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CreateEnterpriseSubscriptionResponse { + return new CreateEnterpriseSubscriptionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CreateEnterpriseSubscriptionResponse { + return new CreateEnterpriseSubscriptionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CreateEnterpriseSubscriptionResponse { + return new CreateEnterpriseSubscriptionResponse().fromJsonString(jsonString, options); + } + + static equals(a: CreateEnterpriseSubscriptionResponse | PlainMessage | undefined, b: CreateEnterpriseSubscriptionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(CreateEnterpriseSubscriptionResponse, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.EnterpriseSubscriptionMembership + */ +export class EnterpriseSubscriptionMembership extends Message { + /** + * The external, prefixed UUID-format identifier of the subscription. + * + * @generated from field: string subscription_id = 1; + */ + subscriptionId = ""; + + /** + * The instance domain associated with this subscription, e.g. "acme.sourcegraphcloud.com". + * + * @generated from field: string instance_domain = 2; + */ + instanceDomain = ""; + + /** + * The SAMS account ID of the member. + * + * @generated from field: string member_sams_account_id = 3; + */ + memberSamsAccountId = ""; + + /** + * The roles of the member. The roles provided are authoritative - all roles + * not on the list are revoked. + * + * @generated from field: repeated enterpriseportal.subscriptions.v1.Role member_roles = 4; + */ + memberRoles: Role[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.EnterpriseSubscriptionMembership"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subscription_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "instance_domain", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "member_sams_account_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "member_roles", kind: "enum", T: proto3.getEnumType(Role), repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EnterpriseSubscriptionMembership { + return new EnterpriseSubscriptionMembership().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EnterpriseSubscriptionMembership { + return new EnterpriseSubscriptionMembership().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EnterpriseSubscriptionMembership { + return new EnterpriseSubscriptionMembership().fromJsonString(jsonString, options); + } + + static equals(a: EnterpriseSubscriptionMembership | PlainMessage | undefined, b: EnterpriseSubscriptionMembership | PlainMessage | undefined): boolean { + return proto3.util.equals(EnterpriseSubscriptionMembership, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionMembershipRequest + */ +export class UpdateEnterpriseSubscriptionMembershipRequest extends Message { + /** + * The membership to update. + * The first non-empty field of the following fields is used to identify the product subscription to update: + * - subscription_id + * - instance_domain + * Multiple fields are treated as AND-concatenated. + * + * @generated from field: enterpriseportal.subscriptions.v1.EnterpriseSubscriptionMembership membership = 1; + */ + membership?: EnterpriseSubscriptionMembership; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionMembershipRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "membership", kind: "message", T: EnterpriseSubscriptionMembership }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateEnterpriseSubscriptionMembershipRequest { + return new UpdateEnterpriseSubscriptionMembershipRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateEnterpriseSubscriptionMembershipRequest { + return new UpdateEnterpriseSubscriptionMembershipRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateEnterpriseSubscriptionMembershipRequest { + return new UpdateEnterpriseSubscriptionMembershipRequest().fromJsonString(jsonString, options); + } + + static equals(a: UpdateEnterpriseSubscriptionMembershipRequest | PlainMessage | undefined, b: UpdateEnterpriseSubscriptionMembershipRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(UpdateEnterpriseSubscriptionMembershipRequest, a, b); + } +} + +/** + * @generated from message enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionMembershipResponse + */ +export class UpdateEnterpriseSubscriptionMembershipResponse extends Message { + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.UpdateEnterpriseSubscriptionMembershipResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateEnterpriseSubscriptionMembershipResponse { + return new UpdateEnterpriseSubscriptionMembershipResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateEnterpriseSubscriptionMembershipResponse { + return new UpdateEnterpriseSubscriptionMembershipResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateEnterpriseSubscriptionMembershipResponse { + return new UpdateEnterpriseSubscriptionMembershipResponse().fromJsonString(jsonString, options); + } + + static equals(a: UpdateEnterpriseSubscriptionMembershipResponse | PlainMessage | undefined, b: UpdateEnterpriseSubscriptionMembershipResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(UpdateEnterpriseSubscriptionMembershipResponse, a, b); + } +} + +/** + * Permission represents a permission that can be performed by a user. + * + * @generated from message enterpriseportal.subscriptions.v1.Permission + */ +export class Permission extends Message { + /** + * The type of the permission. + * + * @generated from field: enterpriseportal.subscriptions.v1.PermissionType type = 1; + */ + type = PermissionType.UNSPECIFIED; + + /** + * The relation between the user and the resource. + * + * @generated from field: enterpriseportal.subscriptions.v1.PermissionRelation relation = 2; + */ + relation = PermissionRelation.UNSPECIFIED; + + /** + * The SAMS account ID of the user. + * + * @generated from field: string sams_account_id = 3; + */ + samsAccountId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "enterpriseportal.subscriptions.v1.Permission"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "type", kind: "enum", T: proto3.getEnumType(PermissionType) }, + { no: 2, name: "relation", kind: "enum", T: proto3.getEnumType(PermissionRelation) }, + { no: 3, name: "sams_account_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): Permission { + return new Permission().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): Permission { + return new Permission().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): Permission { + return new Permission().fromJsonString(jsonString, options); + } + + static equals(a: Permission | PlainMessage | undefined, b: Permission | PlainMessage | undefined): boolean { + return proto3.util.equals(Permission, a, b); + } +} + diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts index 32bd66fd91b..ec2c7a8a2b1 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/utils.ts @@ -40,11 +40,3 @@ export function errorForPath(error: ApolloError | undefined, path: (string | num } export const numberFormatter = new Intl.NumberFormat() - -/** - * Prefixes the ID with subscriptionsv1.EnterpriseSubscriptionIDPrefix to get - * the Enterprise Portal external subscription UUID format. - */ -export function enterprisePortalID(id: string): string { - return `es_${id}` -} diff --git a/client/web/src/site-admin/routes.tsx b/client/web/src/site-admin/routes.tsx index 304b8a5b0e8..f38f5f8692d 100644 --- a/client/web/src/site-admin/routes.tsx +++ b/client/web/src/site-admin/routes.tsx @@ -114,10 +114,6 @@ const SiteAdminProductSubscriptionPage = lazyComponent( () => import('../enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage'), 'SiteAdminProductSubscriptionPage' ) -const SiteAdminProductCustomersPage = lazyComponent( - () => import('../enterprise/site-admin/dotcom/customers/SiteAdminCustomersPage'), - 'SiteAdminProductCustomersPage' -) const SiteAdminCreateProductSubscriptionPage = lazyComponent( () => import('../enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage'), 'SiteAdminCreateProductSubscriptionPage' @@ -367,11 +363,6 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [ ), }, - { - path: '/dotcom/customers', - render: props => , - condition: () => SHOW_BUSINESS_FEATURES, - }, { path: '/dotcom/product/subscriptions/new', render: props => , diff --git a/client/web/src/site-admin/sidebaritems.ts b/client/web/src/site-admin/sidebaritems.ts index 1e3ff9e010b..9bf305964fe 100644 --- a/client/web/src/site-admin/sidebaritems.ts +++ b/client/web/src/site-admin/sidebaritems.ts @@ -234,11 +234,6 @@ export const batchChangesGroup: SiteAdminSideBarGroup = { const businessGroup: SiteAdminSideBarGroup = { header: { label: 'Business', icon: BriefcaseIcon }, items: [ - { - label: 'Enterprise customers', - to: '/site-admin/dotcom/customers', - condition: () => SHOW_BUSINESS_FEATURES, - }, { label: 'Enterprise subscriptions', to: '/site-admin/dotcom/product/subscriptions', diff --git a/cmd/enterprise-portal/internal/database/importer/importer.go b/cmd/enterprise-portal/internal/database/importer/importer.go index 41c2eb731d9..cd8e5b8f8da 100644 --- a/cmd/enterprise-portal/internal/database/importer/importer.go +++ b/cmd/enterprise-portal/internal/database/importer/importer.go @@ -71,7 +71,7 @@ func NewPeriodicImporter( interval time.Duration, ) background.Routine { if interval == 0 { - logger.Warn("importer disabled") + logger.Info("importer disabled") return background.NoopRoutine("dotcom.importer.disabled") } return goroutine.NewPeriodicGoroutine( diff --git a/cmd/enterprise-portal/service/config.go b/cmd/enterprise-portal/service/config.go index a67fa08c16e..8036eb2a301 100644 --- a/cmd/enterprise-portal/service/config.go +++ b/cmd/enterprise-portal/service/config.go @@ -65,7 +65,7 @@ func (c *Config) Load(env *runtime.Env) { "For local dev: custom PostgreSQL DSN, overrides DOTCOM_CLOUDSQL_* options") c.DotComDB.IncludeProductionLicenses = env.GetBool("DOTCOM_INCLUDE_PRODUCTION_LICENSES", "false", "Include production licenses in API results") - c.DotComDB.ImportInterval = env.GetInterval("DOTCOM_IMPORT_INTERVAL", "10m", + c.DotComDB.ImportInterval = env.GetInterval("DOTCOM_IMPORT_INTERVAL", "0s", // disable by default "Interval at which to import data from Sourcegraph.com") c.SAMS.ConnConfig = sams.NewConnConfigFromEnv(env) diff --git a/lib/enterpriseportal/subscriptions/v1/buf.gen.yaml b/lib/enterpriseportal/subscriptions/v1/buf.gen.yaml index e5707836ced..966ac57967b 100644 --- a/lib/enterpriseportal/subscriptions/v1/buf.gen.yaml +++ b/lib/enterpriseportal/subscriptions/v1/buf.gen.yaml @@ -15,3 +15,11 @@ plugins: out: . opt: - paths=source_relative + + # Generate connectrpc bindings in Typescript: https://buf.build/connectrpc/query-es?version=v1.4.1 + - plugin: buf.build/connectrpc/query-es:v1.4.1 + out: ../../../../client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen + opt: target=ts + - plugin: buf.build/bufbuild/es:v1.10.0 + out: ../../../../client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen + opt: target=ts