Skip to content

Commit

Permalink
GraphQL Client: Add Apollo/client and implement on profile settings p…
Browse files Browse the repository at this point in the history
  • Loading branch information
umpox authored Jun 29, 2021
1 parent 80bed31 commit 7b3fd5f
Show file tree
Hide file tree
Showing 23 changed files with 648 additions and 539 deletions.
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = api => {
'web.url-search-params',
// Commonly needed by extensions (used by vscode-jsonrpc)
'web.immediate',
// Avoids issues with RxJS interop
// Always define Symbol.observable before libraries are loaded, ensuring interopability between different libraries.
'esnext.symbol.observable',
// Webpack v4 chokes on optional chaining and nullish coalescing syntax, fix will be released with webpack v5.
'@babel/plugin-proposal-optional-chaining',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as H from 'history'
import React from 'react'
import renderer from 'react-test-renderer'

Expand All @@ -8,11 +7,8 @@ jest.mock('mdi-react/CheckboxBlankCircleIcon', () => 'CheckboxBlankCircleIcon')
jest.mock('mdi-react/CheckIcon', () => 'CheckIcon')

describe('ActivationChecklist', () => {
const history = H.createMemoryHistory({ keyLength: 0 })
test('render loading', () => {
const component = renderer.create(
<ActivationChecklist steps={[]} history={H.createMemoryHistory({ keyLength: 0 })} />
)
const component = renderer.create(<ActivationChecklist steps={[]} />)
expect(component.toJSON()).toMatchSnapshot()
})
test('render 0/1 complete', () => {
Expand All @@ -27,7 +23,6 @@ describe('ActivationChecklist', () => {
},
]}
completed={{}}
history={history}
/>
)
expect(component.toJSON()).toMatchSnapshot()
Expand All @@ -43,7 +38,6 @@ describe('ActivationChecklist', () => {
},
]}
completed={{ EnabledRepository: true }} // another item
history={history}
/>
)
expect(component.toJSON()).toMatchSnapshot()
Expand All @@ -60,7 +54,6 @@ describe('ActivationChecklist', () => {
},
]}
completed={{ ConnectedCodeHost: true }} // same item as in steps
history={H.createMemoryHistory({ keyLength: 0 })}
/>
)
expect(component.toJSON()).toMatchSnapshot()
Expand Down
59 changes: 30 additions & 29 deletions client/shared/src/components/activation/ActivationChecklist.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Accordion, AccordionItem, AccordionButton, AccordionPanel } from '@reach/accordion'
import classNames from 'classnames'
import * as H from 'history'
import CheckboxBlankCircleOutlineIcon from 'mdi-react/CheckboxBlankCircleOutlineIcon'
import CheckCircleIcon from 'mdi-react/CheckCircleIcon'
import ChevronDownIcon from 'mdi-react/ChevronDownIcon'
Expand All @@ -13,7 +12,6 @@ import { ActivationCompletionStatus, ActivationStep } from './Activation'

interface ActivationChecklistItemProps extends ActivationStep {
done: boolean
history: H.History
className?: string
}

Expand Down Expand Up @@ -45,7 +43,6 @@ export const ActivationChecklistItem: React.FunctionComponent<ActivationChecklis
)

