Merge pull request #23405 from AmericanAirlines/feat(apollo-explorer)--allow-callbacks-using-apiholder

feat(apollo explorer): allow callbacks using apiholder
This commit is contained in:
Patrik Oldsberg
2024-04-14 17:58:33 +02:00
committed by GitHub
9 changed files with 124 additions and 36 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-apollo-explorer': minor
---
feat(apollo-explorer): allow callbacks using apiholder
+33
View File
@@ -69,3 +69,36 @@ That's it! You should now see an `Apollo Explorer` item in your sidebar, and if
Once you authenticate, your graph is ready to use 🚀
![Logged In](./docs/img/logged-in.png)
### Authentication Tokens for Apollo Studio
If you need to utilize an ApiRef to supply a token to Apollo, you may do so using an ApiHolder.
In `packages/app/src/App.tsx` perform the following modifications from above. The import `ssoAuthApiRef` is used as an example and **does not exist**.
```typescript
import { ApolloExplorerPage, EndpointProps } from '@backstage/plugin-apollo-explorer';
import { ssoAuthApiRef } from '@companyxyz/devkit';
import { ApiHolder } from '@backstage/core-plugin-api';
async function authCallback(options: { apiHolder: ApiHolder }): Promise<{token: string}> {
const sso = options.apiHolder.get<any>(ssoAuthApiRef)
return await sso.getToken()
}
const routes = (
<FlatRoutes>
{/* other routes... */}
<Route
path="/apollo-explorer"
element={
<ApolloExplorerPage
endpoints={[{
title: 'Github',
graphRef: 'my-github-graph-ref@current',
authCallback: authCallback
}]}
/>
}
/>
```
+25 -17
View File
@@ -5,6 +5,7 @@
```ts
/// <reference types="react" />
import { ApiHolder } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { JSONObject } from '@apollo/explorer/src/helpers/types';
import { JSX as JSX_2 } from 'react';
@@ -14,23 +15,7 @@ import { RouteRef } from '@backstage/core-plugin-api';
export const ApolloExplorerPage: (props: {
title?: string | undefined;
subtitle?: string | undefined;
endpoints: {
title: string;
graphRef: string;
persistExplorerState?: boolean | undefined;
initialState?:
| {
document?: string | undefined;
variables?: JSONObject | undefined;
headers?: Record<string, string> | undefined;
displayOptions: {
docsPanelState?: 'closed' | 'open' | undefined;
showHeadersAndEnvVars?: boolean | undefined;
theme?: 'dark' | 'light' | undefined;
};
}
| undefined;
}[];
endpoints: EndpointProps[];
}) => JSX_2.Element;
// @public
@@ -40,4 +25,27 @@ export const apolloExplorerPlugin: BackstagePlugin<
},
{}
>;
// @public
export type AuthCallback = (options: { apiHolder: ApiHolder }) => Promise<{
token: string;
}>;
// @public
export type EndpointProps = {
title: string;
graphRef: string;
authCallback?: AuthCallback;
persistExplorerState?: boolean;
initialState?: {
document?: string;
variables?: JSONObject;
headers?: Record<string, string>;
displayOptions: {
docsPanelState?: 'open' | 'closed';
showHeadersAndEnvVars?: boolean;
theme?: 'dark' | 'light';
};
};
};
```
+2
View File
@@ -36,7 +36,9 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/lab": "^4.0.0-alpha.61",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-use": "^17.5.0",
"use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
@@ -19,9 +19,11 @@ import Divider from '@material-ui/core/Divider';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import { makeStyles } from '@material-ui/core/styles';
import { JSONObject } from '@apollo/explorer/src/helpers/types';
import { ApolloExplorer } from '@apollo/explorer/react';
import { Content } from '@backstage/core-components';
import { HandleRequest } from '@apollo/explorer/src/helpers/postMessageRelayHelpers';
import { EndpointProps } from '../ApolloExplorerPage';
import { useApiHolder } from '@backstage/core-plugin-api';
const useStyles = makeStyles(theme => ({
tabs: {
@@ -38,30 +40,41 @@ const useStyles = makeStyles(theme => ({
},
}));
export type ApolloEndpointProps = {
title: string;
graphRef: string;
persistExplorerState?: boolean;
initialState?: {
document?: string;
variables?: JSONObject;
headers?: Record<string, string>;
displayOptions: {
docsPanelState?: 'open' | 'closed';
showHeadersAndEnvVars?: boolean;
theme?: 'dark' | 'light';
};
};
type Props = {
endpoints: EndpointProps[];
authCallback?: () => Promise<{ token: string }>;
};
type Props = {
endpoints: ApolloEndpointProps[];
export const handleAuthRequest = ({
authCallback,
}: {
authCallback: Props['authCallback'];
}): HandleRequest => {
const handleRequest: HandleRequest = async (endpointUrl, options) =>
fetch(endpointUrl, {
...options,
headers: {
...options.headers,
...(authCallback && {
Authorization: `Bearer ${(await authCallback()).token}`,
}),
},
});
return handleRequest;
};
export const ApolloExplorerBrowser = ({ endpoints }: Props) => {
const classes = useStyles();
const [tabIndex, setTabIndex] = useState(0);
const apiHolder = useApiHolder();
const getAuthCallback = (index: number) => {
const authCallback = endpoints[index].authCallback;
if (authCallback === undefined) return undefined;
return () => authCallback({ apiHolder });
};
return (
<div className={classes.root}>
<Tabs
@@ -79,6 +92,9 @@ export const ApolloExplorerBrowser = ({ endpoints }: Props) => {
<ApolloExplorer
className={classes.explorer}
graphRef={endpoints[tabIndex].graphRef}
handleRequest={handleAuthRequest({
authCallback: getAuthCallback(tabIndex),
})}
persistExplorerState={endpoints[tabIndex].persistExplorerState}
initialState={endpoints[tabIndex].initialState}
/>
@@ -18,10 +18,17 @@ import React from 'react';
import { Content, Header, Page } from '@backstage/core-components';
import { ApolloExplorerBrowser } from '../ApolloExplorerBrowser';
import { JSONObject } from '@apollo/explorer/src/helpers/types';
import { ApiHolder } from '@backstage/core-plugin-api';
type EndpointProps = {
/**
* Export types to be used with {@link @backstage/apollo-explorer#ApolloExplorerPage}.
*
* @public
*/
export type EndpointProps = {
title: string;
graphRef: string;
authCallback?: AuthCallback;
persistExplorerState?: boolean;
initialState?: {
document?: string;
@@ -35,6 +42,15 @@ type EndpointProps = {
};
};
/**
* Export types to be used with {@link @backstage/apollo-explorer#ApolloExplorerPage}.
*
* @public
*/
export type AuthCallback = (options: {
apiHolder: ApiHolder;
}) => Promise<{ token: string }>;
type Props = {
title?: string | undefined;
subtitle?: string | undefined;
@@ -43,6 +59,7 @@ type Props = {
export const ApolloExplorerPage = (props: Props) => {
const { title, subtitle, endpoints } = props;
return (
<Page themeId="tool">
<Header title={title ?? 'Apollo Explorer 👩‍🚀'} subtitle={subtitle ?? ''} />
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { ApolloExplorerPage } from './ApolloExplorerPage';
export type { EndpointProps, AuthCallback } from './ApolloExplorerPage';
+4
View File
@@ -20,3 +20,7 @@
* @packageDocumentation
*/
export { apolloExplorerPlugin, ApolloExplorerPage } from './plugin';
export type {
EndpointProps,
AuthCallback,
} from './components/ApolloExplorerPage';
+3 -1
View File
@@ -4681,10 +4681,12 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/lab": ^4.0.0-alpha.61
"@testing-library/dom": ^9.0.0
"@testing-library/jest-dom": ^6.0.0
"@testing-library/react": ^15.0.0
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
react-use: ^17.5.0
use-deep-compare-effect: ^1.8.1
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
@@ -39887,7 +39889,7 @@ __metadata:
languageName: node
linkType: hard
"react-use@npm:^17.2.4, react-use@npm:^17.3.1, react-use@npm:^17.3.2, react-use@npm:^17.4.0":
"react-use@npm:^17.2.4, react-use@npm:^17.3.1, react-use@npm:^17.3.2, react-use@npm:^17.4.0, react-use@npm:^17.5.0":
version: 17.5.0
resolution: "react-use@npm:17.5.0"
dependencies: