diff --git a/packages/app/package.json b/packages/app/package.json
index 06858b015b..6ccf48584f 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -9,6 +9,7 @@
"@backstage/plugin-circleci": "^0.1.1-alpha.12",
"@backstage/plugin-explore": "^0.1.1-alpha.12",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.12",
+ "@backstage/plugin-graphiql": "^0.1.1-alpha.12",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.12",
"@backstage/plugin-register-component": "^0.1.1-alpha.12",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.12",
diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts
index 9bfc618216..5011b16eaf 100644
--- a/packages/app/src/apis.ts
+++ b/packages/app/src/apis.ts
@@ -45,6 +45,10 @@ import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { gitOpsApiRef, GitOpsRestApi } from '@backstage/plugin-gitops-profiles';
+import {
+ graphQlBrowseApiRef,
+ GraphQLEndpoints,
+} from '@backstage/plugin-graphiql';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -78,7 +82,7 @@ export const apis = (config: ConfigApi) => {
}),
);
- builder.add(
+ const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7000',
@@ -105,5 +109,22 @@ export const apis = (config: ConfigApi) => {
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
+ builder.add(
+ graphQlBrowseApiRef,
+ GraphQLEndpoints.from([
+ GraphQLEndpoints.create({
+ id: 'gitlab',
+ title: 'GitLab',
+ url: 'https://gitlab.com/api/graphql',
+ }),
+ GraphQLEndpoints.github({
+ id: 'github',
+ title: 'GitHub',
+ errorApi,
+ githubAuthApi,
+ }),
+ ]),
+ );
+
return builder.build();
};
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 453c98bc72..4b92f8281e 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -39,6 +39,7 @@ import {
SidebarPinButton,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
+import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -94,6 +95,11 @@ const Root: FC<{}> = ({ children }) => (
+
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index 05373d7d96..09cd117fc0 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -24,3 +24,4 @@ export { plugin as RegisterComponent } from '@backstage/plugin-register-componen
export { plugin as Sentry } from '@backstage/plugin-sentry';
export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
export { plugin as TechDocs } from '@backstage/plugin-techdocs';
+export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts
index 967918d87a..7ac6aff261 100644
--- a/packages/dev-utils/src/devApp/apiFactories.ts
+++ b/packages/dev-utils/src/devApp/apiFactories.ts
@@ -24,6 +24,10 @@ import {
AlertApiForwarder,
oauthRequestApiRef,
OAuthRequestManager,
+ GoogleAuth,
+ googleAuthApiRef,
+ GithubAuth,
+ githubAuthApiRef,
} from '@backstage/core';
// TODO(rugvip): We should likely figure out how to reuse all of these between apps
@@ -49,3 +53,25 @@ export const oauthRequestApiFactory = createApiFactory({
deps: {},
factory: () => new OAuthRequestManager(),
});
+
+export const googleAuthApiFactory = createApiFactory({
+ implements: googleAuthApiRef,
+ deps: { oauthRequestApi: oauthRequestApiRef },
+ factory: ({ oauthRequestApi }) =>
+ GoogleAuth.create({
+ apiOrigin: 'http://localhost:7000',
+ basePath: '/auth/',
+ oauthRequestApi,
+ }),
+});
+
+export const githubAuthApiFactory = createApiFactory({
+ implements: githubAuthApiRef,
+ deps: { oauthRequestApi: oauthRequestApiRef },
+ factory: ({ oauthRequestApi }) =>
+ GithubAuth.create({
+ apiOrigin: 'http://localhost:7000',
+ basePath: '/auth/',
+ oauthRequestApi,
+ }),
+});
diff --git a/plugins/graphiql/dev/index.tsx b/plugins/graphiql/dev/index.tsx
index 32d18b4f0b..efcf19a89d 100644
--- a/plugins/graphiql/dev/index.tsx
+++ b/plugins/graphiql/dev/index.tsx
@@ -15,15 +15,25 @@
*/
import { createDevApp } from '@backstage/dev-utils';
+import { githubAuthApiRef, errorApiRef } from '@backstage/core';
import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src';
createDevApp()
.registerPlugin(plugin)
.registerApiFactory({
implements: graphQlBrowseApiRef,
- deps: {},
- factory() {
+ deps: {
+ errorApi: errorApiRef,
+ githubAuthApi: githubAuthApiRef,
+ },
+ factory({ errorApi, githubAuthApi }) {
return GraphQLEndpoints.from([
+ GraphQLEndpoints.github({
+ id: 'github',
+ title: 'GitHub',
+ errorApi,
+ githubAuthApi,
+ }),
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
diff --git a/plugins/graphiql/src/assets/graphiql.icon.svg b/plugins/graphiql/src/assets/graphiql.icon.svg
index 66aee7be5a..d847a573cf 100644
--- a/plugins/graphiql/src/assets/graphiql.icon.svg
+++ b/plugins/graphiql/src/assets/graphiql.icon.svg
@@ -1,64 +1,19 @@
diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
index c407d92289..ee0b4bfd4e 100644
--- a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
+++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
@@ -15,6 +15,7 @@
*/
import { GraphQLBrowseApi, GraphQLEndpoint } from './types';
+import { ErrorApi, OAuthApi } from '@backstage/core';
// Helper for generic http endpoints
export type EndpointConfig = {
@@ -28,6 +29,23 @@ export type EndpointConfig = {
headers?: { [name in string]: string };
};
+export type GithubEndpointConfig = {
+ id: string;
+ title: string;
+ /**
+ * Github GraphQL API url, defaults to https://api.github.com/graphql
+ */
+ url?: string;
+ /**
+ * Errors will be posted to the ErrorApi if it is provided.
+ */
+ errorApi?: ErrorApi;
+ /**
+ * GitHub Auth API used to authenticate requests.
+ */
+ githubAuthApi: OAuthApi;
+};
+
export class GraphQLEndpoints implements GraphQLBrowseApi {
// Create a support
static create(config: EndpointConfig): GraphQLEndpoint {
@@ -51,6 +69,84 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
};
}
+ /**
+ * Creates a GitHub GraphQL endpoint that uses the GithubAuth API to authenticate requests.
+ *
+ * If a request requires more permissions than is granted by the existing session,
+ * the fetcher will automatically ask for the additional scopes that are required.
+ */
+ static github(config: GithubEndpointConfig): GraphQLEndpoint {
+ const {
+ id,
+ title,
+ url = 'https://api.github.com/graphql',
+ errorApi,
+ githubAuthApi,
+ } = config;
+ type ResponseBody = {
+ errors?: Array<{ type: string; message: string }>;
+ };
+
+ return {
+ id,
+ title,
+ fetcher: async (params: any) => {
+ let retried = false;
+
+ const doRequest = async (): Promise => {
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${await githubAuthApi.getAccessToken()}`,
+ },
+ body: JSON.stringify(params),
+ });
+ if (!res.ok) {
+ throw new Error(
+ `Request failed with status ${res.status} ${res.statusText}`,
+ );
+ }
+ const data = (await res.json()) as ResponseBody;
+
+ if (!data.errors || retried) {
+ return data;
+ }
+ retried = true;
+
+ const missingScopes = data.errors
+ .filter(({ type }) => type === 'INSUFFICIENT_SCOPES')
+ .flatMap(({ message }) => {
+ const scopesMatch = message.match(
+ /one of the following scopes: (\[.*?\])/,
+ );
+ if (!scopesMatch) {
+ return [];
+ }
+ try {
+ const scopes = JSON.parse(scopesMatch[1].replace(/'/g, '"'));
+ return scopes;
+ } catch (error) {
+ if (errorApi) {
+ errorApi.post(
+ new Error(
+ `Failed to parse scope string "${scopesMatch[1]}", ${error}`,
+ ),
+ );
+ }
+ return [];
+ }
+ });
+
+ await githubAuthApi.getAccessToken(missingScopes);
+ return doRequest();
+ };
+
+ return await doRequest();
+ },
+ };
+ }
+
static from(endpoints: GraphQLEndpoint[]) {
return new GraphQLEndpoints(endpoints);
}