Merge pull request #646 from spotify/rugvip/graphiql
[Plugin] Add GraphiQL Plugin
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
rules: {
|
||||
// Prefer to use rendered.getBy*, which will throw an error
|
||||
'jest/expect-expect': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
# @backstage/plugin-graphiql
|
||||
|
||||
This plugin integrates [GraphiQL](https://github.com/graphql/graphiql) as a tool to browse GraphiQL endpoints inside Backstage.
|
||||
|
||||
The purpose of the plugin is to provide a convenient way for developers to try out GraphQL queries in their own environment.
|
||||
By exposing GraphiQL as a plugin instead of a standalone app, it's possible to provide a preconfigured environment for engineers, and also tie into authentication providers already inside Backstage.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installing the plugin
|
||||
|
||||
Start out by installing the plugin in your Backstage app:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/plugin-graphiql
|
||||
```
|
||||
|
||||
Then add an entry to your App's `plugins.ts` to import the plugin.
|
||||
|
||||
The plugin registers a `/graphiql` route, which you can link to from the Sidebar if desired.
|
||||
|
||||
### Adding GraphQL endpoints
|
||||
|
||||
For the plugin to function, you need to supply GraphQL endpoints through the GraphQLBrowse API, which is done by implementing the `GraphQLBrowseApi` exported by this plugin.
|
||||
|
||||
If all you need is a static list of endpoints, the plugin exports a `GraphQLEndpoints` class that implements the `GraphQLBrowseApi` for you. Here's and example of how you could expose two GraphQL endpoints in your App:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
graphQlBrowseApiRef,
|
||||
GraphQLEndpoints,
|
||||
} from '@backstage/plugin-graphiql';
|
||||
|
||||
// Implement the Graph QL browse API using a static list of endpoints
|
||||
const graphQlBrowseApi = GraphQLEndpoints.from([
|
||||
// Use the .create function if all you need is a static URL and headers.
|
||||
GraphQLEndpoints.create({
|
||||
id: 'gitlab',
|
||||
title: 'GitLab',
|
||||
url: 'https://gitlab.com/api/graphql',
|
||||
// Optional extra headers
|
||||
headers: { Extra: 'Header' },
|
||||
}),
|
||||
{
|
||||
id: 'hooli-search',
|
||||
title: 'Hooli Search',
|
||||
// Custom fetch function, this one is equivalent to using GraphQLEndpoints.create()
|
||||
// with url set to https://internal.hooli.com/search
|
||||
fetcher: async (params: any) => {
|
||||
return fetch('https://internal.hooli.com/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params),
|
||||
}).then(res => res.json());
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// ApiRegistry builder created somewhere in your App
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
// Add the instance to the API registry
|
||||
builder.add(graphQlBrowseApiRef, graphQlBrowseApi);
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import { ThemeProvider, CssBaseline } from '@material-ui/core';
|
||||
import {
|
||||
createApp,
|
||||
SidebarPage,
|
||||
Sidebar,
|
||||
SidebarItem,
|
||||
SidebarSpacer,
|
||||
ApiRegistry,
|
||||
} from '@backstage/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src';
|
||||
|
||||
const graphQlBrowseApi = GraphQLEndpoints.from([
|
||||
GraphQLEndpoints.create({
|
||||
id: 'gitlab',
|
||||
title: 'GitLab',
|
||||
url: 'https://gitlab.com/api/graphql',
|
||||
}),
|
||||
GraphQLEndpoints.create({
|
||||
id: 'countries',
|
||||
title: 'Countries',
|
||||
url: 'https://countries.trevorblades.com/',
|
||||
}),
|
||||
]);
|
||||
|
||||
const app = createApp();
|
||||
app.registerApis(ApiRegistry.from([[graphQlBrowseApiRef, graphQlBrowseApi]]));
|
||||
app.registerPlugin(plugin);
|
||||
const AppComponent = app.build();
|
||||
|
||||
const App: FC<{}> = () => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline>
|
||||
<BrowserRouter>
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarSpacer />
|
||||
<SidebarItem icon={HomeIcon} to="/graphiql" text="Home" />
|
||||
</Sidebar>
|
||||
<AppComponent />
|
||||
</SidebarPage>
|
||||
</BrowserRouter>
|
||||
</CssBaseline>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphiql",
|
||||
"description": "Backstage plugin for browsing GraphQL APIs",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://github.com/spotify/backstage/tree/master/plugins/graphiql#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"directory": "plugins/graphiql"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"react-router-dom": "^5.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"graphiql": "^1.0.0-alpha.8",
|
||||
"graphql": "15.0.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-use": "^13.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { GraphiQLBrowser } from './GraphiQLBrowser';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
|
||||
jest.mock('graphiql', () => () => '<GraphiQL />');
|
||||
|
||||
describe('GraphiQLBrowser', () => {
|
||||
it('should render error text if there are no endpoints', () => {
|
||||
const rendered = render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GraphiQLBrowser endpoints={[]} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
rendered.getByText('No endpoints available');
|
||||
});
|
||||
|
||||
it('should render endpoint tabs', () => {
|
||||
const rendered = render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GraphiQLBrowser
|
||||
endpoints={[
|
||||
{
|
||||
id: 'a',
|
||||
title: 'Endpoint A',
|
||||
async fetcher() {},
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
title: 'Endpoint B',
|
||||
async fetcher() {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
rendered.getByText('Endpoint A');
|
||||
rendered.getByText('Endpoint B');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Tabs, Tab, makeStyles, Typography, Divider } from '@material-ui/core';
|
||||
import 'graphiql/graphiql.css';
|
||||
import GraphiQL from 'graphiql';
|
||||
import { StorageBucket } from 'lib/storage';
|
||||
import { GraphQLEndpoint } from 'lib/api';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
},
|
||||
tabs: {
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
graphiQlWrapper: {
|
||||
flex: 1,
|
||||
'@global': {
|
||||
'.graphiql-container': {
|
||||
boxSizing: 'initial',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
type GraphiQLBrowserProps = {
|
||||
endpoints: GraphQLEndpoint[];
|
||||
};
|
||||
|
||||
export const GraphiQLBrowser: FC<GraphiQLBrowserProps> = ({ endpoints }) => {
|
||||
const classes = useStyles();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
if (!endpoints.length) {
|
||||
return <Typography variant="h4">No endpoints available</Typography>;
|
||||
}
|
||||
|
||||
const { id, fetcher } = endpoints[tabIndex];
|
||||
const storage = StorageBucket.forLocalStorage(`plugin/graphiql/data/${id}`);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Tabs
|
||||
classes={{ root: classes.tabs }}
|
||||
value={tabIndex}
|
||||
onChange={(_, value) => setTabIndex(value)}
|
||||
indicatorColor="primary"
|
||||
>
|
||||
{endpoints.map(({ title }, index) => (
|
||||
<Tab key={index} label={title} value={index} />
|
||||
))}
|
||||
</Tabs>
|
||||
<Divider />
|
||||
<div className={classes.graphiQlWrapper}>
|
||||
<GraphiQL key={tabIndex} fetcher={fetcher} storage={storage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './GraphiQLBrowser';
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GraphiQLPage } from './GraphiQLPage';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import { GraphQLBrowseApi, graphQlBrowseApiRef } from 'lib/api';
|
||||
|
||||
jest.mock('components/GraphiQLBrowser', () => ({
|
||||
GraphiQLBrowser: () => '<GraphiQLBrowser />',
|
||||
}));
|
||||
|
||||
describe('GraphiQLPage', () => {
|
||||
it('should show progress', async () => {
|
||||
const loadingApi: GraphQLBrowseApi = {
|
||||
async getEndpoints() {
|
||||
await new Promise(() => {});
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const rendered = await renderWithEffects(
|
||||
<ApiProvider apis={ApiRegistry.from([[graphQlBrowseApiRef, loadingApi]])}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GraphiQLPage />
|
||||
</ThemeProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
rendered.getByText('GraphiQL');
|
||||
rendered.getByTestId('progress');
|
||||
});
|
||||
|
||||
it('should show error', async () => {
|
||||
const loadingApi: GraphQLBrowseApi = {
|
||||
async getEndpoints() {
|
||||
throw new Error('NOPE');
|
||||
},
|
||||
};
|
||||
|
||||
const rendered = await renderWithEffects(
|
||||
<ApiProvider apis={ApiRegistry.from([[graphQlBrowseApiRef, loadingApi]])}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GraphiQLPage />
|
||||
</ThemeProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
rendered.getByText('GraphiQL');
|
||||
rendered.getByText('Failed to load GraphQL endpoints, Error: NOPE');
|
||||
});
|
||||
|
||||
it('should show GraphiQLBrowser', async () => {
|
||||
const loadingApi: GraphQLBrowseApi = {
|
||||
async getEndpoints() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const rendered = await renderWithEffects(
|
||||
<ApiProvider apis={ApiRegistry.from([[graphQlBrowseApiRef, loadingApi]])}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<GraphiQLPage />
|
||||
</ThemeProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
rendered.getByText('GraphiQL');
|
||||
rendered.getByText('<GraphiQLBrowser />');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
Content,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
Progress,
|
||||
pageTheme,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { useAsync } from 'react-use';
|
||||
import 'graphiql/graphiql.css';
|
||||
import { graphQlBrowseApiRef } from 'lib/api';
|
||||
import { GraphiQLBrowser } from 'components';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
export const GraphiQLPage: FC<{}> = () => {
|
||||
const graphQlBrowseApi = useApi(graphQlBrowseApiRef);
|
||||
const endpoints = useAsync(() => graphQlBrowseApi.getEndpoints());
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
if (endpoints.loading) {
|
||||
content = (
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
);
|
||||
} else if (endpoints.error) {
|
||||
content = (
|
||||
<Content>
|
||||
<Typography variant="h4" color="error">
|
||||
{/* TODO: provide a proper error component */}
|
||||
Failed to load GraphQL endpoints, {String(endpoints.error)}
|
||||
</Typography>
|
||||
</Content>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Content noPadding>
|
||||
<GraphiQLBrowser endpoints={endpoints.value!} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="GraphiQL">
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{content}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './GraphiQLPage';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './GraphiQLPage';
|
||||
export * from './GraphiQLBrowser';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './lib/api';
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GraphQLBrowseApi, GraphQLEndpoint } from './types';
|
||||
|
||||
// Helper for generic http endpoints
|
||||
export type EndpointConfig = {
|
||||
id: string;
|
||||
title: string;
|
||||
// Endpoint URL
|
||||
url: string;
|
||||
// only supports POST right now
|
||||
method?: 'POST';
|
||||
// Defaults to setting Content-Type to application/json
|
||||
headers?: { [name in string]: string };
|
||||
};
|
||||
|
||||
export class GraphQLEndpoints implements GraphQLBrowseApi {
|
||||
// Create a support
|
||||
static create(config: EndpointConfig): GraphQLEndpoint {
|
||||
const { id, title, url, method = 'POST' } = config;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
fetcher: async (params: any) => {
|
||||
const body = JSON.stringify(params);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...config.headers,
|
||||
};
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static from(endpoints: GraphQLEndpoint[]) {
|
||||
return new GraphQLEndpoints(endpoints);
|
||||
}
|
||||
|
||||
private constructor(private readonly endpoints: GraphQLEndpoint[]) {}
|
||||
|
||||
async getEndpoints() {
|
||||
return this.endpoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './GraphQLEndpoints';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiRef } from '@backstage/core';
|
||||
|
||||
export type GraphQLEndpoint = {
|
||||
// Will be used as unique key for storing history and query data
|
||||
id: string;
|
||||
|
||||
// Displayed to the user to identify the source.
|
||||
title: string;
|
||||
|
||||
// Method used send a GraphQL query.
|
||||
// The body parameter is equivalent to the POST body to be JSON-serialized, and the
|
||||
// return value should be the equivalent of a parsed JSON response from that POST.
|
||||
fetcher: (body: any) => Promise<any>;
|
||||
};
|
||||
|
||||
export type GraphQLBrowseApi = {
|
||||
getEndpoints(): Promise<GraphQLEndpoint[]>;
|
||||
};
|
||||
|
||||
export const graphQlBrowseApiRef = new ApiRef<GraphQLBrowseApi>({
|
||||
id: 'plugin.graphiql.browse',
|
||||
description: 'Used to supply GraphQL endpoints for browsing',
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { StorageBucket } from './StorageBucket';
|
||||
|
||||
describe('StorageBucket', () => {
|
||||
it('should forbid access to unknown keys', () => {
|
||||
const bucket = StorageBucket.forStorage(localStorage, 'hello');
|
||||
|
||||
expect(() => {
|
||||
bucket['dunno-this-one'] = 'nope';
|
||||
}).toThrow('Direct property access is not allowed for StorageBuckets');
|
||||
expect(() => {
|
||||
return bucket['dunno-this-one'];
|
||||
}).toThrow('Direct property access is not allowed for StorageBuckets');
|
||||
});
|
||||
|
||||
it('should not implement all methods', () => {
|
||||
const bucket = StorageBucket.forLocalStorage('hello');
|
||||
|
||||
expect(() => bucket.length).toThrow('Method not implemented.');
|
||||
expect(() => bucket.key()).toThrow('Method not implemented.');
|
||||
});
|
||||
|
||||
describe('with mocked underlying storage', () => {
|
||||
const mockStorage = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
};
|
||||
const bucket = StorageBucket.forStorage(
|
||||
(mockStorage as unknown) as Storage,
|
||||
'my-bucket',
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should set a first item', () => {
|
||||
bucket.setItem('x', 'a');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.setItem).toHaveBeenLastCalledWith(
|
||||
'my-bucket',
|
||||
JSON.stringify({ x: 'a' }),
|
||||
);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should set a second item', () => {
|
||||
mockStorage.getItem.mockReturnValueOnce(JSON.stringify({ y: 'b' }));
|
||||
bucket.setItem('x', 'a');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.setItem).toHaveBeenLastCalledWith(
|
||||
'my-bucket',
|
||||
JSON.stringify({ y: 'b', x: 'a' }),
|
||||
);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should clear the bucket', () => {
|
||||
bucket.clear();
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.removeItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
});
|
||||
|
||||
it('should get an item', () => {
|
||||
mockStorage.getItem.mockReturnValueOnce(JSON.stringify({ x: 'X' }));
|
||||
expect(bucket.getItem('x')).toBe('X');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should remove an item', () => {
|
||||
mockStorage.getItem.mockReturnValueOnce(
|
||||
JSON.stringify({ x: 'X', y: 'Y' }),
|
||||
);
|
||||
bucket.removeItem('x');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.setItem).toHaveBeenLastCalledWith(
|
||||
'my-bucket',
|
||||
JSON.stringify({ y: 'Y' }),
|
||||
);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not bother to write when deleting a missing key', () => {
|
||||
mockStorage.getItem.mockReturnValueOnce(JSON.stringify({ y: 'Y' }));
|
||||
bucket.removeItem('x');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should ignore bad data', () => {
|
||||
mockStorage.getItem.mockReturnValue('derp');
|
||||
|
||||
expect(bucket.getItem('x')).toBe(null);
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
|
||||
bucket.removeItem('x');
|
||||
|
||||
expect(mockStorage.getItem).toHaveBeenCalledTimes(2);
|
||||
expect(mockStorage.getItem).toHaveBeenLastCalledWith('my-bucket');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledTimes(0);
|
||||
expect(mockStorage.removeItem).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
type BucketData = { [key in string]: string };
|
||||
|
||||
export class StorageBucket implements Storage {
|
||||
private static noPropAccessProxyHandler: ProxyHandler<StorageBucket> = {
|
||||
get(target, prop) {
|
||||
if (prop in target) {
|
||||
return target[prop as any];
|
||||
}
|
||||
throw new Error(
|
||||
'Direct property access is not allowed for StorageBuckets',
|
||||
);
|
||||
},
|
||||
set() {
|
||||
throw new Error(
|
||||
'Direct property access is not allowed for StorageBuckets',
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
static forStorage(storage: Storage, bucket: string) {
|
||||
const storageBucket = new StorageBucket(storage, bucket);
|
||||
return new Proxy(storageBucket, StorageBucket.noPropAccessProxyHandler);
|
||||
}
|
||||
|
||||
static forLocalStorage(bucket: string): StorageBucket {
|
||||
return StorageBucket.forStorage(localStorage, bucket);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly storage: Storage,
|
||||
private readonly bucket: string,
|
||||
) {}
|
||||
|
||||
[name: string]: any;
|
||||
|
||||
get length(): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.storage.removeItem(this.bucket);
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.read()?.[key] ?? null;
|
||||
}
|
||||
|
||||
key(): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
const data = this.read();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key in data) {
|
||||
delete data[key];
|
||||
this.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
const data = this.read() ?? {};
|
||||
data[key] = value;
|
||||
this.write(data);
|
||||
}
|
||||
|
||||
private read(): BucketData | undefined {
|
||||
const bucketValue = this.storage.getItem(this.bucket);
|
||||
if (!bucketValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(bucketValue);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private write(data: BucketData) {
|
||||
this.storage.setItem(this.bucket, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './StorageBucket';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { plugin } from './plugin';
|
||||
|
||||
describe('graphiql', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { GraphiQLPage } from './components';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'graphiql',
|
||||
register({ router }) {
|
||||
router.registerRoute('/graphiql', GraphiQLPage);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user