diff --git a/plugins/graphiql/.eslintrc.js b/plugins/graphiql/.eslintrc.js new file mode 100644 index 0000000000..6a513fbafe --- /dev/null +++ b/plugins/graphiql/.eslintrc.js @@ -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, + }, +}; diff --git a/plugins/graphiql/README.md b/plugins/graphiql/README.md new file mode 100644 index 0000000000..d2f8cafbfb --- /dev/null +++ b/plugins/graphiql/README.md @@ -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); +``` diff --git a/plugins/graphiql/dev/index.tsx b/plugins/graphiql/dev/index.tsx new file mode 100644 index 0000000000..ed987c1c6a --- /dev/null +++ b/plugins/graphiql/dev/index.tsx @@ -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 ( + + + + + + + + + + + + + + ); +}; + +ReactDOM.render(, document.getElementById('root')); diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json new file mode 100644 index 0000000000..f2cf8b7930 --- /dev/null +++ b/plugins/graphiql/package.json @@ -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" + ] +} diff --git a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.test.tsx b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.test.tsx new file mode 100644 index 0000000000..1726c188c0 --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.test.tsx @@ -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', () => () => ''); + +describe('GraphiQLBrowser', () => { + it('should render error text if there are no endpoints', () => { + const rendered = render( + + + , + ); + rendered.getByText('No endpoints available'); + }); + + it('should render endpoint tabs', () => { + const rendered = render( + + + , + ); + rendered.getByText('Endpoint A'); + rendered.getByText('Endpoint B'); + }); +}); diff --git a/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx new file mode 100644 index 0000000000..e914fd9649 --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLBrowser/GraphiQLBrowser.tsx @@ -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(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 = ({ endpoints }) => { + const classes = useStyles(); + const [tabIndex, setTabIndex] = useState(0); + + if (!endpoints.length) { + return No endpoints available; + } + + const { id, fetcher } = endpoints[tabIndex]; + const storage = StorageBucket.forLocalStorage(`plugin/graphiql/data/${id}`); + + return ( +
+ setTabIndex(value)} + indicatorColor="primary" + > + {endpoints.map(({ title }, index) => ( + + ))} + + +
+ +
+
+ ); +}; diff --git a/plugins/graphiql/src/components/GraphiQLBrowser/index.ts b/plugins/graphiql/src/components/GraphiQLBrowser/index.ts new file mode 100644 index 0000000000..cb9a4a932f --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLBrowser/index.ts @@ -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'; diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx new file mode 100644 index 0000000000..d19e05524e --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx @@ -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: () => '', +})); + +describe('GraphiQLPage', () => { + it('should show progress', async () => { + const loadingApi: GraphQLBrowseApi = { + async getEndpoints() { + await new Promise(() => {}); + return []; + }, + }; + + const rendered = await renderWithEffects( + + + + + , + , + ); + + rendered.getByText('GraphiQL'); + rendered.getByTestId('progress'); + }); + + it('should show error', async () => { + const loadingApi: GraphQLBrowseApi = { + async getEndpoints() { + throw new Error('NOPE'); + }, + }; + + const rendered = await renderWithEffects( + + + + + , + ); + + 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( + + + + + , + ); + + rendered.getByText('GraphiQL'); + rendered.getByText(''); + }); +}); diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.tsx new file mode 100644 index 0000000000..39ee015bed --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.tsx @@ -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 = ( + + + + ); + } else if (endpoints.error) { + content = ( + + + {/* TODO: provide a proper error component */} + Failed to load GraphQL endpoints, {String(endpoints.error)} + + + ); + } else { + content = ( + + + + ); + } + + return ( + +
+ + +
+ {content} +
+ ); +}; diff --git a/plugins/graphiql/src/components/GraphiQLPage/index.ts b/plugins/graphiql/src/components/GraphiQLPage/index.ts new file mode 100644 index 0000000000..ed11f03c40 --- /dev/null +++ b/plugins/graphiql/src/components/GraphiQLPage/index.ts @@ -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'; diff --git a/plugins/graphiql/src/components/index.ts b/plugins/graphiql/src/components/index.ts new file mode 100644 index 0000000000..c0d2228951 --- /dev/null +++ b/plugins/graphiql/src/components/index.ts @@ -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'; diff --git a/plugins/graphiql/src/index.ts b/plugins/graphiql/src/index.ts new file mode 100644 index 0000000000..a1f5587fe1 --- /dev/null +++ b/plugins/graphiql/src/index.ts @@ -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'; diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts new file mode 100644 index 0000000000..c407d92289 --- /dev/null +++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts @@ -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; + } +} diff --git a/plugins/graphiql/src/lib/api/index.ts b/plugins/graphiql/src/lib/api/index.ts new file mode 100644 index 0000000000..bcc7966cbe --- /dev/null +++ b/plugins/graphiql/src/lib/api/index.ts @@ -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'; diff --git a/plugins/graphiql/src/lib/api/types.ts b/plugins/graphiql/src/lib/api/types.ts new file mode 100644 index 0000000000..6c41e2aaa0 --- /dev/null +++ b/plugins/graphiql/src/lib/api/types.ts @@ -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; +}; + +export type GraphQLBrowseApi = { + getEndpoints(): Promise; +}; + +export const graphQlBrowseApiRef = new ApiRef({ + id: 'plugin.graphiql.browse', + description: 'Used to supply GraphQL endpoints for browsing', +}); diff --git a/plugins/graphiql/src/lib/storage/StorageBucket.test.ts b/plugins/graphiql/src/lib/storage/StorageBucket.test.ts new file mode 100644 index 0000000000..d18d038cb5 --- /dev/null +++ b/plugins/graphiql/src/lib/storage/StorageBucket.test.ts @@ -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); + }); + }); +}); diff --git a/plugins/graphiql/src/lib/storage/StorageBucket.ts b/plugins/graphiql/src/lib/storage/StorageBucket.ts new file mode 100644 index 0000000000..9d275e5f0d --- /dev/null +++ b/plugins/graphiql/src/lib/storage/StorageBucket.ts @@ -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 = { + 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)); + } +} diff --git a/plugins/graphiql/src/lib/storage/index.ts b/plugins/graphiql/src/lib/storage/index.ts new file mode 100644 index 0000000000..a760761e44 --- /dev/null +++ b/plugins/graphiql/src/lib/storage/index.ts @@ -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'; diff --git a/plugins/graphiql/src/plugin.test.ts b/plugins/graphiql/src/plugin.test.ts new file mode 100644 index 0000000000..0841d3e071 --- /dev/null +++ b/plugins/graphiql/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/graphiql/src/plugin.ts b/plugins/graphiql/src/plugin.ts new file mode 100644 index 0000000000..6436380c27 --- /dev/null +++ b/plugins/graphiql/src/plugin.ts @@ -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); + }, +}); diff --git a/plugins/graphiql/src/setupTests.ts b/plugins/graphiql/src/setupTests.ts new file mode 100644 index 0000000000..1a907ab8e6 --- /dev/null +++ b/plugins/graphiql/src/setupTests.ts @@ -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(); diff --git a/plugins/graphiql/tsconfig.json b/plugins/graphiql/tsconfig.json new file mode 100644 index 0000000000..55fa1e2384 --- /dev/null +++ b/plugins/graphiql/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "dev"], + "compilerOptions": { + "baseUrl": "src" + } +} diff --git a/yarn.lock b/yarn.lock index d30e71ad42..5126cfa721 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1166,7 +1166,7 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" -"@emotion/core@^10.0.20": +"@emotion/core@^10.0.0", "@emotion/core@^10.0.20", "@emotion/core@^10.0.28": version "10.0.28" resolved "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" integrity sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA== @@ -1192,14 +1192,14 @@ resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@0.8.8": +"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.1": version "0.8.8" resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== dependencies: "@emotion/memoize" "0.7.4" -"@emotion/memoize@0.7.4": +"@emotion/memoize@0.7.4", "@emotion/memoize@^0.7.1": version "0.7.4" resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== @@ -1230,7 +1230,7 @@ "@emotion/serialize" "^0.11.15" "@emotion/utils" "0.11.3" -"@emotion/styled@^10.0.17": +"@emotion/styled@^10.0.0", "@emotion/styled@^10.0.17": version "10.0.27" resolved "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf" integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q== @@ -2491,6 +2491,11 @@ prop-types "^15.7.2" react-is "^16.8.0" +"@mdx-js/react@^1.0.0", "@mdx-js/react@^1.5.2": + version "1.5.9" + resolved "https://registry.npmjs.org/@mdx-js/react/-/react-1.5.9.tgz#31873ab097fbe58c61c7585fc0be64e83182b6df" + integrity sha512-rengdUSedIdIQbXPSeafItCacTYocARAjUA51b6R1KNHmz+59efz7UmyTKr73viJQZ98ouu7iRGmOTtjRrbbWA== + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -3401,6 +3406,105 @@ telejson "^3.2.0" util-deprecate "^1.0.2" +"@styled-system/background@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz#75c63d06b497ab372b70186c0bf608d62847a2ba" + integrity sha512-jtwH2C/U6ssuGSvwTN3ri/IyjdHb8W9X/g8Y0JLcrH02G+BW3OS8kZdHphF1/YyRklnrKrBT2ngwGUK6aqqV3A== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/border@^5.1.5": + version "5.1.5" + resolved "https://registry.npmjs.org/@styled-system/border/-/border-5.1.5.tgz#0493d4332d2b59b74bb0d57d08c73eb555761ba6" + integrity sha512-JvddhNrnhGigtzWRCVuAHepniyVi6hBlimxWDVAdcTuk7aRn9BYJUwfHslURtwYFsF5FoEs8Zmr1oZq2M1AP0A== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/color@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/color/-/color-5.1.2.tgz#b8d6b4af481faabe4abca1a60f8daa4ccc2d9f43" + integrity sha512-1kCkeKDZkt4GYkuFNKc7vJQMcOmTl3bJY3YBUs7fCNM6mMYJeT1pViQ2LwBSBJytj3AB0o4IdLBoepgSgGl5MA== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/core@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/core/-/core-5.1.2.tgz#b8b7b86455d5a0514f071c4fa8e434b987f6a772" + integrity sha512-XclBDdNIy7OPOsN4HBsawG2eiWfCcuFt6gxKn1x4QfMIgeO6TOlA2pZZ5GWZtIhCUqEPTgIBta6JXsGyCkLBYw== + dependencies: + object-assign "^4.1.1" + +"@styled-system/css@^5.1.5": + version "5.1.5" + resolved "https://registry.npmjs.org/@styled-system/css/-/css-5.1.5.tgz#0460d5f3ff962fa649ea128ef58d9584f403bbbc" + integrity sha512-XkORZdS5kypzcBotAMPBoeckDs9aSZVkvrAlq5K3xP8IMAUek+x2O4NtwoSgkYkWWzVBu6DGdFZLR790QWGG+A== + +"@styled-system/flexbox@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/flexbox/-/flexbox-5.1.2.tgz#077090f43f61c3852df63da24e4108087a8beecf" + integrity sha512-6hHV52+eUk654Y1J2v77B8iLeBNtc+SA3R4necsu2VVinSD7+XY5PCCEzBFaWs42dtOEDIa2lMrgL0YBC01mDQ== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/grid@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/grid/-/grid-5.1.2.tgz#7165049877732900b99cd00759679fbe45c6c573" + integrity sha512-K3YiV1KyHHzgdNuNlaw8oW2ktMuGga99o1e/NAfTEi5Zsa7JXxzwEnVSDSBdJC+z6R8WYTCYRQC6bkVFcvdTeg== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/layout@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/layout/-/layout-5.1.2.tgz#12d73e79887e10062f4dbbbc2067462eace42339" + integrity sha512-wUhkMBqSeacPFhoE9S6UF3fsMEKFv91gF4AdDWp0Aym1yeMPpqz9l9qS/6vjSsDPF7zOb5cOKC3tcKKOMuDCPw== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/position@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/position/-/position-5.1.2.tgz#56961266566836f57a24d8e8e33ce0c1adb59dd3" + integrity sha512-60IZfMXEOOZe3l1mCu6sj/2NAyUmES2kR9Kzp7s2D3P4qKsZWxD1Se1+wJvevb+1TP+ZMkGPEYYXRyU8M1aF5A== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/shadow@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/shadow/-/shadow-5.1.2.tgz#beddab28d7de03cd0177a87ac4ed3b3b6d9831fd" + integrity sha512-wqniqYb7XuZM7K7C0d1Euxc4eGtqEe/lvM0WjuAFsQVImiq6KGT7s7is+0bNI8O4Dwg27jyu4Lfqo/oIQXNzAg== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/should-forward-prop@^5.1.2": + version "5.1.5" + resolved "https://registry.npmjs.org/@styled-system/should-forward-prop/-/should-forward-prop-5.1.5.tgz#c392008c6ae14a6eb78bf1932733594f7f7e5c76" + integrity sha512-+rPRomgCGYnUIaFabDoOgpSDc4UUJ1KsmlnzcEp0tu5lFrBQKgZclSo18Z1URhaZm7a6agGtS5Xif7tuC2s52Q== + dependencies: + "@emotion/is-prop-valid" "^0.8.1" + "@emotion/memoize" "^0.7.1" + styled-system "^5.1.5" + +"@styled-system/space@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/space/-/space-5.1.2.tgz#38925d2fa29a41c0eb20e65b7c3efb6e8efce953" + integrity sha512-+zzYpR8uvfhcAbaPXhH8QgDAV//flxqxSjHiS9cDFQQUSznXMQmxJegbhcdEF7/eNnJgHeIXv1jmny78kipgBA== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/typography@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@styled-system/typography/-/typography-5.1.2.tgz#65fb791c67d50cd2900d234583eaacdca8c134f7" + integrity sha512-BxbVUnN8N7hJ4aaPOd7wEsudeT7CxarR+2hns8XCX1zp0DFfbWw4xYa/olA0oQaqx7F1hzDg+eRaGzAJbF+jOg== + dependencies: + "@styled-system/core" "^5.1.2" + +"@styled-system/variant@^5.1.5": + version "5.1.5" + resolved "https://registry.npmjs.org/@styled-system/variant/-/variant-5.1.5.tgz#8446d8aad06af3a4c723d717841df2dbe4ddeafd" + integrity sha512-Yn8hXAFoWIro8+Q5J8YJd/mP85Teiut3fsGVR9CAxwgNfIAiqlYxsk5iHU7VHJks/0KjL4ATSjmbtCDC/4l1qw== + dependencies: + "@styled-system/core" "^5.1.2" + "@styled-system/css" "^5.1.5" + "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": version "4.2.0" resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" @@ -3566,6 +3670,61 @@ resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz#2ad4e844175a3738cb9e7064be5ea070b8863a1c" integrity sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA== +"@theme-ui/color-modes@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.3.1.tgz#8c2b18fb170e6c998287b7381240a8b9cca8b0d1" + integrity sha512-WuZGgFW7M5wOWSse1PVZCEfM0OZip15/D6U3bB3B9KmWax7qiSnAm1yAMLRQKC+QYhndrjq3xU+WAQm11KnhIw== + dependencies: + "@emotion/core" "^10.0.0" + "@theme-ui/core" "^0.3.1" + "@theme-ui/css" "^0.3.1" + deepmerge "^4.2.2" + +"@theme-ui/components@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@theme-ui/components/-/components-0.3.1.tgz#fe023e156c1e1c076d5f2258466426e94adc2765" + integrity sha512-uG4dUM61s4tWv6N34uxs5VIh24bJyA/7TrYJ75WDiI+s72zbcNG7aGRpvX/hSZnAhxjdXpuskdEM3eEgOabdEg== + dependencies: + "@emotion/core" "^10.0.0" + "@emotion/styled" "^10.0.0" + "@styled-system/color" "^5.1.2" + "@styled-system/should-forward-prop" "^5.1.2" + "@styled-system/space" "^5.1.2" + "@theme-ui/css" "^0.3.1" + +"@theme-ui/core@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@theme-ui/core/-/core-0.3.1.tgz#dbe9800b9d6e923e1a7417e6adebce21524f8c02" + integrity sha512-cK6EVSOx0Kyx1Xpi4qb0JTLIxywx0DRh+53Ln1foXMplF2qKaDsFi3vD6duHIlT331E3CNOa9dftHHNM7y4rbA== + dependencies: + "@emotion/core" "^10.0.0" + "@theme-ui/css" "^0.3.1" + deepmerge "^4.2.2" + +"@theme-ui/css@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@theme-ui/css/-/css-0.3.1.tgz#b85c7e8fae948dc0de65aa30b853368993e25cb3" + integrity sha512-QB2/fZBpo4inaLHL3OrB8NOBgNfwnj8GtHzXWHb9iQSRjmtNX8zPXBe32jLT7qQP0+y8JxPT4YChZIkm5ZyIdg== + +"@theme-ui/mdx@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@theme-ui/mdx/-/mdx-0.3.0.tgz#8bb1342204acfaa69914d6b6567c5c49d9a8c1e6" + integrity sha512-/GHBNKqmUptWwkmF+zIASVQtjYs81XMEwtqPCHnHuaaCzhZxcXrtCwvcAgmCXF8hpRttCXVVxw1X3Gt0mhzaTQ== + dependencies: + "@emotion/core" "^10.0.0" + "@emotion/styled" "^10.0.0" + "@mdx-js/react" "^1.0.0" + +"@theme-ui/theme-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.3.1.tgz#910bc43454fd61b1047d7bb0dce05e36ffb6b44b" + integrity sha512-Sjj6lD0gPxBi+hcGCkawcGZECeESV/mW2YfmPqjNgmc296x5tulfNc+0/N5CJwLVOmnkn8zR5KNWZ8BjndfeTg== + dependencies: + "@emotion/core" "^10.0.0" + "@theme-ui/color-modes" "^0.3.1" + "@theme-ui/core" "^0.3.1" + "@theme-ui/mdx" "^0.3.0" + "@tootallnate/once@1": version "1.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" @@ -4483,6 +4642,11 @@ resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb" integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA== +"@xobotyi/scrollbar-width@1.9.5": + version "1.9.5" + resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -6473,6 +6637,19 @@ code-point-at@^1.0.0: resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codemirror-graphql@^0.12.0-alpha.7: + version "0.12.0-alpha.7" + resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.12.0-alpha.7.tgz#fb8a8e633f5e416a9736df2fa697312a0955db20" + integrity sha512-LOsHnAc/pWOqqUEfQX5tNn10jStqIe8il6/vHq7m81uyGcp7tx5Np3O7zwGKQ/HdewDUMjeqXnva5tFGYaKzVg== + dependencies: + graphql-language-service-interface "^2.4.0-alpha.7" + graphql-language-service-parser "^1.6.0-alpha.3" + +codemirror@^5.52.2: + version "5.53.2" + resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz#9799121cf8c50809cca487304e9de3a74d33f428" + integrity sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA== + collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -7618,6 +7795,11 @@ deep-object-diff@^1.1.0: resolved "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -8188,7 +8370,7 @@ entities@^1.1.1, entities@^1.1.2: resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -entities@^2.0.0: +entities@^2.0.0, entities@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== @@ -9968,6 +10150,55 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3 resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graphiql@^1.0.0-alpha.8: + version "1.0.0-alpha.8" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.0.0-alpha.8.tgz#a9cc594a8666e372c919a5c86a32835bc37499ff" + integrity sha512-6cR4NUtR79P2ShXt2P5d4OSIlsqiHzb7fK27LSm4JHIx0uBLYWrlJwHcjjF+Ucujr3fbVn4miyNF+ypvMJp53g== + dependencies: + "@emotion/core" "^10.0.28" + "@mdx-js/react" "^1.5.2" + codemirror "^5.52.2" + codemirror-graphql "^0.12.0-alpha.7" + copy-to-clipboard "^3.2.0" + entities "^2.0.0" + markdown-it "^10.0.0" + regenerator-runtime "^0.13.5" + theme-ui "^0.3.1" + +graphql-language-service-interface@^2.4.0-alpha.7: + version "2.4.0-alpha.7" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.4.0-alpha.7.tgz#5458f1f17a79dde5b51dab0d21eda63dff48a918" + integrity sha512-X5jwTI4AlcKB/t9/hZBn9O69uG9WPbkFnZivhNfztzeudKV+cXHYR6behsR47bR2nzF3YEu7Nv+3HExXKQAlWg== + dependencies: + graphql-language-service-parser "^1.6.0-alpha.3" + graphql-language-service-types "^1.6.0-alpha.5" + graphql-language-service-utils "^2.4.0-alpha.6" + vscode-languageserver-types "^3.15.1" + +graphql-language-service-parser@^1.6.0-alpha.3: + version "1.6.0-alpha.3" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.6.0-alpha.3.tgz#671643018fb4b24f409997a05b021030948562fb" + integrity sha512-gx2u6b2m9X9DjvlfEjmBQ67GtZxPPWAtVb63BQ21u2MPxqYjKsBJkKLq9PiC2u57bJhv+h4MoL4OLJ/vkmq6Ig== + dependencies: + graphql-language-service-types "^1.6.0-alpha.5" + +graphql-language-service-types@^1.6.0-alpha.5: + version "1.6.0-alpha.5" + resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.6.0-alpha.5.tgz#113a92c7544d98c8a8b8c3e04c9f982d3dbe1a00" + integrity sha512-Q/pTif29xM6OiFV9RsYLwX2uDeCMdSG36n4G0aDvfbWxbOBj48vSlLL5R8Gpt/DjocG9Kkv51CZay6pMPtmAfg== + +graphql-language-service-utils@^2.4.0-alpha.6: + version "2.4.0-alpha.6" + resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.4.0-alpha.6.tgz#5c3b30150e4905517b84b71f84956851c2c883e1" + integrity sha512-/+crYcPhOoeu1qpHhH2FZpI7w4i3q4Kq7jzbodXxOZBAqJFd3FSHTnVBYEAfgTgkTbIC7yIXVwnIwXtqDFTNDQ== + dependencies: + graphql-language-service-types "^1.6.0-alpha.5" + +graphql@15.0.0: + version "15.0.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.0.0.tgz#042a5eb5e2506a2e2111ce41eb446a8e570b8be9" + integrity sha512-ZyVO1xIF9F+4cxfkdhOJINM+51B06Friuv4M66W7HzUOeFd+vNzUn4vtswYINPi6sysjf1M2Ri/rwZALqgwbaQ== + growly@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -12910,6 +13141,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkify-it@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" + integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== + dependencies: + uc.micro "^1.0.1" + lint-staged@^10.0.4: version "10.0.8" resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.0.8.tgz#0f7849cdc336061f25f5d4fcbcfa385701ff4739" @@ -13464,6 +13702,17 @@ markdown-escapes@^1.0.0: resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== +markdown-it@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" + integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== + dependencies: + argparse "^1.0.7" + entities "~2.0.0" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: version "6.11.0" resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0" @@ -13515,6 +13764,11 @@ mdn-data@2.0.6: resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + meant@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d" @@ -16967,6 +17221,25 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use@^13.0.0: + version "13.27.1" + resolved "https://registry.npmjs.org/react-use/-/react-use-13.27.1.tgz#e2ae2b708dafc7893c4772628801589aab9de370" + integrity sha512-bAwdqDMXs5lovEanXnL1izledfrPEUUv1afoTVB59eUiYcDyKul+M/dT/2WcgHjoY/R6QlrTcZoW4R7ifwvBfw== + dependencies: + "@types/js-cookie" "2.2.5" + "@xobotyi/scrollbar-width" "1.9.5" + copy-to-clipboard "^3.2.0" + fast-deep-equal "^3.1.1" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.2.1" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^2.1.0" + ts-easing "^0.2.0" + tslib "^1.10.0" + react-use@^13.24.0: version "13.27.0" resolved "https://registry.npmjs.org/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa" @@ -17290,7 +17563,7 @@ regenerator-runtime@^0.11.0: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: version "0.13.5" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== @@ -19023,6 +19296,25 @@ style-loader@^1.0.0: loader-utils "^1.2.3" schema-utils "^2.6.4" +styled-system@^5.1.5: + version "5.1.5" + resolved "https://registry.npmjs.org/styled-system/-/styled-system-5.1.5.tgz#e362d73e1dbb5641a2fd749a6eba1263dc85075e" + integrity sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A== + dependencies: + "@styled-system/background" "^5.1.2" + "@styled-system/border" "^5.1.5" + "@styled-system/color" "^5.1.2" + "@styled-system/core" "^5.1.2" + "@styled-system/flexbox" "^5.1.2" + "@styled-system/grid" "^5.1.2" + "@styled-system/layout" "^5.1.2" + "@styled-system/position" "^5.1.2" + "@styled-system/shadow" "^5.1.2" + "@styled-system/space" "^5.1.2" + "@styled-system/typography" "^5.1.2" + "@styled-system/variant" "^5.1.5" + object-assign "^4.1.1" + stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -19294,6 +19586,18 @@ text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +theme-ui@^0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/theme-ui/-/theme-ui-0.3.1.tgz#b00ee2c03eb3d820536af8b121c64d13b3777cf0" + integrity sha512-My/TSALqp7Dst5Ez7nJA+94Q8zJhc26Z0qGo8kEWyoqHHJ5TU8xdhjLPBltTdQck3T32cSq5USIeSKU3JtxYUQ== + dependencies: + "@theme-ui/color-modes" "^0.3.1" + "@theme-ui/components" "^0.3.1" + "@theme-ui/core" "^0.3.1" + "@theme-ui/css" "^0.3.1" + "@theme-ui/mdx" "^0.3.0" + "@theme-ui/theme-provider" "^0.3.1" + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -19722,6 +20026,11 @@ ua-parser-js@^0.7.18: resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + uglify-js@3.4.x: version "3.4.10" resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" @@ -20176,6 +20485,11 @@ vm-browserify@^1.0.1: resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-languageserver-types@^3.15.1: + version "3.15.1" + resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" + integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== + w3c-hr-time@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"