export interface ActivationChecklistProps {
history: H.History
steps: ActivationStep[]
completed?: ActivationCompletionStatus
className?: string
Expand All @@ -55,31 +52,35 @@ export interface ActivationChecklistProps {
/**
* Renders an activation checklist.
*/
export class ActivationChecklist extends React.PureComponent<ActivationChecklistProps, {}> {
public render(): JSX.Element {
return this.props.completed ? (
<div className={`activation-checklist list-group list-group-flush ${this.props.className || ''}`}>
<Accordion collapsible={true}>
{this.props.steps.map(step => (
<AccordionItem key={step.id} className="activation-checklist__container list-group-item">
<AccordionButton className="activation-checklist__button list-group-item list-group-item-action btn-link">
<ActivationChecklistItem
key={step.id}
{...step}
history={this.props.history}
done={this.props.completed?.[step.id] || false}
className={this.props.buttonClassName}
/>
</AccordionButton>
<AccordionPanel className="px-2">
<div className="activation-checklist__detail pb-1">{step.detail}</div>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</div>
) : (
<LoadingSpinner className="icon-inline my-2" />
)
export const ActivationChecklist: React.FunctionComponent<ActivationChecklistProps> = ({
className,
steps,
completed,
buttonClassName,
}) => {
if (!completed) {
return <LoadingSpinner className="icon-inline my-2" />
}

return (
<div className={`activation-checklist list-group list-group-flush ${className || ''}`}>
<Accordion collapsible={true}>
{steps.map(step => (
<AccordionItem key={step.id} className="activation-checklist__container list-group-item">
<AccordionButton className="activation-checklist__button list-group-item list-group-item-action btn-link">
<ActivationChecklistItem
key={step.id}
{...step}
done={completed?.[step.id] || false}
className={buttonClassName}
/>
</AccordionButton>
<AccordionPanel className="px-2">
<div className="activation-checklist__detail pb-1">{step.detail}</div>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,6 @@ exports[`ActivationDropdown renders the activation dropdown 1`] = `
<ActivationChecklistItem
detail="Configure Sourcegraph to talk to your code host and fetch a list of your repositories."
done={false}
history="[History]"
id="ConnectedCodeHost"
key="ConnectedCodeHost"
title="Add repositories"
Expand Down Expand Up @@ -1345,7 +1344,6 @@ exports[`ActivationDropdown renders the activation dropdown 1`] = `
</span>
}
done={false}
history="[History]"
id="DidSearch"
key="DidSearch"
title="Search your code"
Expand Down Expand Up @@ -1442,7 +1440,6 @@ exports[`ActivationDropdown renders the activation dropdown 1`] = `
<ActivationChecklistItem
detail="To find references of a token, navigate to a code file in one of your repositories, hover over a token to activate the tooltip, and then click \\"Find references\\"."
done={false}
history="[History]"
id="FoundReferences"
key="FoundReferences"
title="Find some references"
Expand Down Expand Up @@ -1526,7 +1523,6 @@ exports[`ActivationDropdown renders the activation dropdown 1`] = `
<ActivationChecklistItem
detail="Configure a single-sign on (SSO) provider or have at least one other teammate sign up."
done={false}
history="[History]"
id="EnabledSharing"
key="EnabledSharing"
title="Configure SSO or share with teammates"
Expand Down
80 changes: 79 additions & 1 deletion client/shared/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import {
gql as apolloGql,
useQuery as useApolloQuery,
useMutation as useApolloMutation,
DocumentNode,
ApolloClient,
InMemoryCache,
createHttpLink,
NormalizedCacheObject,
OperationVariables,
QueryHookOptions,
QueryResult,
MutationHookOptions,
MutationTuple,
} from '@apollo/client'
import { useMemo } from 'react'
import { Observable } from 'rxjs'
import { fromFetch } from 'rxjs/fetch'
import { Omit } from 'utility-types'
Expand Down Expand Up @@ -54,6 +70,8 @@ export interface GraphQLRequestOptions extends Omit<RequestInit, 'method' | 'bod
baseUrl?: string
}

const GRAPHQL_URI = '/.api/graphql'

/**
* This function should not be called directly as it does not
* add the necessary headers to authorize the GraphQL API call.
Expand All @@ -69,11 +87,71 @@ export function requestGraphQLCommon<T, V = object>({
variables?: V
}): Observable<GraphQLResult<T>> {
const nameMatch = request.match(/^\s*(?:query|mutation)\s+(\w+)/)
const apiURL = `/.api/graphql${nameMatch ? '?' + nameMatch[1] : ''}`
const apiURL = `${GRAPHQL_URI}${nameMatch ? '?' + nameMatch[1] : ''}`
return fromFetch(baseUrl ? new URL(apiURL, baseUrl).href : apiURL, {
...options,
method: 'POST',
body: JSON.stringify({ query: request, variables }),
selector: response => checkOk(response).json(),
})
}

export const graphQLClient = ({ headers }: { headers: RequestInit['headers'] }): ApolloClient<NormalizedCacheObject> =>
new ApolloClient({
uri: GRAPHQL_URI,
cache: new InMemoryCache(),
link: createHttpLink({
uri: ({ operationName }) => `${GRAPHQL_URI}?${operationName}`,
headers,
}),
})

type RequestDocument = string | DocumentNode

/**
* Returns a `DocumentNode` value to support integrations with GraphQL clients that require this.
*
* @param document The GraphQL operation payload
* @returns The created `DocumentNode`
*/
export const getDocumentNode = (document: RequestDocument): DocumentNode => {
if (typeof document === 'string') {
return apolloGql(document)
}
return document
}

const useDocumentNode = (document: RequestDocument): DocumentNode =>
useMemo(() => getDocumentNode(document), [document])

/**
* Send a query to GraphQL and respond to updates.
* Wrapper around Apollo `useQuery` that supports `DocumentNode` and `string` types.
*
* @param query GraphQL operation payload.
* @param options Operation variables and request configuration
* @returns GraphQL response
*/
export function useQuery<TData = any, TVariables = OperationVariables>(
query: RequestDocument,
options: QueryHookOptions<TData, TVariables>
): QueryResult<TData, TVariables> {
const documentNode = useDocumentNode(query)
return useApolloQuery(documentNode, options)
}

/**
* Send a mutation to GraphQL and respond to updates.
* Wrapper around Apollo `useMutation` that supports `DocumentNode` and `string` types.
*
* @param mutation GraphQL operation payload.
* @param options Operation variables and request configuration
* @returns GraphQL response
*/
export function useMutation<TData = any, TVariables = OperationVariables>(
mutation: RequestDocument,
options?: MutationHookOptions<TData, TVariables>
): MutationTuple<TData, TVariables> {
const documentNode = useDocumentNode(mutation)
return useApolloMutation(documentNode, options)
}
2 changes: 2 additions & 0 deletions client/shared/src/polyfills/polyfill.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// This gets expanded into only the imports we need by @babel/preset-env
import 'core-js/stable'
// Avoids issues with RxJS interop
import 'core-js/features/symbol/observable'
Loading

0 comments on commit 7b3fd5f

Please sign in to comment.