mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
SSC: Refactor team management (#62694)
This commit is contained in:
parent
d1404951eb
commit
aae97ad078
@ -247,10 +247,11 @@ ts_project(
|
||||
"src/cody/sidebar/useSidebarSize.tsx",
|
||||
"src/cody/subscription/CodySubscriptionPage.tsx",
|
||||
"src/cody/subscription/queries.tsx",
|
||||
"src/cody/subscription/subscriptionSummary.ts",
|
||||
"src/cody/switch-account/CodySwitchAccountPage.tsx",
|
||||
"src/cody/team/CodyManageTeamPage.tsx",
|
||||
"src/cody/team/InviteUsers.tsx",
|
||||
"src/cody/team/TeamMembers.tsx",
|
||||
"src/cody/team/TeamMemberList.tsx",
|
||||
"src/cody/team/WhiteIcon.tsx",
|
||||
"src/cody/upsell/ChatBrandIcon.tsx",
|
||||
"src/cody/upsell/CodyUpsellPage.tsx",
|
||||
|
||||
@ -6,6 +6,8 @@ import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { H3, Text } from '@sourcegraph/wildcard'
|
||||
|
||||
import { requestSSC } from '../../../util'
|
||||
|
||||
/**
|
||||
* CodyProCheckoutForm is essentially an iframe that the Stripe Elements library will
|
||||
* render an iframe into, that will host a Stripe Checkout-hosted form.
|
||||
@ -71,30 +73,19 @@ async function createCheckoutSession(
|
||||
const origin = window.location.origin
|
||||
|
||||
try {
|
||||
// So the request is kinda made to 2x backends. dotcom's .api/ssc/proxy endpoint will
|
||||
// take care of exchanging the Sourcegraph session credentials for a SAMS access token.
|
||||
// And then proxy the request onto the SSC backend, which will actually create the
|
||||
// checkout session.
|
||||
// TODO: Use fetchThroughSSCProxy instead of fetch.
|
||||
const response = await fetch(`${origin}/.api/ssc/proxy/checkout/session`, {
|
||||
// Pass along the "sgs" session cookie to identify the caller.
|
||||
credentials: 'same-origin',
|
||||
method: 'POST',
|
||||
// Object sent to the backend. See `createCheckoutSessionRequest`.
|
||||
body: JSON.stringify({
|
||||
interval: billingInterval,
|
||||
seats: 1,
|
||||
customerEmail,
|
||||
showPromoCodeField,
|
||||
const response = await requestSSC('/checkout/session', 'POST', {
|
||||
interval: billingInterval,
|
||||
seats: 1,
|
||||
customerEmail,
|
||||
showPromoCodeField,
|
||||
|
||||
// URL the user is redirected to when the checkout process is complete.
|
||||
//
|
||||
// BUG: Due to the race conditions between Stripe, the SSC backend,
|
||||
// and Sourcegraph.com, immediately loading the Dashboard page isn't
|
||||
// going to show the right data reliably. We will need to instead show
|
||||
// some interstitial or welcome prompt, to give various things to sync.
|
||||
returnUrl: `${origin}/cody/manage?session_id={CHECKOUT_SESSION_ID}`,
|
||||
}),
|
||||
// URL the user is redirected to when the checkout process is complete.
|
||||
//
|
||||
// BUG: Due to the race conditions between Stripe, the SSC backend,
|
||||
// and Sourcegraph.com, immediately loading the Dashboard page isn't
|
||||
// going to show the right data reliably. We will need to instead show
|
||||
// some interstitial or welcome prompt, to give various things to sync.
|
||||
returnUrl: `${origin}/cody/manage?session_id={CHECKOUT_SESSION_ID}`,
|
||||
})
|
||||
|
||||
const responseBody = await response.text()
|
||||
|
||||
11
client/web/src/cody/subscription/subscriptionSummary.ts
Normal file
11
client/web/src/cody/subscription/subscriptionSummary.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useSSCQuery } from '../util'
|
||||
|
||||
type TeamRole = 'member' | 'admin'
|
||||
|
||||
interface CodySubscriptionSummary {
|
||||
teamId: string
|
||||
userRole: TeamRole
|
||||
}
|
||||
|
||||
export const useCodySubscriptionSummaryData = (): [CodySubscriptionSummary | null, Error | null] =>
|
||||
useSSCQuery<CodySubscriptionSummary>('/team/current/subscription/summary')
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
|
||||
import { mdiPlusThick, mdiOpenInNew } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
@ -11,10 +11,11 @@ import type { AuthenticatedUser } from '../../auth'
|
||||
import { withAuthenticatedUser } from '../../auth/withAuthenticatedUser'
|
||||
import { Page } from '../../components/Page'
|
||||
import { PageTitle } from '../../components/PageTitle'
|
||||
import { fetchThroughSSCProxy } from '../util'
|
||||
import { useCodySubscriptionSummaryData } from '../subscription/subscriptionSummary'
|
||||
import { useSSCQuery } from '../util'
|
||||
|
||||
import { InviteUsers } from './InviteUsers'
|
||||
import { type TeamInvite, TeamMemberList, type TeamMember } from './TeamMembers'
|
||||
import { TeamMemberList, type TeamMember, type TeamInvite } from './TeamMemberList'
|
||||
import { WhiteIcon } from './WhiteIcon'
|
||||
|
||||
import styles from './CodyManageTeamPage.module.scss'
|
||||
@ -23,51 +24,14 @@ interface CodyManageTeamPageProps extends TelemetryV2Props {
|
||||
authenticatedUser: AuthenticatedUser
|
||||
}
|
||||
|
||||
// TODO: Remove this mock data
|
||||
const mockTeamMembers: TeamMember[] = [
|
||||
{
|
||||
accountId: '1',
|
||||
displayName: 'daniel.marques.pt',
|
||||
email: 'daniel.marques@sourcegraph.com',
|
||||
avatarUrl: null,
|
||||
role: 'member',
|
||||
},
|
||||
]
|
||||
type CodySubscriptionStatus = 'active' | 'past_due' | 'unpaid' | 'canceled' | 'trialing' | 'other'
|
||||
|
||||
const mockInvites: TeamInvite[] = [
|
||||
{
|
||||
id: '1',
|
||||
email: 'rob.rhyne@sourcegraph.com',
|
||||
role: 'member',
|
||||
status: 'sent',
|
||||
error: null,
|
||||
sentAt: '2021-09-01T00:00:00Z',
|
||||
acceptedAt: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
email: 'kevin.chen@sourcegraph.com',
|
||||
role: 'admin',
|
||||
status: 'sent',
|
||||
error: null,
|
||||
sentAt: '2021-09-01T00:00:00Z',
|
||||
acceptedAt: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
email: 'test@test.com',
|
||||
role: 'member',
|
||||
status: 'accepted',
|
||||
error: null,
|
||||
sentAt: '2021-09-01T00:00:00Z',
|
||||
acceptedAt: '2021-09-01T00:00:00Z',
|
||||
},
|
||||
]
|
||||
interface CodySubscription {
|
||||
subscriptionStatus: CodySubscriptionStatus
|
||||
maxSeats: number
|
||||
}
|
||||
|
||||
const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPageProps> = ({
|
||||
authenticatedUser,
|
||||
telemetryRecorder,
|
||||
}) => {
|
||||
const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPageProps> = ({ telemetryRecorder }) => {
|
||||
useEffect(() => {
|
||||
telemetryRecorder.recordEvent('cody.team.management', 'view')
|
||||
}, [telemetryRecorder])
|
||||
@ -80,89 +44,31 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPag
|
||||
const newSeatsPurchased: number | null = newSeatsPurchasedParam ? parseInt(newSeatsPurchasedParam, 10) : null
|
||||
|
||||
// Load data
|
||||
const [subscriptionData, setSubscriptionData] = useState<{
|
||||
subscriptionSeatCount: number | null
|
||||
isProUser: boolean | null
|
||||
} | null>(null)
|
||||
const subscriptionSeatCount = subscriptionData?.subscriptionSeatCount
|
||||
const isProUser = subscriptionData?.isProUser
|
||||
const [subscriptionDataError, setSubscriptionDataError] = useState<null | Error>(null)
|
||||
const [subscriptionSummaryData, setSubscriptionSummaryData] = useState<{
|
||||
teamId: string | null
|
||||
isAdmin: boolean | null
|
||||
} | null>(null)
|
||||
const [subscriptionSummaryDataError, setSubscriptionSummaryDataError] = useState<null | Error>(null)
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[] | null>(null)
|
||||
const [membersDataError, setMembersDataError] = useState<null | Error>(null)
|
||||
const [teamInvites, setTeamInvites] = useState<TeamInvite[] | null>(null)
|
||||
const [invitesDataError, setInvitesDataError] = useState<null | Error>(null)
|
||||
useEffect(() => {
|
||||
async function loadSubscriptionData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchThroughSSCProxy('/team/current/subscription', 'GET')
|
||||
const responseJson = (await response.json()) as {
|
||||
subscriptionStatus: 'active' | 'past_due' | 'unpaid' | 'canceled' | 'trialing' | 'other'
|
||||
maxSeats: number
|
||||
} | null
|
||||
setSubscriptionData({
|
||||
subscriptionSeatCount: responseJson?.maxSeats ?? null,
|
||||
isProUser: responseJson && responseJson.subscriptionStatus !== 'canceled',
|
||||
})
|
||||
} catch (error) {
|
||||
setSubscriptionDataError(error)
|
||||
}
|
||||
}
|
||||
async function loadSubscriptionSummaryData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchThroughSSCProxy('/team/current/subscription/summary', 'GET')
|
||||
const responseJson = (await response.json()) as {
|
||||
teamId: string
|
||||
userRole: 'none' | 'member' | 'admin'
|
||||
} | null
|
||||
setSubscriptionSummaryData({
|
||||
teamId: responseJson?.teamId ?? null,
|
||||
isAdmin: responseJson && responseJson.userRole === 'admin',
|
||||
})
|
||||
} catch (error) {
|
||||
setSubscriptionSummaryDataError(error)
|
||||
}
|
||||
}
|
||||
async function loadMemberData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchThroughSSCProxy('/team/current/members', 'GET')
|
||||
const responseJson = await response.json()
|
||||
setTeamMembers((responseJson as { members: TeamMember[] }).members.concat(mockTeamMembers))
|
||||
} catch (error) {
|
||||
setMembersDataError(error)
|
||||
}
|
||||
}
|
||||
async function loadInviteData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetchThroughSSCProxy('/team/current/invites', 'GET')
|
||||
const responseJson = await response.json()
|
||||
setTeamInvites((responseJson as { invites: TeamInvite[] }).invites.concat(mockInvites))
|
||||
} catch (error) {
|
||||
setInvitesDataError(error)
|
||||
}
|
||||
}
|
||||
|
||||
void loadSubscriptionData()
|
||||
void loadSubscriptionSummaryData()
|
||||
void loadMemberData()
|
||||
void loadInviteData()
|
||||
}, [authenticatedUser])
|
||||
const [codySubscription, codySubscriptionError] = useSSCQuery<CodySubscription>('/team/current/subscription')
|
||||
const isPro = codySubscription?.subscriptionStatus !== 'canceled'
|
||||
const [codySubscriptionSummary, codySubscriptionSummaryError] = useCodySubscriptionSummaryData()
|
||||
const isAdmin = codySubscriptionSummary?.userRole === 'admin'
|
||||
const [memberResponse, membersDataError] = useSSCQuery<{ members: TeamMember[] }>('/team/current/members')
|
||||
const teamMembers = memberResponse?.members
|
||||
const [invitesResponse, invitesDataError] = useSSCQuery<{ invites: TeamInvite[] }>('/team/current/invites')
|
||||
const teamInvites = invitesResponse?.invites
|
||||
const errorMessage =
|
||||
codySubscriptionError?.message ||
|
||||
codySubscriptionSummaryError?.message ||
|
||||
membersDataError?.message ||
|
||||
invitesDataError?.message
|
||||
|
||||
useEffect(() => {
|
||||
if (isProUser === false) {
|
||||
if (!isPro) {
|
||||
navigate('/cody/subscription')
|
||||
}
|
||||
}, [isProUser, navigate])
|
||||
}, [isPro, navigate])
|
||||
|
||||
const remainingInviteCount = useMemo(() => {
|
||||
const memberCount = teamMembers?.length ?? 0
|
||||
const invitesUsed = (teamInvites ?? []).filter(invite => invite.status === 'sent').length
|
||||
return Math.max((subscriptionSeatCount ?? 0) - (memberCount + invitesUsed), 0)
|
||||
}, [subscriptionSeatCount, teamMembers, teamInvites])
|
||||
return Math.max((codySubscription?.maxSeats ?? 0) - (memberCount + invitesUsed), 0)
|
||||
}, [codySubscription?.maxSeats, teamMembers, teamInvites])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -171,14 +77,16 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPag
|
||||
<PageHeader
|
||||
className="mb-4 mt-4"
|
||||
actions={
|
||||
subscriptionSummaryData?.isAdmin && (
|
||||
codySubscriptionSummary?.userRole === 'admin' && (
|
||||
<div className="d-flex">
|
||||
<Link
|
||||
to="/cody/manage"
|
||||
className="d-inline-flex align-items-center mr-3"
|
||||
onClick={() =>
|
||||
telemetryRecorder.recordEvent('cody.team.manage.subscription', 'click', {
|
||||
metadata: { tier: isProUser ? 1 : 0 },
|
||||
metadata: {
|
||||
tier: codySubscription?.subscriptionStatus !== 'canceled' ? 1 : 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -211,27 +119,12 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPag
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
|
||||
{subscriptionDataError || subscriptionSummaryDataError || membersDataError || invitesDataError ? (
|
||||
{codySubscriptionError || codySubscriptionSummaryError || membersDataError || invitesDataError ? (
|
||||
<div className={classNames('mb-4', styles.alert, styles.errorAlert)}>
|
||||
<H3>Failed to load team data.</H3>
|
||||
{subscriptionDataError?.message && (
|
||||
<H3>We couldn't load team data this time. Please try a bit later.</H3>
|
||||
{errorMessage ?? (
|
||||
<Text size="small" className="text-muted mb-0">
|
||||
{subscriptionDataError?.message}
|
||||
</Text>
|
||||
)}
|
||||
{subscriptionSummaryDataError?.message && (
|
||||
<Text size="small" className="text-muted mb-0">
|
||||
{subscriptionDataError?.message}
|
||||
</Text>
|
||||
)}
|
||||
{membersDataError?.message && (
|
||||
<Text size="small" className="text-muted mb-0">
|
||||
{membersDataError?.message}
|
||||
</Text>
|
||||
)}
|
||||
{invitesDataError?.message && (
|
||||
<Text size="small" className="text-muted mb-0">
|
||||
{invitesDataError?.message}
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
@ -246,18 +139,18 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent<CodyManageTeamPag
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subscriptionSummaryData?.isAdmin && !!remainingInviteCount && (
|
||||
{isAdmin && !!remainingInviteCount && (
|
||||
<InviteUsers
|
||||
teamId={subscriptionSummaryData?.teamId}
|
||||
teamId={codySubscriptionSummary?.teamId}
|
||||
remainingInviteCount={remainingInviteCount}
|
||||
telemetryRecorder={telemetryRecorder}
|
||||
/>
|
||||
)}
|
||||
<TeamMemberList
|
||||
teamId={subscriptionSummaryData?.teamId ?? null}
|
||||
teamId={codySubscriptionSummary?.teamId ?? null}
|
||||
teamMembers={teamMembers || []}
|
||||
invites={teamInvites || []}
|
||||
isAdmin={subscriptionSummaryData?.isAdmin ?? false}
|
||||
isAdmin={isAdmin}
|
||||
telemetryRecorder={telemetryRecorder}
|
||||
/>
|
||||
</Page>
|
||||
|
||||
@ -6,7 +6,7 @@ import { pluralize } from '@sourcegraph/common'
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import { ButtonLink, H2, Link, Text, H3, TextArea } from '@sourcegraph/wildcard'
|
||||
|
||||
import { isValidEmailAddress, fetchThroughSSCProxy } from '../util'
|
||||
import { isValidEmailAddress, requestSSC } from '../util'
|
||||
|
||||
import styles from './CodyManageTeamPage.module.scss'
|
||||
|
||||
@ -43,7 +43,7 @@ export const InviteUsers: React.FunctionComponent<InviteUsersProps> = ({
|
||||
try {
|
||||
const responses = await Promise.all(
|
||||
emailAddresses.map(emailAddress =>
|
||||
fetchThroughSSCProxy('/team/current/invites', 'POST', { email: emailAddress, role: 'member' })
|
||||
requestSSC('/team/current/invites', 'POST', { email: emailAddress, role: 'member' })
|
||||
)
|
||||
)
|
||||
if (responses.some(response => response.status !== 200)) {
|
||||
|
||||
@ -5,7 +5,7 @@ import classNames from 'classnames'
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import { H2, Text, Badge, Link, ButtonLink } from '@sourcegraph/wildcard'
|
||||
|
||||
import { fetchThroughSSCProxy } from '../util'
|
||||
import { requestSSC } from '../util'
|
||||
|
||||
import styles from './CodyManageTeamPage.module.scss'
|
||||
|
||||
@ -17,6 +17,13 @@ export interface TeamMember {
|
||||
role: 'admin' | 'member'
|
||||
}
|
||||
|
||||
interface TeamMemberListProps extends TelemetryV2Props {
|
||||
teamId: string | null
|
||||
teamMembers: TeamMember[]
|
||||
invites: TeamInvite[]
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export interface TeamInvite {
|
||||
id: string
|
||||
email: string
|
||||
@ -27,13 +34,6 @@ export interface TeamInvite {
|
||||
acceptedAt: string | null
|
||||
}
|
||||
|
||||
interface TeamMemberListProps extends TelemetryV2Props {
|
||||
teamId: string | null
|
||||
teamMembers: TeamMember[]
|
||||
invites: TeamInvite[]
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
teamId,
|
||||
teamMembers,
|
||||
@ -43,7 +43,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionResult, setActionResult] = useState<{ message: string; isError: boolean } | null>(null)
|
||||
const setRole = useCallback(
|
||||
const updateRole = useCallback(
|
||||
async (accountId: string, newRole: 'member' | 'admin'): Promise<void> => {
|
||||
if (!loading) {
|
||||
// Avoids sending multiple requests at once
|
||||
@ -52,19 +52,24 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
privateMetadata: { teamId, accountId },
|
||||
})
|
||||
|
||||
const response = await fetchThroughSSCProxy(
|
||||
`/team/current/members/${accountId}?newRole=${newRole}`,
|
||||
'PATCH'
|
||||
)
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const response = await requestSSC(`/team/current/members/${accountId}?newRole=${newRole}`, 'PATCH')
|
||||
if (!response.ok) {
|
||||
setLoading(false)
|
||||
setActionResult({
|
||||
message: `We couldn't modify the user's role (${response.status}). Please try again later.`,
|
||||
isError: true,
|
||||
})
|
||||
} else {
|
||||
setLoading(false)
|
||||
setActionResult({ message: 'Team role updated.', isError: false })
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
setActionResult({
|
||||
message: `We couldn't modify the user's role (${response.status}). Please try again later.`,
|
||||
message: `We couldn't modify the user's role. The error was: "${error}". Please try again later.`,
|
||||
isError: true,
|
||||
})
|
||||
} else {
|
||||
setLoading(false)
|
||||
setActionResult({ message: 'Team role updated.', isError: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -78,7 +83,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
setLoading(true)
|
||||
telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } })
|
||||
|
||||
const response = await fetchThroughSSCProxy(`/team/current/invites/${inviteId}/cancel`, 'POST')
|
||||
const response = await requestSSC(`/team/current/invites/${inviteId}/cancel`, 'POST')
|
||||
if (!response.ok) {
|
||||
setLoading(false)
|
||||
setActionResult({
|
||||
@ -101,7 +106,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
setLoading(true)
|
||||
telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } })
|
||||
|
||||
const response = await fetchThroughSSCProxy(`/team/current/invites/${inviteId}/resend`, 'POST')
|
||||
const response = await requestSSC(`/team/current/invites/${inviteId}/resend`, 'POST')
|
||||
if (!response.ok) {
|
||||
setLoading(false)
|
||||
setActionResult({
|
||||
@ -127,7 +132,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
setLoading(true)
|
||||
telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } })
|
||||
|
||||
const response = await fetchThroughSSCProxy(`/team/current/members/${accountId}`, 'DELETE')
|
||||
const response = await requestSSC(`/team/current/members/${accountId}`, 'DELETE')
|
||||
if (!response.ok) {
|
||||
setLoading(false)
|
||||
setActionResult({
|
||||
@ -196,7 +201,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
<div className="d-flex flex-column justify-content-center ml-2">
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => setRole(member.accountId, 'member')}
|
||||
onClick={() => updateRole(member.accountId, 'member')}
|
||||
className="ml-2"
|
||||
aria-disabled={adminCount < 2}
|
||||
>
|
||||
@ -208,7 +213,7 @@ export const TeamMemberList: FunctionComponent<TeamMemberListProps> = ({
|
||||
<div className="d-flex flex-column justify-content-center ml-2">
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => setRole(member.accountId, 'admin')}
|
||||
onClick={() => updateRole(member.accountId, 'admin')}
|
||||
className="ml-2"
|
||||
>
|
||||
Make admin
|
||||
@ -1,4 +1,6 @@
|
||||
// The URL to direct users in order to manage their Cody Pro subscription.
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const manageSubscriptionRedirectURL = 'https://accounts.sourcegraph.com/cody/subscription'
|
||||
|
||||
/**
|
||||
@ -34,11 +36,14 @@ export function isValidEmailAddress(emailAddress: string): boolean {
|
||||
const emailRegex = /^[^@]+@[^@]+\.[^@]+$/
|
||||
|
||||
/**
|
||||
* So the request is kinda made to two backends. Dotcom's `.api/ssc/proxy` endpoint
|
||||
* exchanges the Sourcegraph session credentials for a SAMS access token
|
||||
* and then proxy the request to the SSC backend.
|
||||
* @param sscUrl The SSC API URL to call. Example: "/checkout/session".
|
||||
* @param method E.g. "POST".
|
||||
* @param params The body to send to the SSC API. Will be JSON-encoded.
|
||||
*/
|
||||
export function fetchThroughSSCProxy(sscUrl: string, method: string, params?: object): Promise<Response> {
|
||||
export function requestSSC(sscUrl: string, method: string, params?: object): Promise<Response> {
|
||||
// /.api/ssc/proxy endpoint exchanges the Sourcegraph session credentials for a SAMS access token.
|
||||
// And then proxy the request onto the SSC backend, which will actually create the
|
||||
// checkout session.
|
||||
@ -53,3 +58,25 @@ export function fetchThroughSSCProxy(sscUrl: string, method: string, params?: ob
|
||||
...(!['GET', 'HEAD'].includes(method) && params ? { body: JSON.stringify(params) } : null),
|
||||
})
|
||||
}
|
||||
|
||||
// React hook to fetch data through the SSC proxy and convert the response to a more usable format.
|
||||
// This is a low-level hook that is meant to be used by other hooks that need to fetch data from the SSC API.
|
||||
export const useSSCQuery = <T extends object>(endpoint: string): [T | null, Error | null] => {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
useEffect(() => {
|
||||
async function loadData(): Promise<void> {
|
||||
try {
|
||||
const response = await requestSSC(endpoint, 'GET')
|
||||
const responseJson = await response.json()
|
||||
setData(responseJson)
|
||||
} catch (error) {
|
||||
setError(error)
|
||||
}
|
||||
}
|
||||
|
||||
void loadData()
|
||||
}, [endpoint])
|
||||
|
||||
return [data, error]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user